Files
c71ddafe1b Back shared linear memories with anonymous mappings, not the Go heap (#56)
* 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]>
2026-08-27 10:53:10 +09:00

352 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"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/experimental"
)
// u32 packs a signed int32 into the low bits of a uint64 (wazero call ABI).
func u32(v int32) uint64 { return uint64(uint32(v)) }
type call struct {
export string
args []uint64
// Optional: which Go method receives this call. If empty, derived from
// ExportMethodName(export). The Go method's args are the raw uint64 args
// converted to int32/int64 based on argTypes.
argTypes []wasm.ValType
resType wasm.ValType
}
type fixture struct {
name string
calls []call
// threads enables wazero's experimental threads feature for the
// reference run (shared memories + atomics).
threads bool
}
var fixtures = []fixture{
{
name: "memory.wasm",
calls: []call{
// data segment writes "hi\0" at offset 16.
{export: "load_byte", args: []uint64{16}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI32}, // 'h' = 104
{export: "load_byte", args: []uint64{17}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI32}, // 'i' = 105
{export: "store_byte", args: []uint64{32, 0x42}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: 0},
{export: "load_byte", args: []uint64{32}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI32},
{export: "store_i32", args: []uint64{40, 0xdeadbeef}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: 0},
{export: "load_i32", args: []uint64{40}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI32},
{export: "sum_bytes", args: []uint64{16, 2}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32}, // 'h'+'i' = 209
{export: "dispatch", args: []uint64{0, 7}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32}, // double(7)=14
{export: "dispatch", args: []uint64{1, 7}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32}, // square(7)=49
{export: "dispatch", args: []uint64{2, 7}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32}, // triple(7)=21
},
},
{
name: "arith.wasm",
calls: []call{
{export: "add", args: []uint64{7, 35}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
{export: "add", args: []uint64{u32(-100), u32(50)}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
{export: "sub", args: []uint64{1000, 1}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
{export: "mul64", args: []uint64{12345, 67890}, argTypes: []wasm.ValType{wasm.ValI64, wasm.ValI64}, resType: wasm.ValI64},
{export: "div_s", args: []uint64{100, 7}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
{export: "div_s", args: []uint64{u32(-100), 7}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
{export: "shifts", args: []uint64{0xff, 4}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
{export: "rotl", args: []uint64{0x80000001, 1}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
{export: "lt_s", args: []uint64{u32(-1), 1}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
{export: "lt_u", args: []uint64{u32(-1), 1}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
},
},
{
name: "control.wasm",
calls: []call{
{export: "fact", args: []uint64{0}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI32},
{export: "fact", args: []uint64{1}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI32},
{export: "fact", args: []uint64{5}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI32},
{export: "fact", args: []uint64{10}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI32},
{export: "gcd", args: []uint64{48, 18}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
{export: "gcd", args: []uint64{17, 13}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
{export: "max", args: []uint64{10, 20}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
{export: "max", args: []uint64{30, 7}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
{export: "max", args: []uint64{u32(-5), u32(-3)}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
{export: "switch3", args: []uint64{0}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI32},
{export: "switch3", args: []uint64{1}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI32},
{export: "switch3", args: []uint64{2}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI32},
{export: "switch3", args: []uint64{3}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI32},
{export: "switch3", args: []uint64{99}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI32},
},
},
{
// Mirrors a C++ static-initializer pattern: recursive descent
// through a tree where each record has {flag i8, ...,
// child_count @20, children_ptr @24}. Verifies struct-field load
// offsets, recursive call, looped children iteration, and
// side-effect counter writes survive wasm2go's codegen + flush +
// coalesce passes.
name: "recursive_init.wasm",
calls: []call{
{export: "run", args: nil, argTypes: nil, resType: wasm.ValI32},
{export: "flag_of", args: []uint64{32}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI32},
{export: "flag_of", args: []uint64{64}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI32},
{export: "flag_of", args: []uint64{96}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI32},
},
},
{
// Mirrors C++ virtual-method dispatch via call_indirect through a
// vtable stored in linear memory. Verifies that table population
// from element segments + per-object vtable lookup + indirect call
// all chain correctly.
name: "vtable_dispatch.wasm",
calls: []call{
{export: "call_method", args: []uint64{32, 0}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
{export: "call_method", args: []uint64{32, 1}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
{export: "call_method", args: []uint64{48, 0}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
{export: "call_method", args: []uint64{48, 1}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
},
},
{
// Negative zero flowing through non-constant shapes (locals, block
// results, phis, select, data segments, arithmetic).
name: "cg_negzero_flows.wasm",
calls: []call{
{export: "via_local", resType: wasm.ValI64},
{export: "via_block", resType: wasm.ValI64},
{export: "via_phi", args: []uint64{1}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI64},
{export: "via_select", args: []uint64{1}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI64},
{export: "via_copysign_cc", resType: wasm.ValI64},
{export: "via_copysign_cv", resType: wasm.ValI64},
{export: "via_copysign_vc", resType: wasm.ValI64},
{export: "via_neg_zero", resType: wasm.ValI64},
{export: "via_load", resType: wasm.ValI64},
{export: "via_add", resType: wasm.ValI64},
{export: "via_mul_neg1", resType: wasm.ValI64},
},
},
{
// Passive data segments (the shape LLVM emits for every shared-memory
// build): a start function memory.inits them, data.drop discards, a
// dropped segment traps on re-init but tolerates n == 0.
name: "cg_passive_data.wasm",
threads: true,
calls: []call{
{export: "read_at", args: []uint64{16}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI32},
{export: "read_at", args: []uint64{21}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI32},
{export: "read_at", args: []uint64{27}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI32},
{export: "reinit_zero_len"},
},
},
{
// Threads-proposal atomics in a single agent: loads/stores, every
// RMW family incl. subword lanes, cmpxchg, fence, wait/notify.
name: "cg_atomics.wasm",
threads: true,
calls: []call{
{export: "rmw32", resType: wasm.ValI32},
{export: "rmw64", resType: wasm.ValI64},
{export: "xchg", resType: wasm.ValI32},
{export: "cmpxchg", resType: wasm.ValI32},
{export: "subword", resType: wasm.ValI32},
{export: "subword_cmpxchg", resType: wasm.ValI32},
{export: "wait_notify", resType: wasm.ValI32},
{export: "fenced_load", resType: wasm.ValI32},
{export: "store_neg", resType: wasm.ValI32},
},
},
{
// br_table selecting on a comparison result (SSA type bool): must
// verify and route 0/1 correctly. Mirrors SpiderMonkey's Intl build.
name: "cg_brtable_bool.wasm",
calls: []call{
{export: "pick", args: []uint64{3}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI32},
{export: "pick", args: []uint64{5}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI32},
{export: "pick", args: []uint64{9}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI32},
},
},
{
// Negative-zero float constants: Go has no -0.0 literal, so every
// constant-emission path must go through math.Float*frombits for it.
name: "cg_negzero.wasm",
calls: []call{
{export: "f64_negzero_bits", resType: wasm.ValI64},
{export: "f32_negzero_bits", resType: wasm.ValI32},
{export: "f64_negzero_call_bits", resType: wasm.ValI64},
{export: "f32_negzero_call_bits", resType: wasm.ValI32},
{export: "global_f64_negzero_bits", resType: wasm.ValI64},
{export: "global_f32_negzero_bits", resType: wasm.ValI32},
{export: "f64_negzero_mul1_bits", resType: wasm.ValI64},
{export: "f64_min_negzero_bits", resType: wasm.ValI64},
{export: "f64_nan_payload_bits", resType: wasm.ValI64},
},
},
{
// SIMD coverage: every export folds one v128 op family to a scalar
// and is diffed against wazero (CoreFeaturesV2 includes SIMD).
name: "cg_simd.wasm",
calls: []call{
{export: "sat8", args: []uint64{100, 100}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
{export: "sat8", args: []uint64{u32(-100), 60}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
{export: "sat16", args: []uint64{30000, 30000}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
{export: "sat16", args: []uint64{u32(-30000), u32(-30000)}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
{export: "cmpmask", args: []uint64{u32(-5), 3}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
{export: "cmpmask", args: []uint64{7, 7}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
{export: "intarith", args: []uint64{u32(-123456), 789}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
{export: "intarith", args: []uint64{0x80000000, 0xffffffff}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
{export: "widen", args: []uint64{u32(-32768), 32767}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
{export: "widen", args: []uint64{257, u32(-2)}, argTypes: []wasm.ValType{wasm.ValI32, wasm.ValI32}, resType: wasm.ValI32},
{export: "i64ops", args: []uint64{uint64(0x8000000000000000), 999999999999}, argTypes: []wasm.ValType{wasm.ValI64, wasm.ValI64}, resType: wasm.ValI64},
{export: "i64ops", args: []uint64{42, u32(-1)}, argTypes: []wasm.ValType{wasm.ValI64, wasm.ValI64}, resType: wasm.ValI64},
{export: "f32ops", args: []uint64{0x40490fdb /* pi */, 0xc02df854 /* -e */}, argTypes: []wasm.ValType{wasm.ValF32, wasm.ValF32}, resType: wasm.ValI32},
{export: "f32ops", args: []uint64{0x3f000000 /* 0.5 */, 0x80000000 /* -0 */}, argTypes: []wasm.ValType{wasm.ValF32, wasm.ValF32}, resType: wasm.ValI32},
{export: "f64ops", args: []uint64{0x400921fb54442d18 /* pi */, 0xc005bf0a8b145769 /* -e */}, argTypes: []wasm.ValType{wasm.ValF64, wasm.ValF64}, resType: wasm.ValI64},
{export: "fcmp", args: []uint64{0x7fc00000 /* NaN */, 0x3f800000 /* 1.0 */}, argTypes: []wasm.ValType{wasm.ValF32, wasm.ValF32}, resType: wasm.ValI32},
{export: "fcmp", args: []uint64{0x80000000 /* -0 */, 0x00000000 /* +0 */}, argTypes: []wasm.ValType{wasm.ValF32, wasm.ValF32}, resType: wasm.ValI32},
{export: "fcmp", args: []uint64{0x3f800000, 0x40000000}, argTypes: []wasm.ValType{wasm.ValF32, wasm.ValF32}, resType: wasm.ValI32},
{export: "conv", args: []uint64{0x4f800000 /* 2^32 */}, argTypes: []wasm.ValType{wasm.ValF32}, resType: wasm.ValI32},
{export: "conv", args: []uint64{0xcf000000 /* -2^31 */}, argTypes: []wasm.ValType{wasm.ValF32}, resType: wasm.ValI32},
{export: "conv", args: []uint64{0x7fc00000 /* NaN */}, argTypes: []wasm.ValType{wasm.ValF32}, resType: wasm.ValI32},
{export: "shuf", args: []uint64{0x55}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI32},
{export: "shuf", args: []uint64{u32(-1)}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI32},
{export: "memv", args: []uint64{0}, argTypes: []wasm.ValType{wasm.ValI32}, resType: wasm.ValI32},
},
},
}
func TestFixtures(t *testing.T) {
for _, fx := range fixtures {
fx := fx
t.Run(fx.name, func(t *testing.T) {
runFixture(t, fx)
})
}
}
func runFixture(t *testing.T, fx fixture) {
t.Helper()
bin := testfixture.Wasm(t, fx.name)
// Reference: wazero.
ctx := context.Background()
cfg := wazero.NewRuntimeConfig()
if fx.threads {
cfg = cfg.WithCoreFeatures(api.CoreFeaturesV2 | experimental.CoreFeaturesThreads)
}
r := wazero.NewRuntimeWithConfig(ctx, cfg)
t.Cleanup(func() {
if err := r.Close(ctx); err != nil {
t.Errorf("wazero runtime close: %v", err)
}
})
mod, err := r.Instantiate(ctx, bin)
if err != nil {
t.Fatalf("wazero instantiate: %v", err)
}
wantLines := make([]string, 0, len(fx.calls))
for _, c := range fx.calls {
fn := mod.ExportedFunction(c.export)
if fn == nil {
t.Fatalf("missing export %q", c.export)
}
out, err := fn.Call(ctx, c.args...)
if err != nil {
t.Fatalf("wazero %s%v: %v", c.export, c.args, err)
}
var line string
if c.resType == 0 {
line = fmt.Sprintf("%s -> (void)", c.export)
} else {
switch c.resType {
case wasm.ValI32:
line = fmt.Sprintf("%s -> %d", c.export, int32(out[0]))
case wasm.ValI64:
line = fmt.Sprintf("%s -> %d", c.export, int64(out[0]))
default:
line = fmt.Sprintf("%s -> %d", c.export, out[0])
}
}
wantLines = append(wantLines, line)
}
want := strings.Join(wantLines, "\n") + "\n"
// Translate to Go.
parsed, err := wasm.Parse(bytes.NewReader(bin))
if err != nil {
t.Fatal(err)
}
var buf bytes.Buffer
res, err := codegen.Translate(&buf, parsed, codegen.Options{Package: "pkg", OutputImportPath: "gentest/pkg"})
if err != nil {
t.Fatalf("translate: %v\n", err)
}
// The SSA pipeline is always on now; the previous "regenerate with
// UseSSA=true" path is therefore the same as the first translation.
// Keep ssaBuf around so the comparison below still exercises a
// second, independent compilation of the module — useful as a
// regression guard against any non-deterministic emit path.
var ssaBuf bytes.Buffer
if _, err := codegen.Translate(&ssaBuf, parsed, codegen.Options{
Package: "pkg", OutputImportPath: "gentest/pkg",
}); err != nil {
t.Fatalf("translate (second pass): %v\n", err)
}
// Synthesize a main.go that calls each export and prints results.
var mainSB strings.Builder
mainSB.WriteString("package main\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"gentest/pkg\"\n)\n\nvar _ = math.Float32frombits\n\nfunc main() {\n")
mainSB.WriteString("\tm := pkg.New()\n")
for _, c := range fx.calls {
methodName := codegen.ExportMethodName(c.export)
var argList []string
for i, a := range c.args {
switch c.argTypes[i] {
case wasm.ValI32:
argList = append(argList, fmt.Sprintf("int32(%d)", int32(a)))
case wasm.ValI64:
argList = append(argList, fmt.Sprintf("int64(%d)", int64(a)))
case wasm.ValF32:
// Bit-exact: a decimal round-trip would corrupt NaNs and
// is needless for everything else.
argList = append(argList, fmt.Sprintf("math.Float32frombits(0x%x)", uint32(a)))
case wasm.ValF64:
argList = append(argList, fmt.Sprintf("math.Float64frombits(0x%x)", a))
default:
argList = append(argList, fmt.Sprintf("%d", a))
}
}
if c.resType == 0 {
fmt.Fprintf(&mainSB, "\tm.%s(%s)\n\tfmt.Printf(\"%s -> (void)\\n\")\n",
methodName, strings.Join(argList, ", "), c.export)
} else {
fmt.Fprintf(&mainSB, "\tfmt.Printf(\"%s -> %%d\\n\", m.%s(%s))\n",
c.export, methodName, strings.Join(argList, ", "))
}
}
mainSB.WriteString("}\n")
got := runGoSnippet(t, buf.String(), mainSB.String(), res.Sidecars, filesWithAux(res))
if got != want {
t.Errorf("output mismatch for %s\n--want--\n%s\n--got--\n%s\n--generated--\n%s", fx.name, want, got, buf.String())
}
// Run the SSA-mode regenerated source through the same test program
// to confirm the SSA + fallback path keeps semantics intact.
gotSSA := runGoSnippet(t, ssaBuf.String(), mainSB.String(), res.Sidecars, filesWithAux(res))
if gotSSA != want {
t.Errorf("UseSSA output mismatch for %s\n--want--\n%s\n--got--\n%s", fx.name, want, gotSSA)
}
}
// silence unused import if api is dropped.
var _ = api.ValueTypeI32