mirror of
https://github.com/hanzoai/wasm2go.git
synced 2026-10-30 15:06:48 +00:00
The legacy-EH support added for clang's SjLj lowering only covered the single-tag patterns SjLj emits. Clang's -fwasm-exceptions C++ output (e.g. llama.cpp) additionally needs: - try with multiple catch clauses (catch $a / catch $b / catch_all), dispatched by tag in clause order - catch_all-only trys and rethrow inside catch_all (the cleanup-during- unwind shape), including `rethrow l` with l > 0 naming an OUTER catch's exception - `delegate l` forwarding to a specific outer try, skipping every try nested in between Lowering now resolves delegate and rethrow label depths to their target try regions (ctrl frames track handler state), records each region's protected-body block set — the CFG alone cannot delimit it, because a br that exits a try early makes its continuation reachable from inside — and handles `delegate` closing a try inside a dead region. The recover-trampoline emitter drops its single-non-catch_all-handler restriction: every region gets an active flag and its own caught- exception variable, the dispatch is an innermost-first if-chain with per-clause tag tests, a delegate arm clears the flags of the regions it skips, and any edge leaving a protected body clears its flag (a br out of a try previously left the flag set, so a later unrelated throw could misroute into the exited try's handler). The structured emitter registers per-region exception variables so outer-targeting rethrows resolve correctly, bails to the trampoline when a delegate's target is not what a plain re-panic would reach or when a body block escapes the region (its continuation would otherwise be duplicated inside the protective closure), and closes a try no path falls out of with trap+spin so Go's termination analysis accepts the function. SSA verification computes dominators over the CFG augmented with virtual entry-to-handler edges, so handler landing pads (which have no CFG predecessors) may use values defined before their try. gcasm routes functions that call their own EH closures to the pure fallback: the outer function of a structured try has no panic/recover runtime call of its own — the defer/recover lives inside the closure it calls. Verified this session: `go test -race -timeout 20m ./...` exit 0 (all packages ok). New fixtures cover multi-clause dispatch on both emit paths, catch_all+rethrow, a delegate skipping an intermediate handler, an outer-targeting rethrow, and the try-exit flag drop. A full llama.cpp module (wasm32-wasip1, -fwasm-exceptions, SIMD disabled) transpiles and generates correct text end-to-end. The googlesql/wasmify five-step e2e chain was NOT run this session. Co-authored-by: Claude Fable 5 <[email protected]>
113 lines
3.2 KiB
Go
113 lines
3.2 KiB
Go
package transpile_test
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/goccy/wasm2go/internal/testfixture"
|
|
"github.com/goccy/wasm2go/transpile"
|
|
)
|
|
|
|
// runEHFixture translates fixture (which must export "run" () -> i32), builds
|
|
// the generated module with the default backend, executes it, and compares
|
|
// run()'s printed result. Shared by the C++-shaped EH pattern tests below.
|
|
func runEHFixture(t *testing.T, fixture, want string) {
|
|
t.Helper()
|
|
bin := testfixture.Wasm(t, fixture)
|
|
m, err := transpile.Parse(bytes.NewReader(bin))
|
|
if err != nil {
|
|
t.Fatalf("Parse: %v", err)
|
|
}
|
|
var buf bytes.Buffer
|
|
res, err := transpile.Translate(&buf, m, transpile.Options{
|
|
Package: "pkg",
|
|
OutputImportPath: "ehtest/pkg",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Translate: %v", err)
|
|
}
|
|
|
|
dir := t.TempDir()
|
|
writeFile := func(rel string, data []byte) {
|
|
p := filepath.Join(dir, rel)
|
|
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(p, data, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
writeFile("go.mod", []byte("module ehtest\n\ngo 1.25.0\n"))
|
|
writeFile("pkg/gen.go", buf.Bytes())
|
|
for rel, data := range res.Files {
|
|
writeFile("pkg/"+rel, data)
|
|
}
|
|
for name, data := range res.Sidecars {
|
|
writeFile("pkg/"+name, data)
|
|
}
|
|
writeFile("main.go", []byte(`package main
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"ehtest/pkg"
|
|
)
|
|
|
|
func main() {
|
|
fmt.Println(pkg.New().Run())
|
|
}
|
|
`))
|
|
|
|
cmd := exec.Command("go", "run", ".")
|
|
cmd.Dir = dir
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("go run failed: %v\n%s", err, out)
|
|
}
|
|
if got := strings.TrimSpace(string(out)); got != want {
|
|
t.Fatalf("run() printed %q, want %s\n%s", got, want, out)
|
|
}
|
|
}
|
|
|
|
// TestTranspileEHMultiCatch: one try with catch $a / catch $b / catch_all —
|
|
// the clause dispatch must select by tag, in clause order, with catch_all as
|
|
// the final default.
|
|
func TestTranspileEHMultiCatch(t *testing.T) {
|
|
runEHFixture(t, "eh_multi_catch", "132")
|
|
}
|
|
|
|
// TestTranspileEHMultiCatchTrampoline is the same dispatch through the
|
|
// recover-trampoline path (a return inside the try body forces it).
|
|
func TestTranspileEHMultiCatchTrampoline(t *testing.T) {
|
|
runEHFixture(t, "eh_multi_catch_tramp", "139")
|
|
}
|
|
|
|
// TestTranspileEHCatchAllRethrow: catch_all + rethrow 0 — the C++ cleanup-
|
|
// during-unwind shape. The rethrown exception keeps tag and payload.
|
|
func TestTranspileEHCatchAllRethrow(t *testing.T) {
|
|
runEHFixture(t, "eh_catch_all_rethrow", "42")
|
|
}
|
|
|
|
// TestTranspileEHDelegateSkip: delegate 1 forwards past the middle try's
|
|
// catch_all straight to the outer catch $a. The middle handler must not run.
|
|
func TestTranspileEHDelegateSkip(t *testing.T) {
|
|
runEHFixture(t, "eh_delegate_skip", "42")
|
|
}
|
|
|
|
// TestTranspileEHRethrowOuter: rethrow 1 inside an inner catch_all re-raises
|
|
// the OUTER catch's exception (42), not the inner one (99).
|
|
func TestTranspileEHRethrowOuter(t *testing.T) {
|
|
runEHFixture(t, "eh_rethrow_outer", "42")
|
|
}
|
|
|
|
// TestTranspileEHTryExitFlag: a br out of a protected body must deactivate the
|
|
// try — a later throw outside it propagates to the caller instead of being
|
|
// caught by the exited try's handler.
|
|
func TestTranspileEHTryExitFlag(t *testing.T) {
|
|
runEHFixture(t, "eh_try_exit_flag", "222")
|
|
}
|