mirror of
https://github.com/hanzoai/wasm2go.git
synced 2026-10-30 15:06:48 +00:00
* Back shared linear memories with anonymous mappings, not the Go heap A shared memory is allocated at its declared ceiling LENGTH once (agents deref the base pointer concurrently, so the array must never move). On the Go heap that size is a trap: the FIRST make is lazily paged, but a freed ceiling-sized span gets zeroed when the allocator reuses it — so the second instance a process builds and discards (e.g. the private builder engines behind the copy-on-write image tiers) memclrs the whole ceiling. With llama's 8 GiB ceiling that measured ~8 GB of dirty resident pages for memory nothing had touched, on every process, regardless of model size; guests with small ceilings (spidermonkey's 256 MiB) had the same behaviour hidden under the noise floor. Generated constructors for SHARED memories now call the sharedimage runtime's AllocSharedMemory: anonymous, private, zero-fill-on-demand pages outside the GC heap, fresh from the OS on every call, with a finalizer on the module preserving the heap allocation's reclaim semantics (a module kept reachable — say by a parked agent goroutine — keeps its memory). Platforms without mmap keep the historical make. The sharedimage aux files thereby become a compile-time dependency of shared-memory output, which they were always documented to be: the gcasm capture tree and the test snippet runners now include Result.AuxFiles instead of silently dropping them. Pinned by a fixture with a 1 GiB ceiling: the constructor must route through AllocSharedMemory, and building + discarding three instances must hold process RSS far under one ceiling. Co-Authored-By: Claude Fable 5 <[email protected]> * Add in-place image builders: the builder writes the image file directly The image tiers built their template by booting a throwaway instance on private memory, copying its linear memory into a heap buffer, and writing that to the image file — the same bytes crossing memory up to three times, and (before the off-heap change) the trigger for the ceiling-zeroing regression. Elide all of it: NewSharedImageInPlace / NewSharedSnapshotInPlace hand the builder a MAP_SHARED mapping of the (sparse) image file, so every page the build touches is written once, straight into the file's page cache, and IS the image. Building now dirties exactly the touched pages, and they are file-backed — the kernel can write them back and evict under pressure, which anonymous pages cannot offer. The data-segment kind keeps its contract (carry NOTHING above the segments): the builder's BSS is punched back out of the file (truncate down, re-extend sparse) before instances map it. The builder's mapping stays valid until the module is unreachable, via the same finalizer discipline as AllocSharedMemory. Constructing a module ON caller memory needs the declared initial size, so generated packages now export InitialMemoryBytes: what NewWithMemory's memSize must be when the provided memory is fresh zeros rather than an existing image. Pinned by the sharedmem_ceiling fixture: a snapshot instance inherits the builder's BSS write, a data-segment-image instance must not. Co-Authored-By: Claude Fable 5 <[email protected]> * Drop the dead make-expression path for shared memories Review feedback on the constructor emission: the make() call expression was built for every memory and then discarded whenever the shared branch (AllocSharedMemory) or the caller-memory branch replaced it, and the shared branch assigned a capArg nothing consumes. Select the memory expression in one switch instead — each variant builds only what it emits. Co-Authored-By: Claude Fable 5 <[email protected]> * Remove AllocSharedMemory: the in-place builders are the fix Review feedback: with the image tiers built in place, no default path allocates a discarded ceiling-length shared memory any more, and the paths the anonymous-mmap constructor change still claimed to protect do not hold up. The fallback engines exist precisely because mmap is unavailable or failing — an mmap-based allocator cannot be their safety net (its no-mmap variant was plain make anyway), and the debugging escape hatch is not a production path to engineer for. Embedders still building images through throwaway plain-constructor engines migrate to the in-place builders when their bundles roll forward. Generated constructors go back to make for shared memories, the capture tree no longer needs the aux files (nothing in generated constructors references them; the snippet harnesses keep them because embedder-level tests exercise the image API), and the off-heap constructor test goes away with the machinery it pinned. Co-Authored-By: Claude Fable 5 <[email protected]> --------- Co-authored-by: Claude Fable 5 <[email protected]>
430 lines
17 KiB
Go
430 lines
17 KiB
Go
package codegen_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/goccy/wasm2go/internal/codegen"
|
|
"github.com/goccy/wasm2go/internal/testfixture"
|
|
"github.com/goccy/wasm2go/internal/wasm"
|
|
"github.com/tetratelabs/wazero"
|
|
)
|
|
|
|
// runExportMatrix translates a fixture (legacy + SSA), compiles+runs the
|
|
// generated package, and asserts each integer-result export call matches
|
|
// wazero. argTypes/resType per call describe the Go-call shape.
|
|
func runExportMatrix(t *testing.T, fixture string, calls []call) {
|
|
t.Helper()
|
|
bin := testfixture.Wasm(t, fixture)
|
|
mod, err := wasm.Parse(bytes.NewReader(bin))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx := context.Background()
|
|
r := wazero.NewRuntime(ctx)
|
|
t.Cleanup(func() {
|
|
if err := r.Close(ctx); err != nil {
|
|
t.Errorf("wazero runtime close: %v", err)
|
|
}
|
|
})
|
|
wmod, err := r.Instantiate(ctx, bin)
|
|
if err != nil {
|
|
t.Fatalf("wazero instantiate: %v", err)
|
|
}
|
|
|
|
var mainSB strings.Builder
|
|
mainSB.WriteString("package main\n\nimport (\n\t\"fmt\"\n\t\"gentest/pkg\"\n)\n\nfunc main() {\n\tm := pkg.New()\n")
|
|
var want []string
|
|
for _, c := range calls {
|
|
res, err := wmod.ExportedFunction(c.export).Call(ctx, c.args...)
|
|
if err != nil {
|
|
t.Fatalf("wazero %s%v: %v", c.export, c.args, err)
|
|
}
|
|
method := codegen.ExportMethodName(c.export)
|
|
var argList []string
|
|
for i, a := range c.args {
|
|
switch c.argTypes[i] {
|
|
case wasm.ValI64:
|
|
argList = append(argList, fmt.Sprintf("int64(%d)", int64(a)))
|
|
default:
|
|
argList = append(argList, fmt.Sprintf("int32(%d)", int32(a)))
|
|
}
|
|
}
|
|
argStr := strings.Join(argList, ", ")
|
|
switch c.resType {
|
|
case wasm.ValI64:
|
|
want = append(want, fmt.Sprintf("%s=%d", c.export, int64(res[0])))
|
|
fmt.Fprintf(&mainSB, "\tfmt.Printf(\"%s=%%d\\n\", m.%s(%s))\n", c.export, method, argStr)
|
|
case 0:
|
|
want = append(want, fmt.Sprintf("%s=void", c.export))
|
|
fmt.Fprintf(&mainSB, "\tm.%s(%s)\n\tfmt.Printf(\"%s=void\\n\")\n", method, argStr, c.export)
|
|
default:
|
|
want = append(want, fmt.Sprintf("%s=%d", c.export, int32(res[0])))
|
|
fmt.Fprintf(&mainSB, "\tfmt.Printf(\"%s=%%d\\n\", m.%s(%s))\n", c.export, method, argStr)
|
|
}
|
|
}
|
|
mainSB.WriteString("}\n")
|
|
|
|
for _, useSSA := range []bool{false, true} {
|
|
var buf bytes.Buffer
|
|
res, err := codegen.Translate(&buf, mod, codegen.Options{Package: "pkg", OutputImportPath: "gentest/pkg"})
|
|
if err != nil {
|
|
t.Fatalf("translate (UseSSA=%v): %v", useSSA, err)
|
|
}
|
|
out := strings.TrimSpace(runGoSnippet(t, buf.String(), mainSB.String(), res.Sidecars, filesWithAux(res)))
|
|
gotLines := strings.Split(out, "\n")
|
|
if len(gotLines) != len(want) {
|
|
t.Fatalf("%s UseSSA=%v: got %d lines want %d\n%s", fixture, useSSA, len(gotLines), len(want), out)
|
|
}
|
|
for i := range want {
|
|
if strings.TrimSpace(gotLines[i]) != want[i] {
|
|
t.Errorf("%s UseSSA=%v: %s mismatch got %q want %q", fixture, useSSA, calls[i].export, gotLines[i], want[i])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestFrameAllocRuntime drives cg_frame — the Emscripten stack-frame
|
|
// allocation idiom — and confirms the tryEmitFrameAlloc rewrite preserves
|
|
// semantics across both pipelines.
|
|
func TestFrameAllocRuntime(t *testing.T) {
|
|
i32 := wasm.ValI32
|
|
runExportMatrix(t, "cg_frame.wasm", []call{
|
|
{export: "alloc_frame", resType: i32},
|
|
{export: "alloc_frame_add", resType: i32},
|
|
{export: "sp_now", resType: i32},
|
|
{export: "free_frame", args: []uint64{16}, argTypes: []wasm.ValType{i32}, resType: 0},
|
|
{export: "sp_now", resType: i32},
|
|
{export: "alloc64", resType: wasm.ValI64},
|
|
})
|
|
}
|
|
|
|
// TestIndirectCallRuntime drives cg_indirect — call_indirect across several
|
|
// distinct function-type signatures plus direct call chains.
|
|
func TestIndirectCallRuntime(t *testing.T) {
|
|
i32, i64 := wasm.ValI32, wasm.ValI64
|
|
runExportMatrix(t, "cg_indirect.wasm", []call{
|
|
{export: "ind_unary", args: []uint64{0, 9}, argTypes: []wasm.ValType{i32, i32}, resType: i32},
|
|
{export: "ind_unary", args: []uint64{1, 9}, argTypes: []wasm.ValType{i32, i32}, resType: i32},
|
|
{export: "ind_binary", args: []uint64{2, 8, 5}, argTypes: []wasm.ValType{i32, i32, i32}, resType: i32},
|
|
{export: "ind_binary", args: []uint64{3, 8, 5}, argTypes: []wasm.ValType{i32, i32, i32}, resType: i32},
|
|
{export: "ind_i64", args: []uint64{4, 6, 7}, argTypes: []wasm.ValType{i32, i64, i64}, resType: i64},
|
|
{export: "ind_noargs", args: []uint64{5}, argTypes: []wasm.ValType{i32}, resType: i32},
|
|
{export: "ind_void", args: []uint64{6, 1234}, argTypes: []wasm.ValType{i32, i32}, resType: i32},
|
|
{export: "direct_chain", args: []uint64{u32(-3)}, argTypes: []wasm.ValType{i32}, resType: i32},
|
|
})
|
|
}
|
|
|
|
// TestSpecialFPRuntime drives cg_specialfp — NaN/Inf float constants — and
|
|
// confirms the f32ConstExpr/f64ConstExpr special-value emission (which
|
|
// routes through math.Float{32,64}frombits) preserves the exact bit
|
|
// pattern that wazero produces.
|
|
func TestSpecialFPRuntime(t *testing.T) {
|
|
bin := testfixture.Wasm(t, "cg_specialfp")
|
|
mod, err := wasm.Parse(bytes.NewReader(bin))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx := context.Background()
|
|
r := wazero.NewRuntime(ctx)
|
|
t.Cleanup(func() {
|
|
if err := r.Close(ctx); err != nil {
|
|
t.Errorf("wazero runtime close: %v", err)
|
|
}
|
|
})
|
|
wmod, err := r.Instantiate(ctx, bin)
|
|
if err != nil {
|
|
t.Fatalf("wazero instantiate: %v", err)
|
|
}
|
|
|
|
f32Exports := []string{"f32_nan", "f32_inf", "f32_ninf"}
|
|
f64Exports := []string{"f64_nan", "f64_inf", "f64_ninf"}
|
|
var mainSB strings.Builder
|
|
mainSB.WriteString("package main\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"gentest/pkg\"\n)\n\nfunc main() {\n\tm := pkg.New()\n")
|
|
var want []string
|
|
for _, e := range f32Exports {
|
|
res, err := wmod.ExportedFunction(e).Call(ctx)
|
|
if err != nil {
|
|
t.Fatalf("wazero %s: %v", e, err)
|
|
}
|
|
want = append(want, fmt.Sprintf("%s=%08x", e, uint32(res[0])))
|
|
fmt.Fprintf(&mainSB, "\tfmt.Printf(\"%s=%%08x\\n\", math.Float32bits(m.%s()))\n", e, codegen.ExportMethodName(e))
|
|
}
|
|
for _, e := range f64Exports {
|
|
res, err := wmod.ExportedFunction(e).Call(ctx)
|
|
if err != nil {
|
|
t.Fatalf("wazero %s: %v", e, err)
|
|
}
|
|
want = append(want, fmt.Sprintf("%s=%016x", e, res[0]))
|
|
fmt.Fprintf(&mainSB, "\tfmt.Printf(\"%s=%%016x\\n\", math.Float64bits(m.%s()))\n", e, codegen.ExportMethodName(e))
|
|
}
|
|
mainSB.WriteString("}\n")
|
|
|
|
for _, useSSA := range []bool{false, true} {
|
|
var buf bytes.Buffer
|
|
res, err := codegen.Translate(&buf, mod, codegen.Options{Package: "pkg", OutputImportPath: "gentest/pkg"})
|
|
if err != nil {
|
|
t.Fatalf("translate (UseSSA=%v): %v", useSSA, err)
|
|
}
|
|
out := strings.TrimSpace(runGoSnippet(t, buf.String(), mainSB.String(), res.Sidecars, filesWithAux(res)))
|
|
gotLines := strings.Split(out, "\n")
|
|
if len(gotLines) != len(want) {
|
|
t.Fatalf("UseSSA=%v: got %d lines want %d\n%s", useSSA, len(gotLines), len(want), out)
|
|
}
|
|
for i := range want {
|
|
if strings.TrimSpace(gotLines[i]) != want[i] {
|
|
t.Errorf("UseSSA=%v: line %d got %q want %q", useSSA, i, gotLines[i], want[i])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestGlobalsRuntime drives cg_globals — globals of every value type,
|
|
// mutable and immutable — and checks get/set/bump results against wazero
|
|
// across both pipelines. Float globals are compared by bit pattern.
|
|
func TestGlobalsRuntime(t *testing.T) {
|
|
bin := testfixture.Wasm(t, "cg_globals")
|
|
mod, err := wasm.Parse(bytes.NewReader(bin))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx := context.Background()
|
|
r := wazero.NewRuntime(ctx)
|
|
t.Cleanup(func() {
|
|
if err := r.Close(ctx); err != nil {
|
|
t.Errorf("wazero runtime close: %v", err)
|
|
}
|
|
})
|
|
wmod, err := r.Instantiate(ctx, bin)
|
|
if err != nil {
|
|
t.Fatalf("wazero instantiate: %v", err)
|
|
}
|
|
|
|
// Each call is self-contained per fresh module instance; integer
|
|
// getters/bumpers and the const getter give deterministic values.
|
|
type step struct {
|
|
export string
|
|
args []uint64
|
|
argTypes []wasm.ValType
|
|
resType wasm.ValType
|
|
}
|
|
i32, i64 := wasm.ValI32, wasm.ValI64
|
|
steps := []step{
|
|
{"get_i32", nil, nil, i32},
|
|
{"get_i64", nil, nil, i64},
|
|
{"get_const", nil, nil, i32},
|
|
{"bump_i32", []uint64{8}, []wasm.ValType{i32}, i32},
|
|
{"set_i64", []uint64{777}, []wasm.ValType{i64}, 0},
|
|
}
|
|
var mainSB strings.Builder
|
|
mainSB.WriteString("package main\n\nimport (\n\t\"fmt\"\n\t\"gentest/pkg\"\n)\n\nfunc main() {\n\tm := pkg.New()\n")
|
|
var want []string
|
|
for _, s := range steps {
|
|
res, err := wmod.ExportedFunction(s.export).Call(ctx, s.args...)
|
|
if err != nil {
|
|
t.Fatalf("wazero %s: %v", s.export, err)
|
|
}
|
|
method := codegen.ExportMethodName(s.export)
|
|
var argList []string
|
|
for i, a := range s.args {
|
|
if s.argTypes[i] == wasm.ValI64 {
|
|
argList = append(argList, fmt.Sprintf("int64(%d)", int64(a)))
|
|
} else {
|
|
argList = append(argList, fmt.Sprintf("int32(%d)", int32(a)))
|
|
}
|
|
}
|
|
argStr := strings.Join(argList, ", ")
|
|
switch s.resType {
|
|
case wasm.ValI64:
|
|
want = append(want, fmt.Sprintf("%s=%d", s.export, int64(res[0])))
|
|
fmt.Fprintf(&mainSB, "\tfmt.Printf(\"%s=%%d\\n\", m.%s(%s))\n", s.export, method, argStr)
|
|
case 0:
|
|
want = append(want, fmt.Sprintf("%s=void", s.export))
|
|
fmt.Fprintf(&mainSB, "\tm.%s(%s)\n\tfmt.Printf(\"%s=void\\n\")\n", method, argStr, s.export)
|
|
default:
|
|
want = append(want, fmt.Sprintf("%s=%d", s.export, int32(res[0])))
|
|
fmt.Fprintf(&mainSB, "\tfmt.Printf(\"%s=%%d\\n\", m.%s(%s))\n", s.export, method, argStr)
|
|
}
|
|
}
|
|
mainSB.WriteString("}\n")
|
|
|
|
for _, useSSA := range []bool{false, true} {
|
|
var buf bytes.Buffer
|
|
res, err := codegen.Translate(&buf, mod, codegen.Options{Package: "pkg", OutputImportPath: "gentest/pkg"})
|
|
if err != nil {
|
|
t.Fatalf("translate (UseSSA=%v): %v", useSSA, err)
|
|
}
|
|
out := strings.TrimSpace(runGoSnippet(t, buf.String(), mainSB.String(), res.Sidecars, filesWithAux(res)))
|
|
gotLines := strings.Split(out, "\n")
|
|
if len(gotLines) != len(want) {
|
|
t.Fatalf("UseSSA=%v: got %d lines want %d\n%s", useSSA, len(gotLines), len(want), out)
|
|
}
|
|
for i := range want {
|
|
if strings.TrimSpace(gotLines[i]) != want[i] {
|
|
t.Errorf("UseSSA=%v: line %d got %q want %q", useSSA, i, gotLines[i], want[i])
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestEntryExports exercises the EntryExports DCE root-set knob:
|
|
// narrowing the export root set to a single export must still produce
|
|
// a module that compiles, and the empty-slice form (no export is a
|
|
// root) must also be accepted.
|
|
func TestEntryExports(t *testing.T) {
|
|
mod := readFixture(t, "arith.wasm")
|
|
t.Run("single", func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
res, err := codegen.Translate(&buf, mod, codegen.Options{
|
|
Package: "pkg", OutputImportPath: "gentest/pkg",
|
|
EntryExports: []string{"add"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("translate: %v", err)
|
|
}
|
|
assertSingleFileCompiles(t, buf.String(), nil, res.Files)
|
|
})
|
|
t.Run("none", func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
res, err := codegen.Translate(&buf, mod, codegen.Options{
|
|
Package: "pkg", OutputImportPath: "gentest/pkg",
|
|
EntryExports: []string{},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("translate: %v", err)
|
|
}
|
|
assertSingleFileCompiles(t, buf.String(), nil, res.Files)
|
|
})
|
|
}
|
|
|
|
// TestNaNGlobalInit verifies that float globals initialised to NaN/Inf
|
|
// compile and run correctly — the global-init emitter routes such values
|
|
// through math.Float*frombits (Go has no NaN/Inf literal).
|
|
func TestNaNGlobalInit(t *testing.T) {
|
|
bin := testfixture.Wasm(t, "cg_globals_nan")
|
|
mod, err := wasm.Parse(bytes.NewReader(bin))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var buf bytes.Buffer
|
|
res, err := codegen.Translate(&buf, mod, codegen.Options{Package: "pkg", OutputImportPath: "gentest/pkg"})
|
|
if err != nil {
|
|
t.Fatalf("translate: %v", err)
|
|
}
|
|
main := `package main
|
|
|
|
import (
|
|
"fmt"
|
|
"gentest/pkg"
|
|
)
|
|
|
|
func main() {
|
|
m := pkg.New()
|
|
fmt.Printf("is_nan=%d\n", m.IsNan())
|
|
fmt.Printf("inf_gt=%d\n", m.InfGt())
|
|
}
|
|
`
|
|
out := strings.TrimSpace(runGoSnippet(t, buf.String(), main, res.Sidecars, filesWithAux(res)))
|
|
if !strings.Contains(out, "is_nan=1") {
|
|
t.Errorf("NaN global not initialised to NaN: %q", out)
|
|
}
|
|
if !strings.Contains(out, "inf_gt=1") {
|
|
t.Errorf("Inf global not initialised to +Inf: %q", out)
|
|
}
|
|
}
|
|
|
|
// TestMiscOpsRuntime drives cg_misc — nop/drop/typed-select, value-producing
|
|
// blocks and if/else, loops carrying a result, and dead code after return —
|
|
// checking every result against wazero across both pipelines.
|
|
func TestMiscOpsRuntime(t *testing.T) {
|
|
i32, i64 := wasm.ValI32, wasm.ValI64
|
|
runExportMatrix(t, "cg_misc.wasm", []call{
|
|
{export: "with_nop", args: []uint64{42}, argTypes: []wasm.ValType{i32}, resType: i32},
|
|
{export: "with_drop", args: []uint64{7, 9}, argTypes: []wasm.ValType{i32, i32}, resType: i32},
|
|
{export: "block_result", args: []uint64{10}, argTypes: []wasm.ValType{i32}, resType: i32},
|
|
{export: "if_value", args: []uint64{1}, argTypes: []wasm.ValType{i32}, resType: i32},
|
|
{export: "if_value", args: []uint64{0}, argTypes: []wasm.ValType{i32}, resType: i32},
|
|
{export: "loop_sum", args: []uint64{10}, argTypes: []wasm.ValType{i32}, resType: i32},
|
|
{export: "loop_sum", args: []uint64{0}, argTypes: []wasm.ValType{i32}, resType: i32},
|
|
{export: "typed_select", args: []uint64{111, 222, 1}, argTypes: []wasm.ValType{i64, i64, i32}, resType: i64},
|
|
{export: "typed_select", args: []uint64{111, 222, 0}, argTypes: []wasm.ValType{i64, i64, i32}, resType: i64},
|
|
{export: "dead_after_return", args: []uint64{55}, argTypes: []wasm.ValType{i32}, resType: i32},
|
|
{export: "early_return", args: []uint64{1}, argTypes: []wasm.ValType{i32}, resType: i32},
|
|
{export: "early_return", args: []uint64{0}, argTypes: []wasm.ValType{i32}, resType: i32},
|
|
{export: "nested_br", args: []uint64{1}, argTypes: []wasm.ValType{i32}, resType: i32},
|
|
{export: "nested_br", args: []uint64{0}, argTypes: []wasm.ValType{i32}, resType: i32},
|
|
})
|
|
}
|
|
|
|
// TestDeadEndRuntime drives cg_deadend — functions whose every path exits
|
|
// via return/br so the implicit function-end is unreachable. The SSA
|
|
// lowering must synthesise a well-formed dead-block terminator; results
|
|
// must still match wazero.
|
|
func TestDeadEndRuntime(t *testing.T) {
|
|
i32 := wasm.ValI32
|
|
runExportMatrix(t, "cg_deadend.wasm", []call{
|
|
{export: "both_return", args: []uint64{1}, argTypes: []wasm.ValType{i32}, resType: i32},
|
|
{export: "both_return", args: []uint64{0}, argTypes: []wasm.ValType{i32}, resType: i32},
|
|
{export: "br_root", args: []uint64{1}, argTypes: []wasm.ValType{i32}, resType: i32},
|
|
{export: "br_root", args: []uint64{0}, argTypes: []wasm.ValType{i32}, resType: i32},
|
|
{export: "loop_return", args: []uint64{5}, argTypes: []wasm.ValType{i32}, resType: i32},
|
|
{export: "loop_return", args: []uint64{0}, argTypes: []wasm.ValType{i32}, resType: i32},
|
|
})
|
|
}
|
|
|
|
// TestBitTestBranchRuntime drives cg_bittest — branches whose
|
|
// condition is `(x & 1<<bit) != 0` (i32 / i64 variants and an
|
|
// inverted-via-eqz shape). The asm emit's bit-test fusion
|
|
// short-circuits this pattern to BTL+JCC on amd64 and TBZ/TBNZ on
|
|
// arm64; the test exercises the fused branch in both arch outputs
|
|
// via runExportMatrix (wazero as the oracle).
|
|
func TestBitTestBranchRuntime(t *testing.T) {
|
|
i32 := wasm.ValI32
|
|
i64 := wasm.ValI64
|
|
runExportMatrix(t, "cg_bittest.wasm", []call{
|
|
{export: "bit0_set", args: []uint64{1}, argTypes: []wasm.ValType{i32}, resType: i32},
|
|
{export: "bit0_set", args: []uint64{2}, argTypes: []wasm.ValType{i32}, resType: i32},
|
|
{export: "bit3_clear", args: []uint64{8}, argTypes: []wasm.ValType{i32}, resType: i32},
|
|
{export: "bit3_clear", args: []uint64{4}, argTypes: []wasm.ValType{i32}, resType: i32},
|
|
{export: "bit32_set_i64", args: []uint64{1 << 32}, argTypes: []wasm.ValType{i64}, resType: i32},
|
|
{export: "bit32_set_i64", args: []uint64{0xFFFF}, argTypes: []wasm.ValType{i64}, resType: i32},
|
|
})
|
|
}
|
|
|
|
// TestLargeMemoryOffsetRuntime drives cg_largeoff — store/load
|
|
// pairs whose `offset=` immediate exceeds the codegen's
|
|
// largeConstThreshold (4096). The pure-Go fallback routes the
|
|
// constant through a `_consts[N]` table on every such site, and
|
|
// this test guarantees a fixture exists that traverses the table
|
|
// emit + lookup paths end-to-end (constsIndexExpr / useLargeConst
|
|
// / emitLargeConstsDecl). The runtime assertion is the same
|
|
// wazero-reference comparison runExportMatrix performs on every
|
|
// other fixture.
|
|
func TestLargeMemoryOffsetRuntime(t *testing.T) {
|
|
i32 := wasm.ValI32
|
|
runExportMatrix(t, "cg_largeoff.wasm", []call{
|
|
{export: "write_at_8192", args: []uint64{42}, argTypes: []wasm.ValType{i32}, resType: 0},
|
|
{export: "read_at_8192", args: nil, argTypes: nil, resType: i32},
|
|
})
|
|
}
|
|
|
|
// TestReachableUnreachable drives cg_unreachable — a function whose
|
|
// `unreachable` opcode is reachable from the entry under a specific
|
|
// input (x == 0). Calling the export with x != 0 takes the non-
|
|
// trapping path and must return 42; the asm emit's BlockUnreachable
|
|
// branch still has to materialise (even though it's not executed at
|
|
// runtime) so the resulting binary links cleanly. wazero is the
|
|
// reference; the comparison matrix in runExportMatrix asserts that
|
|
// the generated code agrees with it.
|
|
func TestReachableUnreachable(t *testing.T) {
|
|
i32 := wasm.ValI32
|
|
runExportMatrix(t, "cg_unreachable.wasm", []call{
|
|
{export: "trap_if_zero", args: []uint64{1}, argTypes: []wasm.ValType{i32}, resType: i32},
|
|
{export: "trap_if_zero", args: []uint64{42}, argTypes: []wasm.ValType{i32}, resType: i32},
|
|
{export: "always_42", args: nil, argTypes: nil, resType: i32},
|
|
})
|
|
}
|