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]>
90 lines
3.7 KiB
Go
90 lines
3.7 KiB
Go
package codegen
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/goccy/wasm2go/internal/emit"
|
|
"github.com/goccy/wasm2go/internal/wasm"
|
|
)
|
|
|
|
// *translator implements emit.Driver. The interface defines the
|
|
// backend-neutral surface (identifier-resolution rules, helper /
|
|
// import registration, module access) that an SSA-driven emitter
|
|
// needs from its host. The methods here are thin exported wrappers
|
|
// over the package-private translator helpers so the same naming
|
|
// rules are used by every emitter (the Go-source emitter today, the
|
|
// plan9 asm emitter later) without each one having to reinvent
|
|
// multi-package qualification.
|
|
//
|
|
// AST-returning methods (helperRef, funcRef, ...) stay outside the
|
|
// interface because they would couple it to go/ast and prevent an
|
|
// asm host from satisfying it.
|
|
var _ emit.Driver = (*translator)(nil)
|
|
|
|
// Module returns the parsed wasm module being translated.
|
|
func (t *translator) Module() *wasm.Module { return t.mod }
|
|
|
|
// MultiPackage reports whether the translator is producing the
|
|
// multi-package + linkname-split layout.
|
|
func (t *translator) MultiPackage() bool { return t.multiPackage }
|
|
|
|
// FieldName is the exported alias for fieldName, satisfying emit.Driver.
|
|
func (t *translator) FieldName(s string) string { return t.fieldName(s) }
|
|
|
|
// FuncName is the exported alias for funcName, satisfying emit.Driver.
|
|
func (t *translator) FuncName(funcIdx uint32) string { return t.funcName(funcIdx) }
|
|
|
|
// FuncRefName returns the qualified Go-syntax identifier the asm
|
|
// emitter should hand to goAsmSymbol when rendering a CALL. For an
|
|
// own-chunk callee or single-package mode the result is the bare
|
|
// name (e.g. "Fn42") so the CALL stays as a local-package
|
|
// ·Fn42(SB). For a cross-chunk callee in multi-package mode the
|
|
// result is the FULL Go-side qualified name (e.g.
|
|
// "github.com/foo/bar/pX.Fn42") so goAsmSymbol can render a direct
|
|
// cross-package CALL — plan 9 asm's scanner accepts U+2215 in
|
|
// place of "/" as an identifier rune (see
|
|
// src/cmd/asm/internal/lex/tokenizer.go: isIdentRune), so we don't
|
|
// need a per-chunk Go-body trampoline to bounce the asm caller
|
|
// through a local-package symbol. The Go-body trampoline pair is
|
|
// still emitted for the pure-Go fallback bodies in pN_pure.go via
|
|
// funcRef (the AST-side ref), and the Go linker DCEs it on the
|
|
// amd64/arm64 build where the .s files supply the asm bodies.
|
|
func (t *translator) FuncRefName(funcIdx uint32) string {
|
|
if t.multiPackage && t.plan != nil {
|
|
if targetChunk, ok := t.plan.FuncToChunk[funcIdx]; ok && targetChunk != t.currentChunk {
|
|
return fmt.Sprintf("%s/p%d.%s", t.opts.OutputImportPath, targetChunk, t.funcName(funcIdx))
|
|
}
|
|
}
|
|
return t.funcName(funcIdx)
|
|
}
|
|
|
|
// HelperName returns the qualified identifier (possibly `base.Up`)
|
|
// for a runtime helper as it should appear at the call site. Mirrors
|
|
// helperRef's qualification rule but returns text so an asm emitter
|
|
// can spell the same call as a plan9 symbol reference.
|
|
func (t *translator) HelperName(name string) string {
|
|
if t.multiPackage {
|
|
up := capitalize(name)
|
|
if t.currentChunk == chunkBase {
|
|
return up
|
|
}
|
|
return "base." + up
|
|
}
|
|
return name
|
|
}
|
|
|
|
// ImportMethodName is the exported alias for importMethodName,
|
|
// satisfying emit.Driver.
|
|
func (t *translator) ImportMethodName(imp wasm.Import) string {
|
|
return t.importMethodName(imp)
|
|
}
|
|
|
|
// UseHelper is the exported alias for useHelper, satisfying emit.Driver.
|
|
func (t *translator) UseHelper(name string) { t.useHelper(name) }
|
|
|
|
// UsePackage is the exported alias for use, satisfying emit.Driver.
|
|
// (Renamed from `use` so the host-facing name makes its role
|
|
// explicit: it registers an external dependency, not a generic
|
|
// utility.)
|
|
func (t *translator) UsePackage(pkg string) { t.use(pkg) }
|