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

95 lines
3.0 KiB
Go

package codegen
import (
"github.com/goccy/wasm2go/internal/wasm"
)
// computeReachableFuncs returns the set of DEFINED functions — keyed by
// local (defined-relative) index — reachable from the module's entry
// points through the direct-call graph.
//
// A wasm module compiled from a large C++ program carries thousands of
// functions that the program can never actually invoke. The Go linker
// cannot drop them because the generated InvokeExport dispatch
// references every export and the element segment takes the address of
// every table entry. Computing reachability here, at transpile time,
// lets wasm2go simply not emit the dead functions.
//
// Roots:
// - the start function;
// - every function in an element segment — call_indirect can target
// any table slot, so all table entries are conservatively live;
// - the function exports. When entryExports is nil EVERY export is a
// root, so only functions dead in the wasm itself are dropped.
// A non-nil entryExports restricts the export roots to the named
// subset (used by whole-program pruning when the caller knows the
// real entry set).
//
// The returned map is keyed by local index: function localIdx is kept
// iff result[localIdx] is true.
//
// callees, when non-nil, must be the precomputed call-graph adjacency
// list (one entry per defined function, with each entry listing the
// local indices it directly calls). Passing nil falls back to running
// buildCallGraph here — both PlanMultiPackage and reachability use
// the same graph, so the caller pre-computes it once and threads the
// result through to avoid scanning the bytecode three times.
func computeReachableFuncs(mod *wasm.Module, entryExports map[string]bool, callees [][]uint32) (map[uint32]bool, error) {
if callees == nil {
var err error
callees, err = buildCallGraph(mod) // callees[localIdx] = []localIdx
if err != nil {
return nil, err
}
}
nImp := mod.NumImportedFuncs
nDef := uint32(len(mod.Functions))
reachable := make(map[uint32]bool, nDef)
var work []uint32
addLocal := func(d uint32) {
if d >= nDef || reachable[d] {
return
}
reachable[d] = true
work = append(work, d)
}
addGlobal := func(fi uint32) {
if fi < nImp { // imported function — no body to emit or walk
return
}
addLocal(fi - nImp)
}
if mod.Start != nil {
addGlobal(*mod.Start)
}
for _, seg := range mod.Elements {
for _, fi := range seg.FuncIdxs {
addGlobal(fi)
}
}
for _, e := range mod.Exports {
if e.Kind != wasm.ExportFunc {
continue
}
// When entryExports is non-nil it lists the exact set of export
// names to keep as reachability roots. Any export not in the
// set is dropped from the root set (its function can still
// become reachable through the table or a direct call).
if entryExports != nil && !entryExports[e.Name] {
continue
}
addGlobal(e.Index)
}
for len(work) > 0 {
d := work[len(work)-1]
work = work[:len(work)-1]
for _, c := range callees[d] {
addLocal(c)
}
}
return reachable, nil
}