Files
bd85549918 wasm2go: a WebAssembly-to-Go ahead-of-time transpiler (#1)
* wasm2go: a WebAssembly-to-Go ahead-of-time transpiler

wasm2go translates a WebAssembly binary into standalone Go source. The
generated package runs the module natively — no interpreter and no
embedded wasm runtime — so it builds and links like ordinary Go code.
This makes it practical to ship a wasm-compiled library (for example a
large C/C++ codebase built with the WASI SDK) inside a Go program
without the startup and memory cost of a wasm engine.

Components:

- internal/wasm — the WebAssembly binary parser.
- internal/codegen — translates a parsed module into Go source through
  the SSA lowering + optimization + emission pipeline.
- internal/ssa — a lightweight SSA IR plus optimization passes
  (const-propagation, DCE, CSE, branch folding, memory promotion).
- transpile — the public library API: Parse, Translate, and the
  Transpile one-shot convenience wrapper.
- cmd/wasm2go — the command-line interface.
- internal/testfixture — shared helper that compiles testdata/*.wat
  to wasm on demand via wat2wasm.

Library API (Options):

- Required: Package, OutputImportPath.
- Optional: BulkExportPrefix (per-export Inv_<svc>_<mt> dispatch for
  modules with uniformly-named exports), EntryExports (narrow DCE
  root set), KeepDeadFuncs (disable DCE for diff/debug),
  PromotionReportPath (write SSA memory-promotion JSON).

Everything else is auto-derived from the input: the SSA pipeline runs
unconditionally; data segments emit as //go:embed sidecars; wasi
imports translate to Go-native syscalls; modules whose total
function-body bytes exceed an internal threshold are emitted as a
chain of //go:linkname-split sub-packages so the Go compiler stays
inside a bounded per-package memory budget.

WASI runtime: full wasi_snapshot_preview1 coverage. Args/environ,
clocks, fd_*, path_* (open/create_directory/unlink_file/
remove_directory/rename/filestat_get/filestat_set_times/link/
symlink/readlink), readdir, pread/pwrite, sync/datasync, poll_oneoff
(clock + fd-event branches), sched_yield, random_get, proc_exit,
proc_raise, sock_accept/recv/send/shutdown — every call routes to the
Go stdlib or golang.org/x/sys/unix equivalent. Per-platform companion
files (unix/linux/darwin/windows) carry the bits that differ across
OSes; the translated module is a normal Go program with the host's
ordinary filesystem and network reach.

Release engineering for the public repository:

- GitHub Actions CI: build, vet, race-enabled tests behind an 85%
  coverage gate, golangci-lint — all driven through make targets.
- Release workflow runs GoReleaser on version tags, including
  deb/rpm/apk packages, with build-provenance attestations recorded
  for every release asset.
- A release-test workflow runs `make release/check` (GoReleaser
  config validation + a publish-free dry-run build) on every PR and
  main push so a broken release is caught before tag time.
- golangci-lint and goreleaser are pinned in tools/go.mod and run
  via `go tool`, so local and CI invocations use the same versions.
- `make install/wat2wasm` provisions the wat2wasm CLI the test suite
  needs, dispatching to the host's package manager (Homebrew on
  macOS, apt/dnf/pacman/apk on Linux).
- Makefile targets for local development.

Test coverage is 85.2% of statements overall.

* README: document the WASI runtime, performance, and library entry points

- Add a "WASI support" subsection explaining the single-file,
  stdlib-only `wasi_snapshot_preview1` implementation and the
  resulting cross-platform reach.
- Add a "Performance" section with the wazero-vs-wasm2go corpus
  numbers (cold start 8.4×, short queries up to 81×, TPC-H scans
  within ±10%).

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

* ci: align release-test with release.yml — use stable Go

The release-test workflow exercises the same `make release/check`
dry-run that the publish job runs at tag time, so it should pull
the same toolchain. Switch it from `go-version-file: tools/go.mod`
to `go-version: stable` to match release.yml.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-05-24 13:34:54 +09:00

100 lines
2.9 KiB
Go

package ssa
import "testing"
// buildFrameFunc constructs a single-block function with the standard
// stack-frame prologue (g0 - N), one frame store + one frame load, and
// an epilogue. escape, when true, additionally passes the frame
// pointer to a call so the escape analysis trips.
func buildFrameFunc(escape bool) *Func {
b := NewFuncBuilder("frametest", FuncSig{Params: []Type{TypeI32}, Results: []Type{TypeI32}})
entry := b.NewBlock(BlockPlain)
b.SetEntry(entry)
b.SetCurrent(entry)
p0 := b.Param(0, TypeI32)
g0 := b.NewValueAuxInt(OpGlobalGet, TypeI32, 0) // global.get 0
n := b.Const32(16) // frame size
fp := b.NewValue(OpSub32, TypeI32, g0, n) // fp = g0 - 16
b.NewValueAuxInt(OpGlobalSet, TypeMem, 0, fp) // global.set 0
b.NewValueAuxInt(OpStore32, TypeMem, 0, fp, p0) // store [fp+0] = p0
ld := b.NewValueAuxInt(OpLoad32, TypeI32, 0, fp) // load [fp+0]
if escape {
// call f(fp) — passing the frame pointer escapes the frame.
b.NewValueAux(OpCallDirect, TypeI32, nil, fp)
}
restore := b.NewValue(OpAdd32, TypeI32, fp, b.Const32(16))
b.NewValueAuxInt(OpGlobalSet, TypeMem, 0, restore) // global.set 0 (epilogue)
b.FinishRet(ld)
return b.Func()
}
func TestClassifyMemoryFrameNonEscaping(t *testing.T) {
f := buildFrameFunc(false)
mc := ClassifyMemory(f)
if mc.FramePtr == nil {
t.Fatal("frame pointer not detected")
}
if mc.FrameSize != 16 {
t.Errorf("FrameSize = %d, want 16", mc.FrameSize)
}
if mc.FrameEscapes {
t.Errorf("frame wrongly reported as escaping: %s", mc.EscapeReason)
}
frame := 0
for _, c := range mc.Class {
if c == AliasFrame {
frame++
}
}
if frame != 2 {
t.Errorf("expected 2 frame-classified accesses (1 store + 1 load), got %d", frame)
}
}
func TestClassifyMemoryFrameEscaping(t *testing.T) {
f := buildFrameFunc(true)
mc := ClassifyMemory(f)
if mc.FramePtr == nil {
t.Fatal("frame pointer not detected")
}
if !mc.FrameEscapes {
t.Fatal("frame should escape when fp is passed to a call")
}
// Escaping frame demotes every access back to slab.
for id, c := range mc.Class {
if c != AliasSlab {
t.Errorf("v%d classified %s, want slab (frame escaped)", id, c)
}
}
}
func TestMemMetricsAggregate(t *testing.T) {
m := NewMemMetrics()
f1 := buildFrameFunc(false) // 2 frame accesses
f1.Name = "kept"
f2 := buildFrameFunc(true) // 2 slab accesses (escaped)
f2.Name = "escaped"
m.Add(f1, ClassifyMemory(f1))
m.Add(f2, ClassifyMemory(f2))
if m.Total != 4 {
t.Errorf("Total = %d, want 4", m.Total)
}
if m.Frame != 2 {
t.Errorf("Frame = %d, want 2", m.Frame)
}
if m.Slab != 2 {
t.Errorf("Slab = %d, want 2", m.Slab)
}
if m.FuncsWithFrame != 2 || m.FuncsFrameKept != 1 {
t.Errorf("FuncsWithFrame=%d FuncsFrameKept=%d, want 2/1", m.FuncsWithFrame, m.FuncsFrameKept)
}
if got := m.Rate(); got != 0.5 {
t.Errorf("Rate = %v, want 0.5", got)
}
if _, err := m.JSON(); err != nil {
t.Errorf("JSON: %v", err)
}
}