mirror of
https://github.com/hanzoai/wasm2go.git
synced 2026-10-30 15:06:48 +00:00
* codegen: emit bool→i32 through a named helper, not a func literal
The gcasm backend requires every direct call surviving in the compiled
output to be either a package-local FnN or something the Go inliner
removed; anything else reaches the assembler with ABIInternal register
arguments while the linker binds an ABI0 symbol, so transform.go rejects
it rather than emit a silent ABI mismatch.
Comparisons whose result is consumed as a value (rather than by a
branch) were emitted as `func() int32 { if cond { return 1 }; return 0 }()`.
The Go inliner normally erases that, but it gives up once the ENCLOSING
function outgrows its budget — and a single wasm function can translate
to tens of thousands of lines of Go, as an interpreter's bytecode
dispatch loop does. The literal is then outlined into a real closure
symbol, and the transform fails:
transform <pkg>/p0.Fn5528:
unmarshalled direct call "<pkg>/p0.Fn5528.Fn5528.func54.func142"
Route the conversion through a b2i32 helper instead. It is a named
function, so it is always inlinable, and — unlike a nested func literal —
if it ever failed to inline it would surface as its own symbol rather
than as an anonymous closure. This also stops the nesting that a
comparison-of-a-comparison produced, where each level added another
literal for the inliner to unwrap.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* codegen: label the loop when a break sits inside a br_table's switch
Go binds a bare `break` to the innermost for, switch or select. The
structured emitter lowers a br_table to a Go `switch` and lowers a jump
to the enclosing loop's follow to a bare `break` — so a br_table arm that
exits its loop produced a break that left only the switch.
Where the switch is the last statement of the loop body, what follows
that break is the back-edge. The loop's exit therefore became a jump to
its own header, and — because the induction variable is advanced on the
`continue` arm, not the exit arm — it re-entered with the counter
unchanged. Not a slow loop: a stuck one. The generated Go compiles, and
the wasm it came from is correct; only the translation hangs.
Nothing about the shape is exotic: every arm terminates, and one arm
traps, which is what removes the br_table's post-dominator and leaves the
switch as the loop body's final statement. Real modules hit it — a
garbage collector's sweep loop over an array of regions, whose body is a
single switch, spins forever with its index pinned at 0.
Track how many switches are open around the emission point, compare that
against the depth recorded when each loop was opened, and name the loop
when a break has to cross one. Labels are attached lazily and only to
loops that a labelled break actually names, since Go rejects a label
nothing uses, and they are numbered from a per-function counter rather
than from the header block's ID: region() duplicates a shared forward
join into every path that reaches it, so one loop header can be emitted
twice, and two `for`s carrying the same label do not compile.
`continue` needs nothing: it already binds to the innermost for
regardless of any switch in between.
ssa_cf.wat's new table_loop_exit export reproduces the exact shape. Its
dead default arm exists to trap, so that the br_table has no
post-dominator; without it the emitter appends a `break` after the switch
that the mis-bound break lands on, which masks the bug. It is asserted
both on shape (TestStructuredEmit) and against wazero
(TestSSAControlFlow), where before this change the generated Go hangs.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* codegen: cover b2i32 with a test
The helpers file is embedded source, not compiled into wasm2go itself, so
`unused` flags any helper no test in this package references. Every other
identity/conversion helper is covered by TestIdentityHelpers; b2i32 was
not.
The assertions are the contract the emitter depends on: exactly 0 and 1,
because a comparison result feeds arithmetic and bitwise operators, not
just branches.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
194 lines
7.3 KiB
WebAssembly Text Format
194 lines
7.3 KiB
WebAssembly Text Format
;; ssa_cf.wat — targeted control-flow patterns for debugging the
|
|
;; multi-block SSA lowering. Each export exercises one structured-
|
|
;; control shape so a failure pinpoints the broken pattern.
|
|
(module
|
|
;; --- if/else producing a result ---
|
|
(func (export "abs") (param i32) (result i32)
|
|
(if (result i32) (i32.lt_s (local.get 0) (i32.const 0))
|
|
(then (i32.sub (i32.const 0) (local.get 0)))
|
|
(else (local.get 0))))
|
|
|
|
;; --- if WITHOUT else, side-effecting (no result) ---
|
|
(func (export "clamp_if") (param i32) (result i32)
|
|
(local i32)
|
|
(local.set 1 (local.get 0))
|
|
(if (i32.lt_s (local.get 1) (i32.const 0))
|
|
(then (local.set 1 (i32.const 0))))
|
|
(local.get 1))
|
|
|
|
;; --- counted loop with br_if back-edge ---
|
|
;; count(n): increments c until c >= n, returns c. = max(n, 1).
|
|
(func (export "count") (param i32) (result i32)
|
|
(local i32)
|
|
(loop
|
|
(local.set 1 (i32.add (local.get 1) (i32.const 1)))
|
|
(br_if 0 (i32.lt_s (local.get 1) (local.get 0))))
|
|
(local.get 1))
|
|
|
|
;; --- block + br forward exit ---
|
|
;; early(n): if n>10 return 100 else return n.
|
|
(func (export "early") (param i32) (result i32)
|
|
(local i32)
|
|
(local.set 1 (local.get 0))
|
|
(block
|
|
(br_if 0 (i32.le_s (local.get 1) (i32.const 10)))
|
|
(local.set 1 (i32.const 100)))
|
|
(local.get 1))
|
|
|
|
;; --- loop with accumulator (phi for both i and sum) ---
|
|
;; sum(n) = 0+1+...+n (n>=0).
|
|
(func (export "sum") (param i32) (result i32)
|
|
(local i32) ;; 1: i
|
|
(local i32) ;; 2: acc
|
|
(block
|
|
(loop
|
|
(br_if 1 (i32.gt_s (local.get 1) (local.get 0)))
|
|
(local.set 2 (i32.add (local.get 2) (local.get 1)))
|
|
(local.set 1 (i32.add (local.get 1) (i32.const 1)))
|
|
(br 0)))
|
|
(local.get 2))
|
|
|
|
;; --- br followed by a nested block in the dead region ---
|
|
;; deadnest(n): n!=0 -> 0 ; n==0 -> 42. The nested block after the
|
|
;; `br 0` is unreachable and its `end` must not pop the outer frame.
|
|
(func (export "deadnest") (param i32) (result i32)
|
|
(local i32)
|
|
(block
|
|
(br_if 0 (local.get 0))
|
|
(local.set 1 (i32.const 42))
|
|
(br 0)
|
|
(block
|
|
(local.set 1 (i32.const 99))
|
|
(br 0))
|
|
(local.set 1 (i32.const 7)))
|
|
(local.get 1))
|
|
|
|
;; --- block WITH a result value carried across br ---
|
|
;; pick(n): n>0 -> 111 else 222, via a (result i32) block.
|
|
(func (export "pick") (param i32) (result i32)
|
|
(block (result i32)
|
|
(i32.const 111)
|
|
(br_if 0 (i32.gt_s (local.get 0) (i32.const 0)))
|
|
(drop)
|
|
(i32.const 222)))
|
|
|
|
;; --- explicit return from inside nested control ---
|
|
;; ret_nested(n): n<0 -> -1 (early return) ; else n+1.
|
|
(func (export "ret_nested") (param i32) (result i32)
|
|
(block
|
|
(br_if 0 (i32.ge_s (local.get 0) (i32.const 0)))
|
|
(return (i32.const -1)))
|
|
(i32.add (local.get 0) (i32.const 1)))
|
|
|
|
;; --- br to depth 1 out of nested blocks + inner fall-through ---
|
|
;; deep(n): n>5 -> 1000 else n.
|
|
(func (export "deep") (param i32) (result i32)
|
|
(local i32)
|
|
(local.set 1 (local.get 0))
|
|
(block
|
|
(block
|
|
(br_if 0 (i32.le_s (local.get 1) (i32.const 5)))
|
|
(local.set 1 (i32.const 1000))
|
|
(br 1)))
|
|
(local.get 1))
|
|
|
|
;; --- loop with a counter, exits via br 1, value via local ---
|
|
;; mul2(n): returns n*2 by adding n times.
|
|
(func (export "mul2") (param i32) (result i32)
|
|
(local i32) ;; 1: i
|
|
(local i32) ;; 2: acc
|
|
(block
|
|
(loop
|
|
(br_if 1 (i32.ge_s (local.get 1) (local.get 0)))
|
|
(local.set 2 (i32.add (local.get 2) (i32.const 2)))
|
|
(local.set 1 (i32.add (local.get 1) (i32.const 1)))
|
|
(br 0)))
|
|
(local.get 2))
|
|
|
|
;; --- block-result exited via `br` from inside an if, AND via
|
|
;; fall-through. Reproduces a stale-stack bug pattern: the if's
|
|
;; then-branch ends in `br 1` carrying a value, leaving its
|
|
;; pushed value on the operand stack. The post-region stack must
|
|
;; reset to the block-entry height so the base value (1000)
|
|
;; below the result survives intact.
|
|
;; brblock(n): n!=0 -> 1007 ; n==0 -> 1009.
|
|
(func (export "brblock") (param i32) (result i32)
|
|
(i32.const 1000) ;; base value, must survive untouched
|
|
(block (result i32)
|
|
(local.get 0)
|
|
(if
|
|
(then
|
|
(i32.const 7)
|
|
(br 1)))
|
|
(i32.const 9))
|
|
(i32.add)) ;; 1000 + block-result
|
|
|
|
;; --- foldable: all-constant arithmetic. ConstProp collapses the
|
|
;; whole expression to a single constant. fold_arith(_) = 14.
|
|
;; The param is unused — DCE drops the dead OpParam value.
|
|
(func (export "fold_arith") (param i32) (result i32)
|
|
(i32.add (i32.const 2) (i32.mul (i32.const 3) (i32.const 4))))
|
|
|
|
;; --- foldable: a repeated subexpression. The wasm computes
|
|
;; (n+1) twice; CSE merges the duplicate. fold_cse(n) = (n+1)^2.
|
|
(func (export "fold_cse") (param i32) (result i32)
|
|
(i32.mul
|
|
(i32.add (local.get 0) (i32.const 1))
|
|
(i32.add (local.get 0) (i32.const 1))))
|
|
|
|
;; --- foldable: a dead computation alongside the live result.
|
|
;; DCE removes the unused product. fold_dead(n) = n+1.
|
|
(func (export "fold_dead") (param i32) (result i32)
|
|
(local i32)
|
|
(local.set 1 (i32.mul (local.get 0) (local.get 0))) ;; dead: l1 unused
|
|
(i32.add (local.get 0) (i32.const 1)))
|
|
|
|
;; --- nested if inside loop ---
|
|
;; counteven(n) = number of even values in [0, n).
|
|
(func (export "counteven") (param i32) (result i32)
|
|
(local i32) ;; 1: i
|
|
(local i32) ;; 2: cnt
|
|
(block
|
|
(loop
|
|
(br_if 1 (i32.ge_s (local.get 1) (local.get 0)))
|
|
(if (i32.eqz (i32.and (local.get 1) (i32.const 1)))
|
|
(then (local.set 2 (i32.add (local.get 2) (i32.const 1)))))
|
|
(local.set 1 (i32.add (local.get 1) (i32.const 1)))
|
|
(br 0)))
|
|
(local.get 2))
|
|
|
|
;; --- br_table inside a loop, one arm exiting the loop ---
|
|
;; table_loop_exit(n): advance i from n until i is odd and i >= n+3.
|
|
;;
|
|
;; The loop's exit is a `br $done` nested inside one arm of a br_table. The
|
|
;; structured emitter renders the table as a Go `switch`, and every arm here
|
|
;; terminates, so nothing follows the switch and the `for` body ends with it.
|
|
;; Go binds a bare `break` to the innermost switch — so the exit must NAME
|
|
;; the loop. A bare break would leave only the switch and fall off the end of
|
|
;; the loop body, which is the back-edge: the loop would never terminate.
|
|
(func (export "table_loop_exit") (param i32) (result i32)
|
|
(local i32) ;; 1: i
|
|
(local.set 1 (local.get 0))
|
|
(block $done
|
|
(loop $again
|
|
(local.set 1 (i32.add (local.get 1) (i32.const 1)))
|
|
(block $trap
|
|
(block $odd
|
|
(block $even
|
|
;; selector is 0 or 1, so the default ($trap) arm is dead. It is
|
|
;; here to remove the br_table's post-dominator, which is what
|
|
;; makes the `switch` the last statement of the loop body — and
|
|
;; therefore what makes a mis-bound `break` fall onto the
|
|
;; back-edge instead of onto a following statement.
|
|
(br_table $even $odd $trap
|
|
(i32.and (local.get 1) (i32.const 1))))
|
|
;; even: keep going
|
|
(br $again))
|
|
;; odd: leave once i has caught up with n+5
|
|
(br_if $done (i32.ge_s (local.get 1) (i32.add (local.get 0) (i32.const 5))))
|
|
(br $again))
|
|
(unreachable)))
|
|
(local.get 1))
|
|
|
|
)
|