mirror of
https://github.com/hanzoai/wasm2go.git
synced 2026-10-30 15:06:48 +00:00
* 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]>
120 lines
2.9 KiB
Go
120 lines
2.9 KiB
Go
package pass
|
|
|
|
import "github.com/goccy/wasm2go/internal/ssa"
|
|
|
|
// BranchFold rewrites every BlockIf whose condition is a known constant
|
|
// into a BlockPlain that jumps unconditionally to the taken successor.
|
|
//
|
|
// The not-taken edge is deleted — including the phi argument it
|
|
// contributed to the successor — and any block left unreachable from the
|
|
// entry is dropped. ConstProp must run first so a constant condition is
|
|
// actually an OpConst* value; running BranchFold in the same fixpoint
|
|
// loop lets a fold expose a constant branch that the next pass folds
|
|
// again.
|
|
//
|
|
// A malformed rewrite is not a correctness hazard: the caller re-runs
|
|
// ssa.Verify after the pipeline and falls back to the legacy compiler on
|
|
// any violation.
|
|
func BranchFold(f *ssa.Func) bool {
|
|
changed := false
|
|
for _, b := range f.Blocks {
|
|
if b.Kind != ssa.BlockIf || b.Control == nil || len(b.Succs) != 2 {
|
|
continue
|
|
}
|
|
c := b.Control
|
|
if c.Op != ssa.OpConst32 && c.Op != ssa.OpConst64 {
|
|
continue
|
|
}
|
|
// BlockIf: Succs[0] = then, Succs[1] = else; a non-zero
|
|
// condition takes the then-edge.
|
|
notTaken := 1
|
|
if c.AuxInt == 0 {
|
|
notTaken = 0
|
|
}
|
|
removeEdge(b, b.Succs[notTaken].Block)
|
|
b.Kind = ssa.BlockPlain
|
|
b.Control = nil
|
|
changed = true
|
|
}
|
|
if changed {
|
|
removeUnreachable(f)
|
|
}
|
|
return changed
|
|
}
|
|
|
|
// removeEdge deletes the first src→dst CFG edge. It drops the phi
|
|
// argument dst held for that predecessor, removes the pred/succ entries
|
|
// on both blocks, and repairs the Edge.Index back-references that the
|
|
// removal shifted.
|
|
func removeEdge(src, dst *ssa.Block) {
|
|
p := -1
|
|
for i := range dst.Preds {
|
|
if dst.Preds[i].Block == src {
|
|
p = i
|
|
break
|
|
}
|
|
}
|
|
if p < 0 {
|
|
return
|
|
}
|
|
for _, v := range dst.Values {
|
|
if v.Op == ssa.OpPhi && p < len(v.Args) {
|
|
v.Args = append(v.Args[:p], v.Args[p+1:]...)
|
|
}
|
|
}
|
|
dst.Preds = append(dst.Preds[:p], dst.Preds[p+1:]...)
|
|
for i := p; i < len(dst.Preds); i++ {
|
|
e := dst.Preds[i]
|
|
e.Block.Succs[e.Index].Index = i
|
|
}
|
|
|
|
s := -1
|
|
for i := range src.Succs {
|
|
if src.Succs[i].Block == dst {
|
|
s = i
|
|
break
|
|
}
|
|
}
|
|
if s < 0 {
|
|
return
|
|
}
|
|
src.Succs = append(src.Succs[:s], src.Succs[s+1:]...)
|
|
for i := s; i < len(src.Succs); i++ {
|
|
e := src.Succs[i]
|
|
e.Block.Preds[e.Index].Index = i
|
|
}
|
|
}
|
|
|
|
// removeUnreachable drops every block not reachable from f.Entry,
|
|
// detaching their outgoing edges first so phi nodes in surviving
|
|
// successors stay consistent.
|
|
func removeUnreachable(f *ssa.Func) {
|
|
reach := map[*ssa.Block]bool{f.Entry: true}
|
|
work := []*ssa.Block{f.Entry}
|
|
for len(work) > 0 {
|
|
b := work[len(work)-1]
|
|
work = work[:len(work)-1]
|
|
for _, e := range b.Succs {
|
|
if !reach[e.Block] {
|
|
reach[e.Block] = true
|
|
work = append(work, e.Block)
|
|
}
|
|
}
|
|
}
|
|
for _, b := range f.Blocks {
|
|
if reach[b] {
|
|
continue
|
|
}
|
|
for len(b.Succs) > 0 {
|
|
removeEdge(b, b.Succs[0].Block)
|
|
}
|
|
}
|
|
kept := f.Blocks[:0]
|
|
for _, b := range f.Blocks {
|
|
if reach[b] {
|
|
kept = append(kept, b)
|
|
}
|
|
}
|
|
f.Blocks = kept
|
|
}
|