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

531 lines
17 KiB
Go

// Package pass — additional tests to raise statement coverage to ≥85%.
// Covers: uncovered foldValue ops, intConst OpConst64, argIs32 no-args,
// CSE ineligible values and nil-arg cseKey, Simplify edge cases,
// trivialPhiArg nil-arg, BranchFold no-change path, removeEdge no-match paths.
package pass
import (
"testing"
"github.com/goccy/wasm2go/internal/ssa"
)
// ---------------------------------------------------------------------------
// ConstProp: cover all remaining foldValue op branches
// ---------------------------------------------------------------------------
func buildConstFunc(op ssa.Op, typ ssa.Type, a, b int64) (*ssa.Func, *ssa.Value) {
var paramTyp ssa.Type
switch typ {
case ssa.TypeI64:
paramTyp = ssa.TypeI64
default:
paramTyp = ssa.TypeI32
}
sig := ssa.FuncSig{Results: []ssa.Type{paramTyp}}
bl := ssa.NewFuncBuilder("cptest", sig)
entry := bl.NewBlock(ssa.BlockRet)
bl.SetEntry(entry)
bl.SetCurrent(entry)
var va, vb *ssa.Value
if paramTyp == ssa.TypeI64 {
va = bl.NewValueAuxInt(ssa.OpConst64, ssa.TypeI64, a)
vb = bl.NewValueAuxInt(ssa.OpConst64, ssa.TypeI64, b)
} else {
va = bl.NewValueAuxInt(ssa.OpConst32, ssa.TypeI32, a)
vb = bl.NewValueAuxInt(ssa.OpConst32, ssa.TypeI32, b)
}
v := bl.NewValue(op, typ, va, vb)
bl.FinishRet(v)
return bl.Func(), v
}
func assertFoldedInt(t *testing.T, op ssa.Op, typ ssa.Type, a, b, want int64) {
t.Helper()
f, v := buildConstFunc(op, typ, a, b)
if !ConstProp(f) {
t.Errorf("%v: ConstProp returned false", op)
return
}
if v.AuxInt != want {
t.Errorf("%v: folded AuxInt = %d, want %d", op, v.AuxInt, want)
}
}
func assertFoldedBool(t *testing.T, op ssa.Op, typ ssa.Type, a, b int64, want bool) {
t.Helper()
f, v := buildConstFunc(op, typ, a, b)
if !ConstProp(f) {
t.Errorf("%v: ConstProp returned false", op)
return
}
var wantInt int64
if want {
wantInt = 1
}
if v.AuxInt != wantInt {
t.Errorf("%v(%d,%d): folded AuxInt = %d, want %d", op, a, b, v.AuxInt, wantInt)
}
}
func TestConstPropSub32(t *testing.T) { assertFoldedInt(t, ssa.OpSub32, ssa.TypeI32, 10, 3, 7) }
func TestConstPropSub64(t *testing.T) { assertFoldedInt(t, ssa.OpSub64, ssa.TypeI64, 20, 5, 15) }
func TestConstPropMul32(t *testing.T) { assertFoldedInt(t, ssa.OpMul32, ssa.TypeI32, 3, 4, 12) }
func TestConstPropMul64(t *testing.T) { assertFoldedInt(t, ssa.OpMul64, ssa.TypeI64, 5, 6, 30) }
func TestConstPropAnd32(t *testing.T) {
assertFoldedInt(t, ssa.OpAnd32, ssa.TypeI32, 0b1100, 0b1010, 0b1000)
}
func TestConstPropAnd64(t *testing.T) { assertFoldedInt(t, ssa.OpAnd64, ssa.TypeI64, 0xFF, 0x0F, 0x0F) }
func TestConstPropOr32(t *testing.T) {
assertFoldedInt(t, ssa.OpOr32, ssa.TypeI32, 0b1100, 0b0011, 0b1111)
}
func TestConstPropOr64(t *testing.T) { assertFoldedInt(t, ssa.OpOr64, ssa.TypeI64, 1, 2, 3) }
func TestConstPropXor32(t *testing.T) {
assertFoldedInt(t, ssa.OpXor32, ssa.TypeI32, 0b1111, 0b0101, 0b1010)
}
func TestConstPropXor64(t *testing.T) { assertFoldedInt(t, ssa.OpXor64, ssa.TypeI64, 0xF0, 0xFF, 0x0F) }
func TestConstPropShl32(t *testing.T) { assertFoldedInt(t, ssa.OpShl32, ssa.TypeI32, 1, 3, 8) }
func TestConstPropShl64(t *testing.T) { assertFoldedInt(t, ssa.OpShl64, ssa.TypeI64, 1, 4, 16) }
func TestConstPropShrS32(t *testing.T) { assertFoldedInt(t, ssa.OpShrS32, ssa.TypeI32, -8, 1, -4) }
func TestConstPropShrS64(t *testing.T) { assertFoldedInt(t, ssa.OpShrS64, ssa.TypeI64, -16, 1, -8) }
func TestConstPropShrU32(t *testing.T) { assertFoldedInt(t, ssa.OpShrU32, ssa.TypeI32, 8, 1, 4) }
func TestConstPropShrU64(t *testing.T) { assertFoldedInt(t, ssa.OpShrU64, ssa.TypeI64, 16, 1, 8) }
func TestConstPropAdd64(t *testing.T) { assertFoldedInt(t, ssa.OpAdd64, ssa.TypeI64, 100, 200, 300) }
func TestConstPropEq32True(t *testing.T) { assertFoldedBool(t, ssa.OpEq32, ssa.TypeBool, 5, 5, true) }
func TestConstPropEq32False(t *testing.T) { assertFoldedBool(t, ssa.OpEq32, ssa.TypeBool, 5, 6, false) }
func TestConstPropEq64(t *testing.T) { assertFoldedBool(t, ssa.OpEq64, ssa.TypeBool, 7, 7, true) }
func TestConstPropNe32(t *testing.T) { assertFoldedBool(t, ssa.OpNe32, ssa.TypeBool, 1, 2, true) }
func TestConstPropNe64(t *testing.T) { assertFoldedBool(t, ssa.OpNe64, ssa.TypeBool, 3, 3, false) }
func TestConstPropLtS64(t *testing.T) { assertFoldedBool(t, ssa.OpLtS64, ssa.TypeBool, -1, 1, true) }
func TestConstPropLeS32(t *testing.T) { assertFoldedBool(t, ssa.OpLeS32, ssa.TypeBool, 5, 5, true) }
func TestConstPropLeS64(t *testing.T) { assertFoldedBool(t, ssa.OpLeS64, ssa.TypeBool, 10, 5, false) }
func TestConstPropLtU32(t *testing.T) { assertFoldedBool(t, ssa.OpLtU32, ssa.TypeBool, 1, 2, true) }
func TestConstPropLeU32(t *testing.T) { assertFoldedBool(t, ssa.OpLeU32, ssa.TypeBool, 2, 2, true) }
func TestConstPropLtU64(t *testing.T) { assertFoldedBool(t, ssa.OpLtU64, ssa.TypeBool, 3, 5, true) }
func TestConstPropLeU64(t *testing.T) { assertFoldedBool(t, ssa.OpLeU64, ssa.TypeBool, 5, 5, true) }
// OpLtS32 with i32 inputs — exercises the argIs32(v) path for bool result width.
func TestConstPropLtS32With32BitArgs(t *testing.T) {
assertFoldedBool(t, ssa.OpLtS32, ssa.TypeBool, 3, 7, true)
}
// intConst with OpConst64 input.
func TestConstPropWithConst64Inputs(t *testing.T) {
sig := ssa.FuncSig{Results: []ssa.Type{ssa.TypeI64}}
b := ssa.NewFuncBuilder("c64fold", sig)
entry := b.NewBlock(ssa.BlockRet)
b.SetEntry(entry)
b.SetCurrent(entry)
x := b.Const64(10)
y := b.Const64(20)
sum := b.NewValue(ssa.OpAdd64, ssa.TypeI64, x, y)
b.FinishRet(sum)
f := b.Func()
if !ConstProp(f) {
t.Fatal("ConstProp did not fold Const64 + Const64")
}
if sum.AuxInt != 30 {
t.Errorf("folded value = %d, want 30", sum.AuxInt)
}
}
// argIs32 with zero args — exercises the false path.
func TestArgIs32ZeroArgs(t *testing.T) {
v := &ssa.Value{Op: ssa.OpConst32, Args: nil}
if argIs32(v) {
t.Error("argIs32 with zero args should return false")
}
}
// ---------------------------------------------------------------------------
// ConstProp: foldValue with 1-arg or nil-arg values (no-op paths)
// ---------------------------------------------------------------------------
func TestConstPropOneArgValue(t *testing.T) {
sig := ssa.FuncSig{Results: []ssa.Type{ssa.TypeI32}}
b := ssa.NewFuncBuilder("onearg", sig)
entry := b.NewBlock(ssa.BlockRet)
b.SetEntry(entry)
b.SetCurrent(entry)
c := b.Const32(1)
// OpCopy has one arg — foldValue returns false.
cp := b.NewValue(ssa.OpCopy, ssa.TypeI32, c)
b.FinishRet(cp)
f := b.Func()
// ConstProp should not fold a 1-arg value.
if ConstProp(f) {
t.Error("ConstProp should not fold 1-arg OpCopy")
}
}
// ---------------------------------------------------------------------------
// CSE: cover ineligible values (loads, params, phi, side-effecting),
// and nil-arg commutative key, no-change path, control rewrite
// ---------------------------------------------------------------------------
func TestCSEIneligibleLoad(t *testing.T) {
// Two identical loads must NOT be CSE'd.
b := ssa.NewFuncBuilder("cseload", ssa.FuncSig{
Params: []ssa.Type{ssa.TypeI32},
Results: []ssa.Type{ssa.TypeI32},
})
entry := b.NewBlock(ssa.BlockPlain)
b.SetEntry(entry)
b.SetCurrent(entry)
p0 := b.Param(0, ssa.TypeI32)
ld1 := b.NewValueAuxInt(ssa.OpLoad32, ssa.TypeI32, 0, p0)
ld2 := b.NewValueAuxInt(ssa.OpLoad32, ssa.TypeI32, 0, p0)
v := b.NewValue(ssa.OpAdd32, ssa.TypeI32, ld1, ld2)
b.FinishRet(v)
f := b.Func()
if CSE(f) {
t.Error("CSE should not merge loads")
}
if ld1.Op == ssa.OpInvalid || ld2.Op == ssa.OpInvalid {
t.Error("CSE wrongly merged loads")
}
}
func TestCSEIneligibleGlobalGet(t *testing.T) {
// Two identical OpGlobalGet must NOT be CSE'd.
b := ssa.NewFuncBuilder("cseglobget", ssa.FuncSig{
Results: []ssa.Type{ssa.TypeI32},
})
entry := b.NewBlock(ssa.BlockPlain)
b.SetEntry(entry)
b.SetCurrent(entry)
g1 := b.NewValueAuxInt(ssa.OpGlobalGet, ssa.TypeI32, 0)
g2 := b.NewValueAuxInt(ssa.OpGlobalGet, ssa.TypeI32, 0)
v := b.NewValue(ssa.OpAdd32, ssa.TypeI32, g1, g2)
b.FinishRet(v)
f := b.Func()
if CSE(f) {
t.Error("CSE should not merge global loads")
}
}
func TestCSEIneligiblePhi(t *testing.T) {
// Phis must not be merged by CSE.
b := ssa.NewFuncBuilder("csephi", ssa.FuncSig{
Params: []ssa.Type{ssa.TypeI32},
Results: []ssa.Type{ssa.TypeI32},
})
entry := b.NewBlock(ssa.BlockPlain)
merge := b.NewBlock(ssa.BlockRet)
b.SetEntry(entry)
b.SetCurrent(entry)
p0 := b.Param(0, ssa.TypeI32)
ssa.AddEdge(entry, merge)
b.SetCurrent(merge)
phi1 := b.NewValue(ssa.OpPhi, ssa.TypeI32, p0)
phi2 := b.NewValue(ssa.OpPhi, ssa.TypeI32, p0)
v := b.NewValue(ssa.OpAdd32, ssa.TypeI32, phi1, phi2)
b.FinishRet(v)
f := b.Func()
CSE(f)
if phi1.Op == ssa.OpInvalid || phi2.Op == ssa.OpInvalid {
t.Error("CSE wrongly merged phis")
}
}
func TestCSENoChange(t *testing.T) {
// CSE with nothing to merge returns false.
b := ssa.NewFuncBuilder("csenoop", ssa.FuncSig{
Params: []ssa.Type{ssa.TypeI32},
Results: []ssa.Type{ssa.TypeI32},
})
entry := b.NewBlock(ssa.BlockRet)
b.SetEntry(entry)
b.SetCurrent(entry)
p0 := b.Param(0, ssa.TypeI32)
b.FinishRet(p0)
f := b.Func()
if CSE(f) {
t.Error("CSE on unique values should return false")
}
}
func TestCSECommutativeCanonicalisation(t *testing.T) {
// a+b and b+a should be merged by CSE's commutative canonicalisation.
b := ssa.NewFuncBuilder("csecomm", ssa.FuncSig{
Params: []ssa.Type{ssa.TypeI32, ssa.TypeI32},
Results: []ssa.Type{ssa.TypeI32},
})
entry := b.NewBlock(ssa.BlockPlain)
b.SetEntry(entry)
b.SetCurrent(entry)
p0 := b.Param(0, ssa.TypeI32)
p1 := b.Param(1, ssa.TypeI32)
ab := b.NewValue(ssa.OpAdd32, ssa.TypeI32, p0, p1) // a+b
ba := b.NewValue(ssa.OpAdd32, ssa.TypeI32, p1, p0) // b+a
v := b.NewValue(ssa.OpAdd32, ssa.TypeI32, ab, ba)
b.FinishRet(v)
f := b.Func()
if !CSE(f) {
t.Error("CSE should merge a+b and b+a")
}
if ab.Op != ssa.OpInvalid && ba.Op != ssa.OpInvalid {
// one of them should be merged away
if ab.Op != ssa.OpInvalid && ba.Op != ssa.OpInvalid {
t.Error("CSE: neither a+b nor b+a was merged")
}
}
}
func TestCSEControlRewrite(t *testing.T) {
// CSE must also rewrite a block's Control value when the duplicate
// is the condition of an If block.
b := ssa.NewFuncBuilder("csectl", ssa.FuncSig{
Params: []ssa.Type{ssa.TypeI32},
Results: []ssa.Type{ssa.TypeI32},
})
entry := b.NewBlock(ssa.BlockIf)
thenB := b.NewBlock(ssa.BlockRet)
elseB := b.NewBlock(ssa.BlockRet)
b.SetEntry(entry)
b.SetCurrent(entry)
p0 := b.Param(0, ssa.TypeI32)
// Two identical comparisons — the first one is the real condition.
cond1 := b.NewValue(ssa.OpEq32, ssa.TypeBool, p0, p0)
cond2 := b.NewValue(ssa.OpEq32, ssa.TypeBool, p0, p0) // duplicate
entry.Control = cond2
ssa.AddEdge(entry, thenB)
ssa.AddEdge(entry, elseB)
c := b.Const32(1)
b.SetCurrent(thenB)
b.FinishRet(c)
b.SetCurrent(elseB)
b.FinishRet(c)
f := b.Func()
CSE(f)
// After CSE cond2 should be merged away and Control rewritten to cond1.
if entry.Control != cond1 {
t.Errorf("CSE did not rewrite Control: got v%d want v%d", entry.Control.ID, cond1.ID)
}
}
// ---------------------------------------------------------------------------
// Simplify: edge cases
// ---------------------------------------------------------------------------
func TestSimplifyNoChange(t *testing.T) {
// A function with no copies and no trivial phis — Simplify returns false.
b := ssa.NewFuncBuilder("simpnoop", ssa.FuncSig{
Params: []ssa.Type{ssa.TypeI32},
Results: []ssa.Type{ssa.TypeI32},
})
entry := b.NewBlock(ssa.BlockRet)
b.SetEntry(entry)
b.SetCurrent(entry)
p0 := b.Param(0, ssa.TypeI32)
b.FinishRet(p0)
f := b.Func()
if Simplify(f) {
t.Error("Simplify on a pure param-return should return false")
}
}
func TestSimplifyRetMarkerPreserved(t *testing.T) {
// The Ret-tail OpCopy marker must NOT be dissolved by Simplify.
b := ssa.NewFuncBuilder("simpret", ssa.FuncSig{
Params: []ssa.Type{ssa.TypeI32},
Results: []ssa.Type{ssa.TypeI32},
})
entry := b.NewBlock(ssa.BlockRet)
b.SetEntry(entry)
b.SetCurrent(entry)
p0 := b.Param(0, ssa.TypeI32)
b.FinishRet(p0)
f := b.Func()
// The last value in entry is the Ret marker (OpCopy wrapping p0).
retMarker := entry.Values[len(entry.Values)-1]
if retMarker.Op != ssa.OpCopy {
t.Fatalf("expected OpCopy as ret marker, got %v", retMarker.Op)
}
Simplify(f)
if retMarker.Op != ssa.OpCopy {
t.Errorf("Simplify dissolved the Ret marker: op=%v", retMarker.Op)
}
}
func TestSimplifyControlRewrite(t *testing.T) {
// Simplify must rewrite a block's Control value through copies.
b := ssa.NewFuncBuilder("simpctl", ssa.FuncSig{
Params: []ssa.Type{ssa.TypeI32},
Results: []ssa.Type{ssa.TypeI32},
})
entry := b.NewBlock(ssa.BlockIf)
thenB := b.NewBlock(ssa.BlockRet)
elseB := b.NewBlock(ssa.BlockRet)
b.SetEntry(entry)
b.SetCurrent(entry)
p0 := b.Param(0, ssa.TypeI32)
// A copy of p0 used as the If condition.
cp := b.NewValue(ssa.OpCopy, ssa.TypeI32, p0)
entry.Control = cp
ssa.AddEdge(entry, thenB)
ssa.AddEdge(entry, elseB)
c := b.Const32(0)
b.SetCurrent(thenB)
b.FinishRet(c)
b.SetCurrent(elseB)
b.FinishRet(c)
f := b.Func()
if !Simplify(f) {
t.Error("Simplify should propagate copy through Control")
}
if entry.Control != p0 {
t.Errorf("Control not rewritten: got v%d want v%d (p0)", entry.Control.ID, p0.ID)
}
}
// ---------------------------------------------------------------------------
// trivialPhiArg: nil args
// ---------------------------------------------------------------------------
func TestTrivialPhiArgAllNil(t *testing.T) {
// A phi with all nil args returns nil (treated as non-trivial).
v := &ssa.Value{Op: ssa.OpPhi, Args: []*ssa.Value{nil, nil}}
if got := trivialPhiArg(v); got != nil {
t.Errorf("trivialPhiArg all-nil: got v%d, want nil", got.ID)
}
}
func TestTrivialPhiArgNonPhi(t *testing.T) {
// Non-phi values return nil.
v := &ssa.Value{Op: ssa.OpAdd32}
if got := trivialPhiArg(v); got != nil {
t.Errorf("trivialPhiArg non-phi: want nil, got %v", got)
}
}
// ---------------------------------------------------------------------------
// BranchFold: no-change path
// ---------------------------------------------------------------------------
func TestBranchFoldNoChange(t *testing.T) {
// A function with no If blocks (only a Ret) — BranchFold returns false.
b := ssa.NewFuncBuilder("bfnoop", ssa.FuncSig{Results: []ssa.Type{ssa.TypeI32}})
entry := b.NewBlock(ssa.BlockRet)
b.SetEntry(entry)
b.SetCurrent(entry)
c := b.Const32(1)
b.FinishRet(c)
f := b.Func()
if BranchFold(f) {
t.Error("BranchFold on a non-If function should return false")
}
}
func TestBranchFoldNonConstCondition(t *testing.T) {
// If block whose condition is a Param (not a constant) — no fold.
b := ssa.NewFuncBuilder("bfparam", ssa.FuncSig{
Params: []ssa.Type{ssa.TypeI32},
Results: []ssa.Type{ssa.TypeI32},
})
entry := b.NewBlock(ssa.BlockIf)
thenB := b.NewBlock(ssa.BlockRet)
elseB := b.NewBlock(ssa.BlockRet)
b.SetEntry(entry)
b.SetCurrent(entry)
p0 := b.Param(0, ssa.TypeI32)
entry.Control = p0
ssa.AddEdge(entry, thenB)
ssa.AddEdge(entry, elseB)
c := b.Const32(0)
b.SetCurrent(thenB)
b.FinishRet(c)
b.SetCurrent(elseB)
b.FinishRet(c)
f := b.Func()
if BranchFold(f) {
t.Error("BranchFold with param condition should return false")
}
}
// ---------------------------------------------------------------------------
// removeEdge: early return when src not in dst's preds
// ---------------------------------------------------------------------------
func TestRemoveEdgeNotFound(t *testing.T) {
// Call removeEdge when src is not actually a predecessor of dst —
// should not panic and leave edges intact.
b := ssa.NewFuncBuilder("rmedge", ssa.FuncSig{Results: []ssa.Type{ssa.TypeI32}})
src := b.NewBlock(ssa.BlockPlain)
dst := b.NewBlock(ssa.BlockRet)
b.SetEntry(src)
b.SetCurrent(src)
c := b.Const32(1)
b.FinishRet(c)
b.SetCurrent(dst)
b.FinishRet(c)
// Don't wire src→dst; just call removeEdge — should be a no-op.
removeEdge(src, dst)
if len(dst.Preds) != 0 {
t.Error("removeEdge on unwired blocks modified preds")
}
}
// ---------------------------------------------------------------------------
// DCE: control value on non-Ret blocks
// ---------------------------------------------------------------------------
func TestDCEControlKept(t *testing.T) {
// A Control value that is the only reference to a computation must be
// kept alive by DCE (roots include block Control values).
b := ssa.NewFuncBuilder("dcectl", ssa.FuncSig{
Params: []ssa.Type{ssa.TypeI32},
Results: []ssa.Type{ssa.TypeI32},
})
entry := b.NewBlock(ssa.BlockIf)
thenB := b.NewBlock(ssa.BlockRet)
elseB := b.NewBlock(ssa.BlockRet)
b.SetEntry(entry)
b.SetCurrent(entry)
p0 := b.Param(0, ssa.TypeI32)
cond := b.NewValue(ssa.OpEq32, ssa.TypeBool, p0, p0)
entry.Control = cond
ssa.AddEdge(entry, thenB)
ssa.AddEdge(entry, elseB)
c := b.Const32(0)
b.SetCurrent(thenB)
b.FinishRet(c)
b.SetCurrent(elseB)
b.FinishRet(c)
f := b.Func()
DCE(f)
if cond.Op != ssa.OpEq32 {
t.Errorf("DCE killed a Control value: op=%v", cond.Op)
}
}
// ---------------------------------------------------------------------------
// cseKey nil-arg path
// ---------------------------------------------------------------------------
func TestCSEKeyWithNilArg(t *testing.T) {
// A value with a nil arg in a non-commutative op still produces a key
// (no panic).
v := &ssa.Value{
ID: 1,
Op: ssa.OpSub32,
Type: ssa.TypeI32,
Args: []*ssa.Value{nil, nil},
}
resolve := func(x *ssa.Value) *ssa.Value { return x }
key := cseKey(v, resolve)
if key == "" {
t.Error("cseKey with nil args returned empty string")
}
}