mirror of
https://github.com/hanzoai/wasm2go.git
synced 2026-10-30 15:06:48 +00:00
Replace the legacy direct-opcode compiler with an SSA pipeline that
emits per-function amd64.s and arm64.s for every wasm function. A
pure-Go body is also emitted (build-constrained on other GOOS/GOARCH)
and shares the same New() / exported-method surface, so callers never
see the path distinction.
Asm pipeline
- SSA always on; unsupported wasm features are now hard errors.
Translate now drives Parse -> SSA build -> passes (Simplify, DCE,
CSE) -> arch-specific emit.
- Per-arch block-local linear-scan regalloc with slot reuse and
m-pointer caching (the *Module pointer is pinned in a reserved
register across the function body — R11 on amd64, R4 on arm64 —
so per-access reloads from the FP frame are gone).
- Cross-block stack allocation via interference-graph slot sharing
reduces the typical per-function frame to 8-16 bytes.
- Peephole + branch fusion: drop dead spills, elide empty else
fall-through, fold constant displacement into indexed load/store,
fuse AND+NE+BlockIf into BTL+JCC.
- A new cross-block linear-scan allocator with Belady eviction
lives in internal/regalloc/ and is gated behind
WASM2GO_NEWREGALLOC for measurement.
Output shape
- Bulk-dispatch exports: when -bulk-export-prefix matches at least
one export, each <prefix><svc>_<mt> becomes a standalone
Inv_<svc>_<mt> function with inlined trap-recovery (defer +
recover + mutable-global snapshot/restore). The Go linker drops
whichever Inv_* the consumer never calls.
- Cross-package asm CALL: when every byte of an import path maps to
a plan-9 identifier rune ("/" -> U+2215, "." -> U+00B7), the CALL
target emits the direct cross-package symbol; otherwise asm falls
back to a per-chunk Go-body trampoline.
- Multi-package layout for large modules: bodies split across
p0..p<N> with go:linkname bridges in alias.go and per-chunk
amd64.s / arm64.s sidecars. Single-file mode remains supported
for modules under the internal multi-file threshold.
Coverage and tests
- internal/asmgen, internal/codegen, internal/regalloc, internal/ssa,
internal/wasm pass with -race. Total cross-package statement
coverage 85.8%, above the enforced 85% threshold.
- A new TestRecoverTrapsAreSurfacedAsErrors end-to-end test
(testdata/cg_recover.wat) pins the Inv_<svc>_<mt> trap-recovery
contract: wasm traps surface as errors (no Go panic), mutable
globals restore to wrapper-entry values, and 5000 trap+recover
cycles leak no goroutines and keep the Go heap delta under 1 MiB
after a forced GC.
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
52 lines
2.2 KiB
WebAssembly Text Format
52 lines
2.2 KiB
WebAssembly Text Format
;; cg_recover.wat — exercises the per-export Inv_<svc>_<mt> wrapper's
|
|
;; trap-recovery contract. Each bulk-dispatch export mutates the
|
|
;; module's single mutable global before triggering a wasm trap; the
|
|
;; non-bulk system exports `get_g` and `reset_g` give the test
|
|
;; a way to observe and reset that global between calls.
|
|
;;
|
|
;; The contract under test is:
|
|
;; - a wasm trap inside `Inv_<svc>_<mt>` does NOT panic; the
|
|
;; wrapper recovers and surfaces it as an `error`;
|
|
;; - every mutable global is restored to the value it had at
|
|
;; wrapper entry, even when the trap fires after the global
|
|
;; has been mutated;
|
|
;; - repeated trap+recover cycles do not leak goroutines or
|
|
;; accumulate Go heap allocations on the wrapper path.
|
|
(module
|
|
(memory (export "memory") 1)
|
|
(global $g (mut i32) (i32.const 100))
|
|
|
|
;; w_1_0 — mutate global, then trap via integer divide by zero.
|
|
(func (export "w_1_0") (param i32 i32) (result i64)
|
|
(global.set $g (i32.const 999))
|
|
(i64.extend_i32_s (i32.div_s (i32.const 1) (i32.const 0))))
|
|
|
|
;; w_1_1 — mutate global, then trap via `unreachable`.
|
|
(func (export "w_1_1") (param i32 i32) (result i64)
|
|
(global.set $g (i32.const 999))
|
|
(unreachable))
|
|
|
|
;; w_1_2 — mutate global, then trap via i32.trunc_f32_s of NaN
|
|
;; (wasm spec: invalid conversion to integer is a trap).
|
|
(func (export "w_1_2") (param i32 i32) (result i64)
|
|
(global.set $g (i32.const 999))
|
|
(i64.extend_i32_s (i32.trunc_f32_s (f32.const nan))))
|
|
|
|
;; w_2_0 — mutate global, then succeed (returns l0+l1). Used as
|
|
;; the no-trap control so the test can assert that wrapping a
|
|
;; clean call still updates the global and returns the right
|
|
;; value through Inv_<svc>_<mt>.
|
|
(func (export "w_2_0") (param i32 i32) (result i64)
|
|
(global.set $g (i32.const 999))
|
|
(i64.extend_i32_s (i32.add (local.get 0) (local.get 1))))
|
|
|
|
;; get_g — read the mutable global. Non-bulk export so wasm2go
|
|
;; emits its own direct wrapper (`Module.GetG`) instead of an
|
|
;; `Inv_*` entry.
|
|
(func (export "get_g") (result i32)
|
|
(global.get $g))
|
|
|
|
;; reset_g — restore the global to its initial value (100).
|
|
(func (export "reset_g")
|
|
(global.set $g (i32.const 100))))
|