mirror of
https://github.com/hanzoai/wasm2go.git
synced 2026-10-30 15:06:48 +00:00
Transpile wasm modules that use the exception-handling proposal — the
try / catch / catch_all / throw / delegate / rethrow ops plus the tag
section. Clang lowers C/C++ setjmp/longjmp onto these ops under
`-mllvm -wasm-enable-sjlj`, so this is what lets wasm2go handle any
module built that way.
EH constructs are emitted as Go panic/recover: a throw becomes a
panic(*wasmExc); a try body runs inside a closure guarded by a deferred
recover; catch dispatches on the tag and narrows the operand slots.
setjmp-style resume — a longjmp re-entering the protected region — is
handled by a general recover-trampoline that wraps the whole function in
a `for { ... }` loop with a __pc dispatch, so re-entry is a loop
iteration rather than a goto across the closure boundary.
Pieces:
- Parser: decode the EH ops and the tag section.
- SSA / lower: throw -> BlockThrow terminator; try / catch / catch_all
lowering with mutable locals; delegate + rethrow; no local phis at
loop headers in mutable-locals mode.
- Codegen: emit try/catch/throw as panic/recover; real setjmp/longjmp
exception-resume loops; EH in multi-package output; structured
br_table + shared forward joins so more EH functions take the
structured path; the general recover-trampoline for goto-EH /
loop-spanning-try / resume; and bail a try-body function return to the
trampoline (a plain return cannot escape the *wasmExc closure).
- gcasm: fall back to pure Go for non-replayable jump tables so the
pure-Go asm backend never hard-fails a bundle.
New E2E coverage: EH try/catch, setjmp/longjmp resume loops, structured
br_table, multi-package EH, and the recover-trampoline, plus a
checked-in testdata wasm. go build ./..., go test ./... and golangci-lint
are green.
Claude-Session: https://claude.ai/code/session_01QWhxMdYGXtDRtDdKZXqnHG
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
141 lines
3.4 KiB
Go
141 lines
3.4 KiB
Go
package pass
|
|
|
|
import "github.com/goccy/wasm2go/internal/ssa"
|
|
|
|
// Simplify performs copy propagation and trivial-phi elimination.
|
|
//
|
|
// - OpCopy: a value `c = copy(x)` is equivalent to x. Every
|
|
// reference to c is rewritten to x (transitively, following copy
|
|
// and trivial-phi chains).
|
|
// - trivial phi: a phi whose arguments, ignoring self-references,
|
|
// are all the same value V is equivalent to V; references to it
|
|
// are likewise rewritten.
|
|
//
|
|
// The dissolved copies / phis are marked dead (OpInvalid) for Compact;
|
|
// DCE is not needed to clean them, though running it afterwards also
|
|
// reclaims anything that became unused.
|
|
//
|
|
// The Ret-tail OpCopy markers — the canonical return-value slots the
|
|
// emit stage relies on — are preserved: their *arguments* are still
|
|
// forwarded, but the marker value itself is never dissolved.
|
|
//
|
|
// Returns true if anything changed.
|
|
func Simplify(f *ssa.Func) bool {
|
|
ret := retMarkerSet(f)
|
|
|
|
// canon memoises each value's ultimate representative. A value
|
|
// maps to itself unless it is a (non-Ret) copy or a trivial phi.
|
|
canon := make(map[ssa.ValueID]*ssa.Value)
|
|
var resolve func(v *ssa.Value) *ssa.Value
|
|
resolve = func(v *ssa.Value) *ssa.Value {
|
|
if v == nil {
|
|
return nil
|
|
}
|
|
if r, ok := canon[v.ID]; ok {
|
|
return r
|
|
}
|
|
canon[v.ID] = v // tentative self-map breaks cycles
|
|
var next *ssa.Value
|
|
switch {
|
|
case v.Op == ssa.OpCopy && !ret[v.ID] && len(v.Args) == 1:
|
|
next = v.Args[0]
|
|
case v.Op == ssa.OpPhi:
|
|
next = trivialPhiArg(v)
|
|
}
|
|
if next != nil && next != v {
|
|
r := resolve(next)
|
|
canon[v.ID] = r
|
|
return r
|
|
}
|
|
return v
|
|
}
|
|
|
|
changed := false
|
|
for _, b := range f.Blocks {
|
|
for _, v := range b.Values {
|
|
if v.Op == ssa.OpInvalid {
|
|
continue
|
|
}
|
|
for i, a := range v.Args {
|
|
if a == nil {
|
|
continue
|
|
}
|
|
if r := resolve(a); r != a {
|
|
v.Args[i] = r
|
|
changed = true
|
|
}
|
|
}
|
|
}
|
|
if b.Control != nil {
|
|
if r := resolve(b.Control); r != b.Control {
|
|
b.Control = r
|
|
changed = true
|
|
}
|
|
}
|
|
}
|
|
// Mark every dissolved value (a non-Ret copy or trivial phi whose
|
|
// representative is not itself) dead.
|
|
for _, b := range f.Blocks {
|
|
for _, v := range b.Values {
|
|
if v.Op == ssa.OpInvalid || ret[v.ID] {
|
|
continue
|
|
}
|
|
if resolve(v) != v {
|
|
v.Op = ssa.OpInvalid
|
|
v.Args = nil
|
|
changed = true
|
|
}
|
|
}
|
|
}
|
|
return changed
|
|
}
|
|
|
|
// trivialPhiArg returns the single distinct non-self argument of a phi,
|
|
// or nil when the phi has two or more distinct real arguments (so it is
|
|
// a genuine merge and must stay).
|
|
func trivialPhiArg(v *ssa.Value) *ssa.Value {
|
|
if v.Op != ssa.OpPhi {
|
|
return nil
|
|
}
|
|
var uniq *ssa.Value
|
|
for _, a := range v.Args {
|
|
if a == nil || a == v {
|
|
continue // self-reference contributes nothing
|
|
}
|
|
if uniq == nil {
|
|
uniq = a
|
|
continue
|
|
}
|
|
if a != uniq {
|
|
return nil
|
|
}
|
|
}
|
|
return uniq
|
|
}
|
|
|
|
// retMarkerSet returns the IDs of the Ret-tail return markers — the
|
|
// last len(Results) values of each BlockRet, which emit relies on
|
|
// staying as OpCopy.
|
|
func retMarkerSet(f *ssa.Func) map[ssa.ValueID]bool {
|
|
set := map[ssa.ValueID]bool{}
|
|
for _, b := range f.Blocks {
|
|
var nMarkers int
|
|
switch b.Kind {
|
|
case ssa.BlockRet:
|
|
nMarkers = len(f.Sig.Results)
|
|
case ssa.BlockThrow:
|
|
// A BlockThrow's tail markers are its thrown operands.
|
|
nMarkers = b.ThrowArgc
|
|
default:
|
|
continue
|
|
}
|
|
n := len(b.Values)
|
|
for i := n - nMarkers; i < n; i++ {
|
|
if i >= 0 {
|
|
set[b.Values[i].ID] = true
|
|
}
|
|
}
|
|
}
|
|
return set
|
|
}
|