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]>
119 lines
4.1 KiB
Go
119 lines
4.1 KiB
Go
package wasm
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"math"
|
|
"testing"
|
|
)
|
|
|
|
// TestInstrReaderPosAndSrc verifies the cursor-state accessors stay
|
|
// consistent as reads advance the reader. We don't assert on
|
|
// internal field layout — only on the publicly observed `Pos()` /
|
|
// `Src()` return values matching what we set up.
|
|
func TestInstrReaderPosAndSrc(t *testing.T) {
|
|
src := []byte{0x05, 0xe5, 0x8e, 0x26}
|
|
r := NewInstrReader(src)
|
|
if got := r.Pos(); got != 0 {
|
|
t.Errorf("initial Pos()=%d, want 0", got)
|
|
}
|
|
if got := r.Src(); &got[0] != &src[0] || len(got) != len(src) {
|
|
t.Errorf("Src() did not return the underlying slice (len=%d, want %d)", len(got), len(src))
|
|
}
|
|
// Advance one byte and re-check.
|
|
if _, err := r.ReadByte(); err != nil {
|
|
t.Fatalf("ReadByte: %v", err)
|
|
}
|
|
if got := r.Pos(); got != 1 {
|
|
t.Errorf("Pos() after one ReadByte=%d, want 1", got)
|
|
}
|
|
// Skip the rest via the LEB reader (3 bytes for 624485).
|
|
if v, err := r.ReadU32(); err != nil || v != 624485 {
|
|
t.Fatalf("ReadU32: got %d, err=%v", v, err)
|
|
}
|
|
if got := r.Pos(); got != len(src) {
|
|
t.Errorf("Pos() after full read=%d, want %d", got, len(src))
|
|
}
|
|
}
|
|
|
|
// TestInstrReaderLEB covers the LEB128 readers: unsigned u32/u64
|
|
// (incl. overflow) and signed sN over a range of bit widths.
|
|
func TestInstrReaderLEB(t *testing.T) {
|
|
// u32: 624485 = 0xe5 0x8e 0x26 in unsigned LEB128.
|
|
r := NewInstrReader([]byte{0xe5, 0x8e, 0x26})
|
|
if v, err := r.ReadU32(); err != nil || v != 624485 {
|
|
t.Errorf("ReadU32=%d,%v want 624485", v, err)
|
|
}
|
|
// u32 overflow: a value > 0xffffffff must error.
|
|
rov := NewInstrReader([]byte{0x80, 0x80, 0x80, 0x80, 0x10})
|
|
if _, err := rov.ReadU32(); err == nil {
|
|
t.Error("ReadU32 should reject a value over 2^32")
|
|
}
|
|
// u64 overflow: 10+ continuation bytes.
|
|
rbad := NewInstrReader([]byte{0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x01})
|
|
if _, err := rbad.ReadU64(); err == nil {
|
|
t.Error("ReadU64 should reject an over-long encoding")
|
|
}
|
|
// ReadByte EOF.
|
|
empty := NewInstrReader(nil)
|
|
if _, err := empty.ReadByte(); err == nil {
|
|
t.Error("ReadByte on empty input should error")
|
|
}
|
|
if !empty.EOF() {
|
|
t.Error("EOF() should be true for empty input")
|
|
}
|
|
// signed: -123456 in signed LEB128 = 0xc0 0xbb 0x78.
|
|
rs := NewInstrReader([]byte{0xc0, 0xbb, 0x78})
|
|
if v, err := rs.ReadS64(); err != nil || v != -123456 {
|
|
t.Errorf("ReadS64=%d,%v want -123456", v, err)
|
|
}
|
|
// small positive signed value.
|
|
rs2 := NewInstrReader([]byte{0x02})
|
|
if v, err := rs2.ReadS32(); err != nil || v != 2 {
|
|
t.Errorf("ReadS32=%d,%v want 2", v, err)
|
|
}
|
|
// small negative signed value (-1 = 0x7f).
|
|
rs3 := NewInstrReader([]byte{0x7f})
|
|
if v, err := rs3.ReadS64(); err != nil || v != -1 {
|
|
t.Errorf("ReadS64(0x7f)=%d,%v want -1", v, err)
|
|
}
|
|
}
|
|
|
|
// TestInstrReaderFloats covers the fixed-width f32/f64 readers,
|
|
// including the truncated-input error paths.
|
|
func TestInstrReaderFloats(t *testing.T) {
|
|
var f32 [4]byte
|
|
binary.LittleEndian.PutUint32(f32[:], math.Float32bits(3.5))
|
|
r := NewInstrReader(f32[:])
|
|
if v, err := r.ReadF32(); err != nil || v != 3.5 {
|
|
t.Errorf("ReadF32=%v,%v want 3.5", v, err)
|
|
}
|
|
var f64 [8]byte
|
|
binary.LittleEndian.PutUint64(f64[:], math.Float64bits(2.71828))
|
|
r2 := NewInstrReader(f64[:])
|
|
if v, err := r2.ReadF64(); err != nil || v != 2.71828 {
|
|
t.Errorf("ReadF64=%v,%v want 2.71828", v, err)
|
|
}
|
|
if _, err := NewInstrReader([]byte{0, 0}).ReadF32(); err == nil {
|
|
t.Error("ReadF32 on truncated input should error")
|
|
}
|
|
if _, err := NewInstrReader([]byte{0, 0, 0, 0}).ReadF64(); err == nil {
|
|
t.Error("ReadF64 on truncated input should error")
|
|
}
|
|
}
|
|
|
|
// TestInstrReaderMemArgAndVec covers ReadMemArg and ReadVecU32.
|
|
func TestInstrReaderMemArgAndVec(t *testing.T) {
|
|
// memarg: align=2, offset=16.
|
|
r := NewInstrReader([]byte{0x02, 0x10})
|
|
align, offset, err := r.ReadMemArg()
|
|
if err != nil || align != 2 || offset != 16 {
|
|
t.Errorf("ReadMemArg=(%d,%d,%v) want (2,16,nil)", align, offset, err)
|
|
}
|
|
// vec of u32: count=3, then 10,20,30.
|
|
rv := NewInstrReader([]byte{0x03, 0x0a, 0x14, 0x1e})
|
|
vec, err := rv.ReadVecU32()
|
|
if err != nil || len(vec) != 3 || vec[0] != 10 || vec[2] != 30 {
|
|
t.Errorf("ReadVecU32=%v,%v want [10 20 30]", vec, err)
|
|
}
|
|
}
|