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]>
85 lines
2.9 KiB
Go
85 lines
2.9 KiB
Go
package codegen_test
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/goccy/wasm2go/internal/codegen"
|
|
"github.com/goccy/wasm2go/internal/testfixture"
|
|
"github.com/goccy/wasm2go/internal/wasm"
|
|
)
|
|
|
|
// TestPerExportDispatch verifies the per-export dispatch shape that
|
|
// wasm2go always emits when Options.BulkExportPrefix matches at least
|
|
// one export: one standalone `Inv_<svc>_<mt>` function per export.
|
|
// Each Inv_* inlines its own trap-recovery (defer+recover with the
|
|
// mutable-global snapshot+restore) so there is no shared
|
|
// safeInvokeWrap helper and no closure allocation on the call path
|
|
// — the consumer calls Inv_<svc>_<mt> directly, which calls FnN
|
|
// directly. The Go linker can drop whichever exports the
|
|
// consumer never calls. The previously-emitted consolidated
|
|
// `InvokeExport` switch has been removed.
|
|
func TestPerExportDispatch(t *testing.T) {
|
|
bin := testfixture.Wasm(t, "wexports")
|
|
mod, err := wasm.Parse(bytes.NewReader(bin))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if _, err := codegen.Translate(&buf, mod, codegen.Options{
|
|
Package: "pkg",
|
|
OutputImportPath: "gentest/pkg",
|
|
BulkExportPrefix: "w_",
|
|
}); err != nil {
|
|
t.Fatalf("translate: %v", err)
|
|
}
|
|
out := buf.String()
|
|
|
|
for _, name := range []string{"func Inv_0_0(", "func Inv_0_1(", "func Inv_1_0("} {
|
|
if !strings.Contains(out, name) {
|
|
t.Errorf("missing %q:\n%s", name, out)
|
|
}
|
|
}
|
|
if strings.Contains(out, "safeInvokeWrap") {
|
|
t.Errorf("safeInvokeWrap must not appear after inlining (closure + helper removed):\n%s", out)
|
|
}
|
|
if strings.Contains(out, "func InvokeExport") || strings.Contains(out, "func SafeInvokeExport") {
|
|
t.Errorf("consolidated InvokeExport / SafeInvokeExport must not be emitted:\n%s", out)
|
|
}
|
|
// Each Inv_* must contain its own inlined recover. Spot-check by
|
|
// matching the recover() call inside Inv_0_0's body.
|
|
if !strings.Contains(out, "recover()") {
|
|
t.Errorf("expected inlined defer+recover in Inv_*:\n%s", out)
|
|
}
|
|
// The non-bulk "helper" export still gets its direct wrapper.
|
|
if !strings.Contains(out, "Helper") {
|
|
t.Errorf("system export wrapper missing")
|
|
}
|
|
}
|
|
|
|
// TestBulkExportEmpty drives a fixture with no bulk-prefixed exports
|
|
// through the same option. The translator must not emit any Inv_*
|
|
// functions and must surface the "matched zero exports" warning the
|
|
// caller relies on. We don't observe stderr here; we just confirm the
|
|
// generated code stays empty of bulk-dispatch artefacts.
|
|
func TestBulkExportEmpty(t *testing.T) {
|
|
bin := testfixture.Wasm(t, "arith")
|
|
mod, err := wasm.Parse(bytes.NewReader(bin))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var buf bytes.Buffer
|
|
if _, err := codegen.Translate(&buf, mod, codegen.Options{
|
|
Package: "pkg",
|
|
OutputImportPath: "gentest/pkg",
|
|
BulkExportPrefix: "w_",
|
|
}); err != nil {
|
|
t.Fatalf("translate: %v", err)
|
|
}
|
|
if strings.Contains(buf.String(), "func Inv_") {
|
|
t.Errorf("expected no Inv_ functions when prefix matches nothing")
|
|
}
|
|
}
|