From bfb20e0ba7a3f315e283be23fc7fd2beaf3edc5f Mon Sep 17 00:00:00 2001 From: Edoardo Vacchi Date: Sun, 26 Apr 2026 18:12:51 +0200 Subject: [PATCH] feat: exception handling spec (#2489) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add experimental support to the Exception Handling spec. Exception handling adds structured error propagation to WebAssembly through `tags` (typed exception signatures), `throw/throw_ref` (raising exceptions), and `try_table` (catching exceptions with typed `catch` clauses). A new value type, `exnref`, represents a reference to a caught exception. Excluding tests and spec suites, the entire feature amounts to a little less than 2000 lines of code. Feature flag: `experimental.CoreFeaturesExceptionHandling` ## What's the use for this? In the spirit of always having a real use case for the specs we implement, I did check this implementation against the one large Wasm C++ codebase I am aware about, i.e. https://github.com/klippa-app/go-pdfium, and I verified that the entire suite, compiled with `emcc+wasm-opt` does work; incidentally, much of the existing support code, based on Emscripten's longjmp/setjmp, can be dropped (thx @jerbob92 for the help!) Broadly speaking, I don't know how much EH-enabled Wasm code you will find in the wild. For instance, code compiled using Clang's C++ backend does not really throw new-style exceptions, unless you compile WASI-SDK with some flags, or you build with `emcc` and translate old-style exceptions using `wasm-opt` (see exceptions_test.go) In short, this spec should be really seen as a step towards supporting the GC spec. The design decisions (esp. regarding the test suite) follow from this reasoning. ## Spec Suite `wast2json` **does not** support the EH suite, but `wasm-tools` does, albeit with a tiny quirk in the way negative numbers are rendered. In this PR _I am not_ moving the other suites to `wasm-tools` though. The spec test suite passes entirely, except for two tests that are intentionally skipped. These tests assume **a very small subset** of typed references is implemented; specifically, the distinction between nullable and non-nullable function references (`(ref null $t)` vs `(ref $t)`). I have decided to just flatten all non-nullable refs to nullable refs at decode time, so we cannot detect the type mismatch the spec expects in these cases. Passing the **full suite is actually straightforward** because it only requires introducing a non-nullable variant for each ref type: strictly speaking the 2 failing tests are only for related nullable vs non-nullable **func refs** -- so I can special-case these and/or add non-nullable types for each known value; however, we will **need** to rethink the representation of Values for the GC proposal anyway (because this will introduce custom type indices, and these won't fit our currently byte-sized `ValueType` enum). The subtype-checking functions (`isRefSubtypeOf`, `isStrictRefSubtypeOf`) are kept as abstraction points but currently they are just equalities. Nevertheless, as proven by the CPP example and https://github.com/klippa-app/go-pdfium this PR provides a starting point that is already useful (even if it is for limited cases). ## Interpreter Exception handling in the interpreter: **Within a frame**, `try_table` blocks are compiled into a static exception table with PC ranges. When `throw` or `throw_ref` executes, `searchExceptionTable` scans backwards (inner handlers first) for a catch clause whose PC range covers the current instruction. If a match is found, `applyExceptionHandler` adjusts the operand stack depth and jumps to the catch target PC directly. **Across frames**, if no handler is found in the current frame, `panic(&thrownException{...})` propagates up the Go call stack. Each cross-function call goes through `callWithUnwind()`, which uses `defer/recover` to intercept the panic. Its `canRestore` method unwinds `ce.frames` to the caller's depth and searches that frame's exception table. If a handler is found, `doRestore` applies it and `callWithUnwind` returns `true` (frame unwound -- caller refreshes locals and continues). If not, the panic re-propagates to the next outer `callWithUnwind`. Note on the implementation: this mechanism is unified with the existing `snapshot/restore` API through the `restorable` interface: both `*thrownException` and `*snapshot` implement `canRestore`/`doRestore`, so `callWithUnwind` handles both with a single defer/recover path. The short-circuit check (`len(exceptionTable) == 0 && no snapshotter`) skips the defer/recover overhead entirely for frames that don't need it. ## Compiler The compiler could not reuse the snapshot/restore mechanism directly, but it follows a similar pattern. One cool thing is that exception handling could be implemented entirely as an SSA-level lowering with new exit codes; there is no backend-specific code. Throwing uses a two-phase ABI: 1. `ExitCodeThrowAlloc`: exits to the Go runtime to allocate an `Exception` struct on the heap and writes a pointer to its params array into the execution context. Then we return control to compiled code, which stores the tag parameters directly into the struct. 2. `ExitCodeThrow`: exits to the Go runtime again to search for a matching handler, restore the stack checkpoint, and branch to the catch target. The idea is that we NEED to allocate the parameter slice dynamically, so we delegate to the Go runtime. Incidentally, this also allows us to unify `throw` and `throwRef`; in the latter case, we skip the `alloc` phase, and we exit straight to `ExitCodeThrow`. ### Trampolines The compiler uses four trampolines to return to host to manage exception state: | Trampoline | Exit Code | Purpose | |---|---|---| | **ThrowAlloc** | `ExitCodeThrowAlloc` | Allocates an `Exception` struct on the Go heap, writes `&exn.Params[0]` into `execCtx` so compiled code can store tag parameters directly. | | **Throw** | `ExitCodeThrow` | Searches `tryHandlers` for a matching catch clause, restores the stack checkpoint on match, sets `caughtExceptionClauseIdx` for the compiled dispatch. Used for both `throw` (after ThrowAlloc) and `throw_ref` (directly). | | **TryTableEnter** | `ExitCodeTryTableEnter` | Clones the current stack as a checkpoint, pushes a `tryHandler` with the catch clause table and saved module instance. Sets `caughtExceptionClauseIdx = -1` (no exception). | | **TryTableLeave** | `ExitCodeTryTableLeave` | Pops the most recent `tryHandler`. Emitted at `try_table` block ends, and before any branch or return that exits a `try_table` scope. | ### Lowering As mentioned earlier, this entirely resolved at the SSA-level. In particular: - **Entry**: when a `try_table` is encountered, the compiler emits a call to the TryTableEnter trampoline. Control returns to compiled code, which reads `caughtExceptionClauseIdx` from the execution context — if it's -1 (no exception), execution continues into the `try_table` body. - **Normal exit**: when execution reaches the `End` opcode of the `try_table` block, a TryTableLeave trampoline call is emitted. However, every TryTableEnter must have a matching TryTableLeave on every control flow path that doesn't throw; so we also "TryTableLeaves" also for the following: - **Early exit (branch)**: a `br`, `br_if`, or `br_table` can jump to a label outside the `try_table`, skipping its `End`. `emitTryTableLeaves(depth)` walks the control frame stack and emits one TryTableLeave for each `try_table` frame the branch crosses. For `br_if`, this happens in a trampoline basic block that only executes on the taken path. - **Early exit (return)**: `return`, `return_call`, and `return_call_indirect` call `emitTryTableLeaves` with the full control stack depth, popping all active handlers before leaving the frame. - **Throw**: on `throw`/`throw_ref`, compiled code exits to the dispatch loop via ExitCodeThrow. `doHandleException` searches `tryHandlers` innermost-to-outermost. On match, it restores the cloned stack checkpoint, sets `caughtExceptionClauseIdx`, and re-enters compiled code at the handler's return address. The compiled dispatch branches to the matching catch clause's handler block. ### Caveats - As described above, we are skipping 2 spec tests (will be addressed when we implement typed references) - Because of that, I am _not_ enabling by default the fuzzer: I would have to special-case for typed references and/or TODOs I have left behind; I did run the fuzzer with some special-casing while debugging the issue with pdfium (#2488) and did not find issues (the fuzzer did go frequently OOM though); in fact, the pdfium issue was actually unrelated (!) --------- Signed-off-by: Edoardo Vacchi --- .github/workflows/commit.yaml | 7 +- Makefile | 16 + api/features.go | 12 + experimental/exceptions_test.go | 47 ++ experimental/features.go | 5 + experimental/testdata/cpp_exceptions.cpp | 78 +++ experimental/testdata/cpp_exceptions.wasm | Bin 0 -> 15829 bytes internal/engine/interpreter/compiler.go | 138 ++++- internal/engine/interpreter/interpreter.go | 231 +++++++- internal/engine/interpreter/operations.go | 58 ++ internal/engine/interpreter/signature.go | 15 +- .../engine/wazevo/backend/backend_test.go | 252 ++++++++- .../backend/isa/arm64/abi_go_call_test.go | 81 +++ internal/engine/wazevo/call_engine.go | 200 ++++++- internal/engine/wazevo/e2e_test.go | 56 ++ internal/engine/wazevo/engine.go | 70 ++- internal/engine/wazevo/frontend/frontend.go | 118 +++- .../engine/wazevo/frontend/frontend_test.go | 10 + internal/engine/wazevo/frontend/lower.go | 450 +++++++++++++++ internal/engine/wazevo/module_engine.go | 12 + internal/engine/wazevo/ssa/basic_block.go | 30 +- internal/engine/wazevo/testcases/testcases.go | 366 ++++++++++++ internal/engine/wazevo/wazevo_test.go | 7 + internal/engine/wazevo/wazevoapi/exitcode.go | 41 ++ .../engine/wazevo/wazevoapi/offsetdata.go | 32 ++ .../wazevo/wazevoapi/offsetdata_test.go | 7 + .../integration_test/engine/eh_hammer_test.go | 215 +++++++ .../engine/exceptions_test.go | 187 +++++++ .../engine/testdata/eh_br_orphan.wasm | Bin 0 -> 108 bytes .../engine/testdata/eh_br_orphan.wat | 54 ++ .../engine/testdata/eh_br_stale_handler.wasm | Bin 0 -> 96 bytes .../engine/testdata/eh_br_stale_handler.wat | 51 ++ .../engine/testdata/eh_cross_callnative.wasm | Bin 0 -> 181 bytes .../engine/testdata/eh_cross_callnative.wat | 75 +++ .../engine/testdata/eh_pdfium.wasm | Bin 0 -> 182 bytes .../engine/testdata/eh_pdfium.wat | 71 +++ .../engine/testdata/eh_throw_ref_null.wasm | Bin 0 -> 48 bytes .../engine/testdata/eh_throw_ref_null.wat | 7 + .../spectest/exception-handling/spec_test.go | 47 ++ .../exception-handling/testdata/tag.0.wasm | Bin 0 -> 61 bytes .../exception-handling/testdata/tag.1.wasm | Bin 0 -> 63 bytes .../exception-handling/testdata/tag.2.wasm | Bin 0 -> 20 bytes .../exception-handling/testdata/tag.3.wasm | Bin 0 -> 23 bytes .../exception-handling/testdata/tag.4.wasm | Bin 0 -> 51 bytes .../exception-handling/testdata/tag.5.wasm | Bin 0 -> 49 bytes .../exception-handling/testdata/tag.6.wasm | Bin 0 -> 49 bytes .../exception-handling/testdata/tag.7.wasm | Bin 0 -> 39 bytes .../exception-handling/testdata/tag.json | 1 + .../exception-handling/testdata/tag.wast | 65 +++ .../exception-handling/testdata/throw.0.wasm | Bin 0 -> 383 bytes .../exception-handling/testdata/throw.1.wasm | Bin 0 -> 26 bytes .../exception-handling/testdata/throw.2.wasm | Bin 0 -> 35 bytes .../exception-handling/testdata/throw.3.wasm | Bin 0 -> 37 bytes .../exception-handling/testdata/throw.json | 1 + .../exception-handling/testdata/throw.wast | 55 ++ .../testdata/throw_ref.0.wasm | Bin 0 -> 511 bytes .../testdata/throw_ref.1.wasm | Bin 0 -> 25 bytes .../testdata/throw_ref.2.wasm | Bin 0 -> 28 bytes .../testdata/throw_ref.json | 1 + .../testdata/throw_ref.wast | 118 ++++ .../testdata/try_table.0.wasm | Bin 0 -> 61 bytes .../testdata/try_table.1.wasm | Bin 0 -> 1523 bytes .../testdata/try_table.10.wasm | Bin 0 -> 30 bytes .../testdata/try_table.11.wasm | Bin 0 -> 32 bytes .../testdata/try_table.12.wasm | Bin 0 -> 44 bytes .../testdata/try_table.13.wasm | Bin 0 -> 291 bytes .../testdata/try_table.14.wasm | Bin 0 -> 89 bytes .../testdata/try_table.15.wasm | Bin 0 -> 99 bytes .../testdata/try_table.16.wasm | Bin 0 -> 98 bytes .../testdata/try_table.2.wasm | Bin 0 -> 146 bytes .../testdata/try_table.3.wat | 1 + .../testdata/try_table.4.wat | 1 + .../testdata/try_table.5.wasm | Bin 0 -> 123 bytes .../testdata/try_table.6.wasm | Bin 0 -> 29 bytes .../testdata/try_table.7.wasm | Bin 0 -> 31 bytes .../testdata/try_table.8.wasm | Bin 0 -> 36 bytes .../testdata/try_table.9.wasm | Bin 0 -> 41 bytes .../testdata/try_table.json | 1 + .../testdata/try_table.wast | 523 ++++++++++++++++++ .../integration_test/spectest/spectest.go | 59 +- .../spectest/spectest_test.go | 2 +- internal/logging/logging.go | 3 +- internal/testing/binaryencoding/encoder.go | 4 +- internal/testing/binaryencoding/import.go | 3 + internal/testing/binaryencoding/section.go | 10 + internal/wasm/binary/code.go | 3 +- internal/wasm/binary/decoder.go | 22 +- internal/wasm/binary/export.go | 2 +- internal/wasm/binary/import.go | 16 + internal/wasm/binary/section.go | 62 ++- internal/wasm/binary/value.go | 44 +- internal/wasm/counts.go | 2 + internal/wasm/exception.go | 9 + internal/wasm/func_validation.go | 182 +++++- internal/wasm/func_validation_test.go | 92 ++- internal/wasm/instruction.go | 41 ++ internal/wasm/module.go | 151 ++++- internal/wasm/module_test.go | 53 +- internal/wasm/store.go | 19 + internal/wasm/table_test.go | 4 +- internal/wasmruntime/errors.go | 4 + 101 files changed, 4452 insertions(+), 148 deletions(-) create mode 100644 experimental/exceptions_test.go create mode 100644 experimental/testdata/cpp_exceptions.cpp create mode 100644 experimental/testdata/cpp_exceptions.wasm create mode 100644 internal/integration_test/engine/eh_hammer_test.go create mode 100644 internal/integration_test/engine/exceptions_test.go create mode 100644 internal/integration_test/engine/testdata/eh_br_orphan.wasm create mode 100644 internal/integration_test/engine/testdata/eh_br_orphan.wat create mode 100644 internal/integration_test/engine/testdata/eh_br_stale_handler.wasm create mode 100644 internal/integration_test/engine/testdata/eh_br_stale_handler.wat create mode 100644 internal/integration_test/engine/testdata/eh_cross_callnative.wasm create mode 100644 internal/integration_test/engine/testdata/eh_cross_callnative.wat create mode 100644 internal/integration_test/engine/testdata/eh_pdfium.wasm create mode 100644 internal/integration_test/engine/testdata/eh_pdfium.wat create mode 100644 internal/integration_test/engine/testdata/eh_throw_ref_null.wasm create mode 100644 internal/integration_test/engine/testdata/eh_throw_ref_null.wat create mode 100644 internal/integration_test/spectest/exception-handling/spec_test.go create mode 100644 internal/integration_test/spectest/exception-handling/testdata/tag.0.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/tag.1.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/tag.2.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/tag.3.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/tag.4.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/tag.5.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/tag.6.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/tag.7.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/tag.json create mode 100644 internal/integration_test/spectest/exception-handling/testdata/tag.wast create mode 100644 internal/integration_test/spectest/exception-handling/testdata/throw.0.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/throw.1.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/throw.2.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/throw.3.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/throw.json create mode 100644 internal/integration_test/spectest/exception-handling/testdata/throw.wast create mode 100644 internal/integration_test/spectest/exception-handling/testdata/throw_ref.0.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/throw_ref.1.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/throw_ref.2.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/throw_ref.json create mode 100644 internal/integration_test/spectest/exception-handling/testdata/throw_ref.wast create mode 100644 internal/integration_test/spectest/exception-handling/testdata/try_table.0.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/try_table.1.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/try_table.10.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/try_table.11.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/try_table.12.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/try_table.13.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/try_table.14.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/try_table.15.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/try_table.16.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/try_table.2.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/try_table.3.wat create mode 100644 internal/integration_test/spectest/exception-handling/testdata/try_table.4.wat create mode 100644 internal/integration_test/spectest/exception-handling/testdata/try_table.5.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/try_table.6.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/try_table.7.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/try_table.8.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/try_table.9.wasm create mode 100644 internal/integration_test/spectest/exception-handling/testdata/try_table.json create mode 100644 internal/integration_test/spectest/exception-handling/testdata/try_table.wast create mode 100644 internal/wasm/exception.go diff --git a/.github/workflows/commit.yaml b/.github/workflows/commit.yaml index 1f2906a88..de5eff2ac 100644 --- a/.github/workflows/commit.yaml +++ b/.github/workflows/commit.yaml @@ -30,11 +30,14 @@ jobs: name: Pre-commit check runs-on: ubuntu-24.04 steps: - - name: Install latest wast2json - run: | # Needed for build.spectest. wabt includes wast2json. + - name: Install latest wast2json and wasm-tools + run: | # Needed for build.spectest. wabt includes wast2json; wasm-tools is used for exception-handling spectests. wabt_version=1.0.39 wabt_url=https://github.com/WebAssembly/wabt/releases/download/${wabt_version}/wabt-${wabt_version}-linux-x64.tar.gz curl -sSL ${wabt_url} | tar --strip-components 2 -C /usr/local/bin -xzf - wabt-${wabt_version}/bin/wast2json + wasm_tools_version=1.245.1 + wasm_tools_url=https://github.com/bytecodealliance/wasm-tools/releases/download/v${wasm_tools_version}/wasm-tools-${wasm_tools_version}-x86_64-linux.tar.gz + curl -sSL ${wasm_tools_url} | tar --strip-components 1 -C /usr/local/bin -xzf - wasm-tools-${wasm_tools_version}-x86_64-linux/wasm-tools - uses: actions/checkout@v6 diff --git a/Makefile b/Makefile index 582fffce9..97943b5a1 100644 --- a/Makefile +++ b/Makefile @@ -128,6 +128,10 @@ spec_version_tail_call := 88e97b0f742f4c3ee01fea683da130f344dd7b02 spectest_extended_const_dir := $(spectest_base_dir)/extended-const spectest_extended_const_testdata_dir := $(spectest_extended_const_dir)/testdata +spectest_exception_handling_dir := $(spectest_base_dir)/exception-handling +spectest_exception_handling_testdata_dir := $(spectest_exception_handling_dir)/testdata +spec_version_exception_handling := 13734f8fb871a5dab939070f893adbd90bffe28c + .PHONY: build.spectest build.spectest: @$(MAKE) build.spectest.v1 @@ -135,6 +139,7 @@ build.spectest: @$(MAKE) build.spectest.threads @$(MAKE) build.spectest.tail_call @$(MAKE) build.spectest.extended_const + @$(MAKE) build.spectest.exception_handling .PHONY: build.spectest.v1 build.spectest.v1: # Note: wabt by default uses >1.0 features, so wast2json flags might drift as they include more. See WebAssembly/wabt#1878 @@ -203,6 +208,17 @@ build.spectest.tail_call: wast2json --enable-tail-call --debug-names $$f; \ done +.PHONY: build.spectest.exception_handling +build.spectest.exception_handling: + @rm -rf $(spectest_exception_handling_testdata_dir) + @mkdir -p $(spectest_exception_handling_testdata_dir) + @cd $(spectest_exception_handling_testdata_dir) \ + && curl -sSL 'https://api.github.com/repos/WebAssembly/spec/contents/test/core/exceptions?ref=$(spec_version_exception_handling)' \ + | jq -r '.[]| .download_url' | grep -E ".wast" | xargs -Iurl curl -sJL url -O + @cd $(spectest_exception_handling_testdata_dir) && for f in `find . -name '*.wast'`; do \ + wasm-tools json-from-wast --wasm-dir . -o $$(basename $$f .wast).json $$f || true; \ + done + .PHONY: test test: @go test $(go_test_options) ./... diff --git a/api/features.go b/api/features.go index c739d3bf7..29405b093 100644 --- a/api/features.go +++ b/api/features.go @@ -209,6 +209,18 @@ func featureName(f CoreFeatures) string { case CoreFeatureSIMD: // match https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/simd/SIMD.md return "simd" + // The cases below cover features defined in the experimental package + // (experimental.CoreFeaturesThreads, CoreFeaturesTailCall, + // experimental.CoreFeaturesExtendedConst, experimental.CoreFeaturesExceptionHandling). + // They cannot be imported here (circular dependency), so we match by value. + case CoreFeatureSIMD << 1: // experimental.CoreFeaturesThreads + return "threads" + case CoreFeatureSIMD << 2: // experimental.CoreFeaturesTailCall + return "tail-call" + case CoreFeatureSIMD << 3: // experimental.CoreFeaturesExtendedConst + return "extended-const" + case CoreFeatureSIMD << 4: // experimental.CoreFeaturesExceptionHandling + return "exception-handling" } return "" } diff --git a/experimental/exceptions_test.go b/experimental/exceptions_test.go new file mode 100644 index 000000000..088202fdd --- /dev/null +++ b/experimental/exceptions_test.go @@ -0,0 +1,47 @@ +package experimental_test + +import ( + "context" + _ "embed" + "testing" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/experimental" + "github.com/tetratelabs/wazero/internal/testing/require" +) + +//go:embed testdata/cpp_exceptions.wasm +var cppExceptionsWasm []byte + +func TestCppExceptions(t *testing.T) { + ctx := context.Background() + + cfg := wazero.NewRuntimeConfig(). + WithCoreFeatures(api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling) + + r := wazero.NewRuntimeWithConfig(ctx, cfg) + defer r.Close(ctx) + + mod, err := r.InstantiateWithConfig(ctx, cppExceptionsWasm, + wazero.NewModuleConfig().WithStartFunctions("_initialize")) + require.NoError(t, err) + + tests := []struct { + name string + expected int32 + }{ + {"test_no_throw", 42}, + {"test_catch_specific", -1}, + {"test_catch_base", 1}, + {"test_rethrow", -42}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + res, err := mod.ExportedFunction(tc.name).Call(ctx) + require.NoError(t, err) + require.Equal(t, tc.expected, api.DecodeI32(res[0])) + }) + } +} diff --git a/experimental/features.go b/experimental/features.go index c1800b386..4d229fc6e 100644 --- a/experimental/features.go +++ b/experimental/features.go @@ -27,3 +27,8 @@ const CoreFeaturesTailCall = api.CoreFeatureSIMD << 2 // // See https://github.com/WebAssembly/extended-const for further details. const CoreFeaturesExtendedConst = api.CoreFeatureSIMD << 3 + +// CoreFeaturesExceptionHandling enables exception handling instructions. +// +// See https://github.com/WebAssembly/exception-handling for further details. +const CoreFeaturesExceptionHandling = api.CoreFeatureSIMD << 4 diff --git a/experimental/testdata/cpp_exceptions.cpp b/experimental/testdata/cpp_exceptions.cpp new file mode 100644 index 000000000..22661d23d --- /dev/null +++ b/experimental/testdata/cpp_exceptions.cpp @@ -0,0 +1,78 @@ +// Compile with Emscripten (legacy wasm EH), then translate to new EH with Binaryen: +// +// emcc -fwasm-exceptions -o cpp_exceptions_legacy.wasm cpp_exceptions.cpp \ +// -s EXPORTED_FUNCTIONS='["_test_no_throw","_test_catch_specific","_test_catch_base","_test_rethrow"]' \ +// -s ERROR_ON_UNDEFINED_SYMBOLS=0 -s STANDALONE_WASM -O2 --no-entry +// wasm-opt --translate-to-exnref \ +// --enable-exception-handling --enable-reference-types --enable-bulk-memory \ +// --enable-bulk-memory-opt --enable-multivalue --enable-mutable-globals \ +// --enable-sign-ext --enable-nontrapping-float-to-int \ +// -o cpp_exceptions.wasm cpp_exceptions_legacy.wasm + +#include +#include + +class MyException : public std::exception { + int code_; +public: + MyException(int code) : code_(code) {} + int code() const { return code_; } + const char* what() const noexcept override { return "MyException"; } +}; + +static int may_throw(int v) { + if (v < 0) { + throw MyException(v); + } + if (v == 0) { + throw std::runtime_error("zero"); + } + return v * 2; +} + +extern "C" { + +// Returns may_throw(21), expected 42. Returns -1 on unexpected exception. +int test_no_throw() { + try { + return may_throw(21); + } catch (...) { + return -1; + } +} + +// Throws MyException(-1), catches it, returns the code. Expected: -1. +int test_catch_specific() { + try { + may_throw(-1); + return 0; // should not reach + } catch (const MyException& e) { + return e.code(); + } +} + +// Throws std::runtime_error, catches via std::exception base. Returns 1 on match, 0 otherwise. +int test_catch_base() { + try { + may_throw(0); + return 0; + } catch (const std::exception& e) { + return 1; + } +} + +// Throws, catches, rethrows, re-catches. Returns the code from re-catch. Expected: -42. +int test_rethrow() { + try { + try { + may_throw(-42); + } catch (const MyException&) { + throw; + } + } catch (const MyException& e) { + return e.code(); + } + return 0; +} + +} // extern "C" diff --git a/experimental/testdata/cpp_exceptions.wasm b/experimental/testdata/cpp_exceptions.wasm new file mode 100644 index 0000000000000000000000000000000000000000..bcb3295f9412b95d7b0849e90b5058523724a3e1 GIT binary patch literal 15829 zcmbW8UyNK=eaFu^_s;C>-dXP?USMMza^~WILmg~Ga6&+^$0ANm2necHs1IE>_6F}v z{AYJ3PJ&R@ggoSdN>K|Et*Dd-)I=ze{D4{>A{A5~@_;HHLdgSCsf1J>Dzy(tedDCob#WX|B~PK+x+9cJM9Z=7cOl+9o+nAZELi0@zToZ z{N|-6o!@WY&#sQno?qFzymof|vGubqEKIdNy1KRIimtV6uH|9n9V;vA7tgJ4uALpN zJod!Jv!nG(7gt8Bk3PNzRXkcBt*<`5{*^W7+AAxg&DG0pX7)YaC0{Mw@xE5uEiAs{ zj@E$#hYr{8X}!09XX~!_AL;sH?zfju1Xqzy4m=_o>sHuU*_5 zh2`{vr6aWY)8#S^w;u^*-}Ux0M=a;6=oTth{irBrw+n~}BBR_{le=|RFP#2qi@bhO z(WMoO^oxC&yxf6?a?o!lAGXs!%=KMzT<&r1!FhLpS(XbHPksP*#m0m4K02PwJF%iC zwby8uu0H!*Cwj=O!!U-;boFb`KKIl{zMQ5zeLYk7L4Ps>EU?nBVv6}e9b!oSk%N8Y zrg?j_b(r_*JBv0Y0tmMS$C_0tk#tDk6b}sf!9EGrRWUF-qU%H?YFg06u|ya0q8A5V za%9Qq-73UClk$$}|g+_|B*R`deVdpNFDlDxAX6pWPHkJKBYfDMWQZQVA8E zemfRoHdFKBE^0v39z03A83O5>CT3^yIX>uzfS~R!j-R^v%-2s{{nuCi<~OhZ_22)q z`@B6zrboi!@MKs_-`4*I(E)-)J4|HwMI9Yi_1=jCs1Oqb2}wXP0APt&O?n(>=I9dJ zjo^1U0jD?NB&&3(x5eA5r)Iu->bKXg zU%&I~>G_@@=3f13?N8Biec_9+>0heDCtd1|9+`)$7klYA45sacitaHOu&STG-&fo3 zrPHvU%M6Oi>HkpEZ~PhehF)CK;48Dl#=6T_1mS@N+En7g3AzzFaVsUzH%AivlYntUv= z0L@b-ua<75UauHs9oUShUj#%yN`Zf5PtgrGH_q3s9ZHIZ5-`xI!xSa0CpYSLLrV}5 zT}VPw&;l6^5!}hrp=Ai`Murv~#8M*dAoxzgYrt~9NIwv1ecTe9dL9&4Ms;iB!OVOs zwqXKp1*QUGaI7o{KsvFdOP5uQ3LjgU3X=%4n*Lzi7DWo_Ws^ZA=T@ChVT7n}ubH!i z7AWmAQ9l$C4voFqL4xiUxdR1(ep9-K#@sW*VrVwvzU6AN^-IDm0kWL?aoFP`V` z48ZNi8MDXkY?Ze05-$DI_QTe#0t~R%FB|S>arfy?+!~O*etibeqO|vcp3apo*KT-4 zn8baLhUpW7ey0K&5=;g=x)}>i+Hrkz1zeco4j%d|L;82poev!x?Fe!#vvwp!L6MN% zu02$sbGUQeLdY>Ww{g|t{oPkmxiOG1W=+&QjN|Nv z^o6L$D()j5(_pbYtwZQJ!+cy~$XY}J2PZ;BDMc1|RVxk>IS?I!GD|`#Vx}cU&j3Vj z8bx!rSJ9XTty$46Q#4;2MYl}Rpe_b^^{i-+v`f+Uy-{>KD|%+fsGU5|*oa_JMpvZX z=TNOV1Hd_Lf*1>Ij7`;iG#by&LpEBwEG4sAlvMBumF!3($95&e;CKKWGhH*o3TL3> z8fGT~uIICf;H^6)Oo##|CwWB;~FB3}wpa6ndaDJ->Kr`#z?< zx_Ij9i&yz8_3f?wagN}hE^nkazxT86wYIeK3pqZeU+yKZ}fJ&&=T278Z zCWUV>Ar8*W3*CZ*VJNvC-ZX8W2A?f*v8y>ACujnN8;k^lLxzU|l)B*t2^#{A?a$4V z&KZT0NT$`tHbP~=c?>=lXXZ-+Ec0V356$~-ONkGY!K_*hLs_+h(|ABqQYaHC!ou-E zY>RMI-7Tvua!BgL_E41g;YRIyo!ndNf>m{eIks0ry5q`|JHt0S!Ioi1Fi|@_UzF?_~(;{=YMh3M6DP5)><5 z1L-$<()P*lreeOVC&hZtHw-%^|;YcYM9G0 z7QiTo-b8>cELX~aUCOFKO}v*3W`36dO~CJug+wz*$B+VY^Eh3SvQ|2%jH#9WbN}7J z-4}Y?{wm*gXyaQ)`R)w>X~4kqC0w9k0=g7+afyi9DkVsSB@BTeM`4s>M46r;k5iIH z?lQy^SBZ)InO)$vP1G-i}`_=&x74De=~hzOJ2dt*bYU1%4FgQ`st0^QFvGm%S{^5f*3cd z+eXRD2yUFFX;nk@$*?4<0+|7bv&?|RLzw{}1l&2fnWSIA%$CTn0IWzasJ4^_)NKXJ zmPARcK8cph-j=F_gNYuFJx1KsV$3V|C&n}Xl&a#TV|KoTD7tVrut3I>Fvwmd+X({+ z2^3ry@*fz%Fqd%&;l_zgE+V8I+_{-37}C~cNb11{$_4cC1ybk@)I@*Q8@Qf>P{~n)4o`@ z$iCW^De)EMD`lsuA|-YO4g{UI>QX5&%UFL5tW-*D-y5_PIa5L;?3j=tlfLEo58;#p zHHPwk6BMKa@LZgf>_)uY%P@y2H|9t~;|$cmb(~S(p4hUxMPjs-2xY_(EQtqbpdZdf zfHgLkOuq1@G}0a{?mUPc!+8n%NoMgP0()!XEyj+*Fl&gq(NmNYPa)&H!Be!%QVIlAF7gw-28MwzPwAU86PVIhjK6OU1jJw|KFV_?*=obniC z^+u5uaq<{QYT_}N+dM{@JqFT}$1vSXZB|m4$0+47qJ~9kD-KX$75)AdA5dC zF)mRsS*JERbQx{p@Pf@!Ca94L3H)r%QG+1bAc?VuaN!^czXOt5LPlXoVg?tTA&HSz zzSpy~5fm9u$eQ4Yo(+z;14m7%nQ_DlWc|#FGZc<+?#2e~R1?CHJy;w+U~Zoll*hq~ zQtq9+o^VNTiEivD=reXcNY2XjCL9^ckOf+4EZ*NF9x)To6_5Nl9kLFOuyGr5pR%+4XWP?)1@x9{ag$AzIhPK%O>xH$=( z^43{}RBa4dqP}9GW&#y&upnn-xWeJmXR!|2qp&E+*Ipg8>p|f^**Lf+)YBIUbN*G0^p423YBgg zDjhG+WL;j!#v1Wwf-3OPn=LL)1)~di>j0%Y@guC#L`p}bq~N52^En^GOXYk_4pI_0 z6pI`waa_5AYX#gLROO9uQRPRqZEGoFqGcnA{dFiyOM2TW>Kp-+$t@xCIUyrkQ^2(S z=^Ep-s8oICp%soD9DSCtq@@}XtunU zcawPWhOiwkt^&kdqo~@IhB%pIPIDOrf4BDHnrHpV$|+q-czcH4T5H08Fz(O$Rx_Hkrx zyJ+0@l*q4J`|V+5+HIrTrWo3dksY_KVrUOWkZMy5?Z$|#e&V(p z?a(%M+l{hrtZ>4_Z5Ig#xwpc}J8nTxmbT_(7cjURy6k7-0qY) zdgZ3A2YGFzcoVM;YuXpn?4q;bRD28Z0(QfG4p#&Xo?+M;F&0Kuy^E1|7jEx)gX+z; zy<+dr$*k@8Kz`re;e-S8rhjt--tW}2HRE-9q%Q4l!CF}{77ri9NiHb5vW?o&VUTt@ z4nx=U9`ryuZkwyuws|SGvqgsv6P}h{ysO7%xw0!9(xCyTQphsh0TkC6_FY)!#wL$c ziBdhMVc3jT+^EdjlaZnlg1zO0?%8ms5xIMi}<<(V( zm8iAa_A(vALVRd8A`vwEVt)Ijg8j8cju8C@tf-Z5x_NC;)f!dgAcZS6yZmg670Zp| z$F(mh{RPg_hxXj5l@~S?&X4&(y%~lIg76Z{GY9p)g(RGJNpQywB*zjy=CNC*zkZ!= zVI(Ho)|TAIyl~dcsFKg8zv8GDCZ1lxs#OvFkG69xpP6|??_!R5JC+p-ar5FL^J2W$ z7IzfOdUQudcJ(PrE-lqvd2)7~(qA$8Vl#P9y|tas%*|YEDceCC@*45XVH7)B!yCak zE5=}U_AMQ=ap|XSuwdtM?(&|qD><<-!?K6N-qQ5xoa&JH^$IXP9ajC@dl%D)$X>7d za}rsm2!SO8B1e}RnF#%$hZ8~qmM0zN(NA11pYxm2X2s}lbMAnx7&6h&$bqZsSr2)8 zju+YdRz7j}I%f2O@;U+H8@iE5($X;Ll^u6+ICXI8zM>();K<(^NHGT>3y47ZgZ>(yRcLbnk*Hd+32XI z7&Kv#(Mm|9^mZOgh-tctp+<4v7g<41BW=j84y`b54=4@%TxH<3Z9RT zgH=EBzSLQ%g`rM8FP4Ke4>~Dlrm~akxj!3Vsh0! zO_g4S05h-8Wc0i2cbe!lW=cCQ;;PrhRhb_e+oX<@8D6s`;SqInDo^G$nb`(6+uS$@ zZ5(_w^JWvV4>8)J;vk`#WQ%!XHm$sG+ol!Xh4+gver-t`8aRoeo&3>xoyN7cWV8iq z>C|L4UA!ipePn>#q;iab5_&u#js`c03zSn*AGL5~qUoXV(B?5=qw#T)m!_f{%_?Xl zjcbcT{Iwc%erC#hVO<2z05`BhiZSUZf2p}DA&irJCB#*H9pN0P6GR;t5E{zU8OHMb zoc*(=I^5&T!C9}m;9J9Nlus&P{vPN1)Q(*A#LEHbMQx!=Ay##A)p$N#{j8*71i^A0 z9QTx_rk2;beO1NuSSGlmB%fhQEo+`{mdt}8)V=nVKwYL^bOD@2DSx<_dvpT`DKdXrEyd4E~xO#oSd`ZNXo;r#7r9`SL$eNBg9`1ALLdr1%2q~vEV^j0f{=+_^of+bkXtn?n+JM^PFVdUCR*8P1(exY*a8X<8mCo?6#X}aVUJvrX}ghlQl_hBxa?uaA-i2D#1bB8=T89s$BRY8%pWTisFZEXfZfFhmj zglq!h1y-Q$Pjf_$A$TnoS0-Gh|H5%}xc&#BhKYKUm`z^HM@eI&vUP?OR)66215Njvp1Dd4U0@LBDI%sp6?U0pO zSqA($?fmYly7!jPG^3onz^!tCFZ?fARDbIkrPV$XBn+C%9QbLvmnIst6D=0UOd*H~ zr2XR4P_Tx1AlQ;WvU@8uV$oMoWPLR%OokZcK#26}@R<9mdOkQ&AbUgu zy@RGMc@4>mD|pgrCjf-z#iLnn?#V!UPzxx8lQJOs46sZvx&}0ivB5F-KAK8%rpoE= z6`f;=L8$O$QZvi*_qdO+I8#$kT4cM&-3vZ*`o{*=Q4@m&X03h1SUaLyxNOL8lnyEA zdasJc%gOSp>bw3Al>|}ZcZve>?TEr6ZZ3WMv)?~}!;+(kVq%YsCa zWlfw584`DhI=Bhfd*1;ILo?h1p6ZR@3q=IZe-jWGbKNrKEO8y_>W1at^&cY^HulYT zTqw#p{(P&=X?R>ichLI)hv>|OaQ{c#bF8yC=18{Txkv$gcR7oN$D6d3)@HAXCT%*V z)V28g!AV;lOC-Nxtoh+dzdY7l>7>op$UX>NUE%1|zOr;d9z4wJfS`8oawwO7u;U7O zp}*la&J!P)XeEH)>oIW^cTdru<~hmUf}ratRy+9Q9e)L zJSrJ=b~S%NH1X&jF&$MTx8s~dYnDt!=-x;6zbec!?jJ2%zwbyF-3; z1&`IMVls*Pqx~+%%(9)Rb6bJ!ov6HZLNXb$HjbFIdeBUpL0}rXc9Sp4ceAHc;?N~6 zpvA*Fv=&V!NIe+3Fi<&TwE5s&UFs;dPp|z2zO(5yw9~@eb%zm{sjbH;>wLF3Y~D|?MG+T1 zfl=|iN3J8rUGKOqy#reEID)KhcY;580T!K0AofH$;NfbgdVS_PCy2D=`9 zIFrAZJXa4r|3TZik8{zG ztgFU`o+gCeemq%%kK9W|Jdr|k3YJ4 zcDQnZn@eYH_*d38FWrLp$1ZKIt$+FA4!@IrYb)nhFP?jRZ8JV`aqTI7emYt^_xRJ1 zADON_8CR!AOpA3&nuvHj?*E&V->yEfd2#KW+ZvsF;DJY1&#f5o?yP?MD#fhEvyZQC zZN(?oN9Qj+G1B^%9vwi_CxwKpYk{KRgSt)c_oDmf(feml9A8<<625Zc#L1PFt@G;_ zOuqaW_OVM3(%t%>qyG^;yZhhASS`J00As#L-&wA6d@4R&KJ$F`@mb*0)0ou@r;eHWi4KB8wd>3`zGn@?OEtzTGMS=-#aw8?X=^Fp3WKs7DEu5?^XMjyWX*c>+tz>`G06S69xbP literal 0 HcmV?d00001 diff --git a/internal/engine/interpreter/compiler.go b/internal/engine/interpreter/compiler.go index 4269d237b..569f75eec 100644 --- a/internal/engine/interpreter/compiler.go +++ b/internal/engine/interpreter/compiler.go @@ -21,6 +21,7 @@ const ( controlFrameKindLoop controlFrameKindIfWithElse controlFrameKindIfWithoutElse + controlFrameKindTryTable ) type ( @@ -57,7 +58,8 @@ func (c *controlFrame) asLabel() label { case controlFrameKindFunction: return newLabel(labelKindReturn, 0) case controlFrameKindIfWithElse, - controlFrameKindIfWithoutElse: + controlFrameKindIfWithoutElse, + controlFrameKindTryTable: return newLabel(labelKindContinuation, c.frameID) } panic(fmt.Sprintf("unreachable: a bug in interpreterir implementation: %v", c.kind)) @@ -187,6 +189,8 @@ type compiler struct { funcs []uint32 // globals holds the global types for all declared globals in the module where the target function exists. globals []wasm.GlobalType + // tags holds the type indexes for all declared tags in the module where the target function exists. + tags []uint32 // needSourceOffset is true if this module requires DWARF based stack trace. needSourceOffset bool @@ -254,6 +258,9 @@ type compilationResult struct { LabelCallers map[label]uint32 // UsesMemory is true if this function might use memory. UsesMemory bool + // PendingExceptionTable holds unresolved exception table entries, built during + // compilation. Labels are resolved to final PCs in lowerIR. + PendingExceptionTable []pendingExceptionTableEntry // The following fields are per-module values, not per-function. @@ -276,7 +283,7 @@ type compilationResult struct { // newCompiler returns the new *compiler for the given parameters. // Use compiler.Next function to get compilation result per function. func newCompiler(enabledFeatures api.CoreFeatures, callFrameStackSizeInUint64 int, module *wasm.Module, ensureTermination bool) (*compiler, error) { - functions, globals, mem, tables, err := module.AllDeclarations() + functions, globals, mem, tables, tags, err := module.AllDeclarations() if err != nil { return nil, err } @@ -313,6 +320,7 @@ func newCompiler(enabledFeatures api.CoreFeatures, callFrameStackSizeInUint64 in }, globals: globals, funcs: functions, + tags: tags, types: types, ensureTermination: ensureTermination, br: bytes.NewReader(nil), @@ -336,6 +344,7 @@ func (c *compiler) Next() (*compilationResult, error) { c.result.Operations = c.result.Operations[:0] c.result.IROperationSourceOffsetsInWasmBinary = c.result.IROperationSourceOffsetsInWasmBinary[:0] c.result.UsesMemory = false + c.result.PendingExceptionTable = c.result.PendingExceptionTable[:0] // Clears the existing entries in LabelCallers. for frameID := uint32(0); frameID <= c.currentFrameID; frameID++ { for k := labelKind(0); k < labelKindNum; k++ { @@ -667,7 +676,8 @@ operatorSwitch: // Initiate the continuation. c.emit(newOperationLabel(continuationLabel)) case controlFrameKindBlockWithContinuationLabel, - controlFrameKindIfWithElse: + controlFrameKindIfWithElse, + controlFrameKindTryTable: continuationLabel := newLabel(labelKindContinuation, frame.frameID) c.result.LabelCallers[continuationLabel]++ c.emit(dropOp) @@ -800,6 +810,95 @@ operatorSwitch: // That means subsequent instructions in the current control frame are "unreachable" // and can be safely removed. c.markUnreachable() + case wasm.OpcodeThrow: + if c.unreachableState.on { + break operatorSwitch + } + // Pop the tag's param values from the stack. + if index < uint32(len(c.tags)) { + tagType := &c.types[c.tags[index]] + for i := len(tagType.Params) - 1; i >= 0; i-- { + c.stackPop() + } + } + c.emit(newOperationThrow(index)) + c.markUnreachable() + case wasm.OpcodeThrowRef: + if c.unreachableState.on { + break operatorSwitch + } + // Pop the exnref from the stack. + c.stackPop() + c.emit(newOperationThrowRef()) + c.markUnreachable() + case wasm.OpcodeTryTable: + c.br.Reset(c.body[c.pc+1:]) + bt, num, err := wasm.DecodeBlockType(c.types, c.br, c.enabledFeatures) + if err != nil { + return fmt.Errorf("reading block type for try_table instruction: %w", err) + } + c.pc += num + + if c.unreachableState.on { + c.unreachableState.depth++ + // Still need to skip the catch clause bytes. + c.pc++ + catchCount, catchNum, err := leb128.LoadUint32(c.body[c.pc:]) + if err != nil { + return fmt.Errorf("reading catch count for try_table: %w", err) + } + c.pc += catchNum - 1 + for i := uint32(0); i < catchCount; i++ { + if _, _, _, err := c.parseCatchClause(); err != nil { + return err + } + } + break operatorSwitch + } + + // Read catch clause count. + c.pc++ + catchCount, catchNum, err := leb128.LoadUint32(c.body[c.pc:]) + if err != nil { + return fmt.Errorf("reading catch count for try_table: %w", err) + } + c.pc += catchNum - 1 + + // Parse catch clauses. + var pendingClauses []pendingCatchClause + for i := uint32(0); i < catchCount; i++ { + kind, tagIdx, labelIdx, err := c.parseCatchClause() + if err != nil { + return err + } + // Resolve the label from the control frame stack. + targetFrame := c.controlFrames.get(int(labelIdx)) + targetFrame.ensureContinuation() + targetLabel := targetFrame.asLabel() + c.result.LabelCallers[targetLabel]++ + pendingClauses = append(pendingClauses, pendingCatchClause{ + kind: kind, + tagIndex: tagIdx, + targetLabel: targetLabel, + targetStackDepth: targetFrame.originalStackLenWithoutParamUint64, + }) + } + + // Create a control frame for the try_table block. + frameID := c.nextFrameID() + c.result.PendingExceptionTable = append(c.result.PendingExceptionTable, pendingExceptionTableEntry{ + startOpIndex: len(c.result.Operations), + continuationFrameID: frameID, + clauses: pendingClauses, + }) + frame := controlFrame{ + frameID: frameID, + originalStackLenWithoutParam: len(c.stack) - len(bt.Params), + originalStackLenWithoutParamUint64: c.stackLenInUint64 - bt.ParamNumInUint64, + kind: controlFrameKindTryTable, + blockType: bt, + } + c.controlFrames.push(frame) case wasm.OpcodeCall: c.emit( newOperationCall(index), @@ -3492,7 +3591,9 @@ func (c *compiler) applyToStack(opcode wasm.Opcode) (index uint32, err error) { wasm.OpcodeGlobalSet, // tail-call proposal wasm.OpcodeTailCallReturnCall, - wasm.OpcodeTailCallReturnCallIndirect: + wasm.OpcodeTailCallReturnCallIndirect, + // exception handling - throw reads tag index + wasm.OpcodeThrow: // Assumes that we are at the opcode now so skip it before read immediates. v, num, err := leb128.LoadUint32(c.body[c.pc+1:]) if err != nil { @@ -3605,7 +3706,7 @@ func (c *compiler) emitDefaultValue(t wasm.ValueType) { case wasm.ValueTypeI32: c.stackPush(unsignedTypeI32) c.emit(newOperationConstI32(0)) - case wasm.ValueTypeI64, wasm.ValueTypeExternref, wasm.ValueTypeFuncref: + case wasm.ValueTypeI64, wasm.ValueTypeExternref, wasm.ValueTypeFuncref, wasm.ValueTypeExnref: c.stackPush(unsignedTypeI64) c.emit(newOperationConstI64(0)) case wasm.ValueTypeF32: @@ -3673,3 +3774,30 @@ func (c *compiler) readMemoryArg(tag string) (memoryArg, error) { c.pc += num return memoryArg{Offset: offset, Alignment: alignment}, nil } + +// parseCatchClause parses a single catch clause from the bytecode at c.pc, +// advancing c.pc past the clause. Returns the kind, tag index (0 for catch_all +// variants), and label index. +func (c *compiler) parseCatchClause() (kind byte, tagIdx, labelIdx uint32, err error) { + var n uint64 + c.pc++ + kind = c.body[c.pc] + switch kind { + case wasm.CatchKindCatch, wasm.CatchKindCatchRef: + c.pc++ + tagIdx, n, err = leb128.LoadUint32(c.body[c.pc:]) + if err != nil { + err = fmt.Errorf("reading catch tag index: %w", err) + return + } + c.pc += n - 1 + } + c.pc++ + labelIdx, n, err = leb128.LoadUint32(c.body[c.pc:]) + if err != nil { + err = fmt.Errorf("reading catch label index: %w", err) + return + } + c.pc += n - 1 + return +} diff --git a/internal/engine/interpreter/interpreter.go b/internal/engine/interpreter/interpreter.go index 6b4d5b23f..6e6a51224 100644 --- a/internal/engine/interpreter/interpreter.go +++ b/internal/engine/interpreter/interpreter.go @@ -130,6 +130,39 @@ func (e *moduleEngine) OwnsGlobals() bool { return false } // MemoryGrown implements wasm.ModuleEngine. func (e *moduleEngine) MemoryGrown() {} +// restorable is implemented by panic values that can restore callEngine state. +// Both *snapshot (snapshotter API) and *thrownException (exception handling) +// implement this interface. +type restorable interface { + // canRestore unwinds ce.frames to callerFrameCount and checks whether a + // handler exists at that depth. If no handler is found, the caller + // re-panics and the next outer callWithUnwind unwinds further. + canRestore(ce *callEngine, callerFrameCount int) bool + // doRestore restores the callEngine state to the given stack frame depth. + doRestore(ce *callEngine, callerFrameCount int) +} + +// thrownException is the panic value for wasm exception propagation. +type thrownException struct { + exception *wasm.Exception + // Fields populated by canRestore for doRestore. + clause *exceptionTableCatchClause + values []uint64 +} + +func (t *thrownException) canRestore(ce *callEngine, callerFrameCount int) bool { + ce.frames = ce.frames[:callerFrameCount] + frame := ce.frames[callerFrameCount-1] + t.clause, t.values = searchExceptionTable(t.exception, frame) + return t.clause != nil +} + +func (t *thrownException) doRestore(ce *callEngine, callerFrameCount int) { + frame := ce.frames[callerFrameCount-1] + ce.applyExceptionHandler(frame, t.clause, t.values) + t.clause, t.values = nil, nil +} + // callEngine holds context per moduleEngine.Call, and shared across all the // function calls originating from the same moduleEngine.Call execution. // @@ -151,6 +184,94 @@ type callEngine struct { stackIterator stackIterator } +// matchCatchClause checks whether a single catch clause matches the given exception. +// Returns whether it matched and the values to push onto the stack. +func matchCatchClause(kind byte, clauseTag *wasm.TagInstance, exn *wasm.Exception) (matched bool, values []uint64) { + switch kind { + case wasm.CatchKindCatch: + if exn.Tag == clauseTag { + return true, slices.Clone(exn.Params) + } + case wasm.CatchKindCatchRef: + if exn.Tag == clauseTag { + values = slices.Clone(exn.Params) + values = append(values, uint64(uintptr(unsafe.Pointer(exn)))) + return true, values + } + case wasm.CatchKindCatchAll: + return true, nil + case wasm.CatchKindCatchAllRef: + return true, []uint64{uint64(uintptr(unsafe.Pointer(exn)))} + } + return false, nil +} + +// searchExceptionTable searches the compiled function's static exception table +// for a handler matching the given exception at the current PC. Returns the +// matched clause and catch values, or nil if no handler matches. Searches +// backwards so inner try_tables (which have higher indices) are checked first. +// This function is pure — it does not modify callEngine state. +func searchExceptionTable(exn *wasm.Exception, frame *callFrame) (*exceptionTableCatchClause, []uint64) { + table := frame.f.parent.exceptionTable + pc := frame.pc + for i := len(table) - 1; i >= 0; i-- { + entry := &table[i] + if pc < entry.startPC || pc >= entry.endPC { + continue + } + for j := range entry.clauses { + clause := &entry.clauses[j] + var clauseTag *wasm.TagInstance + if clause.kind == wasm.CatchKindCatch || clause.kind == wasm.CatchKindCatchRef { + clauseTag = frame.f.moduleInstance.Tags[clause.tagIndex] + } + matched, values := matchCatchClause(clause.kind, clauseTag, exn) + if matched { + return clause, values + } + } + } + return nil, nil +} + +// applyExceptionHandler applies a matched exception table clause to the callEngine state. +func (ce *callEngine) applyExceptionHandler(frame *callFrame, clause *exceptionTableCatchClause, values []uint64) { + ce.stack = ce.stack[:frame.base-frame.f.funcType.ParamNumInUint64+clause.targetStackDepth] + ce.stack = append(ce.stack, values...) + frame.pc = clause.targetPC +} + +// callWithUnwind calls the target function with support for stack unwinding +// (exception handling and snapshot restores). Returns true if the frame was +// unwound (caller should refresh frame/body/bodyLen and continue the loop). +// Returns false on normal return (caller should do frame.pc++). +func (ce *callEngine) callWithUnwind(ctx context.Context, m *wasm.ModuleInstance, tf *function) bool { + // Short-circuit: skip defer/recover overhead when neither exception + // handlers nor the snapshotter are active for the calling frame. + frame := ce.frames[len(ce.frames)-1] + if len(frame.f.parent.exceptionTable) == 0 && ctx.Value(expctxkeys.EnableSnapshotterKey{}) == nil { + ce.callFunction(ctx, m, tf) + return false + } + + callerFrameCount := len(ce.frames) + caught := false + func() { + defer func() { + if r := recover(); r != nil { + if v, ok := r.(restorable); ok && v.canRestore(ce, callerFrameCount) { + v.doRestore(ce, callerFrameCount) + caught = true + return + } + panic(r) + } + }() + ce.callFunction(ctx, m, tf) + }() + return caught +} + func (e *moduleEngine) newCallEngine(compiled *function) *callEngine { return &callEngine{f: compiled} } @@ -233,6 +354,7 @@ type callFrame struct { type compiledFunction struct { source *wasm.Module body []unionOperation + exceptionTable []exceptionTableEntry listener experimental.FunctionListener offsetsInWasmBinary []uint64 hostFn interface{} @@ -284,7 +406,11 @@ func (s *snapshot) Restore(ret []uint64) { panic(s) } -func (s *snapshot) doRestore() { +func (s *snapshot) canRestore(ce *callEngine, _ int) bool { + return s.ce == ce +} + +func (s *snapshot) doRestore(_ *callEngine, _ int) { ce := s.ce ce.stack = s.stack @@ -495,6 +621,35 @@ func (e *engine) lowerIR(ir *compilationResult, ret *compiledFunction) error { e.setLabelAddress(&op.Us[1], label(op.Us[1]), labelAddressResolutions) } } + + // Resolve exception table entries (translate labels to PC). + if len(ir.PendingExceptionTable) > 0 { + ret.exceptionTable = make([]exceptionTableEntry, len(ir.PendingExceptionTable)) + for i, pe := range ir.PendingExceptionTable { + contLabel := newLabel(labelKindContinuation, pe.continuationFrameID) + var endPC uint64 + e.setLabelAddress(&endPC, contLabel, labelAddressResolutions) + + clauses := make([]exceptionTableCatchClause, len(pe.clauses)) + for j, clause := range pe.clauses { + var targetPC uint64 + e.setLabelAddress(&targetPC, clause.targetLabel, labelAddressResolutions) + clauses[j] = exceptionTableCatchClause{ + kind: clause.kind, + tagIndex: clause.tagIndex, + targetPC: targetPC, + targetStackDepth: clause.targetStackDepth, + } + } + + ret.exceptionTable[i] = exceptionTableEntry{ + startPC: uint64(pe.startOpIndex), + endPC: endPC, + clauses: clauses, + } + } + } + return nil } @@ -644,6 +799,11 @@ func (ce *callEngine) recoverOnCall(ctx context.Context, m *wasm.ModuleInstance, panic(s) } + // If an exception reached the top level without being caught, convert it to an uncaught exception error. + if _, ok := v.(*thrownException); ok { + v = wasmruntime.ErrRuntimeUncaughtException + } + builder := wasmdebug.NewErrorBuilder() frameCount := len(ce.frames) functionListeners := make([]functionListenerInvocation, 0, 16) @@ -761,30 +921,26 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, m *wasm.ModuleInstance ce.drop(op.Us[v+1]) frame.pc = op.Us[v] case operationKindCall: - func() { - if ctx.Value(expctxkeys.EnableSnapshotterKey{}) != nil { - defer func() { - if r := recover(); r != nil { - if s, ok := r.(*snapshot); ok && s.ce == ce { - s.doRestore() - frame = ce.frames[len(ce.frames)-1] - body = frame.f.parent.body - bodyLen = uint64(len(body)) - } else { - panic(r) - } - } - }() - } - ce.callFunction(ctx, f.moduleInstance, &functions[op.U1]) - }() + frameUnwound := ce.callWithUnwind(ctx, f.moduleInstance, &functions[op.U1]) + if frameUnwound { + frame = ce.frames[len(ce.frames)-1] + body = frame.f.parent.body + bodyLen = uint64(len(body)) + continue + } frame.pc++ case operationKindCallIndirect: offset := ce.popValue() table := tables[op.U2] tf := ce.functionForOffset(table, offset, typeIDs[op.U1]) - ce.callFunction(ctx, f.moduleInstance, tf) + frameUnwound := ce.callWithUnwind(ctx, f.moduleInstance, tf) + if frameUnwound { + frame = ce.frames[len(ce.frames)-1] + body = frame.f.parent.body + bodyLen = uint64(len(body)) + continue + } frame.pc++ case operationKindDrop: ce.drop(op.U1) @@ -4372,6 +4528,35 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, m *wasm.ModuleInstance memoryInst.Mux.Unlock() ce.pushValue(uint64(old)) frame.pc++ + case operationKindThrow: + tagIndex := uint32(op.U1) + tag := moduleInst.Tags[tagIndex] + paramCount := len(tag.Type.Params) + params := make([]uint64, paramCount) + for i := paramCount - 1; i >= 0; i-- { + params[i] = ce.popValue() + } + exn := &wasm.Exception{Tag: tag, Params: params} + if clause, values := searchExceptionTable(exn, frame); clause != nil { + ce.applyExceptionHandler(frame, clause, values) + continue + } + panic(&thrownException{exception: exn}) + + case operationKindThrowRef: + v := ce.popValue() + if v == 0 { + panic(wasmruntime.ErrRuntimeNullReference) // throw_ref on null exnref traps + } + // Read the Exception pointer directly from the uint64 value to avoid + // conversion from uintptr into unsafe.Pointer, which triggers checkptr. + exn := *(**wasm.Exception)(unsafe.Pointer(&v)) + if clause, values := searchExceptionTable(exn, frame); clause != nil { + ce.applyExceptionHandler(frame, clause, values) + continue + } + panic(&thrownException{exception: exn}) + case operationKindTailCallReturnCall: f := &functions[op.U1] ce.dropForTailCall(frame, f) @@ -4387,7 +4572,13 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, m *wasm.ModuleInstance // For details, see internal/engine/RATIONALE.md if tf.moduleInstance != f.moduleInstance { // Revert to a normal call. - ce.callFunction(ctx, f.moduleInstance, tf) + frameUnwound := ce.callWithUnwind(ctx, f.moduleInstance, tf) + if frameUnwound { + frame = ce.frames[len(ce.frames)-1] + body = frame.f.parent.body + bodyLen = uint64(len(body)) + continue + } // Return ce.drop(op.Us[0]) // Jump to the function frame (return) diff --git a/internal/engine/interpreter/operations.go b/internal/engine/interpreter/operations.go index db3cfa250..2fc115462 100644 --- a/internal/engine/interpreter/operations.go +++ b/internal/engine/interpreter/operations.go @@ -449,6 +449,10 @@ func (o operationKind) String() (ret string) { ret = "operationKindTailCallReturnCall" case operationKindTailCallReturnCallIndirect: ret = "operationKindTailCallReturnCallIndirect" + case operationKindThrow: + ret = "operationKindThrow" + case operationKindThrowRef: + ret = "operationKindThrowRef" default: panic(fmt.Errorf("unknown operation %d", o)) } @@ -777,6 +781,11 @@ const ( // operationKindTailCallReturnCallIndirect is the Kind for newOperationKindTailCallReturnCallIndirect. operationKindTailCallReturnCallIndirect + // operationKindThrow is the Kind for throw instruction. + operationKindThrow + // operationKindThrowRef is the Kind for throw_ref instruction. + operationKindThrowRef + // operationKindEnd is always placed at the bottom of this iota definition to be used in the test. operationKindEnd ) @@ -1112,6 +1121,12 @@ func (o unionOperation) String() string { case operationKindTailCallReturnCallIndirect: return fmt.Sprintf("%s %d %d", o.Kind, o.U1, o.U2) + case operationKindThrow: + return fmt.Sprintf("%s %d", o.Kind, o.U1) + + case operationKindThrowRef: + return o.Kind.String() + default: panic(fmt.Sprintf("TODO: %v", o.Kind)) } @@ -2843,3 +2858,46 @@ func newOperationTailCallReturnCall(functionIndex uint32) unionOperation { func newOperationTailCallReturnCallIndirect(typeIndex, tableIndex uint32, dropDepth inclusiveRange, l label) unionOperation { return unionOperation{Kind: operationKindTailCallReturnCallIndirect, U1: uint64(typeIndex), U2: uint64(tableIndex), Us: []uint64{dropDepth.AsU64(), uint64(l)}} } + +// newOperationThrow is a constructor for unionOperation with operationKindThrow. +// U1 stores the tag index. +func newOperationThrow(tagIndex uint32) unionOperation { + return unionOperation{Kind: operationKindThrow, U1: uint64(tagIndex)} +} + +// newOperationThrowRef is a constructor for unionOperation with operationKindThrowRef. +func newOperationThrowRef() unionOperation { + return unionOperation{Kind: operationKindThrowRef} +} + +// exceptionTableEntry represents one try_table's exception handling scope. +// Built at compile time and stored per compiledFunction. +type exceptionTableEntry struct { + startPC uint64 // first PC inside the try_table body + endPC uint64 // PC of continuation label (exclusive) + clauses []exceptionTableCatchClause +} + +// exceptionTableCatchClause is a single catch clause within an exception table entry. +type exceptionTableCatchClause struct { + kind byte // CatchKindCatch, CatchKindCatchRef, CatchKindCatchAll, CatchKindCatchAllRef + tagIndex uint32 // tag index for catch/catch_ref + targetPC uint64 // resolved PC to jump to on match + targetStackDepth int // = targetFrame.originalStackLenWithoutParamUint64 +} + +// pendingExceptionTableEntry is an unresolved exception table entry built during compilation. +// Labels are resolved to final PCs in lowerIR. +type pendingExceptionTableEntry struct { + startOpIndex int // index in Operations[] of the first instruction inside the try_table body + continuationFrameID uint32 + clauses []pendingCatchClause +} + +// pendingCatchClause is an unresolved catch clause within a pending exception table entry. +type pendingCatchClause struct { + kind byte + tagIndex uint32 + targetLabel label // unresolved label, resolved in lowerIR + targetStackDepth int // = targetFrame.originalStackLenWithoutParamUint64 +} diff --git a/internal/engine/interpreter/signature.go b/internal/engine/interpreter/signature.go index da5ca3c15..a579b1069 100644 --- a/internal/engine/interpreter/signature.go +++ b/internal/engine/interpreter/signature.go @@ -268,6 +268,9 @@ func (c *compiler) wasmOpcodeSignature(op wasm.Opcode, index uint32) (*signature return signature_I32_None, nil case wasm.OpcodeElse, wasm.OpcodeEnd, wasm.OpcodeBr: return signature_None_None, nil + case wasm.OpcodeThrow, wasm.OpcodeThrowRef, wasm.OpcodeTryTable: + // Stack manipulation handled dynamically by the compiler. + return signature_None_None, nil case wasm.OpcodeBrIf, wasm.OpcodeBrTable: return signature_I32_None, nil case wasm.OpcodeReturn: @@ -700,7 +703,8 @@ func wasmValueTypeTounsignedType(vt wasm.ValueType) unsignedType { return unsignedTypeI32 case wasm.ValueTypeI64, // From interpreterir layer, ref type values are opaque 64-bit pointers. - wasm.ValueTypeExternref, wasm.ValueTypeFuncref: + wasm.ValueTypeExternref, wasm.ValueTypeFuncref, + wasm.ValueTypeExnref: return unsignedTypeI64 case wasm.ValueTypeF32: return unsignedTypeF32 @@ -718,7 +722,8 @@ func wasmValueTypeToUnsignedOutSignature(vt wasm.ValueType) *signature { return signature_None_I32 case wasm.ValueTypeI64, // From interpreterir layer, ref type values are opaque 64-bit pointers. - wasm.ValueTypeExternref, wasm.ValueTypeFuncref: + wasm.ValueTypeExternref, wasm.ValueTypeFuncref, + wasm.ValueTypeExnref: return signature_None_I64 case wasm.ValueTypeF32: return signature_None_F32 @@ -736,7 +741,8 @@ func wasmValueTypeToUnsignedInSignature(vt wasm.ValueType) *signature { return signature_I32_None case wasm.ValueTypeI64, // From interpreterir layer, ref type values are opaque 64-bit pointers. - wasm.ValueTypeExternref, wasm.ValueTypeFuncref: + wasm.ValueTypeExternref, wasm.ValueTypeFuncref, + wasm.ValueTypeExnref: return signature_I64_None case wasm.ValueTypeF32: return signature_F32_None @@ -754,7 +760,8 @@ func wasmValueTypeToUnsignedInOutSignature(vt wasm.ValueType) *signature { return signature_I32_I32 case wasm.ValueTypeI64, // At interpreterir layer, ref type values are opaque 64-bit pointers. - wasm.ValueTypeExternref, wasm.ValueTypeFuncref: + wasm.ValueTypeExternref, wasm.ValueTypeFuncref, + wasm.ValueTypeExnref: return signature_I64_I64 case wasm.ValueTypeF32: return signature_F32_F32 diff --git a/internal/engine/wazevo/backend/backend_test.go b/internal/engine/wazevo/backend/backend_test.go index 561aeee43..58670c5d6 100644 --- a/internal/engine/wazevo/backend/backend_test.go +++ b/internal/engine/wazevo/backend/backend_test.go @@ -2368,6 +2368,256 @@ L5 (SSA Block: blk5): add sp, sp, #0x10 ldr x30, [sp], #0x10 b f0 +`, + }, + { + name: "try_table_catch_all_empty", m: testcases.TryTableCatchAllEmpty.Module, + afterFinalizeARM64: ` +L0 (SSA Block: blk0): + stp x30, xzr, [sp, #-0x10]! + sub sp, sp, #0x10 + orr x27, xzr, #0x10 + str x27, [sp, #-0x10]! + str x0, [sp, #0x10] + str x1, [sp, #0x18] + str x1, [x0, #0x8] + ldr x8, [x0, #0x4b0] + movz x9, #0x1b, lsl 0 + mov x1, x9 + bl x8 + ldr x8, [sp, #0x10] + ldr x9, [x8, #0x4d0] + orr w10, wzr, #0x1 + subs wzr, w9, w10 + csel w9, w10, w9, hs + br_table_sequence x9, table_index=0 +L4 (SSA Block: blk4): +L1 (SSA Block: blk1): + mov x0, xzr + add sp, sp, #0x10 + add sp, sp, #0x10 + ldr x30, [sp], #0x10 + ret +L3 (SSA Block: blk3): + ldr x9, [sp, #0x18] + str x9, [x8, #0x8] + ldr x9, [x8, #0x4b8] + mov x0, x8 + bl x9 + movz w0, #0x2a, lsl 0 + add sp, sp, #0x10 + add sp, sp, #0x10 + ldr x30, [sp], #0x10 + ret +`, + afterLoweringARM64: ` +L0 (SSA Block: blk0): + mov x128?, x0 + mov x129?, x1 + str x129?, [x128?, #0x8] + ldr x130?, [x128?, #0x4b0] + mov x0, x128? + movz x131?, #0x1b, lsl 0 + mov x1, x131? + bl x130? + ldr x132?, [x128?, #0x4d0] + orr w136?, wzr, #0x1 + subs wzr, w132?, w136? + csel w137?, w136?, w132?, hs + br_table_sequence x137?, table_index=0 +L4 (SSA Block: blk4): +L1 (SSA Block: blk1): + mov x133?, xzr + mov x0, x133? + ret +L3 (SSA Block: blk3): + str x129?, [x128?, #0x8] + ldr x135?, [x128?, #0x4b8] + mov x0, x128? + bl x135? + movz w134?, #0x2a, lsl 0 + mov x0, x134? + ret +`, + }, + { + name: "try_table_catch_all_throw", m: testcases.TryTableCatchAllThrow.Module, + afterFinalizeARM64: ` +L0 (SSA Block: blk0): + stp x30, xzr, [sp, #-0x10]! + sub sp, sp, #0x10 + orr x27, xzr, #0x10 + str x27, [sp, #-0x10]! + str x0, [sp, #0x10] + str x1, [sp, #0x18] + str x1, [x0, #0x8] + ldr x8, [x0, #0x4b0] + movz x9, #0x1b, lsl 0 + mov x1, x9 + bl x8 + ldr x8, [sp, #0x10] + ldr x9, [x8, #0x4d0] + orr w10, wzr, #0x1 + subs wzr, w9, w10 + csel w9, w10, w9, hs + br_table_sequence x9, table_index=0 +L4 (SSA Block: blk4): +L1 (SSA Block: blk1): + movz w0, #0x2a, lsl 0 + add sp, sp, #0x10 + add sp, sp, #0x10 + ldr x30, [sp], #0x10 + ret +L3 (SSA Block: blk3): + ldr x9, [sp, #0x18] + str x9, [x8, #0x8] + ldr x9, [x8, #0x4a0] + mov x0, x8 + mov x1, xzr + bl x9 + mov x1, x0 + ldr x8, [sp, #0x10] + ldr x9, [x8, #0x4a8] + mov x0, x8 + bl x9 + movz x8, #0x3, lsl 0 + ldr x9, [sp, #0x10] + str w8, [x9] + mov x8, sp + str x8, [x9, #0x38] + adr x8, #0x0 + str x8, [x9, #0x30] + exit_sequence x9 +`, + }, + { + // Exercises tags with 5 i32 parameters: verifies that the two-phase + // throw (throwAlloc + throw) correctly passes all 5 params through the + // Exception heap object, and the catch handler reads all 5 via the + // exceptionParamsPtr pointer. + name: "try_table_catch_many_param_throw", m: testcases.TryTableCatchManyParamThrow.Module, + afterFinalizeARM64: ` +L0 (SSA Block: blk0): + stp x30, xzr, [sp, #-0x10]! + sub sp, sp, #0x30 + orr x27, xzr, #0x30 + str x27, [sp, #-0x10]! + str x0, [sp, #0x10] + str x1, [sp, #0x18] + str w2, [sp, #0x20] + str w3, [sp, #0x24] + str w4, [sp, #0x28] + str w5, [sp, #0x2c] + str w6, [sp, #0x30] + str x1, [x0, #0x8] + ldr x8, [x0, #0x4b0] + movz x9, #0x1b, lsl 0 + mov x1, x9 + bl x8 + ldr x8, [sp, #0x10] + ldr x9, [x8, #0x4d0] + orr w10, wzr, #0x1 + subs wzr, w9, w10 + csel w9, w10, w9, hs + br_table_sequence x9, table_index=0 +L4 (SSA Block: blk4): + ldr x8, [x8, #0x4c8] + ldr w9, [x8] + ldr w10, [x8, #0x8] + ldr w11, [x8, #0x10] + ldr w12, [x8, #0x18] + ldr w8, [x8, #0x20] +L1 (SSA Block: blk1): + mov x4, x8 + mov x3, x12 + mov x2, x11 + mov x1, x10 + mov x0, x9 + add sp, sp, #0x10 + add sp, sp, #0x30 + ldr x30, [sp], #0x10 + ret +L3 (SSA Block: blk3): + ldr x9, [sp, #0x18] + str x9, [x8, #0x8] + ldr x9, [x8, #0x4a0] + mov x0, x8 + mov x1, xzr + bl x9 + mov x1, x0 + ldr x8, [sp, #0x10] + ldr x9, [x8, #0x4c8] + ldr w10, [sp, #0x20] + str w10, [x9] + ldr w10, [sp, #0x24] + str w10, [x9, #0x8] + ldr w10, [sp, #0x28] + str w10, [x9, #0x10] + ldr w10, [sp, #0x2c] + str w10, [x9, #0x18] + ldr w10, [sp, #0x30] + str w10, [x9, #0x20] + ldr x9, [x8, #0x4a8] + mov x0, x8 + bl x9 + movz x8, #0x3, lsl 0 + ldr x9, [sp, #0x10] + str w8, [x9] + mov x8, sp + str x8, [x9, #0x38] + adr x8, #0x0 + str x8, [x9, #0x30] + exit_sequence x9 +`, + }, + { + name: "try_table_catch_throw", m: testcases.TryTableCatchThrow.Module, + afterFinalizeARM64: ` +L0 (SSA Block: blk0): + stp x30, xzr, [sp, #-0x10]! + sub sp, sp, #0x10 + orr x27, xzr, #0x10 + str x27, [sp, #-0x10]! + str x0, [sp, #0x10] + str x1, [sp, #0x18] + str x1, [x0, #0x8] + ldr x8, [x0, #0x4b0] + movz x9, #0x1b, lsl 0 + mov x1, x9 + bl x8 + ldr x8, [sp, #0x10] + ldr x9, [x8, #0x4d0] + orr w10, wzr, #0x1 + subs wzr, w9, w10 + csel w9, w10, w9, hs + br_table_sequence x9, table_index=0 +L4 (SSA Block: blk4): +L1 (SSA Block: blk1): + movz w0, #0x17, lsl 0 + add sp, sp, #0x10 + add sp, sp, #0x10 + ldr x30, [sp], #0x10 + ret +L3 (SSA Block: blk3): + ldr x9, [sp, #0x18] + str x9, [x8, #0x8] + ldr x9, [x8, #0x4a0] + mov x0, x8 + mov x1, xzr + bl x9 + mov x1, x0 + ldr x8, [sp, #0x10] + ldr x9, [x8, #0x4a8] + mov x0, x8 + bl x9 + movz x8, #0x3, lsl 0 + ldr x9, [sp, #0x10] + str w8, [x9] + mov x8, sp + str x8, [x9, #0x38] + adr x8, #0x0 + str x8, [x9, #0x30] + exit_sequence x9 `, }, } { @@ -2386,7 +2636,7 @@ L5 (SSA Block: blk5): t.Fail() } - err := tc.m.Validate(api.CoreFeaturesV2 | experimental.CoreFeaturesThreads | experimental.CoreFeaturesTailCall) + err := tc.m.Validate(api.CoreFeaturesV2 | experimental.CoreFeaturesThreads | experimental.CoreFeaturesTailCall | experimental.CoreFeaturesExceptionHandling) require.NoError(t, err) ssab := ssa.NewBuilder() diff --git a/internal/engine/wazevo/backend/isa/arm64/abi_go_call_test.go b/internal/engine/wazevo/backend/isa/arm64/abi_go_call_test.go index ad428d70a..0a1bf6371 100644 --- a/internal/engine/wazevo/backend/isa/arm64/abi_go_call_test.go +++ b/internal/engine/wazevo/backend/isa/arm64/abi_go_call_test.go @@ -442,6 +442,87 @@ func TestMachine_CompileGoFunctionTrampoline(t *testing.T) { ldr x30, [sp], #0x10 ldr w0, [x15], #0x8 ret +`, + }, + { + name: "try_table_enter", + exitCode: wazevoapi.ExitCodeTryTableEnter, + sig: &ssa.Signature{ + Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64}, + Results: []ssa.Type{}, + }, + exp: ` + stp x30, xzr, [sp, #-0x10]! + sub x27, sp, #0x20 + ldr x11, [x0, #0x28] + subs xzr, x27, x11 + b.ge #0x14 + orr x27, xzr, #0x20 + str x27, [x0, #0x40] + ldr x27, [x0, #0x50] + bl x27 + str x19, [x0, #0x60] + str x20, [x0, #0x70] + str x21, [x0, #0x80] + str x22, [x0, #0x90] + str x23, [x0, #0xa0] + str x24, [x0, #0xb0] + str x25, [x0, #0xc0] + str x26, [x0, #0xd0] + str x28, [x0, #0xe0] + str q18, [x0, #0xf0] + str q19, [x0, #0x100] + str q20, [x0, #0x110] + str q21, [x0, #0x120] + str q22, [x0, #0x130] + str q23, [x0, #0x140] + str q24, [x0, #0x150] + str q25, [x0, #0x160] + str q26, [x0, #0x170] + str q27, [x0, #0x180] + str q28, [x0, #0x190] + str q29, [x0, #0x1a0] + str q30, [x0, #0x1b0] + str q31, [x0, #0x1c0] + sub sp, sp, #0x10 + mov x15, sp + str x1, [x15], #0x8 + orr x27, xzr, #0x10 + orr x16, xzr, #0x1 + stp x27, x16, [sp, #-0x10]! + movz w17, #0x1b, lsl 0 + str w17, [x0] + mov x27, sp + str x27, [x0, #0x38] + adr x27, #0x20 + str x27, [x0, #0x30] + exit_sequence x0 + ldr x19, [x0, #0x60] + ldr x20, [x0, #0x70] + ldr x21, [x0, #0x80] + ldr x22, [x0, #0x90] + ldr x23, [x0, #0xa0] + ldr x24, [x0, #0xb0] + ldr x25, [x0, #0xc0] + ldr x26, [x0, #0xd0] + ldr x28, [x0, #0xe0] + ldr q18, [x0, #0xf0] + ldr q19, [x0, #0x100] + ldr q20, [x0, #0x110] + ldr q21, [x0, #0x120] + ldr q22, [x0, #0x130] + ldr q23, [x0, #0x140] + ldr q24, [x0, #0x150] + ldr q25, [x0, #0x160] + ldr q26, [x0, #0x170] + ldr q27, [x0, #0x180] + ldr q28, [x0, #0x190] + ldr q29, [x0, #0x1a0] + ldr q30, [x0, #0x1b0] + ldr q31, [x0, #0x1c0] + add sp, sp, #0x20 + ldr x30, [sp], #0x10 + ret `, }, } { diff --git a/internal/engine/wazevo/call_engine.go b/internal/engine/wazevo/call_engine.go index 639429a63..83140bb42 100644 --- a/internal/engine/wazevo/call_engine.go +++ b/internal/engine/wazevo/call_engine.go @@ -43,6 +43,29 @@ type ( execCtxPtr uintptr numberOfResults int stackIteratorImpl stackIterator + // tryHandlers is the stack of active try_table exception handlers, + // used to match catch clauses when a throw exits to the dispatch loop. + tryHandlers []tryHandler + // pendingException holds the most recently caught exception, so handler + // code can read its params after re-entry. + pendingException *wasm.Exception + } + + // tryHandler records the state at a try_table entry for exception handling. + // On match, we restore the stack to the checkpoint state and re-enter at returnAddress. + tryHandler struct { + // Cloned stack and state from the try_table entry checkpoint, + // using the same approach as experimental.Snapshot. + sp, fp, top uintptr + returnAddress *byte + savedRegisters [64][2]uint64 + stack []byte // cloned stack + // catchClauses describes what exceptions this handler catches. + catchClauses []wazevoapi.CatchClauseInstance + // moduleInstance is the module that set up this try handler. + // Used for tag matching in doHandleException (the tag index in + // catch clauses is relative to this module's tag index space). + moduleInstance *wasm.ModuleInstance } // executionContext is the struct to be read/written by assembly functions. @@ -90,6 +113,29 @@ type ( memoryWait64TrampolineAddress *byte // memoryNotifyTrampolineAddress holds the address of the memory_notify trampoline function. memoryNotifyTrampolineAddress *byte + // throwAllocTrampolineAddress holds the address of the throw-alloc trampoline: + // phase 1 of throw, which allocates the Exception heap object. + throwAllocTrampolineAddress *byte + // throwTrampolineAddress holds the address of the throw/throw_ref trampoline function. + throwTrampolineAddress *byte + // tryTableEnterTrampolineAddress holds the address of the try_table enter trampoline function. + tryTableEnterTrampolineAddress *byte + // tryTableLeaveTrampolineAddress holds the address of the try_table leave trampoline function. + tryTableLeaveTrampolineAddress *byte + // exceptionPtr holds the pointer to the Exception struct, + // used on the throw side (throwAlloc stores the new Exception) + // and on the catch side (catch_ref/catch_all_ref retrieve the exnref). + exceptionPtr uintptr + // exceptionParamsPtr points into exceptionPtr's Params slice + // backing array. On the throw side, throwAlloc sets it so compiled + // code can store params at [ptr + i*8]. On the catch side, compiled + // handler blocks load params from the same pointer. + exceptionParamsPtr uintptr + // caughtExceptionClauseIdx is set by the dispatch loop to -1 on + // TryTableEnter (normal path) or to the matched catch clause index + // when an exception is caught. Compiled code loads this from execCtx + // after the trampoline call to decide which handler to dispatch to. + caughtExceptionClauseIdx int64 } ) @@ -217,6 +263,9 @@ func (c *callEngine) callWithStack(ctx context.Context, paramResultStack []uint6 } } + // Clear any stale try_table handlers from a previous call. + c.tryHandlers = c.tryHandlers[:0] + var paramResultPtr *uint64 if len(paramResultStack) > 0 { paramResultPtr = ¶mResultStack[0] @@ -236,21 +285,25 @@ func (c *callEngine) callWithStack(ctx context.Context, paramResultStack []uint6 var listeners []listenerForAbort builder := wasmdebug.NewErrorBuilder() - def, lsn := c.addFrame(builder, uintptr(unsafe.Pointer(c.execCtx.goCallReturnAddress))) - if lsn != nil { - listeners = append(listeners, listenerForAbort{def, lsn}) - } - returnAddrs := unwindStack( - uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)), - c.execCtx.framePointerBeforeGoCall, - c.stackTop, - nil, - ) - for _, retAddr := range returnAddrs[:len(returnAddrs)-1] { // the last return addr is the trampoline, so we skip it. - def, lsn = c.addFrame(builder, retAddr) + if c.execCtx.stackPointerBeforeGoCall != nil { + def, lsn := c.addFrame(builder, uintptr(unsafe.Pointer(c.execCtx.goCallReturnAddress))) if lsn != nil { listeners = append(listeners, listenerForAbort{def, lsn}) } + returnAddrs := unwindStack( + uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)), + c.execCtx.framePointerBeforeGoCall, + c.stackTop, + nil, + ) + if len(returnAddrs) > 1 { + for _, retAddr := range returnAddrs[:len(returnAddrs)-1] { // the last return addr is the trampoline, so we skip it. + def, lsn = c.addFrame(builder, retAddr) + if lsn != nil { + listeners = append(listeners, listenerForAbort{def, lsn}) + } + } + } } err = builder.FromRecovered(r) @@ -266,6 +319,7 @@ func (c *callEngine) callWithStack(ctx context.Context, paramResultStack []uint6 if err != nil { // Ensures that we can reuse this callEngine even after an error. c.execCtx.exitCode = wazevoapi.ExitCodeOK + c.tryHandlers = c.tryHandlers[:0] } }() @@ -505,12 +559,134 @@ func (c *callEngine) callWithStack(ctx context.Context, paramResultStack []uint6 panic(wasmruntime.ErrRuntimeInvalidConversionToInteger) case wazevoapi.ExitCodeUnalignedAtomic: panic(wasmruntime.ErrRuntimeUnalignedAtomic) + case wazevoapi.ExitCodeThrowAlloc: + // Allocate the Exception heap object sized exactly to the tag's + // param count. Sets exceptionParamsPtr so compiled code can + // store params, and returns the exnref via the stack slot. + s := goCallStackView(c.execCtx.stackPointerBeforeGoCall) + tagIndex := int(s[0]) + mod := c.callerModuleInstance() + tag := mod.Tags[tagIndex] + nParams := len(tag.Type.Params) + exn := &wasm.Exception{Tag: tag, Params: make([]uint64, nParams)} + c.pendingException = exn // GC root: keeps exn alive while compiled code writes params + if nParams > 0 { + c.execCtx.exceptionParamsPtr = uintptr(unsafe.Pointer(&exn.Params[0])) + } + // Return the exnref to compiled code via the stack slot. + s[0] = uint64(uintptr(unsafe.Pointer(exn))) + c.execCtx.exitCode = wazevoapi.ExitCodeOK + afterGoFunctionCallEntrypoint(c.execCtx.goCallReturnAddress, c.execCtxPtr, + uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)), c.execCtx.framePointerBeforeGoCall) + case wazevoapi.ExitCodeThrow: + // Throw trampoline: (execCtx, exnref) → (). + // Reads the exnref from the stack, searches for a matching handler. + s := goCallStackView(c.execCtx.stackPointerBeforeGoCall) + // Read the Exception pointer directly from the uint64 value to avoid + // conversion from uintptr into unsafe.Pointer, which triggers checkptr. + exn := *(**wasm.Exception)(unsafe.Pointer(&s[0])) + if !c.doHandleException(exn) { + panic(wasmruntime.ErrRuntimeUncaughtException) + } + if len(exn.Params) > 0 { + c.execCtx.exceptionParamsPtr = uintptr(unsafe.Pointer(&exn.Params[0])) + } + c.execCtx.exceptionPtr = uintptr(unsafe.Pointer(exn)) + c.execCtx.exitCode = wazevoapi.ExitCodeOK + afterGoFunctionCallEntrypoint(c.execCtx.goCallReturnAddress, c.execCtxPtr, + uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)), c.execCtx.framePointerBeforeGoCall) + case wazevoapi.ExitCodeNullReference: + panic(wasmruntime.ErrRuntimeNullReference) + case wazevoapi.ExitCodeTryTableEnter: + // Save current state as a try handler checkpoint using stack cloning + // (same approach as experimental.Snapshot). + // The encoded exit code (with tryTableID in upper bits) is on the + // Go call stack as the second trampoline argument, not in execCtx.exitCode. + tryTableEnterStack := goCallStackView(c.execCtx.stackPointerBeforeGoCall) + catchClauseTableIdx := wazevoapi.TryTableIDFromExitCode(wazevoapi.ExitCode(tryTableEnterStack[0])) + mod := c.callerModuleInstance() + me := mod.Engine.(*moduleEngine) + clauses := me.parent.catchClauseTable[catchClauseTableIdx] + returnAddress := c.execCtx.goCallReturnAddress + oldTop, oldSp := c.stackTop, uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)) + newSP, newFP, newTop, newStack := c.cloneStack(uintptr(len(c.stack)) + 16) + adjustClonedStack(oldSp, oldTop, newSP, newFP, newTop) + c.tryHandlers = append(c.tryHandlers, tryHandler{ + sp: newSP, + fp: newFP, + top: newTop, + returnAddress: returnAddress, + savedRegisters: c.execCtx.savedRegisters, + stack: newStack, + catchClauses: clauses, + moduleInstance: mod, + }) + // Set clauseIdx = -1 (no exception) in execCtx for the compiled code + // to read after the trampoline returns. + c.execCtx.caughtExceptionClauseIdx = -1 + c.execCtx.exitCode = wazevoapi.ExitCodeOK + afterGoFunctionCallEntrypoint(c.execCtx.goCallReturnAddress, c.execCtxPtr, + uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)), c.execCtx.framePointerBeforeGoCall) + case wazevoapi.ExitCodeTryTableLeave: + // Pop the most recent try handler. + if len(c.tryHandlers) > 0 { + c.tryHandlers = c.tryHandlers[:len(c.tryHandlers)-1] + } + c.execCtx.exitCode = wazevoapi.ExitCodeOK + afterGoFunctionCallEntrypoint(c.execCtx.goCallReturnAddress, c.execCtxPtr, + uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)), c.execCtx.framePointerBeforeGoCall) default: panic("BUG") } } } +// doHandleException tries to match the given exception against active try handlers. +// If a match is found, it restores the execution state to the handler's checkpoint +// (like snapshot.doRestore) and writes the matched clause index as the trampoline +// return value. Returns true if handled. +func (c *callEngine) doHandleException(exn *wasm.Exception) bool { + // Search try handlers from innermost (last) to outermost (first). + for i := len(c.tryHandlers) - 1; i >= 0; i-- { + h := &c.tryHandlers[i] + for clauseIdx, clause := range h.catchClauses { + // Use the module that set up the handler (not the one that threw) + // because clause.TagIndex is relative to that module's tag space. + mod := h.moduleInstance + matched := false + switch clause.Kind { + case wasm.CatchKindCatch, wasm.CatchKindCatchRef: + matched = mod.Tags[clause.TagIndex] == exn.Tag + case wasm.CatchKindCatchAll, wasm.CatchKindCatchAllRef: + matched = true + } + if matched { + // Pop all handlers at and above this one. + c.tryHandlers = c.tryHandlers[:i] + + // Store the caught exception so handler code can read params. + c.pendingException = exn + + // Restore the cloned stack (like snapshot.doRestore). + spp := *(**uint64)(unsafe.Pointer(&h.sp)) + c.stack = h.stack + c.stackTop = h.top + ec := &c.execCtx + ec.stackBottomPtr = &c.stack[0] + ec.stackPointerBeforeGoCall = spp + ec.framePointerBeforeGoCall = h.fp + ec.goCallReturnAddress = h.returnAddress + ec.savedRegisters = h.savedRegisters + + // Set the matched clause index in execCtx for compiled code to read. + ec.caughtExceptionClauseIdx = int64(clauseIdx) + return true + } + } + } + return false +} + func (c *callEngine) callerModuleInstance() *wasm.ModuleInstance { return moduleInstanceFromOpaquePtr(c.execCtx.callerModuleContextPtr) } diff --git a/internal/engine/wazevo/e2e_test.go b/internal/engine/wazevo/e2e_test.go index 215ebe8c8..0dd9b03a1 100644 --- a/internal/engine/wazevo/e2e_test.go +++ b/internal/engine/wazevo/e2e_test.go @@ -896,6 +896,62 @@ func TestE2E(t *testing.T) { {params: []uint64{1, 2, 3, 4, 5, 6, 7}, expResults: []uint64{156}}, }, }, + // ---- Exception Handling ---- + { + name: "throw_only", + m: testcases.ThrowOnly.Module, + features: api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling, + calls: []callCase{{expErr: "uncaught exception"}}, + }, + { + name: "throw_with_param", + m: testcases.ThrowWithParam.Module, + features: api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling, + calls: []callCase{{params: []uint64{42}, expErr: "uncaught exception"}}, + }, + { + name: "try_table_catch_all_empty", + m: testcases.TryTableCatchAllEmpty.Module, + features: api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling, + calls: []callCase{{expResults: []uint64{42}}}, + }, + { + name: "try_table_catch_all_throw", + m: testcases.TryTableCatchAllThrow.Module, + features: api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling, + calls: []callCase{{expResults: []uint64{42}}}, + }, + { + name: "try_table_catch_throw", + m: testcases.TryTableCatchThrow.Module, + features: api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling, + calls: []callCase{{expResults: []uint64{23}}}, + }, + { + name: "try_table_catch_param_throw", + m: testcases.TryTableCatchParamThrow.Module, + features: api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling, + calls: []callCase{{params: []uint64{42}, expResults: []uint64{42}}}, + }, + { + name: "try_table_catch_many_param_throw", + m: testcases.TryTableCatchManyParamThrow.Module, + features: api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling, + calls: []callCase{{params: []uint64{1, 2, 3, 4, 5}, expResults: []uint64{1, 2, 3, 4, 5}}}, + }, + { + // Verifies that removeUntilRet does not accidentally remove the catch handler + // block code that follows the tail-call path in the instruction stream. + // param=0: throw $e is caught → returns 42 (catch handler intact). + // param=1: return_call $target (tail call) → returns 99. + name: "try_table_catch_with_return_call", + m: testcases.TryTableCatchWithReturnCall.Module, + features: api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling | experimental.CoreFeaturesTailCall, + calls: []callCase{ + {params: []uint64{0}, expResults: []uint64{42}}, // throw path: catch handler intact → 42 + {params: []uint64{1}, expResults: []uint64{99}}, // tail-call path: return_call $target → 99 + }, + }, } { tc := tc t.Run(tc.name, func(t *testing.T) { diff --git a/internal/engine/wazevo/engine.go b/internal/engine/wazevo/engine.go index f09dd1791..bee9d2084 100644 --- a/internal/engine/wazevo/engine.go +++ b/internal/engine/wazevo/engine.go @@ -67,7 +67,16 @@ type ( memoryWait64Address *byte // memoryNotifyAddress is the address of memory.notify builtin function memoryNotifyAddress *byte - listenerTrampolines listenerTrampolines + // throwAllocTrampolineAddress is the address of the throw-alloc trampoline: + // phase 1 of throw, which allocates the Exception heap object. + throwAllocTrampolineAddress *byte + // throwTrampolineAddress is the address of the throw/throw_ref trampoline function. + throwTrampolineAddress *byte + // tryTableEnterAddress is the address of try_table enter trampoline. + tryTableEnterAddress *byte + // tryTableLeaveAddress is the address of try_table leave trampoline. + tryTableLeaveAddress *byte + listenerTrampolines listenerTrampolines } listenerTrampolines = map[*wasm.FunctionType]struct { @@ -93,6 +102,9 @@ type ( offsets wazevoapi.ModuleContextOffsetData sharedFunctions *sharedFunctions sourceMap sourceMap + // catchClauseTable stores catch clause info for each try_table, + // indexed by a try_table ID assigned during compilation. + catchClauseTable [][]wazevoapi.CatchClauseInstance } executables struct { @@ -269,6 +281,7 @@ func (e *engine) compileModule(ctx context.Context, module *wasm.Module, listene relocator.appendFunction(fctx, module, cm, i, fidx, body, relsPerFunc, be.SourceOffsetInfo()) } + cm.catchClauseTable = fe.CatchClauseTable() } else { // Compile with N worker goroutines. // Collect compiled functions across workers in a slice, @@ -287,6 +300,10 @@ func (e *engine) compileModule(ctx context.Context, module *wasm.Module, listene ctx, cancel := context.WithCancelCause(ctx) defer cancel(nil) + // Catch clause table IDs are baked into compiled machine code, so all + // workers must share a single table to ensure globally unique IDs. + sharedCCT := frontend.NewSharedCatchClauseTable() + var count atomic.Uint32 var wg sync.WaitGroup wg.Add(workers) @@ -299,7 +316,9 @@ func (e *engine) compileModule(ctx context.Context, module *wasm.Module, listene machine := newMachine() ssaBuilder := ssa.NewBuilder() be := backend.NewCompiler(ctx, machine, ssaBuilder) - fe := frontend.NewFrontendCompiler(module, ssaBuilder, &cm.offsets, ensureTermination, withListener, needSourceInfo) + fe := frontend.NewFrontendCompiler( + module, ssaBuilder, &cm.offsets, ensureTermination, withListener, needSourceInfo). + WithCatchClauseTable(sharedCCT) for { if err := ctx.Err(); err != nil { @@ -346,6 +365,7 @@ func (e *engine) compileModule(ctx context.Context, module *wasm.Module, listene fn := &compiledFuncs[i] relocator.appendFunction(fn.fctx, module, cm, fn.fnum, fn.fidx, fn.body, fn.relsPerFunc, fn.offsPerFunc) } + cm.catchClauseTable = sharedCCT.Table() } // Allocate executable memory and then copy the generated machine code. @@ -733,7 +753,7 @@ func (e *engine) NewModuleEngine(m *wasm.Module, mi *wasm.ModuleInstance) (wasm. } func (e *engine) compileSharedFunctions() { - var sizes [8]int + var sizes [12]int var trampolines []byte addTrampoline := func(i int, buf []byte) { @@ -801,6 +821,38 @@ func (e *engine) compileSharedFunctions() { Results: []ssa.Type{ssa.TypeI32}, }, false)) + e.be.Init() + addTrampoline(8, + e.machine.CompileGoFunctionTrampoline(wazevoapi.ExitCodeThrowAlloc, &ssa.Signature{ + // exec context, tag index → exnref + Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64}, + Results: []ssa.Type{ssa.TypeI64}, + }, false)) + + e.be.Init() + addTrampoline(9, + e.machine.CompileGoFunctionTrampoline(wazevoapi.ExitCodeThrow, &ssa.Signature{ + // exec context, exnref + Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64}, + Results: []ssa.Type{}, + }, false)) + + e.be.Init() + addTrampoline(10, + e.machine.CompileGoFunctionTrampoline(wazevoapi.ExitCodeTryTableEnter, &ssa.Signature{ + // exec context, catch clause info (encoded) + Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64}, + Results: []ssa.Type{}, + }, false)) + + e.be.Init() + addTrampoline(11, + e.machine.CompileGoFunctionTrampoline(wazevoapi.ExitCodeTryTableLeave, &ssa.Signature{ + // exec context + Params: []ssa.Type{ssa.TypeI64}, + Results: []ssa.Type{}, + }, false)) + fns := &sharedFunctions{ executable: mmapExecutable(trampolines), listenerTrampolines: make(listenerTrampolines), @@ -823,6 +875,14 @@ func (e *engine) compileSharedFunctions() { fns.memoryWait64Address = &fns.executable[offset] offset += sizes[6] fns.memoryNotifyAddress = &fns.executable[offset] + offset += sizes[7] + fns.throwAllocTrampolineAddress = &fns.executable[offset] + offset += sizes[8] + fns.throwTrampolineAddress = &fns.executable[offset] + offset += sizes[9] + fns.tryTableEnterAddress = &fns.executable[offset] + offset += sizes[10] + fns.tryTableLeaveAddress = &fns.executable[offset] if wazevoapi.PerfMapEnabled { wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(fns.memoryGrowAddress)), uint64(sizes[0]), "memory_grow_trampoline") @@ -833,6 +893,10 @@ func (e *engine) compileSharedFunctions() { wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(fns.memoryWait32Address)), uint64(sizes[5]), "memory_wait32_trampoline") wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(fns.memoryWait64Address)), uint64(sizes[6]), "memory_wait64_trampoline") wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(fns.memoryNotifyAddress)), uint64(sizes[7]), "memory_notify_trampoline") + wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(fns.throwAllocTrampolineAddress)), uint64(sizes[8]), "throw_alloc_trampoline") + wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(fns.throwTrampolineAddress)), uint64(sizes[9]), "throw_trampoline") + wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(fns.tryTableEnterAddress)), uint64(sizes[10]), "try_table_enter_trampoline") + wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(fns.tryTableLeaveAddress)), uint64(sizes[11]), "try_table_leave_trampoline") } e.sharedFunctions = fns diff --git a/internal/engine/wazevo/frontend/frontend.go b/internal/engine/wazevo/frontend/frontend.go index 5749e03c7..1e4979f39 100644 --- a/internal/engine/wazevo/frontend/frontend.go +++ b/internal/engine/wazevo/frontend/frontend.go @@ -4,6 +4,7 @@ package frontend import ( "bytes" "math" + "sync" "github.com/tetratelabs/wazero/internal/engine/wazevo/ssa" "github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi" @@ -62,6 +63,20 @@ type Compiler struct { execCtxPtrValue, moduleCtxPtrValue ssa.Value + // throwAllocSig is the signature for the throw-alloc trampoline: + // (execCtx, tagIndex) → (exnref). Allocates the Exception and returns + // its pointer so compiled code can pass it to the throw trampoline. + throwAllocSig ssa.Signature + // throwSig is the signature for the throw/throw_ref trampoline: + // (execCtx, exnref) → (). Searches for a matching handler and restores. + throwSig ssa.Signature + // tryTableEnterSig is the signature for the try_table enter trampoline. + tryTableEnterSig ssa.Signature + // tryTableLeaveSig is the signature for the try_table leave trampoline. + tryTableLeaveSig ssa.Signature + // catchClauseTable accumulates catch clause info for each try_table during compilation. + catchClauseTable catchClauseTable + // Following are reused for the known safe bounds analysis. pointers []int @@ -95,12 +110,75 @@ func NewFrontendCompiler(m *wasm.Module, ssaBuilder ssa.Builder, offset *wazevoa offset: offset, ensureTermination: ensureTermination, needSourceOffsetInfo: sourceInfo, + catchClauseTable: &localCatchClauseTable{}, varLengthKnownSafeBoundWithIDPool: wazevoapi.NewVarLengthPool[knownSafeBoundWithID](), } c.declareSignatures(listenerOn) return c } +// catchClauseTable accumulates catch clause entries during compilation. +type catchClauseTable interface { + Append(clauses []wazevoapi.CatchClauseInstance) int + Table() [][]wazevoapi.CatchClauseInstance +} + +// localCatchClauseTable is the single-threaded implementation. +type localCatchClauseTable struct { + table [][]wazevoapi.CatchClauseInstance +} + +func (t *localCatchClauseTable) Append(clauses []wazevoapi.CatchClauseInstance) int { + id := len(t.table) + t.table = append(t.table, clauses) + return id +} + +func (t *localCatchClauseTable) Table() [][]wazevoapi.CatchClauseInstance { + return t.table +} + +// SharedCatchClauseTable is the thread-safe implementation for parallel compilation. +type SharedCatchClauseTable struct { + mu sync.Mutex + table [][]wazevoapi.CatchClauseInstance + finalized bool +} + +// NewSharedCatchClauseTable creates a new SharedCatchClauseTable. +func NewSharedCatchClauseTable() *SharedCatchClauseTable { + return &SharedCatchClauseTable{} +} + +func (s *SharedCatchClauseTable) Append(clauses []wazevoapi.CatchClauseInstance) int { + if s.finalized { + panic("already finalized") + } + s.mu.Lock() + id := len(s.table) + s.table = append(s.table, clauses) + s.mu.Unlock() + return id +} + +func (s *SharedCatchClauseTable) Table() [][]wazevoapi.CatchClauseInstance { + s.finalized = true + return s.table +} + +// WithCatchClauseTable replaces the catch clause table implementation. +// Used by the parallel compilation path to share a single mutex-protected +// table across workers. +func (c *Compiler) WithCatchClauseTable(t catchClauseTable) *Compiler { + c.catchClauseTable = t + return c +} + +// CatchClauseTable returns the accumulated catch clause table. +func (c *Compiler) CatchClauseTable() [][]wazevoapi.CatchClauseInstance { + return c.catchClauseTable.Table() +} + func (c *Compiler) declareSignatures(listenerOn bool) { m := c.m c.signatures = make(map[*wasm.FunctionType]*ssa.Signature, len(m.TypeSection)+2) @@ -194,6 +272,34 @@ func (c *Compiler) declareSignatures(listenerOn bool) { Results: []ssa.Type{ssa.TypeI32}, } c.ssaBuilder.DeclareSignature(&c.memoryNotifySig) + + c.throwAllocSig = ssa.Signature{ + ID: c.memoryNotifySig.ID + 1, + Params: []ssa.Type{ssa.TypeI64 /* exec context */, ssa.TypeI64 /* tag index */}, + Results: []ssa.Type{ssa.TypeI64 /* exnref */}, + } + c.ssaBuilder.DeclareSignature(&c.throwAllocSig) + + c.throwSig = ssa.Signature{ + ID: c.throwAllocSig.ID + 1, + Params: []ssa.Type{ssa.TypeI64 /* exec context */, ssa.TypeI64 /* exnref */}, + Results: []ssa.Type{}, + } + c.ssaBuilder.DeclareSignature(&c.throwSig) + + c.tryTableEnterSig = ssa.Signature{ + ID: c.throwSig.ID + 1, + Params: []ssa.Type{ssa.TypeI64 /* exec context */, ssa.TypeI64 /* encoded exit code */}, + Results: []ssa.Type{}, + } + c.ssaBuilder.DeclareSignature(&c.tryTableEnterSig) + + c.tryTableLeaveSig = ssa.Signature{ + ID: c.tryTableEnterSig.ID + 1, + Params: []ssa.Type{ssa.TypeI64 /* exec context */}, + Results: []ssa.Type{}, + } + c.ssaBuilder.DeclareSignature(&c.tryTableLeaveSig) } // SignatureForWasmFunctionType returns the ssa.Signature for the given wasm.FunctionType. @@ -234,7 +340,8 @@ func (c *Compiler) Init(idx, typIndex wasm.Index, typ *wasm.FunctionType, localT // Note: this assumes 64-bit platform (I believe we won't have 32-bit backend ;)). const executionContextPtrTyp, moduleContextPtrTyp = ssa.TypeI64, ssa.TypeI64 -// LowerToSSA lowers the current function to SSA function which will be held by ssaBuilder. +// LowerToSSA lowers the current function to SSA IR which will be held by ssaBuilder. +// // After calling this, the caller will be able to access the SSA info in *Compiler.ssaBuilder. // // Note that this only does the naive lowering, and do not do any optimization, instead the caller is expected to do so. @@ -345,8 +452,8 @@ func (c *Compiler) declareWasmGlobal(typ wasm.ValueType, mutable bool) { case wasm.ValueTypeI32: st = ssa.TypeI32 case wasm.ValueTypeI64, - // Both externref and funcref are represented as I64 since we only support 64-bit platforms. - wasm.ValueTypeExternref, wasm.ValueTypeFuncref: + // externref, funcref, and exnref are represented as I64 since we only support 64-bit platforms. + wasm.ValueTypeExternref, wasm.ValueTypeFuncref, wasm.ValueTypeExnref: st = ssa.TypeI64 case wasm.ValueTypeF32: st = ssa.TypeF32 @@ -372,8 +479,9 @@ func WasmTypeToSSAType(vt wasm.ValueType) ssa.Type { case wasm.ValueTypeI32: return ssa.TypeI32 case wasm.ValueTypeI64, - // Both externref and funcref are represented as I64 since we only support 64-bit platforms. - wasm.ValueTypeExternref, wasm.ValueTypeFuncref: + // externref, funcref, and exnref are represented as I64 since we only support 64-bit platforms. + wasm.ValueTypeExternref, wasm.ValueTypeFuncref, + wasm.ValueTypeExnref: return ssa.TypeI64 case wasm.ValueTypeF32: return ssa.TypeF32 diff --git a/internal/engine/wazevo/frontend/frontend_test.go b/internal/engine/wazevo/frontend/frontend_test.go index b982556d8..7921db552 100644 --- a/internal/engine/wazevo/frontend/frontend_test.go +++ b/internal/engine/wazevo/frontend/frontend_test.go @@ -3095,6 +3095,11 @@ func TestCompiler_declareSignatures(t *testing.T) { {ID: 9, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64, ssa.TypeI32, ssa.TypeI64}, Results: []ssa.Type{ssa.TypeI32}}, {ID: 10, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64, ssa.TypeI64, ssa.TypeI64}, Results: []ssa.Type{ssa.TypeI32}}, {ID: 11, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI32, ssa.TypeI64}, Results: []ssa.Type{ssa.TypeI32}}, + // EH signatures. + {ID: 12, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64}, Results: []ssa.Type{ssa.TypeI64}}, + {ID: 13, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64}}, + {ID: 14, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64}}, + {ID: 15, Params: []ssa.Type{ssa.TypeI64}}, } require.Equal(t, len(expected), len(declaredSigs)) @@ -3134,6 +3139,11 @@ func TestCompiler_declareSignatures(t *testing.T) { {ID: 17, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64, ssa.TypeI32, ssa.TypeI64}, Results: []ssa.Type{ssa.TypeI32}}, {ID: 18, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64, ssa.TypeI64, ssa.TypeI64}, Results: []ssa.Type{ssa.TypeI32}}, {ID: 19, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI32, ssa.TypeI64}, Results: []ssa.Type{ssa.TypeI32}}, + // EH signatures. + {ID: 20, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64}, Results: []ssa.Type{ssa.TypeI64}}, + {ID: 21, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64}}, + {ID: 22, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64}}, + {ID: 23, Params: []ssa.Type{ssa.TypeI64}}, } require.Equal(t, len(expected), len(declaredSigs)) for i := 0; i < len(declaredSigs); i++ { diff --git a/internal/engine/wazevo/frontend/lower.go b/internal/engine/wazevo/frontend/lower.go index 1277db0bf..11224f501 100644 --- a/internal/engine/wazevo/frontend/lower.go +++ b/internal/engine/wazevo/frontend/lower.go @@ -65,6 +65,7 @@ const ( controlFrameKindIfWithElse controlFrameKindIfWithoutElse controlFrameKindBlock + controlFrameKindTryTable ) // String implements fmt.Stringer for debugging. @@ -80,6 +81,8 @@ func (k controlFrameKind) String() string { return "if_without_else" case controlFrameKindBlock: return "block" + case controlFrameKindTryTable: + return "try_table" default: panic(k) } @@ -1443,6 +1446,11 @@ func (c *Compiler) lowerCurrentOpcode() { unreachable := state.unreachable if !unreachable { + // For try_table, emit the leave trampoline before the jump to the following block. + if ctrl.kind == controlFrameKindTryTable { + c.emitTryTableLeave() + } + // Top n-th args will be used as a result of the current control frame. args := c.nPeekDup(len(ctrl.blockType.Results)) @@ -1477,6 +1485,7 @@ func (c *Compiler) lowerCurrentOpcode() { break } + c.emitTryTableLeaves(int(labelIndex)) targetBlk, argNum := state.brTargetArgNumFor(labelIndex) args := c.nPeekDup(argNum) c.insertJumpToBlock(args, targetBlk) @@ -1494,6 +1503,21 @@ func (c *Compiler) lowerCurrentOpcode() { targetBlk, argNum := state.brTargetArgNumFor(labelIndex) args := c.nPeekDup(argNum) var sealTargetBlk bool + + // If the branch exits any try_table frames, emit TryTableLeave + // calls in a trampoline block that only runs on the taken path. + if c.branchExitsTryTable(int(labelIndex)) { + current := builder.CurrentBlock() + trampolineBlk := builder.AllocateBasicBlock() + builder.SetCurrentBlock(trampolineBlk) + c.emitTryTableLeaves(int(labelIndex)) + c.insertJumpToBlock(args, targetBlk) + builder.SetCurrentBlock(current) + targetBlk = trampolineBlk + sealTargetBlk = true + args = ssa.ValuesNil + } + if c.needListener && targetBlk.ReturnBlock() { // In this case, we have to call the listener before returning. // Save the currently active block. current := builder.CurrentBlock() @@ -1559,6 +1583,7 @@ func (c *Compiler) lowerCurrentOpcode() { if state.unreachable { break } + c.emitTryTableLeaves(len(state.controlFrames)) if c.needListener { c.callListenerAfter() } @@ -3399,6 +3424,9 @@ func (c *Compiler) lowerCurrentOpcode() { if state.unreachable { break } + // Per spec, return_call leaves the current frame, so all enclosing + // try_table handlers must be popped before the tail call. + c.emitTryTableLeaves(len(c.state().controlFrames)) _, _ = typeIndex, tableIndex c.lowerTailCallReturnCallIndirect(typeIndex, tableIndex) state.unreachable = true @@ -3408,9 +3436,251 @@ func (c *Compiler) lowerCurrentOpcode() { if state.unreachable { break } + // Per spec, return_call leaves the current frame, so all enclosing + // try_table handlers must be popped before the tail call. + c.emitTryTableLeaves(len(c.state().controlFrames)) c.lowerTailCallReturnCall(fnIndex) state.unreachable = true + case wasm.OpcodeThrow: + tagIndex := c.readI32u() + if state.unreachable { + break + } + tagType := c.resolveTagType(tagIndex) + // Pop the tag's param values from the stack. + var throwParams []ssa.Value + if tagType != nil { + throwParams = make([]ssa.Value, len(tagType.Params)) + for i := len(tagType.Params) - 1; i >= 0; i-- { + throwParams[i] = state.pop() + } + } + + c.storeCallerModuleContext() + + tagIdxVal := builder.AllocateInstruction().AsIconst64(uint64(tagIndex)).Insert(builder).Return() + + // We need to store the throwParams in the exception and then throw it. + // However, each exception might have a variable number of parameters, + // so we let Go allocate the reference on the heap. + // The Go side allocates the Exception object (Params sized to nParams) + // and stores the pointer to the backing-array into execCtx.exceptionParamsPtr. + throwAllocPtr := builder.AllocateInstruction(). + AsLoad(c.execCtxPtrValue, + wazevoapi.ExecutionContextOffsetThrowAllocTrampolineAddress.U32(), + ssa.TypeI64, + ).Insert(builder).Return() + throwAllocArgs := c.allocateVarLengthValues(2, c.execCtxPtrValue, tagIdxVal) + exnref := builder.AllocateInstruction(). + AsCallIndirect(throwAllocPtr, &c.throwAllocSig, throwAllocArgs). + Insert(builder).Return() + + // Reload memory pointers invalidated by the Go call. + c.reloadAfterCall() + + // We can now store each param directly into Exception.Params using the pointer + // stored into execCtx.exceptionParamsPtr. + if len(throwParams) > 0 { + paramsPtr := builder.AllocateInstruction(). + AsLoad(c.execCtxPtrValue, + wazevoapi.ExecutionContextOffsetExceptionParamsPtr.U32(), + ssa.TypeI64, + ).Insert(builder).Return() + for i, v := range throwParams { + switch v.Type() { + case ssa.TypeF32: + v = builder.AllocateInstruction().AsBitcast(v, ssa.TypeI32).Insert(builder).Return() + case ssa.TypeF64: + v = builder.AllocateInstruction().AsBitcast(v, ssa.TypeI64).Insert(builder).Return() + } + builder.AllocateInstruction(). + AsStore(ssa.OpcodeStore, v, paramsPtr, uint32(i)*8). + Insert(builder) + } + } + + // We return again control to Go to search and dispatch to a matching catch clause. + c.emitThrow(exnref) + state.unreachable = true + + case wasm.OpcodeThrowRef: + if state.unreachable { + break + } + exnref := state.pop() + // Check for null exnref. + zero := builder.AllocateInstruction().AsIconst64(0).Insert(builder).Return() + isNull := builder.AllocateInstruction() + isNull.AsIcmp(exnref, zero, ssa.IntegerCmpCondEqual) + builder.InsertInstruction(isNull) + exitIfNull := builder.AllocateInstruction() + exitIfNull.AsExitIfTrueWithCode(c.execCtxPtrValue, isNull.Return(), wazevoapi.ExitCodeNullReference) + builder.InsertInstruction(exitIfNull) + + c.storeCallerModuleContext() + + c.emitThrow(exnref) + state.unreachable = true + + case wasm.OpcodeTryTable: + bt := c.readBlockType() + + if state.unreachable { + state.unreachableDepth++ + // Still need to skip the catch clause bytes in the unreachable case. + c.skipTryTableCatchClauses() + break + } + + // Parse catch clauses. + c.loweringState.pc++ + catchCount, catchNum, _ := leb128.LoadUint32(c.wasmFunctionBody[c.loweringState.pc:]) + c.loweringState.pc += int(catchNum) - 1 + + var catchClauses []catchClause + for i := uint32(0); i < catchCount; i++ { + c.loweringState.pc++ + kind := c.wasmFunctionBody[c.loweringState.pc] + var tagIdx uint32 + switch kind { + case wasm.CatchKindCatch, wasm.CatchKindCatchRef: + c.loweringState.pc++ + var n uint64 + tagIdx, n, _ = leb128.LoadUint32(c.wasmFunctionBody[c.loweringState.pc:]) + c.loweringState.pc += int(n) - 1 + case wasm.CatchKindCatchAll, wasm.CatchKindCatchAllRef: + // No tagIdx for catch_all variants. + } + c.loweringState.pc++ + labelIdx, n, _ := leb128.LoadUint32(c.wasmFunctionBody[c.loweringState.pc:]) + c.loweringState.pc += int(n) - 1 + catchClauses = append(catchClauses, catchClause{kind: kind, tagIndex: tagIdx, labelIdx: labelIdx}) + } + + // Register catch clauses in the table and get the try_table ID. + var clauseInstances []wazevoapi.CatchClauseInstance + for _, cc := range catchClauses { + clauseInstances = append(clauseInstances, wazevoapi.CatchClauseInstance{ + Kind: cc.kind, + TagIndex: cc.tagIndex, + }) + } + tryTableID := c.catchClauseTable.Append(clauseInstances) + + // Allocate the following block (after try_table end) and body block. + followingBlk := builder.AllocateBasicBlock() + c.addBlockParamsFromWasmTypes(bt.Results, followingBlk) + bodyBlk := builder.AllocateBasicBlock() + + if len(catchClauses) > 0 { + // Store the caller module context so the dispatch loop can find the module. + c.storeCallerModuleContext() + + // For each catch clause, create a handler block that loads exception + // params and jumps to the wasm target label. + // NOTE: catch clause label indices do NOT include the try_table itself + // (the try_table is pushed onto the control stack after the catch clauses + // are processed, per the spec). So we resolve labels BEFORE pushing. + varPool := builder.VarLengthPool() + targets := varPool.Allocate(len(catchClauses) + 1) // +1 for bodyBlk + + currentBlk := builder.CurrentBlock() + for _, cc := range catchClauses { + handlerBlk := builder.AllocateBasicBlock() + builder.SetCurrentBlock(handlerBlk) + c.reloadAfterCall() + + // Resolve the wasm target label. + targetBlk, _ := state.brTargetArgNumFor(cc.labelIdx) + + // Load exception params and jump to wasm target. + var brArgs []ssa.Value + switch cc.kind { + case wasm.CatchKindCatch: + if tagType := c.resolveTagType(cc.tagIndex); tagType != nil { + brArgs = c.loadExceptionParams(tagType) + } + case wasm.CatchKindCatchRef: + if tagType := c.resolveTagType(cc.tagIndex); tagType != nil { + brArgs = c.loadExceptionParams(tagType) + } + brArgs = append(brArgs, c.loadExnRef()) + case wasm.CatchKindCatchAll: + // No values. + case wasm.CatchKindCatchAllRef: + brArgs = append(brArgs, c.loadExnRef()) + } + + jmpArgs := c.allocateVarLengthValues(len(brArgs), brArgs...) + c.insertJumpToBlock(jmpArgs, targetBlk) + + targets = targets.Append(varPool, ssa.Value(handlerBlk.ID())) + } + // Last target is the body block (default for clauseIdx == -1 / out of range). + targets = targets.Append(varPool, ssa.Value(bodyBlk.ID())) + + // Back to the original block: call the try_table enter trampoline, + // then dispatch on the caught clause index. + builder.SetCurrentBlock(currentBlk) + encodedExitCode := uint64(wazevoapi.ExitCodeTryTableEnter | wazevoapi.ExitCode(tryTableID<<8)) + + // Load trampoline address from execCtx. + enterPtr := builder.AllocateInstruction(). + AsLoad(c.execCtxPtrValue, + wazevoapi.ExecutionContextOffsetTryTableEnterTrampolineAddress.U32(), + ssa.TypeI64, + ).Insert(builder).Return() + + // Call the trampoline: (execCtx, encodedExitCode) -> (). + exitCodeVal := builder.AllocateInstruction().AsIconst64(encodedExitCode).Insert(builder).Return() + args := c.allocateVarLengthValues(2, c.execCtxPtrValue, exitCodeVal) + builder.AllocateInstruction(). + AsCallIndirect(enterPtr, &c.tryTableEnterSig, args). + Insert(builder) + + // Load the caught clause index written by the dispatch loop. + clauseIdx := builder.AllocateInstruction(). + AsLoad(c.execCtxPtrValue, + wazevoapi.ExecutionContextOffsetCaughtExceptionClauseIdx.U32(), + ssa.TypeI64, + ).Insert(builder).Return() + + // Dispatch to handler blocks or body block via br_table. + brTable := builder.AllocateInstruction() + brTable.AsBrTable(clauseIdx, targets) + builder.InsertInstruction(brTable) + + // Seal handler blocks after BrTable is inserted (so predecessors are registered). + for _, targetID := range targets.View() { + blk := builder.BasicBlock(ssa.BasicBlockID(targetID)) + if !blk.Sealed() { + builder.Seal(blk) + } + } + } else { + // No catch clauses — try_table acts as a plain block. + // Jump directly to body without entering exception handling. + c.insertJumpToBlock(ssa.ValuesNil, bodyBlk) + } + + if !bodyBlk.Sealed() { + builder.Seal(bodyBlk) + } + builder.SetCurrentBlock(bodyBlk) + if len(catchClauses) > 0 { + // Body block is entered after the trampoline call, so we need to reload. + c.reloadAfterCall() + } + + // Push the try_table control frame AFTER resolving catch labels. + state.ctrlPush(controlFrame{ + kind: controlFrameKindTryTable, + originalStackLenWithoutParam: len(state.values) - len(bt.Params), + followingBlock: followingBlk, + blockType: bt, + }) + default: panic("TODO: unsupported in wazevo yet: " + wasm.InstructionName(op)) } @@ -4026,6 +4296,185 @@ func (c *Compiler) storeCallerModuleContext() { builder.InsertInstruction(store) } +// resolveTagType returns the FunctionType for the tag at the given module-local index. +func (c *Compiler) resolveTagType(tagIndex uint32) *wasm.FunctionType { + if tagIndex < c.m.ImportTagCount { + cur := uint32(0) + for i := range c.m.ImportSection { + imp := &c.m.ImportSection[i] + if imp.Type != wasm.ExternTypeTag { + continue + } + if tagIndex == cur { + return &c.m.TypeSection[imp.DescTag] + } + cur++ + } + } else { + tagSectionIdx := tagIndex - c.m.ImportTagCount + if tagSectionIdx < uint32(len(c.m.TagSection)) { + typeIdx := c.m.TagSection[tagSectionIdx].Type + return &c.m.TypeSection[typeIdx] + } + } + return nil +} + +// emitThrow emits a call to the shared throw trampoline with the given exnref, +// followed by an unreachable exit (throw never returns). +func (c *Compiler) emitThrow(exnref ssa.Value) { + builder := c.ssaBuilder + throwPtr := builder.AllocateInstruction(). + AsLoad(c.execCtxPtrValue, + wazevoapi.ExecutionContextOffsetThrowTrampolineAddress.U32(), + ssa.TypeI64, + ).Insert(builder).Return() + throwArgs := c.allocateVarLengthValues(2, c.execCtxPtrValue, exnref) + builder.AllocateInstruction(). + AsCallIndirect(throwPtr, &c.throwSig, throwArgs). + Insert(builder) + + exit := builder.AllocateInstruction() + exit.AsExitWithCode(c.execCtxPtrValue, wazevoapi.ExitCodeUnreachable) + builder.InsertInstruction(exit) +} + +// emitTryTableLeave emits a trampoline call to pop the try handler in the dispatch loop. +func (c *Compiler) emitTryTableLeave() { + builder := c.ssaBuilder + c.storeCallerModuleContext() + + leavePtr := builder.AllocateInstruction(). + AsLoad(c.execCtxPtrValue, + wazevoapi.ExecutionContextOffsetTryTableLeaveTrampolineAddress.U32(), + ssa.TypeI64, + ).Insert(builder).Return() + + args := c.allocateVarLengthValues(1, c.execCtxPtrValue) + builder.AllocateInstruction(). + AsCallIndirect(leavePtr, &c.tryTableLeaveSig, args). + Insert(builder) +} + +// branchExitsTryTable returns true if a branch to the given depth would +// exit at least one try_table frame. +func (c *Compiler) branchExitsTryTable(depth int) bool { + state := c.state() + tail := len(state.controlFrames) - 1 + for i := 0; i < depth; i++ { + if state.controlFrames[tail-i].kind == controlFrameKindTryTable { + return true + } + } + return false +} + +// emitTryTableLeaves emits TryTableLeave calls for try_table frames +// that would be exited by a branch to the given depth. +func (c *Compiler) emitTryTableLeaves(depth int) { + state := c.state() + tail := len(state.controlFrames) - 1 + for i := 0; i < depth; i++ { + if state.controlFrames[tail-i].kind == controlFrameKindTryTable { + c.emitTryTableLeave() + } + } +} + +// catchClause holds a parsed catch clause from a try_table instruction. +type catchClause struct { + kind byte + tagIndex uint32 + labelIdx uint32 +} + +// loadExceptionParams loads the exception params from the caught Exception's +// Params slice. The dispatch loop sets execCtx.exceptionParamsPtr to the +// slice's backing-array pointer after matching a handler. We load that pointer +// and then read each param from [ptr + i*8], mirroring the stores emitted by +// the throw lowering. Float params were bitcast to integers at the throw site, +// so we load as integer and bitcast back to the original type. +func (c *Compiler) loadExceptionParams(tagType *wasm.FunctionType) []ssa.Value { + if len(tagType.Params) == 0 { + return nil + } + builder := c.ssaBuilder + + // Load the pointer to the caught Exception's Params backing array. + paramsPtr := builder.AllocateInstruction(). + AsLoad(c.execCtxPtrValue, + wazevoapi.ExecutionContextOffsetExceptionParamsPtr.U32(), + ssa.TypeI64, + ).Insert(builder).Return() + + var values []ssa.Value + for i, vt := range tagType.Params { + offset := uint32(i) * 8 + ssaType := WasmTypeToSSAType(vt) + switch ssaType { + case ssa.TypeF32: + // Stored as i32 at throw site; bitcast back to f32. + raw := builder.AllocateInstruction(). + AsLoad(paramsPtr, offset, ssa.TypeI32). + Insert(builder).Return() + val := builder.AllocateInstruction().AsBitcast(raw, ssa.TypeF32).Insert(builder).Return() + values = append(values, val) + case ssa.TypeF64: + // Stored as i64 at throw site; bitcast back to f64. + raw := builder.AllocateInstruction(). + AsLoad(paramsPtr, offset, ssa.TypeI64). + Insert(builder).Return() + val := builder.AllocateInstruction().AsBitcast(raw, ssa.TypeF64).Insert(builder).Return() + values = append(values, val) + default: + val := builder.AllocateInstruction(). + AsLoad(paramsPtr, offset, ssaType). + Insert(builder).Return() + values = append(values, val) + } + } + return values +} + +// loadExnRef loads the exnref (pointer to Exception) from the executionContext. +// The dispatch loop writes it to exceptionPtr after matching a handler. +func (c *Compiler) loadExnRef() ssa.Value { + builder := c.ssaBuilder + return builder.AllocateInstruction(). + AsLoad(c.execCtxPtrValue, + wazevoapi.ExecutionContextOffsetExceptionPtr.U32(), + ssa.TypeI64, + ).Insert(builder).Return() +} + +// skipTryTableCatchClauses advances the bytecode PC past the catch clauses +// of a try_table instruction. This is used both in reachable and unreachable states. +func (c *Compiler) skipTryTableCatchClauses() { + c.loweringState.pc++ + catchCount, catchNum, _ := leb128.LoadUint32(c.wasmFunctionBody[c.loweringState.pc:]) + c.loweringState.pc += int(catchNum) - 1 + for i := uint32(0); i < catchCount; i++ { + c.loweringState.pc++ + kind := c.wasmFunctionBody[c.loweringState.pc] + switch kind { + case wasm.CatchKindCatch, wasm.CatchKindCatchRef: + // Read tag index. + c.loweringState.pc++ + _, n, _ := leb128.LoadUint32(c.wasmFunctionBody[c.loweringState.pc:]) + c.loweringState.pc += int(n) - 1 + // Read label index. + c.loweringState.pc++ + _, n, _ = leb128.LoadUint32(c.wasmFunctionBody[c.loweringState.pc:]) + c.loweringState.pc += int(n) - 1 + case wasm.CatchKindCatchAll, wasm.CatchKindCatchAllRef: + // Read label index. + c.loweringState.pc++ + _, n, _ := leb128.LoadUint32(c.wasmFunctionBody[c.loweringState.pc:]) + c.loweringState.pc += int(n) - 1 + } + } +} + func (c *Compiler) readByte() byte { v := c.wasmFunctionBody[c.loweringState.pc+1] c.loweringState.pc++ @@ -4181,6 +4630,7 @@ func (c *Compiler) lowerBrTable(labels []uint32, index ssa.Value) { targetBlk, _ := state.brTargetArgNumFor(l) trampoline := builder.AllocateBasicBlock() builder.SetCurrentBlock(trampoline) + c.emitTryTableLeaves(int(l)) c.insertJumpToBlock(args, targetBlk) trampolineBlockIDs = trampolineBlockIDs.Append(builder.VarLengthPool(), ssa.Value(trampoline.ID())) } diff --git a/internal/engine/wazevo/module_engine.go b/internal/engine/wazevo/module_engine.go index 41c38b86b..e740c988b 100644 --- a/internal/engine/wazevo/module_engine.go +++ b/internal/engine/wazevo/module_engine.go @@ -136,6 +136,14 @@ func (m *moduleEngine) setupOpaque() { } } + if tagOffset := offsets.TagsBegin; tagOffset >= 0 { + for _, tag := range inst.Tags { + binary.LittleEndian.PutUint64(opaque[tagOffset:], + uint64(uintptr(unsafe.Pointer(tag)))) + tagOffset += 8 + } + } + if beforeListenerOffset := offsets.BeforeListenerTrampolines1stElement; beforeListenerOffset >= 0 { binary.LittleEndian.PutUint64(opaque[beforeListenerOffset:], uint64(uintptr(unsafe.Pointer(&m.parent.listenerBeforeTrampolines[0])))) } @@ -208,6 +216,10 @@ func (m *moduleEngine) NewFunction(index wasm.Index) api.Function { ce.execCtx.memoryWait32TrampolineAddress = sharedFunctions.memoryWait32Address ce.execCtx.memoryWait64TrampolineAddress = sharedFunctions.memoryWait64Address ce.execCtx.memoryNotifyTrampolineAddress = sharedFunctions.memoryNotifyAddress + ce.execCtx.throwAllocTrampolineAddress = sharedFunctions.throwAllocTrampolineAddress + ce.execCtx.throwTrampolineAddress = sharedFunctions.throwTrampolineAddress + ce.execCtx.tryTableEnterTrampolineAddress = sharedFunctions.tryTableEnterAddress + ce.execCtx.tryTableLeaveTrampolineAddress = sharedFunctions.tryTableLeaveAddress ce.execCtx.memmoveAddress = memmovPtr ce.init() return ce diff --git a/internal/engine/wazevo/ssa/basic_block.go b/internal/engine/wazevo/ssa/basic_block.go index cf7f14d3b..25bdccdf4 100644 --- a/internal/engine/wazevo/ssa/basic_block.go +++ b/internal/engine/wazevo/ssa/basic_block.go @@ -291,9 +291,9 @@ func (bb *basicBlock) addPred(blk BasicBlock, branch *Instruction) { for i := range bb.preds { existingPred := &bb.preds[i] if existingPred.blk == pred && existingPred.branch != branch { - // If the target is already added, then this must come from the same BrTable, + // If the target is already added, then this must come from the same BrTable or TryTableDispatch, // otherwise such redundant branch should be eliminated by the frontend. (which should be simpler). - panic(fmt.Sprintf("BUG: redundant non BrTable jumps in %s whose targes are the same", bb.Name())) + panic(fmt.Sprintf("BUG: redundant non BrTable/TryTableDispatch jumps in %s whose targets are the same", bb.Name())) } } @@ -344,19 +344,21 @@ func (bb *basicBlock) validate(b *builder) { } } - var exp int - if bb.ReturnBlock() { - exp = len(b.currentSignature.Results) - } else { - exp = len(bb.params.View()) - } + if pred.branch.opcode != OpcodeBrTable { + var exp int + if bb.ReturnBlock() { + exp = len(b.currentSignature.Results) + } else { + exp = len(bb.params.View()) + } - if len(pred.branch.vs.View()) != exp { - panic(fmt.Sprintf( - "BUG: len(argument at %s) != len(params at %s): %d != %d: %s", - pred.blk.Name(), bb.Name(), - len(pred.branch.vs.View()), len(bb.params.View()), pred.branch.Format(b), - )) + if len(pred.branch.vs.View()) != exp { + panic(fmt.Sprintf( + "BUG: len(argument at %s) != len(params at %s): %d != %d: %s", + pred.blk.Name(), bb.Name(), + len(pred.branch.vs.View()), len(bb.params.View()), pred.branch.Format(b), + )) + } } } diff --git a/internal/engine/wazevo/testcases/testcases.go b/internal/engine/wazevo/testcases/testcases.go index 103aa3e5b..22a8daeaf 100644 --- a/internal/engine/wazevo/testcases/testcases.go +++ b/internal/engine/wazevo/testcases/testcases.go @@ -2723,6 +2723,372 @@ func VecShuffleWithLane(lane ...byte) *wasm.Module { } } +// Exception Handling test cases. + +// ThrowOnly: a function that always throws tag 0 (no params). +// Expected: uncaught exception trap. +// +// (module +// (tag $e) +// (func (export "f") (throw $e)) +// ) +var ThrowOnly = TestCase{ + Name: "throw_only", + Module: &wasm.Module{ + TypeSection: []wasm.FunctionType{vv}, + FunctionSection: []wasm.Index{0}, + TagSection: []wasm.Tag{{Type: 0}}, + CodeSection: []wasm.Code{{ + Body: []byte{ + wasm.OpcodeThrow, 0, // throw $e (tag index 0) + wasm.OpcodeEnd, + }, + }}, + ExportSection: []wasm.Export{{Name: ExportedFunctionName, Type: wasm.ExternTypeFunc, Index: 0}}, + }, +} + +// ThrowWithParam: throws tag 0 (i32 param) with the function's i32 parameter. +// Expected: uncaught exception trap. +// +// (module +// (tag $e (param i32)) +// (func (export "f") (param i32) (local.get 0) (throw $e)) +// ) +var ThrowWithParam = TestCase{ + Name: "throw_with_param", + Module: &wasm.Module{ + TypeSection: []wasm.FunctionType{ + {Params: []wasm.ValueType{i32}}, // type 0: tag type (i32) -> () + {Params: []wasm.ValueType{i32}}, // type 1: func type (i32) -> () + }, + FunctionSection: []wasm.Index{1}, + TagSection: []wasm.Tag{{Type: 0}}, + CodeSection: []wasm.Code{{ + Body: []byte{ + wasm.OpcodeLocalGet, 0, + wasm.OpcodeThrow, 0, // throw $e (tag index 0) + wasm.OpcodeEnd, + }, + }}, + ExportSection: []wasm.Export{{Name: ExportedFunctionName, Type: wasm.ExternTypeFunc, Index: 0}}, + }, +} + +// TryTableCatchAllEmpty: try_table with catch_all where the body does not throw. +// The try_table body pushes 42 and returns. catch_all is never triggered. +// +// (module +// (func (export "f") (result i32) +// (block $caught ;; catch_all target, expects 0 values +// (try_table (catch_all $caught) +// (i32.const 42) +// (return) +// ) +// (i32.const 99) +// (return) +// ) +// (i32.const 0) +// ) +// ) +var TryTableCatchAllEmpty = TestCase{ + Name: "try_table_catch_all_empty", + Module: &wasm.Module{ + TypeSection: []wasm.FunctionType{{Results: []wasm.ValueType{i32}}}, + FunctionSection: []wasm.Index{0}, + CodeSection: []wasm.Code{{ + Body: []byte{ + // block $caught -- void block (catch_all delivers 0 values) + wasm.OpcodeBlock, blockSignature_vv, + // try_table (catch_all $caught) -- void block + wasm.OpcodeTryTable, blockSignature_vv, + 1, // catch clause count = 1 + wasm.CatchKindCatchAll, // catch_all + 0, // label index 0 = $caught (try_table not yet on stack) + // body: return 42 directly + wasm.OpcodeI32Const, 42, + wasm.OpcodeReturn, + wasm.OpcodeEnd, // end try_table + // After try_table (catch_all path): return 99 + wasm.OpcodeI32Const, 0xe3, 0x00, // 99 in signed LEB128 + wasm.OpcodeReturn, + wasm.OpcodeEnd, // end block $caught + // Should not reach here. + wasm.OpcodeI32Const, 0, + wasm.OpcodeEnd, // end function + }, + }}, + ExportSection: []wasm.Export{{Name: ExportedFunctionName, Type: wasm.ExternTypeFunc, Index: 0}}, + }, +} + +// TryTableCatchAllThrow: try_table with catch_all where the body throws. +// The catch_all catches the exception, falls through, and returns 42. +// +// (module +// (tag $e) +// (func (export "f") (result i32) +// (block $caught +// (try_table (catch_all $caught) +// (throw $e) +// ) +// (unreachable) +// ) +// (i32.const 42) +// ) +// ) +var TryTableCatchAllThrow = TestCase{ + Name: "try_table_catch_all_throw", + Module: &wasm.Module{ + TypeSection: []wasm.FunctionType{vv, {Results: []wasm.ValueType{i32}}}, + FunctionSection: []wasm.Index{1}, // func type 1: () -> i32 + TagSection: []wasm.Tag{{Type: 0}}, // tag type 0: () -> () + CodeSection: []wasm.Code{{ + Body: []byte{ + wasm.OpcodeBlock, blockSignature_vv, // block $caught + wasm.OpcodeTryTable, blockSignature_vv, // try_table + 1, // 1 catch clause + wasm.CatchKindCatchAll, // catch_all + 0, // label 0 = $caught + wasm.OpcodeThrow, 0, // throw tag 0 + wasm.OpcodeEnd, // end try_table + wasm.OpcodeUnreachable, // after try_table (unreachable) + wasm.OpcodeEnd, // end block $caught + wasm.OpcodeI32Const, 42, + wasm.OpcodeEnd, // end func + }, + }}, + ExportSection: []wasm.Export{{Name: ExportedFunctionName, Type: wasm.ExternTypeFunc, Index: 0}}, + }, +} + +// TryTableCatchThrow: try_table with catch $e0 (specific tag, no params) where +// the body throws. The catch dispatches to the enclosing block. +// +// (module +// (tag $e0) +// (func (export "f") (result i32) +// (block $h ;; label 0 +// (try_table (result i32) (catch $e0 $h) +// (throw $e0) +// (i32.const 42) +// ) +// (return) +// ) +// (i32.const 23) +// ) +// ) +var TryTableCatchThrow = TestCase{ + Name: "try_table_catch_throw", + Module: &wasm.Module{ + TypeSection: []wasm.FunctionType{vv, {Results: []wasm.ValueType{i32}}}, + FunctionSection: []wasm.Index{1}, // func type 1: () -> i32 + TagSection: []wasm.Tag{{Type: 0}}, // tag type 0: () -> () + CodeSection: []wasm.Code{{ + Body: []byte{ + wasm.OpcodeBlock, blockSignature_vv, // block $h (label 0) + wasm.OpcodeTryTable, 0x7F, // try_table (result i32) + 1, // 1 catch clause + wasm.CatchKindCatch, // catch + 0, // tag index 0 ($e0) + 0, // label 0 ($h) + wasm.OpcodeThrow, 0, // throw tag 0 + wasm.OpcodeI32Const, 42, // (unreachable but needed for type) + wasm.OpcodeEnd, // end try_table + wasm.OpcodeReturn, // return try_table result + wasm.OpcodeEnd, // end block $h + wasm.OpcodeI32Const, 23, + wasm.OpcodeEnd, // end func + }, + }}, + ExportSection: []wasm.Export{{Name: ExportedFunctionName, Type: wasm.ExternTypeFunc, Index: 0}}, + }, +} + +// TryTableCatchParamThrow: try_table with catch $e-i32 where the tag has an i32 parameter. +// The exception parameter is forwarded to the catch target block. +// +// (module +// (tag $e-i32 (param i32)) +// (func (export "f") (param i32) (result i32) +// (block $h (result i32) +// (try_table (result i32) (catch $e-i32 $h) +// (throw $e-i32 (local.get 0)) +// (i32.const 2) +// ) +// (return) +// ) +// (return) +// ) +// ) +var TryTableCatchParamThrow = TestCase{ + Name: "try_table_catch_param_throw", + Module: &wasm.Module{ + TypeSection: []wasm.FunctionType{ + {Params: []wasm.ValueType{i32}}, // type 0: (i32) -> () for tag + {Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i32}}, // type 1: (i32) -> (i32) for func + }, + FunctionSection: []wasm.Index{1}, // func type 1 + TagSection: []wasm.Tag{{Type: 0}}, // tag type 0: (i32) -> () + CodeSection: []wasm.Code{{ + Body: []byte{ + wasm.OpcodeBlock, 0x7F, // block $h (result i32) -- label 0 + wasm.OpcodeTryTable, 0x7F, // try_table (result i32) + 1, // 1 catch clause + wasm.CatchKindCatch, // catch + 0, // tag index 0 ($e-i32) + 0, // label 0 ($h) + wasm.OpcodeLocalGet, 0, // local.get 0 + wasm.OpcodeThrow, 0, // throw tag 0 + wasm.OpcodeI32Const, 2, // (unreachable but needed for type) + wasm.OpcodeEnd, // end try_table + wasm.OpcodeReturn, // return try_table result + wasm.OpcodeEnd, // end block $h + wasm.OpcodeReturn, // return + wasm.OpcodeEnd, // end func + }, + }}, + ExportSection: []wasm.Export{{Name: ExportedFunctionName, Type: wasm.ExternTypeFunc, Index: 0}}, + }, +} + +// TryTableCatchManyParamThrow: try_table with catch $e where the tag has 5 i32 parameters. +// This exercises tags with more than 4 parameters, exposing the hardcoded 4-param limit in +// caughtExceptionParams [4]uint64. The 5th (and beyond) parameter will be silently dropped +// under the current implementation. +// +// (module +// (tag $e (param i32 i32 i32 i32 i32)) +// (func (export "f") (param i32 i32 i32 i32 i32) (result i32 i32 i32 i32 i32) +// (block $h (result i32 i32 i32 i32 i32) +// (try_table (result i32 i32 i32 i32 i32) (catch $e $h) +// (local.get 0) +// (local.get 1) +// (local.get 2) +// (local.get 3) +// (local.get 4) +// (throw $e) +// (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0) +// ) +// (return) +// ) +// (return) +// ) +// ) +var TryTableCatchManyParamThrow = TestCase{ + Name: "try_table_catch_many_param_throw", + Module: &wasm.Module{ + TypeSection: []wasm.FunctionType{ + {Params: []wasm.ValueType{i32, i32, i32, i32, i32}}, // type 0: (i32x5) -> () for tag + {Params: []wasm.ValueType{i32, i32, i32, i32, i32}, Results: []wasm.ValueType{i32, i32, i32, i32, i32}}, // type 1: (i32x5) -> (i32x5) for func + {Results: []wasm.ValueType{i32, i32, i32, i32, i32}}, // type 2: () -> (i32x5) for block/try_table + }, + FunctionSection: []wasm.Index{1}, // func type 1 + TagSection: []wasm.Tag{{Type: 0}}, // tag type 0: (i32x5) -> () + CodeSection: []wasm.Code{{ + Body: []byte{ + wasm.OpcodeBlock, 0x02, // block $h (result i32 i32 i32 i32 i32) -- type index 2 + wasm.OpcodeTryTable, 0x02, // try_table (result i32 i32 i32 i32 i32) -- type index 2 + 1, // 1 catch clause + wasm.CatchKindCatch, // catch + 0, // tag index 0 ($e) + 0, // label 0 ($h) + wasm.OpcodeLocalGet, 0, // local.get 0 + wasm.OpcodeLocalGet, 1, // local.get 1 + wasm.OpcodeLocalGet, 2, // local.get 2 + wasm.OpcodeLocalGet, 3, // local.get 3 + wasm.OpcodeLocalGet, 4, // local.get 4 + wasm.OpcodeThrow, 0, // throw tag 0 + wasm.OpcodeI32Const, 0, // unreachable dummy results + wasm.OpcodeI32Const, 0, + wasm.OpcodeI32Const, 0, + wasm.OpcodeI32Const, 0, + wasm.OpcodeI32Const, 0, + wasm.OpcodeEnd, // end try_table + wasm.OpcodeReturn, // return try_table result + wasm.OpcodeEnd, // end block $h + wasm.OpcodeReturn, // return + wasm.OpcodeEnd, // end func + }, + }}, + ExportSection: []wasm.Export{{Name: ExportedFunctionName, Type: wasm.ExternTypeFunc, Index: 0}}, + }, +} + +// TryTableCatchWithReturnCall exercises the interaction between try_table and tail calls. +// The try_table body performs a conditional: +// - param=1: TryTableLeave (pops handler) + return_call $target → returns 99 +// - param=0: throw $e (caught by the catch clause) → catch handler returns 42 +// +// This verifies two things: +// +// 1. removeUntilRet does not accidentally remove the catch handler block that follows +// the tail-call path in the instruction stream. +// +// 2. TryTableLeave + return_call correctly pass control to the tail callee. +// +// (module +// (tag $e) +// (func $target (result i32) (i32.const 99)) +// (func (export "f") (param i32) (result i32) +// (block $h +// (try_table (catch $e $h) +// (if (local.get 0) +// (then (return_call $target)) +// (else (throw $e)) +// ) +// (unreachable) +// ) +// (return) +// ) +// (i32.const 42) +// ) +// ) +var TryTableCatchWithReturnCall = TestCase{ + Name: "try_table_catch_with_return_call", + Module: &wasm.Module{ + TypeSection: []wasm.FunctionType{ + vv, // type 0: () -> () for tag $e + {Results: []wasm.ValueType{i32}}, // type 1: () -> i32 for $target + {Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i32}}, // type 2: (i32) -> i32 for $f + }, + FunctionSection: []wasm.Index{1, 2}, // $target = func 0 (type 1), $f = func 1 (type 2) + TagSection: []wasm.Tag{{Type: 0}}, + CodeSection: []wasm.Code{ + {Body: []byte{wasm.OpcodeI32Const, 0xe3, 0x00, wasm.OpcodeEnd}}, // $target: () -> i32 (99 in signed LEB128) + {Body: []byte{ + // block $h (void) = label 0; catch $e delivers 0 values to label 0. + wasm.OpcodeBlock, blockSignature_vv, + // try_table (result i32): type is declared even though the body is unreachable + // (both if-branches diverge). The result is consumed by the return on the normal + // path (which is also unreachable), satisfying the validator. + wasm.OpcodeTryTable, 0x7F, + 1, // 1 catch clause + wasm.CatchKindCatch, // catch $e (no params) → label 0 ($h) + 0, // tag index 0 + 0, // label 0 ($h) + // body: conditional — both branches diverge. + wasm.OpcodeLocalGet, 0, + wasm.OpcodeIf, blockSignature_vv, // void if — both branches diverge + // then: TryTableLeave (pops handler) + return_call $target (tail call) + wasm.OpcodeTailCallReturnCall, 0, + wasm.OpcodeElse, + // else: throw $e → caught by try_table → branches to end of block $h + wasm.OpcodeThrow, 0, + wasm.OpcodeEnd, // end if + wasm.OpcodeUnreachable, // unreachable + wasm.OpcodeEnd, // end try_table + wasm.OpcodeReturn, // unreachable (never reached) + wasm.OpcodeEnd, // end block $h + // catch handler: reached when $e is caught (jumped to end of block $h) + wasm.OpcodeI32Const, 42, + wasm.OpcodeEnd, // end func + }}, + }, + ExportSection: []wasm.Export{{Name: ExportedFunctionName, Type: wasm.ExternTypeFunc, Index: 1}}, + }, +} + type TestCase struct { Name string Imported, Module *wasm.Module diff --git a/internal/engine/wazevo/wazevo_test.go b/internal/engine/wazevo/wazevo_test.go index e7fa3049d..6242b3b9e 100644 --- a/internal/engine/wazevo/wazevo_test.go +++ b/internal/engine/wazevo/wazevo_test.go @@ -73,4 +73,11 @@ func Test_ExecutionContextOffsets(t *testing.T) { require.Equal(t, wazevoapi.Offset(unsafe.Offsetof(execCtx.memoryWait32TrampolineAddress)), wazevoapi.ExecutionContextOffsetMemoryWait32TrampolineAddress) require.Equal(t, wazevoapi.Offset(unsafe.Offsetof(execCtx.memoryWait64TrampolineAddress)), wazevoapi.ExecutionContextOffsetMemoryWait64TrampolineAddress) require.Equal(t, wazevoapi.Offset(unsafe.Offsetof(execCtx.memoryNotifyTrampolineAddress)), wazevoapi.ExecutionContextOffsetMemoryNotifyTrampolineAddress) + require.Equal(t, wazevoapi.Offset(unsafe.Offsetof(execCtx.throwTrampolineAddress)), wazevoapi.ExecutionContextOffsetThrowTrampolineAddress) + require.Equal(t, wazevoapi.Offset(unsafe.Offsetof(execCtx.tryTableEnterTrampolineAddress)), wazevoapi.ExecutionContextOffsetTryTableEnterTrampolineAddress) + require.Equal(t, wazevoapi.Offset(unsafe.Offsetof(execCtx.tryTableLeaveTrampolineAddress)), wazevoapi.ExecutionContextOffsetTryTableLeaveTrampolineAddress) + require.Equal(t, wazevoapi.Offset(unsafe.Offsetof(execCtx.throwAllocTrampolineAddress)), wazevoapi.ExecutionContextOffsetThrowAllocTrampolineAddress) + require.Equal(t, wazevoapi.Offset(unsafe.Offsetof(execCtx.exceptionPtr)), wazevoapi.ExecutionContextOffsetExceptionPtr) + require.Equal(t, wazevoapi.Offset(unsafe.Offsetof(execCtx.exceptionParamsPtr)), wazevoapi.ExecutionContextOffsetExceptionParamsPtr) + require.Equal(t, wazevoapi.Offset(unsafe.Offsetof(execCtx.caughtExceptionClauseIdx)), wazevoapi.ExecutionContextOffsetCaughtExceptionClauseIdx) } diff --git a/internal/engine/wazevo/wazevoapi/exitcode.go b/internal/engine/wazevo/wazevoapi/exitcode.go index 5ad594982..1055e478a 100644 --- a/internal/engine/wazevo/wazevoapi/exitcode.go +++ b/internal/engine/wazevo/wazevoapi/exitcode.go @@ -30,6 +30,25 @@ const ( ExitCodeMemoryWait64 ExitCodeMemoryNotify ExitCodeUnalignedAtomic + // ExitCodeThrowAlloc is the first phase of wasm throw: Go allocates the + // Exception heap object (with Params sized to the tag's param count) and + // writes its Params data pointer to execCtx.exceptionParamsPtr. + // Compiled code then stores params directly into the Exception.Params slice, + // followed by ExitCodeThrow to search for a matching handler. + ExitCodeThrowAlloc + // ExitCodeThrow is the shared throw/throw_ref exit code. + // The exnref is passed on the stack. The handler searches for a + // matching catch clause and restores the stack checkpoint. + ExitCodeThrow + // ExitCodeNullReference is an exit code for a null reference trap (throw_ref with null exnref). + ExitCodeNullReference + // ExitCodeTryTableEnter is an exit code for entering a try_table block. + // The catch clause info is encoded in the upper bits. The dispatch loop + // saves the current SP/FP/returnAddress as a try handler checkpoint. + ExitCodeTryTableEnter + // ExitCodeTryTableLeave is an exit code for leaving a try_table block. + // The dispatch loop pops the most recent try handler. + ExitCodeTryTableLeave exitCodeMax ) @@ -86,6 +105,16 @@ func (e ExitCode) String() string { return "memory_wait64" case ExitCodeMemoryNotify: return "memory_notify" + case ExitCodeThrowAlloc: + return "throw_alloc" + case ExitCodeThrow: + return "throw" + case ExitCodeNullReference: + return "null_reference" + case ExitCodeTryTableEnter: + return "try_table_enter" + case ExitCodeTryTableLeave: + return "try_table_leave" } panic("TODO") } @@ -107,3 +136,15 @@ func ExitCodeCallGoFunctionWithIndex(index int, withListener bool) ExitCode { func GoFunctionIndexFromExitCode(exitCode ExitCode) int { return int(exitCode >> 8) } + +// TryTableIDFromExitCode extracts the try-table ID from an ExitCodeTryTableEnter +// exit code. Uses the same encoding as GoFunctionIndexFromExitCode (upper 24 bits). +func TryTableIDFromExitCode(exitCode ExitCode) int { + return GoFunctionIndexFromExitCode(exitCode) +} + +// CatchClauseInstance is a runtime catch clause with resolved tag index. +type CatchClauseInstance struct { + Kind byte // wasm.CatchKindCatch, etc. + TagIndex uint32 // module-local tag index +} diff --git a/internal/engine/wazevo/wazevoapi/offsetdata.go b/internal/engine/wazevo/wazevoapi/offsetdata.go index fe6161b04..854365f90 100644 --- a/internal/engine/wazevo/wazevoapi/offsetdata.go +++ b/internal/engine/wazevo/wazevoapi/offsetdata.go @@ -53,6 +53,23 @@ const ( ExecutionContextOffsetMemoryWait32TrampolineAddress Offset = 1160 ExecutionContextOffsetMemoryWait64TrampolineAddress Offset = 1168 ExecutionContextOffsetMemoryNotifyTrampolineAddress Offset = 1176 + // ExecutionContextOffsetThrowAllocTrampolineAddress is the address of the + // throw-alloc trampoline, which allocates the Exception heap object, + // sets exceptionParamsPtr, and returns the exnref. + ExecutionContextOffsetThrowAllocTrampolineAddress Offset = 1184 + ExecutionContextOffsetThrowTrampolineAddress Offset = 1192 + ExecutionContextOffsetTryTableEnterTrampolineAddress Offset = 1200 + ExecutionContextOffsetTryTableLeaveTrampolineAddress Offset = 1208 + // ExecutionContextOffsetExceptionPtr holds the pointer to the Exception struct, + // used on the throw side and by catch_ref/catch_all_ref handlers. + ExecutionContextOffsetExceptionPtr Offset = 1216 + // ExecutionContextOffsetExceptionParamsPtr points into the Exception's + // Params slice backing array. Used by both throw (store params) and + // catch (load params) sides. + ExecutionContextOffsetExceptionParamsPtr Offset = 1224 + // ExecutionContextOffsetCaughtExceptionClauseIdx is the matched catch clause index + // written by handleException and read by compiled handler dispatch code. + ExecutionContextOffsetCaughtExceptionClauseIdx Offset = 1232 ) // ModuleContextOffsetData allows the compilers to get the information about offsets to the fields of wazevo.moduleContextOpaque, @@ -66,6 +83,7 @@ type ModuleContextOffsetData struct { GlobalsBegin, TypeIDs1stElement, TablesBegin, + TagsBegin, BeforeListenerTrampolines1stElement, AfterListenerTrampolines1stElement, DataInstances1stElement, @@ -122,6 +140,11 @@ func (m *ModuleContextOffsetData) TableOffset(tableIndex int) Offset { return m.TablesBegin + Offset(tableIndex)*8 } +// TagOffset returns an offset of the i-th tag instance pointer. +func (m *ModuleContextOffsetData) TagOffset(tagIndex int) Offset { + return m.TagsBegin + Offset(tagIndex)*8 +} + // NewModuleContextOffsetData creates a ModuleContextOffsetData determining the structure of moduleContextOpaque for the given Module. // The structure is described in the comment of wazevo.moduleContextOpaque. func NewModuleContextOffsetData(m *wasm.Module, withListener bool) ModuleContextOffsetData { @@ -185,6 +208,15 @@ func NewModuleContextOffsetData(m *wasm.Module, withListener bool) ModuleContext ret.TablesBegin = -1 } + if tags := int(m.ImportTagCount) + len(m.TagSection); tags > 0 { + offset = align8(offset) + ret.TagsBegin = offset + // Pointers to *wasm.TagInstance. + offset += Offset(tags) * 8 + } else { + ret.TagsBegin = -1 + } + if withListener { offset = align8(offset) ret.BeforeListenerTrampolines1stElement = offset diff --git a/internal/engine/wazevo/wazevoapi/offsetdata_test.go b/internal/engine/wazevo/wazevoapi/offsetdata_test.go index 09cf6b712..23751b690 100644 --- a/internal/engine/wazevo/wazevoapi/offsetdata_test.go +++ b/internal/engine/wazevo/wazevoapi/offsetdata_test.go @@ -24,6 +24,7 @@ func TestNewModuleContextOffsetData(t *testing.T) { GlobalsBegin: -1, TypeIDs1stElement: -1, TablesBegin: -1, + TagsBegin: -1, BeforeListenerTrampolines1stElement: -1, AfterListenerTrampolines1stElement: -1, DataInstances1stElement: 8, @@ -41,6 +42,7 @@ func TestNewModuleContextOffsetData(t *testing.T) { GlobalsBegin: -1, TypeIDs1stElement: -1, TablesBegin: -1, + TagsBegin: -1, BeforeListenerTrampolines1stElement: -1, AfterListenerTrampolines1stElement: -1, DataInstances1stElement: 24, @@ -58,6 +60,7 @@ func TestNewModuleContextOffsetData(t *testing.T) { GlobalsBegin: -1, TypeIDs1stElement: -1, TablesBegin: -1, + TagsBegin: -1, BeforeListenerTrampolines1stElement: -1, AfterListenerTrampolines1stElement: -1, DataInstances1stElement: 24, @@ -75,6 +78,7 @@ func TestNewModuleContextOffsetData(t *testing.T) { GlobalsBegin: -1, TypeIDs1stElement: -1, TablesBegin: -1, + TagsBegin: -1, BeforeListenerTrampolines1stElement: -1, AfterListenerTrampolines1stElement: -1, DataInstances1stElement: 10*FunctionInstanceSize + 8, @@ -92,6 +96,7 @@ func TestNewModuleContextOffsetData(t *testing.T) { GlobalsBegin: -1, TypeIDs1stElement: -1, TablesBegin: -1, + TagsBegin: -1, BeforeListenerTrampolines1stElement: -1, AfterListenerTrampolines1stElement: -1, DataInstances1stElement: 10*FunctionInstanceSize + 24, @@ -117,6 +122,7 @@ func TestNewModuleContextOffsetData(t *testing.T) { GlobalsBegin: 32 + 10*FunctionInstanceSize, TypeIDs1stElement: 32 + 10*FunctionInstanceSize + 16*30, TablesBegin: 32 + 10*FunctionInstanceSize + 16*30 + 8, + TagsBegin: -1, BeforeListenerTrampolines1stElement: -1, AfterListenerTrampolines1stElement: -1, DataInstances1stElement: 32 + 10*FunctionInstanceSize + 16*30 + 8 + 8*15, @@ -143,6 +149,7 @@ func TestNewModuleContextOffsetData(t *testing.T) { GlobalsBegin: 32 + 10*FunctionInstanceSize, TypeIDs1stElement: 32 + 10*FunctionInstanceSize + 16*30, TablesBegin: 32 + 10*FunctionInstanceSize + 16*30 + 8, + TagsBegin: -1, BeforeListenerTrampolines1stElement: 32 + 10*FunctionInstanceSize + 16*30 + 8 + 8*15, AfterListenerTrampolines1stElement: 32 + 10*FunctionInstanceSize + 16*30 + 8 + 8*15 + 8, DataInstances1stElement: 32 + 10*FunctionInstanceSize + 16*30 + 8 + 8*15 + 16, diff --git a/internal/integration_test/engine/eh_hammer_test.go b/internal/integration_test/engine/eh_hammer_test.go new file mode 100644 index 000000000..c72347abf --- /dev/null +++ b/internal/integration_test/engine/eh_hammer_test.go @@ -0,0 +1,215 @@ +package adhoc + +import ( + "context" + "fmt" + "testing" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/experimental" + "github.com/tetratelabs/wazero/internal/platform" + "github.com/tetratelabs/wazero/internal/testing/require" + "github.com/tetratelabs/wazero/internal/wasm" +) + +// TestEHParallelCompilation is a hammer test that exercises parallel compilation +// of a module with many functions, each containing a try_table. Without proper +// synchronization of catch clause table IDs, parallel workers will assign +// conflicting IDs, causing runtime panics or wrong handler dispatch. +func TestEHParallelCompilation(t *testing.T) { + if !platform.CompilerSupported() { + t.Skip() + } + + const numFuncs = 32 + bin := buildEHModule(numFuncs) + + for _, workers := range []int{2, 4, 8} { + workers := workers + t.Run(fmt.Sprintf("workers=%d", workers), func(t *testing.T) { + ctx := experimental.WithCompilationWorkers(context.Background(), workers) + cfg := wazero.NewRuntimeConfigCompiler(). + WithCoreFeatures(api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling) + r := wazero.NewRuntimeWithConfig(ctx, cfg) + defer r.Close(ctx) + + mod, err := r.InstantiateWithConfig(ctx, bin, + wazero.NewModuleConfig().WithStartFunctions()) + require.NoError(t, err) + + for i := 0; i < numFuncs; i++ { + name := fmt.Sprintf("f%d", i) + res, err := mod.ExportedFunction(name).Call(ctx) + require.NoError(t, err, "function %s", name) + require.Equal(t, uint64(i), res[0], "function %s returned wrong value", name) + } + }) + } +} + +// buildEHModule constructs a wasm binary with `n` exported functions, each +// containing a try_table that catches a throw and returns the function index. +// +// Each function looks like: +// +// (func (export "fN") (result i32) +// block $caught +// try_table (catch_all $caught) +// throw $tag +// end +// i32.const -1 ;; unreachable +// return +// end +// i32.const N ;; caught: return function index +// ) +func buildEHModule(n int) []byte { + m := &wasm.Module{ + // One type: () -> (i32) + TypeSection: []wasm.FunctionType{ + {Results: []wasm.ValueType{wasm.ValueTypeI32}}, + {}, // tag type: () -> () + }, + // One tag with type 1 (empty params, empty results) + TagSection: []wasm.Tag{{Type: 1}}, + } + + for i := 0; i < n; i++ { + m.FunctionSection = append(m.FunctionSection, 0) // type 0: () -> (i32) + m.ExportSection = append(m.ExportSection, wasm.Export{ + Type: wasm.ExternTypeFunc, + Name: fmt.Sprintf("f%d", i), + Index: wasm.Index(i), + }) + + // Build the function body: + // block void ;; label 0 = $caught + // try_table (catch_all 0) + // throw 0 + // end + // i32.const -1 + // return + // end + // i32.const + // end + body := []byte{ + wasm.OpcodeBlock, 0x40, // block void + wasm.OpcodeTryTable, 0x40, // try_table void + 1, // catch count = 1 + wasm.CatchKindCatchAll, // catch_all + 0, // label index 0 (the enclosing block) + wasm.OpcodeThrow, 0, // throw tag 0 + wasm.OpcodeEnd, // end try_table + wasm.OpcodeI32Const, 0x7f, // i32.const -1 (signed LEB128) + wasm.OpcodeReturn, + wasm.OpcodeEnd, // end block + } + // i32.const — encode i as signed LEB128 + body = append(body, wasm.OpcodeI32Const) + body = appendSleb128(body, int32(i)) + body = append(body, wasm.OpcodeEnd) // end function + + m.CodeSection = append(m.CodeSection, wasm.Code{Body: body}) + } + + return encodeModule(m) +} + +// appendSleb128 appends a signed LEB128 encoding of v to b. +func appendSleb128(b []byte, v int32) []byte { + for { + c := byte(v & 0x7f) + v >>= 7 + if (v == 0 && c&0x40 == 0) || (v == -1 && c&0x40 != 0) { + return append(b, c) + } + b = append(b, c|0x80) + } +} + +// encodeModule encodes the wasm.Module into a valid wasm binary. +// This is a minimal encoder that handles the sections used by buildEHModule. +func encodeModule(m *wasm.Module) []byte { + var buf []byte + // Magic + version + buf = append(buf, 0x00, 0x61, 0x73, 0x6d) // \0asm + buf = append(buf, 0x01, 0x00, 0x00, 0x00) // version 1 + + // Type section (id=1) + buf = appendSection(buf, 1, func(s []byte) []byte { + s = appendUleb128(s, uint32(len(m.TypeSection))) + for _, ft := range m.TypeSection { + s = append(s, 0x60) // func type + s = appendUleb128(s, uint32(len(ft.Params))) + s = append(s, ft.Params...) + s = appendUleb128(s, uint32(len(ft.Results))) + s = append(s, ft.Results...) + } + return s + }) + + // Function section (id=3) + buf = appendSection(buf, 3, func(s []byte) []byte { + s = appendUleb128(s, uint32(len(m.FunctionSection))) + for _, idx := range m.FunctionSection { + s = appendUleb128(s, idx) + } + return s + }) + + // Tag section (id=13) + buf = appendSection(buf, 13, func(s []byte) []byte { + s = appendUleb128(s, uint32(len(m.TagSection))) + for _, tag := range m.TagSection { + s = append(s, 0x00) // attribute byte (must be 0) + s = appendUleb128(s, tag.Type) + } + return s + }) + + // Export section (id=7) + buf = appendSection(buf, 7, func(s []byte) []byte { + s = appendUleb128(s, uint32(len(m.ExportSection))) + for _, exp := range m.ExportSection { + s = appendUleb128(s, uint32(len(exp.Name))) + s = append(s, exp.Name...) + s = append(s, exp.Type) + s = appendUleb128(s, exp.Index) + } + return s + }) + + // Code section (id=10) + buf = appendSection(buf, 10, func(s []byte) []byte { + s = appendUleb128(s, uint32(len(m.CodeSection))) + for _, code := range m.CodeSection { + // Each code entry: size + locals_count(0) + body + funcBody := appendUleb128(nil, 0) // 0 locals + funcBody = append(funcBody, code.Body...) + s = appendUleb128(s, uint32(len(funcBody))) + s = append(s, funcBody...) + } + return s + }) + + return buf +} + +func appendSection(buf []byte, id byte, buildContent func([]byte) []byte) []byte { + content := buildContent(nil) + buf = append(buf, id) + buf = appendUleb128(buf, uint32(len(content))) + buf = append(buf, content...) + return buf +} + +func appendUleb128(b []byte, v uint32) []byte { + for { + c := byte(v & 0x7f) + v >>= 7 + if v == 0 { + return append(b, c) + } + b = append(b, c|0x80) + } +} diff --git a/internal/integration_test/engine/exceptions_test.go b/internal/integration_test/engine/exceptions_test.go new file mode 100644 index 000000000..a53f20957 --- /dev/null +++ b/internal/integration_test/engine/exceptions_test.go @@ -0,0 +1,187 @@ +package adhoc + +// Exception handling integration tests for the interpreter engine. +// +// Background: these tests cover the Emscripten/pdfium-style EH pattern where +// exceptions propagate across multiple function call frames, each of which may +// have its own try_table handler (catch_all_ref + throw_ref cleanup pattern). +// +// The interpreter bug fixed here: when an inner callWithUnwind (e.g., in a +// "child" function) recovered a *thrownException whose matching try_table +// handler belonged to an outer (grandparent) callNativeFunc invocation, +// doRestore incorrectly restored grandparent's frame while still inside +// child's callNativeFunc. The child then started executing grandparent's +// body, eventually calling popTryHandler on an already-empty slice and +// panicking with "slice bounds out of range [:-1]". +// +// The fix: callWithUnwind only handles handlers whose savedFrames length is >= +// the caller's frame count (i.e., handlers set up at the current or deeper +// call depth). Handlers from outer invocations are re-panicked so that the +// correct outer callWithUnwind catches them. + +import ( + "context" + _ "embed" + "testing" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/experimental" + "github.com/tetratelabs/wazero/internal/platform" + "github.com/tetratelabs/wazero/internal/testing/require" + "github.com/tetratelabs/wazero/internal/wasmruntime" +) + +//go:embed testdata/eh_cross_callnative.wasm +var ehCrossCallnativeWasm []byte + +//go:embed testdata/eh_pdfium.wasm +var ehPdfiumWasm []byte + +//go:embed testdata/eh_throw_ref_null.wasm +var ehThrowRefNullWasm []byte + +//go:embed testdata/eh_br_orphan.wasm +var ehBrOrphanWasm []byte + +//go:embed testdata/eh_br_stale_handler.wasm +var ehBrStaleHandlerWasm []byte + +// TestExceptionHandlingInterpreter runs EH tests only for the interpreter. +func TestExceptionHandlingInterpreter(t *testing.T) { + cfg := wazero.NewRuntimeConfigInterpreter(). + WithCoreFeatures(api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling) + runEHTests(t, cfg) +} + +// TestExceptionHandlingCompiler runs EH tests for the compiler where supported. +func TestExceptionHandlingCompiler(t *testing.T) { + if !platform.CompilerSupported() { + t.Skip() + } + cfg := wazero.NewRuntimeConfigCompiler(). + WithCoreFeatures(api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling) + runEHTests(t, cfg) +} + +func runEHTests(t *testing.T, cfg wazero.RuntimeConfig) { + t.Run("cross_frame_catch", func(t *testing.T) { + testEHCrossFrameCatch(t, cfg) + }) + t.Run("pdfium_rethrow_pattern", func(t *testing.T) { + testEHPdfiumRethrow(t, cfg) + }) + t.Run("throw_ref_null", func(t *testing.T) { + testThrowRefNull(t, cfg) + }) + t.Run("br_exits_try_table", func(t *testing.T) { + testBrExitsTryTable(t, cfg) + }) + t.Run("br_stale_handler", func(t *testing.T) { + testBrStaleHandler(t, cfg) + }) +} + +// testEHCrossFrameCatch is the core reproducer for the interpreter bug: +// try_table in grandparent, exception thrown in grandchild, +// propagating through child which has no handler of its own. +// The grandparent's handler must catch it correctly. +func testEHCrossFrameCatch(t *testing.T, cfg wazero.RuntimeConfig) { + ctx := context.Background() + r := wazero.NewRuntimeWithConfig(ctx, cfg) + defer r.Close(ctx) + + mod, err := r.InstantiateWithConfig(ctx, ehCrossCallnativeWasm, + wazero.NewModuleConfig().WithStartFunctions()) + require.NoError(t, err) + + // grandparent has a try_table, calls child, child calls grandchild which throws. + // Grandparent's handler must catch via cross-frame propagation. + res, err := mod.ExportedFunction("test_cross_frame_catch").Call(ctx) + require.NoError(t, err) + require.Equal(t, int32(1), api.DecodeI32(res[0])) + + // Rethrow pattern: child has catch_all_ref + throw_ref, grandparent catches the rethrow. + res, err = mod.ExportedFunction("test_rethrow_cross_frame").Call(ctx) + require.NoError(t, err) + require.Equal(t, int32(2), api.DecodeI32(res[0])) +} + +// testEHPdfiumRethrow tests the Emscripten destructor-cleanup pattern: +// catch_all_ref captures the exnref, runs cleanup, then rethrows via throw_ref. +// This pattern appears in pdfium.wasm for C++ exception handling. +func testEHPdfiumRethrow(t *testing.T, cfg wazero.RuntimeConfig) { + ctx := context.Background() + r := wazero.NewRuntimeWithConfig(ctx, cfg) + defer r.Close(ctx) + + mod, err := r.InstantiateWithConfig(ctx, ehPdfiumWasm, + wazero.NewModuleConfig().WithStartFunctions()) + require.NoError(t, err) + + // One-level: leaf throws, level2 catches + rethrows, outer catches. + res, err := mod.ExportedFunction("test_one_level_rethrow").Call(ctx) + require.NoError(t, err) + require.Equal(t, int32(1), api.DecodeI32(res[0])) + + // Two-level: throw → catch_all_ref + throw_ref → catch_all_ref + throw_ref → catch. + res, err = mod.ExportedFunction("test_two_level_rethrow").Call(ctx) + require.NoError(t, err) + require.Equal(t, int32(1), api.DecodeI32(res[0])) +} + +// testThrowRefNull verifies that throw_ref on a null exnref traps with +// "null reference" (not "unreachable"). This was a bug where the interpreter +// used ErrRuntimeUnreachable instead of ErrRuntimeNullReference. +func testThrowRefNull(t *testing.T, cfg wazero.RuntimeConfig) { + ctx := context.Background() + r := wazero.NewRuntimeWithConfig(ctx, cfg) + defer r.Close(ctx) + + mod, err := r.InstantiateWithConfig(ctx, ehThrowRefNullWasm, + wazero.NewModuleConfig().WithStartFunctions()) + require.NoError(t, err) + + // Call with null exnref (0) — should trap as "null reference". + _, err = mod.ExportedFunction("throw_ref_null").Call(ctx, 0) + require.ErrorIs(t, err, wasmruntime.ErrRuntimeNullReference) +} + +// testBrExitsTryTable verifies that br/br_if that exits a try_table block +// correctly pops the try handler. Without the fix, orphaned handlers would +// cause a popTryHandler underflow panic ("slice bounds out of range [:-1]"). +func testBrExitsTryTable(t *testing.T, cfg wazero.RuntimeConfig) { + ctx := context.Background() + r := wazero.NewRuntimeWithConfig(ctx, cfg) + defer r.Close(ctx) + + mod, err := r.InstantiateWithConfig(ctx, ehBrOrphanWasm, + wazero.NewModuleConfig().WithStartFunctions()) + require.NoError(t, err) + + // The function calls loop_with_try (which exits try_table via br_if), + // then catches a throw in its own try_table. Should return 1. + res, err := mod.ExportedFunction("test").Call(ctx) + require.NoError(t, err) + require.Equal(t, int32(1), api.DecodeI32(res[0])) +} + +// testBrStaleHandler verifies that br exiting a try_table pops the handler +// so it doesn't interfere with later exception dispatch. Without the fix, +// the stale handler from try_table A incorrectly catches a throw meant for +// the outer handler, returning 99 instead of 1. +func testBrStaleHandler(t *testing.T, cfg wazero.RuntimeConfig) { + ctx := context.Background() + r := wazero.NewRuntimeWithConfig(ctx, cfg) + defer r.Close(ctx) + + mod, err := r.InstantiateWithConfig(ctx, ehBrStaleHandlerWasm, + wazero.NewModuleConfig().WithStartFunctions()) + require.NoError(t, err) + + // Without fix: stale handler A catches $tag1 -> wrong checkpoint restore. + // With fix: outer handler catches $tag1 -> returns 1. + res, err := mod.ExportedFunction("test").Call(ctx) + require.NoError(t, err) + require.Equal(t, int32(1), api.DecodeI32(res[0])) +} diff --git a/internal/integration_test/engine/testdata/eh_br_orphan.wasm b/internal/integration_test/engine/testdata/eh_br_orphan.wasm new file mode 100644 index 0000000000000000000000000000000000000000..475227966fb22bd7cb7caa56a4f977e05c028063 GIT binary patch literal 108 zcmXZSF%H5o5CzfsyOxMpN2Cu@!bz|T?qLclM3wZpdR#EYRFC)e6QJx3nhqK|Jh`y3 skISI6f3_XFKK+iBrO5EI@)~WW>iZxQy~IGwRG+%~yGfB1Rkx_b36E+Dm;e9( literal 0 HcmV?d00001 diff --git a/internal/integration_test/engine/testdata/eh_br_orphan.wat b/internal/integration_test/engine/testdata/eh_br_orphan.wat new file mode 100644 index 000000000..a10243237 --- /dev/null +++ b/internal/integration_test/engine/testdata/eh_br_orphan.wat @@ -0,0 +1,54 @@ +;; Hypothesis: br_if inside try_table body that exits the try_table block +;; skips the popTryHandler at the continuation label, leaving an orphaned handler. +;; The orphaned handler then incorrectly catches a later exception. +(module + (tag $e0) + + ;; This function has a try_table wrapping a loop. + ;; When br_if fires to exit the loop, it also exits the try_table + ;; without going through try_table's `end` (so popTryHandler is skipped). + (func $loop_with_try (param $n i32) + block $outer ;; $outer is the catch target + try_table (catch_all $outer) ;; handler pushed, targets $outer end + loop $loop + local.get $n + i32.eqz + br_if $outer ;; exits try_table body — skips popTryHandler! + local.get $n + i32.const 1 + i32.sub + local.set $n + br $loop + end + ;; only reached if n was 0 from the start; popTryHandler runs + end ;; try_table end: emits popTryHandler (only on fall-through path) + end ;; $outer + ) + + (func $throw + throw $e0 + ) + + (func (export "test") (result i32) + ;; Call loop_with_try — leaves orphaned handler in ce.tryHandlers + i32.const 3 + call $loop_with_try + + ;; Now throw inside a proper try_table. + ;; Correct: caught by the inner try_table → returns 1. + ;; Bug: caught by the orphaned handler from loop_with_try → wrong result. + block $done + block $catch + try_table (catch_all $catch) + call $throw + end + br $done + end + ;; exception caught — expected path + i32.const 1 + return + end + ;; no exception — shouldn't happen + i32.const 0 + ) +) diff --git a/internal/integration_test/engine/testdata/eh_br_stale_handler.wasm b/internal/integration_test/engine/testdata/eh_br_stale_handler.wasm new file mode 100644 index 0000000000000000000000000000000000000000..c235c8769f3255e04795b9e9ca8c4cdb52d6ef9e GIT binary patch literal 96 zcmXYnu?>JQ5ChL25D@_}0#hK`WRp-(QiYyjS%e@ceDYalD~AAi%aGCKOd4CO8=Y5k f?&XAj)q=%2zF&)f)1W~3M0lzsn?ky&a5V}Kq;2AnU$@nZCVQEAv*^N10LX9) zw4hZPog$^%t0el)r#OV@dcTC=p1zw0*LSfWq3RF#A&!1|{SL4#AG&~n6sRCau)SDf kR3y!MkcM*4aweMC@{~;F-HzmjD@u|2Pl~Td!})I`Z*0gNr~m)} literal 0 HcmV?d00001 diff --git a/internal/integration_test/engine/testdata/eh_cross_callnative.wat b/internal/integration_test/engine/testdata/eh_cross_callnative.wat new file mode 100644 index 000000000..9108925e5 --- /dev/null +++ b/internal/integration_test/engine/testdata/eh_cross_callnative.wat @@ -0,0 +1,75 @@ +;; Reproducer: try_table in grandparent, exception thrown in grandchild, +;; caught by grandparent's handler via cross-frame doRestore. +;; The `eh` branch interpreter bug: after doRestore removes the parent frame, +;; parent's callNativeFunc wrongly continues executing grandparent's body. +(module + (tag $e0 (param)) + + ;; grandchild: just throws + (func $grandchild + throw $e0 + ) + + ;; child: calls grandchild (no try_table of its own) + ;; exception propagates out of child to grandparent's handler + (func $child + call $grandchild + ) + + ;; grandparent: has a try_table, calls child + ;; When child's exception is caught by grandparent's handler, + ;; doRestore removes child's frame from ce.frames. + ;; child's callNativeFunc (running in callWithUnwind) must return, + ;; not continue executing grandparent's body. + (func $grandparent (result i32) + block $caught + try_table (catch_all $caught) + call $child + end + ;; No exception (unreachable in this test) + i32.const -1 + return + end + ;; Exception caught: return 1 + i32.const 1 + ) + + (func (export "test_cross_frame_catch") (result i32) + call $grandparent + ) + + ;; More realistic pdfium-style: grandparent has catch_all_ref, + ;; child has inner catch_all_ref + throw_ref, exception re-caught by grandparent + (func $child_rethrow + (local $x exnref) + block $done + block $catch (result exnref) + try_table (catch_all_ref $catch) + call $grandchild + end + br $done + end + local.set $x + local.get $x + throw_ref ;; rethrow: grandparent's handler must catch this + end + ) + + (func $grandparent_catches_rethrow (result i32) + (local $x exnref) + block $done + block $catch (result exnref) + try_table (catch_all_ref $catch) + call $child_rethrow + end + br $done + end + local.set $x + end + i32.const 2 ;; caught the rethrow + ) + + (func (export "test_rethrow_cross_frame") (result i32) + call $grandparent_catches_rethrow + ) +) diff --git a/internal/integration_test/engine/testdata/eh_pdfium.wasm b/internal/integration_test/engine/testdata/eh_pdfium.wasm new file mode 100644 index 0000000000000000000000000000000000000000..bc1876dca9ddd4598a96c3f89dd786560f24b71d GIT binary patch literal 182 zcmZ9?K?=e!6olcKmsU%kVsRn3QqSN8(vu`E0v1{WZLrH;-HM3P<^12^Hgp8QLOkHX z$*l7C=GM%8eU(EBLvm@1t_{!7x;~_PA4ixE#79^_R{A_c0X>q9SZPj{1{HKez2J;e W7s5Z;L+tz)RHF5?)0+BA{H7PP6&{2D literal 0 HcmV?d00001 diff --git a/internal/integration_test/engine/testdata/eh_pdfium.wat b/internal/integration_test/engine/testdata/eh_pdfium.wat new file mode 100644 index 000000000..ce8fbe3fa --- /dev/null +++ b/internal/integration_test/engine/testdata/eh_pdfium.wat @@ -0,0 +1,71 @@ +;; Tests pdfium/Emscripten EH pattern: +;; - throw in leaf +;; - catch_all_ref + throw_ref (Emscripten cleanup/rethrow pattern) at multiple call levels +;; - cross-frame exception propagation +(module + (tag $e0 (param)) + + ;; leaf: throws + (func $leaf + throw $e0 + ) + + ;; level2: catch_all_ref, cleanup, rethrow — like pdfium's inner destructor wrapper + (func $level2 + (local $x exnref) + block $done + block $catch (result exnref) + try_table (catch_all_ref $catch) + call $leaf + end + br $done + end + local.set $x + ;; cleanup ... + local.get $x + throw_ref + end + ) + + ;; level1: outer catch_all_ref, calls level2, then itself rethrows + ;; This tests: inner catch_all_ref -> throw_ref -> outer catch_all_ref -> throw_ref + (func $level1 + (local $x exnref) + block $done + block $catch (result exnref) + try_table (catch_all_ref $catch) + call $level2 + end + br $done + end + local.set $x + ;; outer cleanup ... + local.get $x + throw_ref + end + ) + + ;; top: calls level1 and catches with catch_all + (func (export "test_two_level_rethrow") (result i32) + block $caught + try_table (catch_all $caught) + call $level1 + end + i32.const -1 + return + end + i32.const 1 + ) + + ;; Verify the leaf throws are caught correctly at each level + (func (export "test_one_level_rethrow") (result i32) + block $caught + try_table (catch_all $caught) + call $level2 + end + i32.const -1 + return + end + i32.const 1 + ) +) diff --git a/internal/integration_test/engine/testdata/eh_throw_ref_null.wasm b/internal/integration_test/engine/testdata/eh_throw_ref_null.wasm new file mode 100644 index 0000000000000000000000000000000000000000..be7b7c31b862597a45665425e3e603a33beb3e05 GIT binary patch literal 48 zcmZQbEY4+QU|?WmWlUhqWMF1uWMCI! JW@Tgs@&WY42k-y@ literal 0 HcmV?d00001 diff --git a/internal/integration_test/spectest/exception-handling/testdata/tag.1.wasm b/internal/integration_test/spectest/exception-handling/testdata/tag.1.wasm new file mode 100644 index 0000000000000000000000000000000000000000..4a1a7f1478df3bdb8db275d77ee806a1642fe4e5 GIT binary patch literal 63 zcmZQbEY4+QU|?Y6VoG4FXGmbGuVr8oXJRQyEiPdyF=An0fHI9)7#JA@Sn?8cQ@J^r M7??^77@0~80dwsOqyPW_ literal 0 HcmV?d00001 diff --git a/internal/integration_test/spectest/exception-handling/testdata/tag.2.wasm b/internal/integration_test/spectest/exception-handling/testdata/tag.2.wasm new file mode 100644 index 0000000000000000000000000000000000000000..a223c51550ff108c54010e07e1061572b42140ed GIT binary patch literal 20 ZcmZQbEY4+QU|?WmWlUgTtmkD0asVBu0y_Wz literal 0 HcmV?d00001 diff --git a/internal/integration_test/spectest/exception-handling/testdata/tag.3.wasm b/internal/integration_test/spectest/exception-handling/testdata/tag.3.wasm new file mode 100644 index 0000000000000000000000000000000000000000..c3c1d6fe01c768d363c31ddc6ec471c521196a5e GIT binary patch literal 23 ccmZQbEY4+QU|?WmWlUgTtY=~aa#$D`03;#;Hvj+t literal 0 HcmV?d00001 diff --git a/internal/integration_test/spectest/exception-handling/testdata/tag.4.wasm b/internal/integration_test/spectest/exception-handling/testdata/tag.4.wasm new file mode 100644 index 0000000000000000000000000000000000000000..d277b404bfb1e2383a1d5d36c43553aa4cc79a92 GIT binary patch literal 51 zcmZQbEY4+QU|?Y6Wb|W7U|;|uUS^;GJ3AwDNn$z+1A_odUSe)43nvo;Q;8uXQ;87( Dqq7Fp literal 0 HcmV?d00001 diff --git a/internal/integration_test/spectest/exception-handling/testdata/tag.5.wasm b/internal/integration_test/spectest/exception-handling/testdata/tag.5.wasm new file mode 100644 index 0000000000000000000000000000000000000000..5cef55b2595d4f1c1936980b88a6032581c04c50 GIT binary patch literal 49 zcmZQbEY4+QU|?Y6Wb|W7U|;|uCN4%sU*?j;bQYkv083tCZYm2W69ZF;AtO_X5dfw7 B2M_=N literal 0 HcmV?d00001 diff --git a/internal/integration_test/spectest/exception-handling/testdata/tag.6.wasm b/internal/integration_test/spectest/exception-handling/testdata/tag.6.wasm new file mode 100644 index 0000000000000000000000000000000000000000..d0c2c860c1d83cf28e7294bf6a1e19f8ada73ced GIT binary patch literal 49 zcmZQbEY4+QU|?Y6Wb|W7U|;|uCN4%sU*?j;bQT6i1_745#N1RCP9_GX5<^C&5+eYn C4F?eb literal 0 HcmV?d00001 diff --git a/internal/integration_test/spectest/exception-handling/testdata/tag.7.wasm b/internal/integration_test/spectest/exception-handling/testdata/tag.7.wasm new file mode 100644 index 0000000000000000000000000000000000000000..5aab07164124c51a8b5ac16d9e0b3ff5ea7ec04c GIT binary patch literal 39 tcmZQbEY4+QU|?WmVN76PVB%tA^kptdOlJWKakJzl=BBc+FfuTf003uh1$6)b literal 0 HcmV?d00001 diff --git a/internal/integration_test/spectest/exception-handling/testdata/tag.json b/internal/integration_test/spectest/exception-handling/testdata/tag.json new file mode 100644 index 000000000..66cc76514 --- /dev/null +++ b/internal/integration_test/spectest/exception-handling/testdata/tag.json @@ -0,0 +1 @@ +{"source_filename":"./tag.wast","commands":[{"type":"module","line":3,"filename":"tag.0.wasm","module_type":"binary"},{"type":"register","line":11,"as":"test"},{"type":"module","line":13,"filename":"tag.1.wasm","module_type":"binary"},{"type":"assert_invalid","line":19,"filename":"tag.2.wasm","module_type":"binary","text":"non-empty tag result type"},{"type":"assert_invalid","line":23,"filename":"tag.3.wasm","module_type":"binary","text":"non-empty tag result type"},{"type":"module","line":30,"filename":"tag.4.wasm","module_type":"binary"},{"type":"register","line":38,"as":"M"},{"type":"module","line":40,"filename":"tag.5.wasm","module_type":"binary"},{"type":"assert_unlinkable","line":49,"filename":"tag.6.wasm","module_type":"binary","text":"incompatible import type"},{"type":"assert_unlinkable","line":60,"filename":"tag.7.wasm","module_type":"binary","text":"incompatible import type"}]} \ No newline at end of file diff --git a/internal/integration_test/spectest/exception-handling/testdata/tag.wast b/internal/integration_test/spectest/exception-handling/testdata/tag.wast new file mode 100644 index 000000000..b2ec2864f --- /dev/null +++ b/internal/integration_test/spectest/exception-handling/testdata/tag.wast @@ -0,0 +1,65 @@ +;; Test tag section + +(module + (tag) + (tag (param i32)) + (tag (export "t2") (param i32)) + (tag $t3 (param i32 f32)) + (export "t3" (tag 3)) +) + +(register "test") + +(module + (tag $t0 (import "test" "t2") (param i32)) + (import "test" "t3" (tag $t1 (param i32 f32))) +) + +(assert_invalid + (module (tag (result i32))) + "non-empty tag result type" +) +(assert_invalid + (module (import "" "" (tag (result i32)))) + "non-empty tag result type" +) + + +;; Link-time typing + +(module + (rec + (type $t1 (func)) + (type $t2 (func)) + ) + (tag (export "tag") (type $t1)) +) + +(register "M") + +(module + (rec + (type $t1 (func)) + (type $t2 (func)) + ) + (tag (import "M" "tag") (type $t1)) +) + +(assert_unlinkable + (module + (rec + (type $t1 (func)) + (type $t2 (func)) + ) + (tag (import "M" "tag") (type $t2)) + ) + "incompatible import type" +) + +(assert_unlinkable + (module + (type $t (func)) + (tag (import "M" "tag") (type $t)) + ) + "incompatible import type" +) diff --git a/internal/integration_test/spectest/exception-handling/testdata/throw.0.wasm b/internal/integration_test/spectest/exception-handling/testdata/throw.0.wasm new file mode 100644 index 0000000000000000000000000000000000000000..7f5c17d772313750c54a2c78ea06696ab0ac11ee GIT binary patch literal 383 zcmZWkO-sX25S)EqdGXO!FM1LMuRhQ;@Y-YwO2uLXC;Fkj1S6%zz>k?Q@ za+`N~}@`zFtOxiT{^lmBebSl30iPY|Q@v_m4}Y literal 0 HcmV?d00001 diff --git a/internal/integration_test/spectest/exception-handling/testdata/throw.1.wasm b/internal/integration_test/spectest/exception-handling/testdata/throw.1.wasm new file mode 100644 index 0000000000000000000000000000000000000000..9d56f8a9bf6b593fa531f01a9954faa591af7121 GIT binary patch literal 26 hcmZQbEY4+QU|?WmVN76PU}j=u;9_HBVc=lk1^^{J0oMQk literal 0 HcmV?d00001 diff --git a/internal/integration_test/spectest/exception-handling/testdata/throw.2.wasm b/internal/integration_test/spectest/exception-handling/testdata/throw.2.wasm new file mode 100644 index 0000000000000000000000000000000000000000..b22d59e6a6bc608b3aaf9788d79a8dec25deb418 GIT binary patch literal 35 pcmZQbEY4+QU|?Y6U`k-DXGmaRU}j=up&X*?gFr;+d2HM8zsMhEX`PvAZOZlAQ>kC3GvJRF zfzBE(T`rGM{!fk|27AAiu4?@ZIowO7Zp`3a?zO5;v)Acnz5FQF<;LVyfllyE{s5d` zE$q$cLO}&;?1b%3=&KO-k^^t7;&XNCS~&@aT5{BqO?eF75$CN7p-ZZ-CRR8wop6X4 z4{{MwU^m3%QV!gX8=~E4bJ5|bJEfV3udU_S!WS+4`FsAvdW8rS9_1=6G>?R2lKOoW rv5Rxhen)rgleD;Po@MMP8FoRqAQ@~TW;3@h++~h8>crN1My#VB|jv0=gHPC_b?N`0ziiKJ=XDnT!)`W3~2rMTiIW0FP!{AK;J_MtO2 zp%48p%J;p`s3IBQA?nT(Xuva`HUI*z0|z;R58b3`n-7R&S?2r$63 z{u=f2BACp_+DqbieLw1tK-8DhQ2YIne@mL_EFymSV-$hmM4>vDP5n#KY^) zILI&W0y0mSOR#EtvAww;`V%jBcM0}0{E+zUGkkD;1-WPVA@S;TFaGyX5BGB9U(j)$ z!V5ai<-DNdeC~FK|94Vy(k@UZXJMoVP_7;lo<9!!1yl+NJ=Tkb7ljW{JqdNR4AX9M zSBcz?p#J($H3&l8j}C1bG>r%EK8Qx?j^U&{SkA`*CDDU4j$XiNJ*|ZX=H2SyEY#Z+ z&1@D0bU^{ua=*={1Lq`tH9NLY#5O^6S}}hq=kSF|sk(2KtGW$UU{!~lfn;To!8uM< zHFaS7b1a%{s?_^!6yqowCBqwvYe?Hvsw-3;Ro7rIQ?<#OvkMSNAL?TPM=oqk%i!EG z7c0rumK4mTUxN-)YmJa$GV=-ONq92ev^EG_xNUt*jnC-3wN?l=llEse<|lEefyhyG z+>73H)d>hi;qfp$Dabre@o_Icv9CZBC;=WPAY~Fms>*H~T`Hqkr0mQka{%gBk?Wz| z?Q=G!9PM^HuU5>?sF+7(0?pH5Wrv4Zt|_HTJFyED+N4rh8hs0)sgk*~G4&p({p3s; zxyi0NCAUO%q=7$ZPW_3-4-%ZmW02k{Xs5?B7@Ec)xGTD;xH~FYwvm(ooV^9ovh601b;4+jL$_y2TDnp&2!JxinLG4P_Z7l5x h>46n==Ndv^m_lDlsrlB*&>vkM7E9MxHcQu6{{Uk$DkuN| literal 0 HcmV?d00001 diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.10.wasm b/internal/integration_test/spectest/exception-handling/testdata/try_table.10.wasm new file mode 100644 index 0000000000000000000000000000000000000000..d8aff845f2ad248ffcf542177a2c472931afacd9 GIT binary patch literal 30 lcmZQbEY4+QU|?WmVN76PU}j=u;NoKBV32oUWM<&z1^_Pw0!07- literal 0 HcmV?d00001 diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.11.wasm b/internal/integration_test/spectest/exception-handling/testdata/try_table.11.wasm new file mode 100644 index 0000000000000000000000000000000000000000..1d0b4d697e107797935515bcd93f5703b74c8c6a GIT binary patch literal 32 ncmZQbEY4+QU|?WmWlUgT%w%R_WZ>dv*E0f{j#}K@0Dt2I)c^nh literal 0 HcmV?d00001 diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.13.wasm b/internal/integration_test/spectest/exception-handling/testdata/try_table.13.wasm new file mode 100644 index 0000000000000000000000000000000000000000..5e2d6101901e2e7257be080cca6689082e386b4f GIT binary patch literal 291 zcmZ9HK@NgI3`KuC6p)F^8W)~H4q)7P4j_Pm1T;a6@tU5Z{fp3OY^_|yQ8Xp7z((&MwfR;vr1ut}Xd7(G6|`ecZZx5;SCdo8 dCk&G!Zf9BlBM<_!f6{$eTSIhtUoGMH_yEVY39A4A literal 0 HcmV?d00001 diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.15.wasm b/internal/integration_test/spectest/exception-handling/testdata/try_table.15.wasm new file mode 100644 index 0000000000000000000000000000000000000000..753e3287c412e8f7216a3721982edf6138e04b7e GIT binary patch literal 99 zcmWm5F%Ezr5Jb@#AR65eD+?>H;!T7AMia46?;qFZRsSPCl>j)p00brl%!v5mIi>74q?Z}|YCy9|f` literal 0 HcmV?d00001 diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.16.wasm b/internal/integration_test/spectest/exception-handling/testdata/try_table.16.wasm new file mode 100644 index 0000000000000000000000000000000000000000..75ad15761b5f7752b1a3323d83b9f406bfa65ac0 GIT binary patch literal 98 zcmZQbEY4+QU|?WmWlUgTtY>CsVqjobVd6lFC7MxEwA8T7Z@>t7FnO~(vzg*+UerIjIHlD6t7IJG>%CE9E@C19bgKS|euEo5`m9idvD+ mv|&GjMRp_|CXxB`!Q$f9-c1DOLVuVGg?a(TyN%8$kp2SxCoWZ>dpWMh!8XW-@r05rG){Qv*} literal 0 HcmV?d00001 diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.7.wasm b/internal/integration_test/spectest/exception-handling/testdata/try_table.7.wasm new file mode 100644 index 0000000000000000000000000000000000000000..501903cfa49287c3cdc7c59a5e32638fb885bb8e GIT binary patch literal 31 mcmZQbEY4+QU|?WmWlUgTtY>CoWZ>drQu<^}*fGy`n_ literal 0 HcmV?d00001 diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.8.wasm b/internal/integration_test/spectest/exception-handling/testdata/try_table.8.wasm new file mode 100644 index 0000000000000000000000000000000000000000..6f506f45c6caff98269c3fbfa2cab408ec2d5e47 GIT binary patch literal 36 ocmZQbEY4+QU|?WmVN76PU}j=u;AIB#xwsiQ8RQ)pflO|006N$LRsaA1 literal 0 HcmV?d00001 diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.9.wasm b/internal/integration_test/spectest/exception-handling/testdata/try_table.9.wasm new file mode 100644 index 0000000000000000000000000000000000000000..c7ff46b19b5c7eb835b2aac3271e542989492184 GIT binary patch literal 41 scmZQbEY4+QU|?Y6U`k+MNMK;hWM*Py_CjbBd literal 0 HcmV?d00001 diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.json b/internal/integration_test/spectest/exception-handling/testdata/try_table.json new file mode 100644 index 000000000..6072f023f --- /dev/null +++ b/internal/integration_test/spectest/exception-handling/testdata/try_table.json @@ -0,0 +1 @@ +{"source_filename":"./try_table.wast","commands":[{"type":"module","line":3,"filename":"try_table.0.wasm","module_type":"binary"},{"type":"register","line":8,"as":"test"},{"type":"module","line":10,"filename":"try_table.1.wasm","module_type":"binary"},{"type":"assert_return","line":282,"action":{"type":"invoke","field":"simple-throw-catch","args":[{"type":"i32","value":"0"}]},"expected":[{"type":"i32","value":"23"}]},{"type":"assert_return","line":283,"action":{"type":"invoke","field":"simple-throw-catch","args":[{"type":"i32","value":"1"}]},"expected":[{"type":"i32","value":"42"}]},{"type":"assert_trap","line":285,"action":{"type":"invoke","field":"unreachable-not-caught","args":[]},"text":"unreachable"},{"type":"assert_return","line":287,"action":{"type":"invoke","field":"trap-in-callee","args":[{"type":"i32","value":"7"},{"type":"i32","value":"2"}]},"expected":[{"type":"i32","value":"3"}]},{"type":"assert_trap","line":288,"action":{"type":"invoke","field":"trap-in-callee","args":[{"type":"i32","value":"1"},{"type":"i32","value":"0"}]},"text":"integer divide by zero"},{"type":"assert_return","line":290,"action":{"type":"invoke","field":"catch-complex-1","args":[{"type":"i32","value":"0"}]},"expected":[{"type":"i32","value":"3"}]},{"type":"assert_return","line":291,"action":{"type":"invoke","field":"catch-complex-1","args":[{"type":"i32","value":"1"}]},"expected":[{"type":"i32","value":"4"}]},{"type":"assert_exception","line":292,"action":{"type":"invoke","field":"catch-complex-1","args":[{"type":"i32","value":"2"}]}},{"type":"assert_return","line":294,"action":{"type":"invoke","field":"catch-complex-2","args":[{"type":"i32","value":"0"}]},"expected":[{"type":"i32","value":"3"}]},{"type":"assert_return","line":295,"action":{"type":"invoke","field":"catch-complex-2","args":[{"type":"i32","value":"1"}]},"expected":[{"type":"i32","value":"4"}]},{"type":"assert_exception","line":296,"action":{"type":"invoke","field":"catch-complex-2","args":[{"type":"i32","value":"2"}]}},{"type":"assert_return","line":298,"action":{"type":"invoke","field":"throw-catch-param-i32","args":[{"type":"i32","value":"0"}]},"expected":[{"type":"i32","value":"0"}]},{"type":"assert_return","line":299,"action":{"type":"invoke","field":"throw-catch-param-i32","args":[{"type":"i32","value":"1"}]},"expected":[{"type":"i32","value":"1"}]},{"type":"assert_return","line":300,"action":{"type":"invoke","field":"throw-catch-param-i32","args":[{"type":"i32","value":"10"}]},"expected":[{"type":"i32","value":"10"}]},{"type":"assert_return","line":302,"action":{"type":"invoke","field":"throw-catch-param-f32","args":[{"type":"f32","value":"1084227584"}]},"expected":[{"type":"f32","value":"1084227584"}]},{"type":"assert_return","line":303,"action":{"type":"invoke","field":"throw-catch-param-f32","args":[{"type":"f32","value":"1093140480"}]},"expected":[{"type":"f32","value":"1093140480"}]},{"type":"assert_return","line":305,"action":{"type":"invoke","field":"throw-catch-param-i64","args":[{"type":"i64","value":"5"}]},"expected":[{"type":"i64","value":"5"}]},{"type":"assert_return","line":306,"action":{"type":"invoke","field":"throw-catch-param-i64","args":[{"type":"i64","value":"0"}]},"expected":[{"type":"i64","value":"0"}]},{"type":"assert_return","line":307,"action":{"type":"invoke","field":"throw-catch-param-i64","args":[{"type":"i64","value":"-1"}]},"expected":[{"type":"i64","value":"-1"}]},{"type":"assert_return","line":309,"action":{"type":"invoke","field":"throw-catch-param-f64","args":[{"type":"f64","value":"4617315517961601024"}]},"expected":[{"type":"f64","value":"4617315517961601024"}]},{"type":"assert_return","line":310,"action":{"type":"invoke","field":"throw-catch-param-f64","args":[{"type":"f64","value":"4622100592565682176"}]},"expected":[{"type":"f64","value":"4622100592565682176"}]},{"type":"assert_return","line":312,"action":{"type":"invoke","field":"throw-catch_ref-param-i32","args":[{"type":"i32","value":"0"}]},"expected":[{"type":"i32","value":"0"}]},{"type":"assert_return","line":313,"action":{"type":"invoke","field":"throw-catch_ref-param-i32","args":[{"type":"i32","value":"1"}]},"expected":[{"type":"i32","value":"1"}]},{"type":"assert_return","line":314,"action":{"type":"invoke","field":"throw-catch_ref-param-i32","args":[{"type":"i32","value":"10"}]},"expected":[{"type":"i32","value":"10"}]},{"type":"assert_return","line":316,"action":{"type":"invoke","field":"throw-catch_ref-param-f32","args":[{"type":"f32","value":"1084227584"}]},"expected":[{"type":"f32","value":"1084227584"}]},{"type":"assert_return","line":317,"action":{"type":"invoke","field":"throw-catch_ref-param-f32","args":[{"type":"f32","value":"1093140480"}]},"expected":[{"type":"f32","value":"1093140480"}]},{"type":"assert_return","line":319,"action":{"type":"invoke","field":"throw-catch_ref-param-i64","args":[{"type":"i64","value":"5"}]},"expected":[{"type":"i64","value":"5"}]},{"type":"assert_return","line":320,"action":{"type":"invoke","field":"throw-catch_ref-param-i64","args":[{"type":"i64","value":"0"}]},"expected":[{"type":"i64","value":"0"}]},{"type":"assert_return","line":321,"action":{"type":"invoke","field":"throw-catch_ref-param-i64","args":[{"type":"i64","value":"-1"}]},"expected":[{"type":"i64","value":"-1"}]},{"type":"assert_return","line":323,"action":{"type":"invoke","field":"throw-catch_ref-param-f64","args":[{"type":"f64","value":"4617315517961601024"}]},"expected":[{"type":"f64","value":"4617315517961601024"}]},{"type":"assert_return","line":324,"action":{"type":"invoke","field":"throw-catch_ref-param-f64","args":[{"type":"f64","value":"4622100592565682176"}]},"expected":[{"type":"f64","value":"4622100592565682176"}]},{"type":"assert_return","line":326,"action":{"type":"invoke","field":"catch-param-i32","args":[{"type":"i32","value":"5"}]},"expected":[{"type":"i32","value":"5"}]},{"type":"assert_return","line":328,"action":{"type":"invoke","field":"catch-imported","args":[]},"expected":[{"type":"i32","value":"2"}]},{"type":"assert_return","line":329,"action":{"type":"invoke","field":"catch-imported-alias","args":[]},"expected":[{"type":"i32","value":"2"}]},{"type":"assert_return","line":331,"action":{"type":"invoke","field":"catchless-try","args":[{"type":"i32","value":"0"}]},"expected":[{"type":"i32","value":"0"}]},{"type":"assert_return","line":332,"action":{"type":"invoke","field":"catchless-try","args":[{"type":"i32","value":"1"}]},"expected":[{"type":"i32","value":"1"}]},{"type":"assert_exception","line":334,"action":{"type":"invoke","field":"return-call-in-try-catch","args":[]}},{"type":"assert_exception","line":335,"action":{"type":"invoke","field":"return-call-indirect-in-try-catch","args":[]}},{"type":"assert_return","line":337,"action":{"type":"invoke","field":"try-with-param","args":[]},"expected":[]},{"type":"assert_return","line":339,"action":{"type":"invoke","field":"duplicated-catches","args":[]},"expected":[{"type":"i32","value":"2"}]},{"type":"assert_return","line":340,"action":{"type":"invoke","field":"catch-all-before-catch","args":[]},"expected":[{"type":"i32","value":"2"}]},{"type":"module","line":342,"filename":"try_table.2.wasm","module_type":"binary"},{"type":"assert_return","line":364,"action":{"type":"invoke","field":"imported-mismatch","args":[]},"expected":[{"type":"i32","value":"3"}]},{"type":"assert_malformed","line":367,"filename":"try_table.3.wat","module_type":"text","text":"unexpected token"},{"type":"assert_malformed","line":372,"filename":"try_table.4.wat","module_type":"text","text":"unexpected token"},{"type":"module","line":376,"filename":"try_table.5.wasm","module_type":"binary"},{"type":"assert_invalid","line":387,"filename":"try_table.6.wasm","module_type":"binary","text":"type mismatch"},{"type":"assert_invalid","line":391,"filename":"try_table.7.wasm","module_type":"binary","text":"type mismatch"},{"type":"assert_invalid","line":396,"filename":"try_table.8.wasm","module_type":"binary","text":"type mismatch"},{"type":"assert_invalid","line":400,"filename":"try_table.9.wasm","module_type":"binary","text":"type mismatch"},{"type":"assert_invalid","line":404,"filename":"try_table.10.wasm","module_type":"binary","text":"type mismatch"},{"type":"assert_invalid","line":408,"filename":"try_table.11.wasm","module_type":"binary","text":"type mismatch"},{"type":"assert_invalid","line":412,"filename":"try_table.12.wasm","module_type":"binary","text":"type mismatch"},{"type":"module","line":420,"filename":"try_table.13.wasm","module_type":"binary"},{"type":"assert_return","line":464,"action":{"type":"invoke","field":"catch","args":[]},"expected":[{"type":"funcref"}]},{"type":"assert_return","line":465,"action":{"type":"invoke","field":"catch_ref1","args":[]},"expected":[{"type":"funcref"}]},{"type":"assert_return","line":466,"action":{"type":"invoke","field":"catch_ref2","args":[]},"expected":[{"type":"funcref"}]},{"type":"assert_return","line":467,"action":{"type":"invoke","field":"catch_all_ref1","args":[]},"expected":[]},{"type":"assert_return","line":468,"action":{"type":"invoke","field":"catch_all_ref2","args":[]},"expected":[]},{"type":"assert_invalid","line":471,"filename":"try_table.14.wasm","module_type":"binary","text":"type mismatch"},{"type":"assert_invalid","line":484,"filename":"try_table.15.wasm","module_type":"binary","text":"type mismatch"},{"type":"module","line":499,"filename":"try_table.16.wasm","module_type":"binary"},{"type":"assert_return","line":522,"action":{"type":"invoke","field":"as-br-target","args":[]},"expected":[{"type":"i32","value":"111"}]},{"type":"assert_return","line":523,"action":{"type":"invoke","field":"as-value-provider","args":[]},"expected":[{"type":"i32","value":"333"}]}]} \ No newline at end of file diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.wast b/internal/integration_test/spectest/exception-handling/testdata/try_table.wast new file mode 100644 index 000000000..f402ffb4a --- /dev/null +++ b/internal/integration_test/spectest/exception-handling/testdata/try_table.wast @@ -0,0 +1,523 @@ +;; Test try-catch blocks. + +(module + (tag $e0 (export "e0")) + (func (export "throw") (throw $e0)) +) + +(register "test") + +(module + (tag $imported-e0 (import "test" "e0")) + (tag $imported-e0-alias (import "test" "e0")) + (func $imported-throw (import "test" "throw")) + (tag $e0) + (tag $e1) + (tag $e2) + (tag $e-i32 (param i32)) + (tag $e-f32 (param f32)) + (tag $e-i64 (param i64)) + (tag $e-f64 (param f64)) + + (func $throw-if (param i32) (result i32) + (local.get 0) + (i32.const 0) (if (i32.ne) (then (throw $e0))) + (i32.const 0) + ) + + (func (export "simple-throw-catch") (param i32) (result i32) + (block $h + (try_table (result i32) (catch $e0 $h) + (if (i32.eqz (local.get 0)) (then (throw $e0)) (else)) + (i32.const 42) + ) + (return) + ) + (i32.const 23) + ) + + (func (export "unreachable-not-caught") + (block $h + (try_table (catch_all $h) (unreachable)) + (return) + ) + ) + + (func $div (param i32 i32) (result i32) + (local.get 0) (local.get 1) (i32.div_u) + ) + (func (export "trap-in-callee") (param i32 i32) (result i32) + (block $h + (try_table (result i32) (catch_all $h) + (call $div (local.get 0) (local.get 1)) + ) + (return) + ) + (i32.const 11) + ) + + (func (export "catch-complex-1") (param i32) (result i32) + (block $h1 + (try_table (result i32) (catch $e1 $h1) + (block $h0 + (try_table (result i32) (catch $e0 $h0) + (if (i32.eqz (local.get 0)) + (then (throw $e0)) + (else + (if (i32.eq (local.get 0) (i32.const 1)) + (then (throw $e1)) + (else (throw $e2)) + ) + ) + ) + (i32.const 2) + ) + (br 1) + ) + (i32.const 3) + ) + (return) + ) + (i32.const 4) + ) + + (func (export "catch-complex-2") (param i32) (result i32) + (block $h0 + (block $h1 + (try_table (result i32) (catch $e0 $h0) (catch $e1 $h1) + (if (i32.eqz (local.get 0)) + (then (throw $e0)) + (else + (if (i32.eq (local.get 0) (i32.const 1)) + (then (throw $e1)) + (else (throw $e2)) + ) + ) + ) + (i32.const 2) + ) + (return) + ) + (return (i32.const 4)) + ) + (i32.const 3) + ) + + (func (export "throw-catch-param-i32") (param i32) (result i32) + (block $h (result i32) + (try_table (result i32) (catch $e-i32 $h) + (throw $e-i32 (local.get 0)) + (i32.const 2) + ) + (return) + ) + (return) + ) + + (func (export "throw-catch-param-f32") (param f32) (result f32) + (block $h (result f32) + (try_table (result f32) (catch $e-f32 $h) + (throw $e-f32 (local.get 0)) + (f32.const 0) + ) + (return) + ) + (return) + ) + + (func (export "throw-catch-param-i64") (param i64) (result i64) + (block $h (result i64) + (try_table (result i64) (catch $e-i64 $h) + (throw $e-i64 (local.get 0)) + (i64.const 2) + ) + (return) + ) + (return) + ) + + (func (export "throw-catch-param-f64") (param f64) (result f64) + (block $h (result f64) + (try_table (result f64) (catch $e-f64 $h) + (throw $e-f64 (local.get 0)) + (f64.const 0) + ) + (return) + ) + (return) + ) + + (func (export "throw-catch_ref-param-i32") (param i32) (result i32) + (block $h (result i32 exnref) + (try_table (result i32) (catch_ref $e-i32 $h) + (throw $e-i32 (local.get 0)) + (i32.const 2) + ) + (return) + ) + (drop) (return) + ) + + (func (export "throw-catch_ref-param-f32") (param f32) (result f32) + (block $h (result f32 exnref) + (try_table (result f32) (catch_ref $e-f32 $h) + (throw $e-f32 (local.get 0)) + (f32.const 0) + ) + (return) + ) + (drop) (return) + ) + + (func (export "throw-catch_ref-param-i64") (param i64) (result i64) + (block $h (result i64 exnref) + (try_table (result i64) (catch_ref $e-i64 $h) + (throw $e-i64 (local.get 0)) + (i64.const 2) + ) + (return) + ) + (drop) (return) + ) + + (func (export "throw-catch_ref-param-f64") (param f64) (result f64) + (block $h (result f64 exnref) + (try_table (result f64) (catch_ref $e-f64 $h) + (throw $e-f64 (local.get 0)) + (f64.const 0) + ) + (return) + ) + (drop) (return) + ) + + (func $throw-param-i32 (param i32) (throw $e-i32 (local.get 0))) + (func (export "catch-param-i32") (param i32) (result i32) + (block $h (result i32) + (try_table (result i32) (catch $e-i32 $h) + (i32.const 0) + (call $throw-param-i32 (local.get 0)) + ) + (return) + ) + ) + + (func (export "catch-imported") (result i32) + (block $h + (try_table (result i32) (catch $imported-e0 $h) + (call $imported-throw (i32.const 1)) + ) + (return) + ) + (i32.const 2) + ) + + (func (export "catch-imported-alias") (result i32) + (block $h + (try_table (result i32) (catch $imported-e0 $h) + (throw $imported-e0-alias (i32.const 1)) + ) + (return) + ) + (i32.const 2) + ) + + (func (export "catchless-try") (param i32) (result i32) + (block $h + (try_table (result i32) (catch $e0 $h) + (try_table (result i32) (call $throw-if (local.get 0))) + ) + (return) + ) + (i32.const 1) + ) + + (func $throw-void (throw $e0)) + (func (export "return-call-in-try-catch") + (block $h + (try_table (catch $e0 $h) + (return_call $throw-void) + ) + ) + ) + + (table funcref (elem $throw-void)) + (func (export "return-call-indirect-in-try-catch") + (block $h + (try_table (catch $e0 $h) + (return_call_indirect (i32.const 0)) + ) + ) + ) + + (func (export "try-with-param") + (i32.const 0) (try_table (param i32) (drop)) + ) + + (func (export "duplicated-catches") (result i32) + (block + (block + (try_table (catch $e0 0) (catch $e0 1) + (throw $e0) + ) + ) + (return (i32.const 2)) + ) + (return (i32.const 3)) + ) + + (func (export "catch-all-before-catch") (result i32) + (block + (block + (try_table (catch_all 0) (catch $e0 1) + (throw $e0) + ) + ) + (return (i32.const 2)) + ) + (return (i32.const 3)) + ) +) + +(assert_return (invoke "simple-throw-catch" (i32.const 0)) (i32.const 23)) +(assert_return (invoke "simple-throw-catch" (i32.const 1)) (i32.const 42)) + +(assert_trap (invoke "unreachable-not-caught") "unreachable") + +(assert_return (invoke "trap-in-callee" (i32.const 7) (i32.const 2)) (i32.const 3)) +(assert_trap (invoke "trap-in-callee" (i32.const 1) (i32.const 0)) "integer divide by zero") + +(assert_return (invoke "catch-complex-1" (i32.const 0)) (i32.const 3)) +(assert_return (invoke "catch-complex-1" (i32.const 1)) (i32.const 4)) +(assert_exception (invoke "catch-complex-1" (i32.const 2))) + +(assert_return (invoke "catch-complex-2" (i32.const 0)) (i32.const 3)) +(assert_return (invoke "catch-complex-2" (i32.const 1)) (i32.const 4)) +(assert_exception (invoke "catch-complex-2" (i32.const 2))) + +(assert_return (invoke "throw-catch-param-i32" (i32.const 0)) (i32.const 0)) +(assert_return (invoke "throw-catch-param-i32" (i32.const 1)) (i32.const 1)) +(assert_return (invoke "throw-catch-param-i32" (i32.const 10)) (i32.const 10)) + +(assert_return (invoke "throw-catch-param-f32" (f32.const 5.0)) (f32.const 5.0)) +(assert_return (invoke "throw-catch-param-f32" (f32.const 10.5)) (f32.const 10.5)) + +(assert_return (invoke "throw-catch-param-i64" (i64.const 5)) (i64.const 5)) +(assert_return (invoke "throw-catch-param-i64" (i64.const 0)) (i64.const 0)) +(assert_return (invoke "throw-catch-param-i64" (i64.const -1)) (i64.const -1)) + +(assert_return (invoke "throw-catch-param-f64" (f64.const 5.0)) (f64.const 5.0)) +(assert_return (invoke "throw-catch-param-f64" (f64.const 10.5)) (f64.const 10.5)) + +(assert_return (invoke "throw-catch_ref-param-i32" (i32.const 0)) (i32.const 0)) +(assert_return (invoke "throw-catch_ref-param-i32" (i32.const 1)) (i32.const 1)) +(assert_return (invoke "throw-catch_ref-param-i32" (i32.const 10)) (i32.const 10)) + +(assert_return (invoke "throw-catch_ref-param-f32" (f32.const 5.0)) (f32.const 5.0)) +(assert_return (invoke "throw-catch_ref-param-f32" (f32.const 10.5)) (f32.const 10.5)) + +(assert_return (invoke "throw-catch_ref-param-i64" (i64.const 5)) (i64.const 5)) +(assert_return (invoke "throw-catch_ref-param-i64" (i64.const 0)) (i64.const 0)) +(assert_return (invoke "throw-catch_ref-param-i64" (i64.const -1)) (i64.const -1)) + +(assert_return (invoke "throw-catch_ref-param-f64" (f64.const 5.0)) (f64.const 5.0)) +(assert_return (invoke "throw-catch_ref-param-f64" (f64.const 10.5)) (f64.const 10.5)) + +(assert_return (invoke "catch-param-i32" (i32.const 5)) (i32.const 5)) + +(assert_return (invoke "catch-imported") (i32.const 2)) +(assert_return (invoke "catch-imported-alias") (i32.const 2)) + +(assert_return (invoke "catchless-try" (i32.const 0)) (i32.const 0)) +(assert_return (invoke "catchless-try" (i32.const 1)) (i32.const 1)) + +(assert_exception (invoke "return-call-in-try-catch")) +(assert_exception (invoke "return-call-indirect-in-try-catch")) + +(assert_return (invoke "try-with-param")) + +(assert_return (invoke "duplicated-catches") (i32.const 2)) +(assert_return (invoke "catch-all-before-catch") (i32.const 2)) + +(module + (func $imported-throw (import "test" "throw")) + (tag $e0) + + (func (export "imported-mismatch") (result i32) + (block $h + (try_table (result i32) (catch_all $h) + (block $h0 + (try_table (result i32) (catch $e0 $h0) + (i32.const 1) + (call $imported-throw) + ) + (return) + ) + (i32.const 2) + ) + (return) + ) + (i32.const 3) + ) +) + +(assert_return (invoke "imported-mismatch") (i32.const 3)) + +(assert_malformed + (module quote "(module (func (catch_all)))") + "unexpected token" +) + +(assert_malformed + (module quote "(module (tag $e) (func (catch $e)))") + "unexpected token" +) + +(module + (tag $e) + (func (try_table (catch $e 0) (catch $e 0))) + (func (try_table (catch_all 0) (catch $e 0))) + (func (try_table (catch_all 0) (catch_all 0))) + (func (result exnref) (try_table (catch_ref $e 0) (catch_ref $e 0)) (unreachable)) + (func (result exnref) (try_table (catch_all_ref 0) (catch_ref $e 0)) (unreachable)) + (func (result exnref) (try_table (catch_all_ref 0) (catch_all_ref 0)) (unreachable)) +) + +(assert_invalid + (module (func (result i32) (try_table (result i32)))) + "type mismatch" +) +(assert_invalid + (module (func (result i32) (try_table (result i32) (i64.const 42)))) + "type mismatch" +) + +(assert_invalid + (module (tag) (func (try_table (catch_ref 0 0)))) + "type mismatch" +) +(assert_invalid + (module (tag) (func (result exnref) (try_table (catch 0 0)) (unreachable))) + "type mismatch" +) +(assert_invalid + (module (func (try_table (catch_all_ref 0)))) + "type mismatch" +) +(assert_invalid + (module (func (result exnref) (try_table (catch_all 0)) (unreachable))) + "type mismatch" +) +(assert_invalid + (module + (tag (param i64)) + (func (result i32 exnref) (try_table (result i32) (catch_ref 0 0) (i32.const 42))) + ) + "type mismatch" +) + + +(module + (type $t (func)) + (func $dummy) + (elem declare func $dummy) + + (tag $e (param (ref $t))) + (func $throw (throw $e (ref.func $dummy))) + + (func (export "catch") (result (ref null $t)) + (block $l (result (ref null $t)) + (try_table (catch $e $l) (call $throw)) + (unreachable) + ) + ) + (func (export "catch_ref1") (result (ref null $t)) + (block $l (result (ref null $t) (ref exn)) + (try_table (catch_ref $e $l) (call $throw)) + (unreachable) + ) + (drop) + ) + (func (export "catch_ref2") (result (ref null $t)) + (block $l (result (ref null $t) (ref null exn)) + (try_table (catch_ref $e $l) (call $throw)) + (unreachable) + ) + (drop) + ) + (func (export "catch_all_ref1") + (block $l (result (ref exn)) + (try_table (catch_all_ref $l) (call $throw)) + (unreachable) + ) + (drop) + ) + (func (export "catch_all_ref2") + (block $l (result (ref null exn)) + (try_table (catch_all_ref $l) (call $throw)) + (unreachable) + ) + (drop) + ) +) + +(assert_return (invoke "catch") (ref.func)) +(assert_return (invoke "catch_ref1") (ref.func)) +(assert_return (invoke "catch_ref2") (ref.func)) +(assert_return (invoke "catch_all_ref1")) +(assert_return (invoke "catch_all_ref2")) + +(assert_invalid + (module + (type $t (func)) + (tag $e (param (ref null $t))) + (func (export "catch") (result (ref $t)) + (block $l (result (ref $t)) + (try_table (catch $e $l)) + (unreachable) + ) + ) + ) + "type mismatch" +) +(assert_invalid + (module + (type $t (func)) + (tag $e (param (ref null $t))) + (func (export "catch_ref") (result (ref $t)) + (block $l (result (ref $t) (ref exn)) + (try_table (catch_ref $e $l)) + (unreachable) + ) + ) + ) + "type mismatch" +) + +;; try_table acts a regular block for br, etc. + +(module + (func (export "as-br-target") (result i32) + (block + (try_table + (br 0) + (unreachable) + ) + (return (i32.const 111)) + ) + (i32.const 222) + ) + + (func (export "as-value-provider") (result i32) + (block + (try_table (result i32) + (br 0 (i32.const 333)) + ) + (return) + ) + (unreachable) + ) +) + +(assert_return (invoke "as-br-target") (i32.const 111)) +(assert_return (invoke "as-value-provider") (i32.const 333)) diff --git a/internal/integration_test/spectest/spectest.go b/internal/integration_test/spectest/spectest.go index 0638883ef..694ed341b 100644 --- a/internal/integration_test/spectest/spectest.go +++ b/internal/integration_test/spectest/spectest.go @@ -114,6 +114,8 @@ func (c commandActionVal) String() string { case "funcref": // All the in and out funcref params are null in spectest (cannot represent non-null as it depends on runtime impl). v = "null" + case "exnref": + v = "null" case "v128": simdValues, ok := c.Value.([]interface{}) if !ok { @@ -260,6 +262,9 @@ func getNaNBits(strValue string, is32bit bool) (ret uint64) { } func (c commandActionVal) toUint64() (ret uint64) { + if c.Value == nil || c.Value == "null" { + return 0 + } strValue := c.Value.(string) if strings.Contains(strValue, "nan") { ret = getNaNBits(strValue, c.ValType == "f32") @@ -273,9 +278,20 @@ func (c commandActionVal) toUint64() (ret uint64) { ret = original + 1 } } else if strings.Contains(c.ValType, "32") { - ret, _ = strconv.ParseUint(strValue, 10, 32) + // wasm-tools may output signed decimals (e.g. "-1"); handle both. + if strings.HasPrefix(strValue, "-") { + v, _ := strconv.ParseInt(strValue, 10, 32) + ret = uint64(uint32(int32(v))) + } else { + ret, _ = strconv.ParseUint(strValue, 10, 32) + } } else { - ret, _ = strconv.ParseUint(strValue, 10, 64) + if strings.HasPrefix(strValue, "-") { + v, _ := strconv.ParseInt(strValue, 10, 64) + ret = uint64(v) + } else { + ret, _ = strconv.ParseUint(strValue, 10, 64) + } } return } @@ -305,6 +321,8 @@ func (c command) expectedError() (err error) { err = wasmruntime.ErrRuntimeUnalignedAtomic case "unreachable": err = wasmruntime.ErrRuntimeUnreachable + case "uncaught exception": + err = wasmruntime.ErrRuntimeUncaughtException default: if strings.HasPrefix(c.Text, "uninitialized") { err = wasmruntime.ErrRuntimeInvalidTableAccess @@ -419,12 +437,17 @@ func RunCase(t *testing.T, testDataFS embed.FS, f string, ctx context.Context, c require.NoError(t, err, msg) require.Equal(t, len(exps), len(results), msg) laneTypes := map[int]string{} + skipIndices := map[int]bool{} for i, expV := range c.Exps { if expV.ValType == "v128" { laneTypes[i] = expV.LaneType } + // When value is nil for ref types, it means "any ref" — skip comparison. + if expV.Value == nil && (expV.ValType == "funcref" || expV.ValType == "externref" || expV.ValType == "exnref") { + skipIndices[i] = true + } } - matched, valuesMsg := valuesEq(results, exps, fn.Definition().ResultTypes(), laneTypes) + matched, valuesMsg := valuesEq(results, exps, fn.Definition().ResultTypes(), laneTypes, skipIndices) require.True(t, matched, msg+"\n"+valuesMsg) case "get": _, exps := c.getAssertReturnArgsExps() @@ -495,6 +518,23 @@ func RunCase(t *testing.T, testDataFS embed.FS, f string, ctx context.Context, c require.NoError(t, err, msg) _, err = r.InstantiateWithConfig(ctx, buf, wazero.NewModuleConfig()) require.Error(t, err, msg) + case "assert_exception": + m := lastInstantiatedModule + if c.Action.Module != "" { + m = modules[c.Action.Module] + } + switch c.Action.ActionType { + case "invoke": + args := c.getAssertReturnArgs() + msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args) + if c.Action.Module != "" { + msg += " in module " + c.Action.Module + } + _, err := m.ExportedFunction(c.Action.Field).Call(ctx, args...) + require.ErrorIs(t, err, wasmruntime.ErrRuntimeUncaughtException, msg) + default: + t.Fatalf("unsupported action type type: %v", c) + } case "assert_uninstantiable": buf, err := testDataFS.ReadFile(testdataPath(c.Filename)) require.NoError(t, err, msg) @@ -539,12 +579,23 @@ func testdataPath(filename string) string { // - laneTypes maps the index of valueTypes to laneType if valueTypes[i] == wasm.ValueTypeV128. // // Also, if matched == false this returns non-empty valuesMsg which can be used to augment the test failure message. -func valuesEq(actual, exps []uint64, valTypes []wasm.ValueType, laneTypes map[int]laneType) (matched bool, valuesMsg string) { +func valuesEq(actual, exps []uint64, valTypes []wasm.ValueType, laneTypes map[int]laneType, skipIndices map[int]bool) (matched bool, valuesMsg string) { matched = true var msgExpValuesStrs, msgActualValuesStrs []string var uint64RepPos int // the index to actual and exps slice. for i, tp := range valTypes { + if skipIndices[i] { + // Skip comparison for this result (e.g., "any funcref"). + if tp == wasm.ValueTypeV128 { + uint64RepPos += 2 + } else { + uint64RepPos++ + } + msgExpValuesStrs = append(msgExpValuesStrs, "*") + msgActualValuesStrs = append(msgActualValuesStrs, "*") + continue + } switch tp { case wasm.ValueTypeI32: msgExpValuesStrs = append(msgExpValuesStrs, fmt.Sprintf("%d", uint32(exps[uint64RepPos]))) diff --git a/internal/integration_test/spectest/spectest_test.go b/internal/integration_test/spectest/spectest_test.go index ed8b3b8d7..45b98ca40 100644 --- a/internal/integration_test/spectest/spectest_test.go +++ b/internal/integration_test/spectest/spectest_test.go @@ -456,7 +456,7 @@ func Test_valuesEq(t *testing.T) { for _, tc := range tests { tc := tc t.Run(tc.name, func(t *testing.T) { - actualMatched, actualValuesMsg := valuesEq(tc.actual, tc.exps, tc.valueTypes, tc.laneTypes) + actualMatched, actualValuesMsg := valuesEq(tc.actual, tc.exps, tc.valueTypes, tc.laneTypes, nil) require.Equal(t, tc.expMatched, actualMatched) require.Equal(t, tc.expValuesMsg, actualValuesMsg) }) diff --git a/internal/logging/logging.go b/internal/logging/logging.go index 577efff19..c66aa9240 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -25,6 +25,7 @@ const ( ValueTypeV128 ValueType = 0x7b // same as wasm.ValueTypeV128 ValueTypeFuncref ValueType = 0x70 // same as wasm.ValueTypeFuncref ValueTypeExternref = api.ValueTypeExternref + ValueTypeExnref ValueType = 0x69 // same as wasm.ValueTypeExnref // ValueTypeMemI32 is a non-standard type which writes ValueTypeI32 from the memory offset. ValueTypeMemI32 = 0xfd @@ -211,7 +212,7 @@ func ValWriterForType(vt ValueType) ValWriter { return writeF64 case ValueTypeV128: return writeV128 - case ValueTypeExternref, ValueTypeFuncref: + case ValueTypeExternref, ValueTypeFuncref, ValueTypeExnref: return writeRef case ValueTypeMemI32: return writeMemI32 diff --git a/internal/testing/binaryencoding/encoder.go b/internal/testing/binaryencoding/encoder.go index 4314a9533..82b435314 100644 --- a/internal/testing/binaryencoding/encoder.go +++ b/internal/testing/binaryencoding/encoder.go @@ -27,7 +27,9 @@ func EncodeModule(m *wasm.Module) (bytes []byte) { if m.SectionElementCount(wasm.SectionIDMemory) > 0 { bytes = append(bytes, encodeMemorySection(m.MemorySection)...) } - // wasm.SectionIDTag (used for Exceptions) would be here. + if m.SectionElementCount(wasm.SectionIDTag) > 0 { + bytes = append(bytes, encodeTagSection(m.TagSection)...) + } if m.SectionElementCount(wasm.SectionIDGlobal) > 0 { bytes = append(bytes, encodeGlobalSection(m.GlobalSection)...) } diff --git a/internal/testing/binaryencoding/import.go b/internal/testing/binaryencoding/import.go index 8abffde20..c4b82c4f7 100644 --- a/internal/testing/binaryencoding/import.go +++ b/internal/testing/binaryencoding/import.go @@ -33,6 +33,9 @@ func EncodeImport(i *wasm.Import) []byte { mutable = 1 } data = append(data, g.ValType, mutable) + case wasm.ExternTypeTag: + data = append(data, 0x00) // attribute byte + data = append(data, leb128.EncodeUint32(i.DescTag)...) default: panic(fmt.Errorf("invalid externtype: %s", wasm.ExternTypeName(i.Type))) } diff --git a/internal/testing/binaryencoding/section.go b/internal/testing/binaryencoding/section.go index 531c6fcef..90d7856a7 100644 --- a/internal/testing/binaryencoding/section.go +++ b/internal/testing/binaryencoding/section.go @@ -89,6 +89,16 @@ func encodeMemorySection(memory *wasm.Memory) []byte { return encodeSection(wasm.SectionIDMemory, contents) } +// encodeTagSection encodes a wasm.SectionIDTag for the given tags. +func encodeTagSection(tags []wasm.Tag) []byte { + contents := leb128.EncodeUint32(uint32(len(tags))) + for _, tag := range tags { + contents = append(contents, 0x00) // attribute byte (always 0) + contents = append(contents, leb128.EncodeUint32(tag.Type)...) + } + return encodeSection(wasm.SectionIDTag, contents) +} + // encodeGlobalSection encodes a wasm.SectionIDGlobal for the given globals in WebAssembly 1.0 (20191205) Binary // Format. // diff --git a/internal/wasm/binary/code.go b/internal/wasm/binary/code.go index 2fac9196c..66988fd1b 100644 --- a/internal/wasm/binary/code.go +++ b/internal/wasm/binary/code.go @@ -47,7 +47,8 @@ func decodeCode(r *bytes.Reader, codeSectionStart uint64, ret *wasm.Code) (err e bytesRead += n + 1 switch vt := b; vt { case wasm.ValueTypeI32, wasm.ValueTypeF32, wasm.ValueTypeI64, wasm.ValueTypeF64, - wasm.ValueTypeFuncref, wasm.ValueTypeExternref, wasm.ValueTypeV128: + wasm.ValueTypeFuncref, wasm.ValueTypeExternref, wasm.ValueTypeV128, + wasm.ValueTypeExnref: default: return fmt.Errorf("invalid local type: 0x%x", vt) } diff --git a/internal/wasm/binary/decoder.go b/internal/wasm/binary/decoder.go index 8eca1d149..b794ce626 100644 --- a/internal/wasm/binary/decoder.go +++ b/internal/wasm/binary/decoder.go @@ -8,6 +8,7 @@ import ( "io" "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/experimental" "github.com/tetratelabs/wazero/internal/leb128" "github.com/tetratelabs/wazero/internal/wasm" "github.com/tetratelabs/wazero/internal/wasmdebug" @@ -113,7 +114,7 @@ func DecodeModule( case wasm.SectionIDType: m.TypeSection, err = decodeTypeSection(enabledFeatures, r) case wasm.SectionIDImport: - m.ImportSection, m.ImportPerModule, m.ImportFunctionCount, m.ImportGlobalCount, m.ImportMemoryCount, m.ImportTableCount, err = decodeImportSection(r, memSizer, memoryLimitPages, enabledFeatures) + m.ImportSection, m.ImportPerModule, m.ImportFunctionCount, m.ImportGlobalCount, m.ImportMemoryCount, m.ImportTableCount, m.ImportTagCount, err = decodeImportSection(r, memSizer, memoryLimitPages, enabledFeatures) if err != nil { return nil, err // avoid re-wrapping the error. } @@ -123,6 +124,11 @@ func DecodeModule( m.TableSection, err = decodeTableSection(r, enabledFeatures) case wasm.SectionIDMemory: m.MemorySection, err = decodeMemorySection(r, enabledFeatures, memSizer, memoryLimitPages) + case wasm.SectionIDTag: + if err := enabledFeatures.RequireEnabled(experimental.CoreFeaturesExceptionHandling); err != nil { + return nil, fmt.Errorf("tag section not supported as %v", err) + } + m.TagSection, err = decodeTagSection(r) case wasm.SectionIDGlobal: if m.GlobalSection, err = decodeGlobalSection(r, enabledFeatures); err != nil { return nil, err // avoid re-wrapping the error. @@ -176,8 +182,15 @@ func checkSectionOrder(current, previous wasm.SectionID) (byte, bool) { return previous, true } - // DataCount was introduced in Wasm 2.0, - // and it's the maximum we support so far. + // Tag section (ID 13) must come after Memory (5) and before Global (6). + if current == wasm.SectionIDTag { + return current, previous <= wasm.SectionIDMemory + } + if previous == wasm.SectionIDTag { + return current, current >= wasm.SectionIDGlobal + } + + // DataCount was introduced in Wasm 2.0. // It must come after Element and before Code. if current > wasm.SectionIDDataCount { return current, false @@ -189,9 +202,6 @@ func checkSectionOrder(current, previous wasm.SectionID) (byte, bool) { return current, current >= wasm.SectionIDCode } - // Tag will be introduced in Wasm 3.0. - // It must come after Memory and before Global. - // Otherwise, strictly increasing order. return current, current > previous } diff --git a/internal/wasm/binary/export.go b/internal/wasm/binary/export.go index 925e9c499..76f222a04 100644 --- a/internal/wasm/binary/export.go +++ b/internal/wasm/binary/export.go @@ -21,7 +21,7 @@ func decodeExport(r *bytes.Reader, ret *wasm.Export) (err error) { ret.Type = b switch ret.Type { - case wasm.ExternTypeFunc, wasm.ExternTypeTable, wasm.ExternTypeMemory, wasm.ExternTypeGlobal: + case wasm.ExternTypeFunc, wasm.ExternTypeTable, wasm.ExternTypeMemory, wasm.ExternTypeGlobal, wasm.ExternTypeTag: if ret.Index, _, err = leb128.DecodeUint32(r); err != nil { err = fmt.Errorf("error decoding export index: %w", err) } diff --git a/internal/wasm/binary/import.go b/internal/wasm/binary/import.go index 39d310c55..1e830cc4d 100644 --- a/internal/wasm/binary/import.go +++ b/internal/wasm/binary/import.go @@ -42,6 +42,22 @@ func decodeImport( ret.DescMem, err = decodeMemory(r, enabledFeatures, memorySizer, memoryLimitPages) case wasm.ExternTypeGlobal: ret.DescGlobal, err = decodeGlobalType(r) + case wasm.ExternTypeTag: + if err = enabledFeatures.RequireEnabled(api.CoreFeatureSIMD << 4); err != nil { // CoreFeaturesExceptionHandling + err = fmt.Errorf("tag imports require exception handling feature: %w", err) + break + } + // Tag import: read attribute byte (must be 0x00) then type index. + var attr byte + attr, err = r.ReadByte() + if err != nil { + break + } + if attr != 0x00 { + err = fmt.Errorf("invalid tag attribute: %#x", attr) + break + } + ret.DescTag, _, err = leb128.DecodeUint32(r) default: err = fmt.Errorf("%w: invalid byte for importdesc: %#x", ErrInvalidByte, b) } diff --git a/internal/wasm/binary/section.go b/internal/wasm/binary/section.go index 622ee5923..1ec4b105a 100644 --- a/internal/wasm/binary/section.go +++ b/internal/wasm/binary/section.go @@ -16,11 +16,39 @@ func decodeTypeSection(enabledFeatures api.CoreFeatures, r *bytes.Reader) ([]was return nil, fmt.Errorf("get size of vector: %w", err) } - result := make([]wasm.FunctionType, vs) + var result []wasm.FunctionType for i := uint32(0); i < vs; i++ { - if err = decodeFunctionType(enabledFeatures, r, &result[i]); err != nil { + // Peek at the leading byte to check for rec group (0x4e, GC proposal). + b, err := r.ReadByte() + if err != nil { return nil, fmt.Errorf("read %d-th type: %v", i, err) } + if b == 0x4e { + // Rec group: contains multiple types. + recCount, _, err := leb128.DecodeUint32(r) + if err != nil { + return nil, fmt.Errorf("read rec group count: %v", err) + } + for j := uint32(0); j < recCount; j++ { + var ft wasm.FunctionType + if err = decodeFunctionType(enabledFeatures, r, &ft); err != nil { + return nil, fmt.Errorf("read %d-th type in rec group: %v", j, err) + } + ft.RecGroupSize = int(recCount) + ft.RecGroupPosition = int(j) + result = append(result, ft) + } + } else { + // Put back the byte and decode as a regular function type. + if err := r.UnreadByte(); err != nil { + return nil, err + } + var ft wasm.FunctionType + if err = decodeFunctionType(enabledFeatures, r, &ft); err != nil { + return nil, fmt.Errorf("read %d-th type: %v", i, err) + } + result = append(result, ft) + } } return result, nil } @@ -33,7 +61,7 @@ func decodeImportSection( enabledFeatures api.CoreFeatures, ) (result []wasm.Import, perModule map[string][]*wasm.Import, - funcCount, globalCount, memoryCount, tableCount wasm.Index, err error, + funcCount, globalCount, memoryCount, tableCount, tagCount wasm.Index, err error, ) { vs, _, err := leb128.DecodeUint32(r) if err != nil { @@ -61,6 +89,9 @@ func decodeImportSection( case wasm.ExternTypeTable: imp.IndexPerType = tableCount tableCount++ + case wasm.ExternTypeTag: + imp.IndexPerType = tagCount + tagCount++ } perModule[imp.Module] = append(perModule[imp.Module], imp) } @@ -216,6 +247,31 @@ func decodeDataSection(r *bytes.Reader, enabledFeatures api.CoreFeatures) ([]was return result, nil } +func decodeTagSection(r *bytes.Reader) ([]wasm.Tag, error) { + vs, _, err := leb128.DecodeUint32(r) + if err != nil { + return nil, fmt.Errorf("get size of vector: %w", err) + } + + result := make([]wasm.Tag, vs) + for i := uint32(0); i < vs; i++ { + // Read attribute byte (must be 0x00 per spec). + attr, err := r.ReadByte() + if err != nil { + return nil, fmt.Errorf("read tag[%d] attribute: %w", i, err) + } + if attr != 0x00 { + return nil, fmt.Errorf("tag[%d] has invalid attribute: %#x", i, attr) + } + // Read type index. + result[i].Type, _, err = leb128.DecodeUint32(r) + if err != nil { + return nil, fmt.Errorf("read tag[%d] type index: %w", i, err) + } + } + return result, nil +} + func decodeDataCountSection(r *bytes.Reader) (count *uint32, err error) { v, _, err := leb128.DecodeUint32(r) if err != nil && err != io.EOF { diff --git a/internal/wasm/binary/value.go b/internal/wasm/binary/value.go index dfc4417ed..d2f9675e7 100644 --- a/internal/wasm/binary/value.go +++ b/internal/wasm/binary/value.go @@ -16,18 +16,42 @@ func decodeValueTypes(r *bytes.Reader, num uint32) ([]wasm.ValueType, error) { return nil, nil } - ret := make([]wasm.ValueType, num) - _, err := io.ReadFull(r, ret) - if err != nil { - return nil, err - } - - for _, v := range ret { - switch v { + ret := make([]wasm.ValueType, 0, num) + for i := uint32(0); i < num; i++ { + b, err := r.ReadByte() + if err != nil { + return nil, err + } + switch b { case wasm.ValueTypeI32, wasm.ValueTypeF32, wasm.ValueTypeI64, wasm.ValueTypeF64, - wasm.ValueTypeExternref, wasm.ValueTypeFuncref, wasm.ValueTypeV128: + wasm.ValueTypeExternref, wasm.ValueTypeFuncref, wasm.ValueTypeV128, + wasm.ValueTypeExnref: + ret = append(ret, b) + case wasm.RefPrefixNullable, wasm.RefPrefixNonNullable: + ht, _, err := leb128.DecodeInt33AsInt64(r) + if err != nil { + return nil, fmt.Errorf("read ref heap type: %w", err) + } + // The following nullable refs are an alternative representation of the corresponding ref types: + // - (ref null exn) is equivalent to exnref + // - (ref null func) is equivalent to funcref + // - (ref null extern) is equivalent to externref + // See https://webassembly.github.io/gc/core/syntax/types.html#reference-types + // Current limitation: we desugar NON-NULLABLE types to NULLABLE types internally. + // This technically breaks type-checking in some cases, but we will fix this + // when we introduce proper ref types. + switch ht { + case wasm.HeapTypeExn: + ret = append(ret, wasm.ValueTypeExnref) + case wasm.HeapTypeFunc: + ret = append(ret, wasm.ValueTypeFuncref) + case wasm.HeapTypeExtern: + ret = append(ret, wasm.ValueTypeExternref) + default: // concrete type index — treat as nullable funcref + ret = append(ret, wasm.ValueTypeFuncref) + } default: - return nil, fmt.Errorf("invalid value type: %d", v) + return nil, fmt.Errorf("invalid value type: %d", b) } } return ret, nil diff --git a/internal/wasm/counts.go b/internal/wasm/counts.go index 685a40941..39e87272d 100644 --- a/internal/wasm/counts.go +++ b/internal/wasm/counts.go @@ -45,6 +45,8 @@ func (m *Module) SectionElementCount(sectionID SectionID) uint32 { // element as return uint32(len(m.CodeSection)) case SectionIDData: return uint32(len(m.DataSection)) + case SectionIDTag: + return uint32(len(m.TagSection)) default: panic(fmt.Errorf("BUG: unknown section: %d", sectionID)) } diff --git a/internal/wasm/exception.go b/internal/wasm/exception.go new file mode 100644 index 000000000..d47c44f6c --- /dev/null +++ b/internal/wasm/exception.go @@ -0,0 +1,9 @@ +package wasm + +// Exception represents a thrown WebAssembly exception. +type Exception struct { + // Tag is the tag instance that was thrown. + Tag *TagInstance + // Params holds the argument values matching the tag's function type params. + Params []uint64 +} diff --git a/internal/wasm/func_validation.go b/internal/wasm/func_validation.go index b0e7f7cfb..798546a0a 100644 --- a/internal/wasm/func_validation.go +++ b/internal/wasm/func_validation.go @@ -29,9 +29,9 @@ const maximumValuesOnStack = 1 << 27 // Returns an error if the instruction sequence is not valid, // or potentially it can exceed the maximum number of values on the stack. func (m *Module) validateFunction(sts *stacks, enabledFeatures api.CoreFeatures, idx Index, functions []Index, - globals []GlobalType, memory *Memory, tables []Table, declaredFunctionIndexes map[Index]struct{}, br *bytes.Reader, + globals []GlobalType, memory *Memory, tables []Table, tags []Index, declaredFunctionIndexes map[Index]struct{}, br *bytes.Reader, ) error { - return m.validateFunctionWithMaxStackValues(sts, enabledFeatures, idx, functions, globals, memory, tables, maximumValuesOnStack, declaredFunctionIndexes, br) + return m.validateFunctionWithMaxStackValues(sts, enabledFeatures, idx, functions, globals, memory, tables, tags, maximumValuesOnStack, declaredFunctionIndexes, br) } func readMemArg(pc uint64, body []byte) (align, offset uint32, read uint64, err error) { @@ -69,6 +69,7 @@ func (m *Module) validateFunctionWithMaxStackValues( globals []GlobalType, memory *Memory, tables []Table, + tags []Index, maxStackValues int, declaredFunctionIndexes map[Index]struct{}, br *bytes.Reader, @@ -538,6 +539,143 @@ func (m *Module) validateFunctionWithMaxStackValues( // br_table instruction is stack-polymorphic. valueTypeStack.unreachable() + } else if op == OpcodeThrow { + if err := enabledFeatures.RequireEnabled(experimental.CoreFeaturesExceptionHandling); err != nil { + return fmt.Errorf("%s invalid as %v", OpcodeThrowName, err) + } + pc++ + tagIndex, num, err := leb128.LoadUint32(body[pc:]) + if err != nil { + return fmt.Errorf("read immediate: %v", err) + } + pc += num - 1 + if tagIndex >= uint32(len(tags)) { + return fmt.Errorf("invalid tag index for %s: %d", OpcodeThrowName, tagIndex) + } + tagType := &m.TypeSection[tags[tagIndex]] + // Pop values matching the tag's params in reverse order. + for i := len(tagType.Params) - 1; i >= 0; i-- { + if err := valueTypeStack.popAndVerifyType(tagType.Params[i]); err != nil { + return fmt.Errorf("type mismatch on %s operation: %v", OpcodeThrowName, err) + } + } + // throw is stack-polymorphic (never returns). + valueTypeStack.unreachable() + } else if op == OpcodeThrowRef { + if err := enabledFeatures.RequireEnabled(experimental.CoreFeaturesExceptionHandling); err != nil { + return fmt.Errorf("%s invalid as %v", OpcodeThrowRefName, err) + } + if err := valueTypeStack.popAndVerifyType(ValueTypeExnref); err != nil { + return fmt.Errorf("type mismatch on %s: %v", OpcodeThrowRefName, err) + } + // throw_ref is stack-polymorphic (never returns). + valueTypeStack.unreachable() + } else if op == OpcodeTryTable { + if err := enabledFeatures.RequireEnabled(experimental.CoreFeaturesExceptionHandling); err != nil { + return fmt.Errorf("%s invalid as %v", OpcodeTryTableName, err) + } + br.Reset(body[pc+1:]) + bt, num, err := DecodeBlockType(m.TypeSection, br, enabledFeatures) + if err != nil { + return fmt.Errorf("read block: %w", err) + } + pc += num + + // Read catch clause count. + catchCount, catchNum, err := leb128.LoadUint32(body[pc+1:]) + if err != nil { + return fmt.Errorf("read catch count: %v", err) + } + pc += catchNum + + // Validate each catch clause. + for i := uint32(0); i < catchCount; i++ { + pc++ + catchKind := body[pc] + switch catchKind { + case CatchKindCatch, CatchKindCatchRef: + pc++ + tagIdx, tagNum, err := leb128.LoadUint32(body[pc:]) + if err != nil { + return fmt.Errorf("read catch tag index: %v", err) + } + pc += tagNum - 1 + if tagIdx >= uint32(len(tags)) { + return fmt.Errorf("invalid tag index in catch clause: %d", tagIdx) + } + tagType := &m.TypeSection[tags[tagIdx]] + pc++ + labelIdx, labelNum, err := leb128.LoadUint32(body[pc:]) + if err != nil { + return fmt.Errorf("read catch label index: %v", err) + } + pc += labelNum - 1 + if int(labelIdx) >= len(controlBlockStack.stack) { + return fmt.Errorf("invalid label index in catch clause: %d", labelIdx) + } + // Validate that the target label can accept the catch values. + target := &controlBlockStack.stack[len(controlBlockStack.stack)-int(labelIdx)-1] + var expectedTypes []ValueType + if target.op == OpcodeLoop { + expectedTypes = target.blockType.Params + } else { + expectedTypes = target.blockType.Results + } + var catchTypes []ValueType + catchTypes = append(catchTypes, tagType.Params...) + if catchKind == CatchKindCatchRef { + catchTypes = append(catchTypes, ValueTypeExnref) + } + if len(catchTypes) != len(expectedTypes) { + return fmt.Errorf("catch clause type mismatch: catch delivers %d values but label expects %d", len(catchTypes), len(expectedTypes)) + } + for j := range catchTypes { + if !isStrictRefSubtypeOf(catchTypes[j], expectedTypes[j]) { + return fmt.Errorf("catch clause type mismatch at index %d: %v is not a subtype of %v", j, catchTypes[j], expectedTypes[j]) + } + } + case CatchKindCatchAll, CatchKindCatchAllRef: + pc++ + labelIdx, labelNum, err := leb128.LoadUint32(body[pc:]) + if err != nil { + return fmt.Errorf("read catch_all label index: %v", err) + } + pc += labelNum - 1 + if int(labelIdx) >= len(controlBlockStack.stack) { + return fmt.Errorf("invalid label index in catch_all clause: %d", labelIdx) + } + target := &controlBlockStack.stack[len(controlBlockStack.stack)-int(labelIdx)-1] + var expectedTypes []ValueType + if target.op == OpcodeLoop { + expectedTypes = target.blockType.Params + } else { + expectedTypes = target.blockType.Results + } + var catchTypes []ValueType + if catchKind == CatchKindCatchAllRef { + catchTypes = append(catchTypes, ValueTypeExnref) + } + if len(catchTypes) != len(expectedTypes) { + return fmt.Errorf("catch_all clause type mismatch: catch delivers %d values but label expects %d", len(catchTypes), len(expectedTypes)) + } + for j := range catchTypes { + if catchTypes[j] != expectedTypes[j] { + return fmt.Errorf("catch_all clause type mismatch at index %d", j) + } + } + default: + return fmt.Errorf("invalid catch kind: %#x", catchKind) + } + } + + controlBlockStack.push(pc, 0, 0, bt, 0, op) + if err = valueTypeStack.popParams(op, bt.Params, false); err != nil { + return err + } + for _, p := range bt.Params { + valueTypeStack.push(p) + } + valueTypeStack.pushStackLimit(len(bt.Params)) } else if op == OpcodeCall || op == OpcodeTailCallReturnCall { pc++ index, num, err := leb128.LoadUint32(body[pc:]) @@ -885,6 +1023,7 @@ func (m *Module) validateFunctionWithMaxStackValues( return fmt.Errorf("undeclared function index %d for ref.func", index) } pc += num - 1 + // ref.func always produces a non-null reference. valueTypeStack.push(ValueTypeFuncref) } } else if op == OpcodeTableGet || op == OpcodeTableSet { @@ -1973,6 +2112,10 @@ func (m *Module) validateFunctionWithMaxStackValues( // unreachable instruction is stack-polymorphic. valueTypeStack.unreachable() } else if op == OpcodeNop { + } else if enabledFeatures.IsEnabled(experimental.CoreFeaturesExceptionHandling) && + (op == OpcodeLegacyTry || op == OpcodeLegacyCatch || op == OpcodeLegacyRethrow || + op == OpcodeLegacyDelegate || op == OpcodeLegacyCatchAll) { + return fmt.Errorf("legacy exception handling instruction 0x%x not supported; recompile with wasm-opt --translate-to-exnref", op) } else { return fmt.Errorf("invalid instruction 0x%x", op) } @@ -2125,7 +2268,7 @@ func (s *valueTypeStack) popAndVerifyType(expected ValueType) error { if !ok { return fmt.Errorf("%s missing", ValueTypeName(expected)) } - if have != expected && have != valueTypeUnknown && expected != valueTypeUnknown { + if have != expected && have != valueTypeUnknown && expected != valueTypeUnknown && !isRefSubtypeOf(have, expected) { return fmt.Errorf("type mismatch: expected %s, but was %s", ValueTypeName(expected), ValueTypeName(have)) } return nil @@ -2207,7 +2350,7 @@ func (s *valueTypeStack) requireStackValues( // Finally, check the types of the values: for i, v := range s.requireStackValuesTmp { nextWant := want[countWanted-i-1] // have is in reverse order (stack) - if v != nextWant && v != valueTypeUnknown && nextWant != valueTypeUnknown { + if v != nextWant && v != valueTypeUnknown && nextWant != valueTypeUnknown && !isRefSubtypeOf(v, nextWant) { return typeMismatchError(isParam, context, v, nextWant, i) } } @@ -2336,6 +2479,36 @@ func DecodeBlockType(types []FunctionType, r *bytes.Reader, enabledFeatures api. ret = blockType_v_funcref case -17: // 0x6f in original byte = externref ret = blockType_v_externref + case -23: // 0x69 in original byte = exnref + ret = blockType_v_exnref + case -29: // 0x63 = ref null (nullable) — GC proposal + ht, htNum, err := leb128.DecodeInt33AsInt64(r) + if err != nil { + return nil, 0, fmt.Errorf("read ref heap type in block: %w", err) + } + num += htNum + switch ht { + case -23: // exn + ret = blockType_v_exnref + case -16: // func + ret = blockType_v_funcref + case -17: // extern + ret = blockType_v_externref + default: // concrete type index — treat as nullable funcref + ret = blockType_v_funcref + } + case -28: // 0x64 = ref (non-nullable) — GC proposal + ht, htNum, err := leb128.DecodeInt33AsInt64(r) + if err != nil { + return nil, 0, fmt.Errorf("read ref heap type in block: %w", err) + } + num += htNum + switch ht { + case -23: // exn + ret = blockType_v_exnref // TODO: non-null exnref + default: + ret = blockType_v_funcref + } default: if err = enabledFeatures.RequireEnabled(api.CoreFeatureMultiValue); err != nil { return nil, num, fmt.Errorf("block with function type return invalid as %v", err) @@ -2358,6 +2531,7 @@ var ( blockType_v_v128 = &FunctionType{Results: []ValueType{ValueTypeV128}, ResultNumInUint64: 2} blockType_v_funcref = &FunctionType{Results: []ValueType{ValueTypeFuncref}, ResultNumInUint64: 1} blockType_v_externref = &FunctionType{Results: []ValueType{ValueTypeExternref}, ResultNumInUint64: 1} + blockType_v_exnref = &FunctionType{Results: []ValueType{ValueTypeExnref}, ResultNumInUint64: 1} ) // SplitCallStack returns the input stack resliced to the count of params and diff --git a/internal/wasm/func_validation_test.go b/internal/wasm/func_validation_test.go index 453ae4571..94cec87cb 100644 --- a/internal/wasm/func_validation_test.go +++ b/internal/wasm/func_validation_test.go @@ -37,12 +37,12 @@ func TestModule_ValidateFunction_validateFunctionWithMaxStackValues(t *testing.T t.Run("not exceed", func(t *testing.T) { err := m.validateFunctionWithMaxStackValues(&stacks{}, api.CoreFeaturesV1, - 0, []Index{0}, nil, nil, nil, max+1, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, nil, nil, nil, max+1, nil, bytes.NewReader(nil)) require.NoError(t, err) }) t.Run("exceed", func(t *testing.T) { err := m.validateFunctionWithMaxStackValues(&stacks{}, api.CoreFeaturesV1, - 0, []Index{0}, nil, nil, nil, max, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, nil, nil, nil, max, nil, bytes.NewReader(nil)) require.Error(t, err) expMsg := fmt.Sprintf("function may have %d stack values, which exceeds limit %d", valuesNum, max) require.Equal(t, expMsg, err.Error()) @@ -86,7 +86,7 @@ func TestModule_ValidateFunction_SignExtensionOps(t *testing.T) { CodeSection: []Code{{Body: []byte{tc.input}}}, } err := m.validateFunction(&stacks{}, api.CoreFeaturesV1, - 0, []Index{0}, nil, nil, nil, nil, + 0, []Index{0}, nil, nil, nil, nil, nil, bytes.NewReader(nil)) require.EqualError(t, err, tc.expectedErrOnDisable) }) @@ -105,7 +105,7 @@ func TestModule_ValidateFunction_SignExtensionOps(t *testing.T) { CodeSection: []Code{{Body: body}}, } err := m.validateFunction(&stacks{}, api.CoreFeatureSignExtensionOps, - 0, []Index{0}, nil, nil, nil, + 0, []Index{0}, nil, nil, nil, nil, nil, bytes.NewReader(nil)) require.NoError(t, err) }) @@ -162,7 +162,7 @@ func TestModule_ValidateFunction_NonTrappingFloatToIntConversion(t *testing.T) { CodeSection: []Code{{Body: []byte{OpcodeMiscPrefix, tc.input}}}, } err := m.validateFunction(&stacks{}, api.CoreFeaturesV1, - 0, []Index{0}, nil, nil, nil, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, nil, nil, nil, nil, bytes.NewReader(nil)) require.EqualError(t, err, tc.expectedErrOnDisable) }) t.Run("enabled", func(t *testing.T) { @@ -181,7 +181,7 @@ func TestModule_ValidateFunction_NonTrappingFloatToIntConversion(t *testing.T) { CodeSection: []Code{{Body: body}}, } err := m.validateFunction(&stacks{}, api.CoreFeatureNonTrappingFloatToIntConversion, - 0, []Index{0}, nil, nil, nil, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, nil, nil, nil, nil, bytes.NewReader(nil)) require.NoError(t, err) }) }) @@ -259,12 +259,12 @@ func TestModule_ValidateFunction_MultiValue(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Run("disabled", func(t *testing.T) { err := tc.module.validateFunction(&stacks{}, api.CoreFeaturesV1, - 0, []Index{0}, nil, nil, nil, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, nil, nil, nil, nil, bytes.NewReader(nil)) require.EqualError(t, err, tc.expectedErrOnDisable) }) t.Run("enabled", func(t *testing.T) { err := tc.module.validateFunction(&stacks{}, api.CoreFeatureMultiValue, - 0, []Index{0}, nil, nil, nil, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, nil, nil, nil, nil, bytes.NewReader(nil)) require.NoError(t, err) }) }) @@ -302,7 +302,7 @@ func TestModule_ValidateFunction_BulkMemoryOperations(t *testing.T) { DataCountSection: &c, } err := m.validateFunction(&stacks{}, api.CoreFeatureBulkMemoryOperations, - 0, []Index{0}, nil, &Memory{}, []Table{{}, {}}, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, &Memory{}, []Table{{}, {}}, nil, nil, bytes.NewReader(nil)) require.NoError(t, err) }) } @@ -666,7 +666,7 @@ func TestModule_ValidateFunction_BulkMemoryOperations(t *testing.T) { c := uint32(0) m.DataCountSection = &c } - err := m.validateFunction(&stacks{}, tc.flag, 0, []Index{0}, nil, tc.memory, tc.tables, nil, bytes.NewReader(nil)) + err := m.validateFunction(&stacks{}, tc.flag, 0, []Index{0}, nil, tc.memory, tc.tables, nil, nil, bytes.NewReader(nil)) require.EqualError(t, err, tc.expectedErr) }) } @@ -2203,7 +2203,7 @@ func TestModule_ValidateFunction_MultiValue_TypeMismatch(t *testing.T) { t.Run(tc.name, func(t *testing.T) { err := tc.module.validateFunction(&stacks{}, api.CoreFeatureMultiValue, - 0, []Index{0}, nil, nil, nil, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, nil, nil, nil, nil, bytes.NewReader(nil)) require.EqualError(t, err, tc.expectedErr) }) } @@ -2221,7 +2221,7 @@ func TestModule_funcValidation_CallIndirect(t *testing.T) { }}}, } err := m.validateFunction(&stacks{}, api.CoreFeatureReferenceTypes, - 0, []Index{0}, nil, &Memory{}, []Table{{Type: RefTypeFuncref}}, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, &Memory{}, []Table{{Type: RefTypeFuncref}}, nil, nil, bytes.NewReader(nil)) require.NoError(t, err) }) t.Run("non zero table index", func(t *testing.T) { @@ -2236,12 +2236,12 @@ func TestModule_funcValidation_CallIndirect(t *testing.T) { } t.Run("disabled", func(t *testing.T) { err := m.validateFunction(&stacks{}, api.CoreFeaturesV1, - 0, []Index{0}, nil, &Memory{}, []Table{{}, {}}, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, &Memory{}, []Table{{}, {}}, nil, nil, bytes.NewReader(nil)) require.EqualError(t, err, "table index must be zero but was 100: feature \"reference-types\" is disabled") }) t.Run("enabled but out of range", func(t *testing.T) { err := m.validateFunction(&stacks{}, api.CoreFeatureReferenceTypes, - 0, []Index{0}, nil, &Memory{}, []Table{{}, {}}, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, &Memory{}, []Table{{}, {}}, nil, nil, bytes.NewReader(nil)) require.EqualError(t, err, "unknown table index: 100") }) }) @@ -2256,7 +2256,7 @@ func TestModule_funcValidation_CallIndirect(t *testing.T) { }}}, } err := m.validateFunction(&stacks{}, api.CoreFeatureReferenceTypes, - 0, []Index{0}, nil, &Memory{}, []Table{{Type: RefTypeExternref}}, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, &Memory{}, []Table{{Type: RefTypeExternref}}, nil, nil, bytes.NewReader(nil)) require.EqualError(t, err, "table is not funcref type but was externref for call_indirect") }) } @@ -2352,7 +2352,7 @@ func TestModule_funcValidation_RefTypes(t *testing.T) { CodeSection: []Code{{Body: tc.body}}, } err := m.validateFunction(&stacks{}, tc.flag, - 0, []Index{0}, nil, nil, nil, tc.declaredFunctionIndexes, bytes.NewReader(nil)) + 0, []Index{0}, nil, nil, nil, nil, tc.declaredFunctionIndexes, bytes.NewReader(nil)) if tc.expectedErr != "" { require.EqualError(t, err, tc.expectedErr) } else { @@ -2521,7 +2521,7 @@ func TestModule_funcValidation_TableGrowSizeFill(t *testing.T) { CodeSection: []Code{{Body: tc.body}}, } err := m.validateFunction(&stacks{}, tc.flag, - 0, []Index{0}, nil, nil, tables, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, nil, tables, nil, nil, bytes.NewReader(nil)) if tc.expectedErr != "" { require.EqualError(t, err, tc.expectedErr) } else { @@ -2634,7 +2634,7 @@ func TestModule_funcValidation_TableGetSet(t *testing.T) { CodeSection: []Code{{Body: tc.body}}, } err := m.validateFunction(&stacks{}, tc.flag, - 0, []Index{0}, nil, nil, tables, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, nil, tables, nil, nil, bytes.NewReader(nil)) if tc.expectedErr != "" { require.EqualError(t, err, tc.expectedErr) } else { @@ -2692,7 +2692,7 @@ func TestModule_funcValidation_Select_error(t *testing.T) { CodeSection: []Code{{Body: tc.body}}, } err := m.validateFunction(&stacks{}, tc.flag, - 0, []Index{0}, nil, nil, nil, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, nil, nil, nil, nil, bytes.NewReader(nil)) require.EqualError(t, err, tc.expectedErr) }) } @@ -3178,7 +3178,7 @@ func TestModule_funcValidation_SIMD(t *testing.T) { CodeSection: []Code{{Body: tc.body}}, } err := m.validateFunction(&stacks{}, api.CoreFeatureSIMD, - 0, []Index{0}, nil, &Memory{}, nil, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, &Memory{}, nil, nil, nil, bytes.NewReader(nil)) require.NoError(t, err) }) } @@ -3324,7 +3324,7 @@ func TestModule_funcValidation_SIMD_error(t *testing.T) { CodeSection: []Code{{Body: tc.body}}, } err := m.validateFunction(&stacks{}, tc.flag, - 0, []Index{0}, nil, &Memory{}, nil, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, &Memory{}, nil, nil, nil, bytes.NewReader(nil)) require.EqualError(t, err, tc.expectedErr) }) } @@ -3442,7 +3442,7 @@ func TestFuncValidation_UnreachableBrTable_NotModifyTypes(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { err := tc.m.validateFunction(&stacks{}, api.CoreFeaturesV2, - 0, nil, nil, nil, nil, nil, bytes.NewReader(nil)) + 0, nil, nil, nil, nil, nil, nil, bytes.NewReader(nil)) require.NoError(t, err) // Ensures that funcType has remained intact. @@ -3567,7 +3567,7 @@ func TestModule_funcValidation_loopWithParams(t *testing.T) { CodeSection: []Code{{Body: tc.body}}, } err := m.validateFunction(&stacks{}, api.CoreFeatureMultiValue, - 0, []Index{0}, nil, nil, nil, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, nil, nil, nil, nil, bytes.NewReader(nil)) if tc.expErr != "" { require.EqualError(t, err, tc.expErr) } else { @@ -3585,7 +3585,7 @@ func TestFunctionValidation_redundantEnd(t *testing.T) { CodeSection: []Code{{Body: []byte{OpcodeEnd, OpcodeEnd}}}, } err := m.validateFunction(&stacks{}, api.CoreFeaturesV2, - 0, nil, nil, nil, nil, nil, bytes.NewReader(nil)) + 0, nil, nil, nil, nil, nil, nil, bytes.NewReader(nil)) require.EqualError(t, err, "unexpected end of function at pc=0x1") } @@ -3618,7 +3618,7 @@ func TestFunctionValidation_redundantElse(t *testing.T) { t.Run(tc.expErr, func(t *testing.T) { m := &Module{TypeSection: []FunctionType{{}}, FunctionSection: []Index{0}, CodeSection: []Code{{Body: tc.body}}} err := m.validateFunction(&stacks{}, api.CoreFeaturesV2, - 0, nil, nil, nil, nil, nil, bytes.NewReader(nil)) + 0, nil, nil, nil, nil, nil, nil, bytes.NewReader(nil)) require.EqualError(t, err, tc.expErr) }) } @@ -4272,13 +4272,13 @@ func TestModule_funcValidation_Atomic(t *testing.T) { t.Run("with memory", func(t *testing.T) { err := m.validateFunction(&stacks{}, experimental.CoreFeaturesThreads, - 0, []Index{0}, nil, &Memory{}, []Table{}, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, &Memory{}, []Table{}, nil, nil, bytes.NewReader(nil)) require.NoError(t, err) }) t.Run("without memory", func(t *testing.T) { err := m.validateFunction(&stacks{}, experimental.CoreFeaturesThreads, - 0, []Index{0}, nil, nil, []Table{}, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, nil, []Table{}, nil, nil, bytes.NewReader(nil)) // Only fence doesn't require memory if tc.name == "memory.atomic.fence" { require.NoError(t, err) @@ -4301,7 +4301,7 @@ func TestModule_funcValidation_Atomic(t *testing.T) { CodeSection: []Code{{Body: body}}, } err := m.validateFunction(&stacks{}, experimental.CoreFeaturesThreads, - 0, []Index{0}, nil, &Memory{}, []Table{}, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, &Memory{}, []Table{}, nil, nil, bytes.NewReader(nil)) require.Error(t, err, "invalid immediate value for atomic.fence") }) @@ -4864,9 +4864,43 @@ func TestModule_funcValidation_Atomic(t *testing.T) { CodeSection: []Code{{Body: body}}, } err := m.validateFunction(&stacks{}, experimental.CoreFeaturesThreads, - 0, []Index{0}, nil, &Memory{}, []Table{}, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, &Memory{}, []Table{}, nil, nil, bytes.NewReader(nil)) require.Error(t, err, "invalid memory alignment") }) } }) } + +func TestValidation_LegacyExceptionHandlingOpcodes(t *testing.T) { + for _, tc := range []struct { + opcode byte + name string + }{ + {OpcodeLegacyTry, "try"}, + {OpcodeLegacyCatch, "catch"}, + {OpcodeLegacyRethrow, "rethrow"}, + {OpcodeLegacyDelegate, "delegate"}, + {OpcodeLegacyCatchAll, "catch_all"}, + } { + t.Run(tc.name, func(t *testing.T) { + m := &Module{ + TypeSection: []FunctionType{v_v}, + FunctionSection: []Index{0}, + CodeSection: []Code{{Body: []byte{tc.opcode, OpcodeEnd}}}, + } + t.Run("with EH enabled", func(t *testing.T) { + err := m.validateFunction(&stacks{}, api.CoreFeaturesV2|experimental.CoreFeaturesExceptionHandling, + 0, []Index{0}, nil, nil, nil, nil, nil, bytes.NewReader(nil)) + require.Error(t, err) + require.Contains(t, err.Error(), "legacy exception handling instruction") + require.Contains(t, err.Error(), "wasm-opt --translate-to-exnref") + }) + t.Run("without EH enabled", func(t *testing.T) { + err := m.validateFunction(&stacks{}, api.CoreFeaturesV2, + 0, []Index{0}, nil, nil, nil, nil, nil, bytes.NewReader(nil)) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid instruction") + }) + }) + } +} diff --git a/internal/wasm/instruction.go b/internal/wasm/instruction.go index 7f76b1e52..06222b76e 100644 --- a/internal/wasm/instruction.go +++ b/internal/wasm/instruction.go @@ -20,6 +20,20 @@ const ( // OpcodeElse brackets a sequence of instructions enclosed by an OpcodeIf. A branch instruction on a then label // breaks out to after the OpcodeEnd on the enclosing OpcodeIf. OpcodeElse Opcode = 0x05 + + // Exception handling instructions (toggled with CoreFeaturesExceptionHandling) + + // OpcodeThrow throws an exception with the given tag. + OpcodeThrow Opcode = 0x08 + // OpcodeThrowRef re-throws the exception referenced by an exnref value. + OpcodeThrowRef Opcode = 0x0a + + // Legacy exception handling opcodes (not supported; use wasm-opt --translate-to-exnref) + + OpcodeLegacyTry Opcode = 0x06 + OpcodeLegacyCatch Opcode = 0x07 + OpcodeLegacyRethrow Opcode = 0x09 + // OpcodeEnd terminates a control instruction OpcodeBlock, OpcodeLoop or OpcodeIf. OpcodeEnd Opcode = 0x0b @@ -48,6 +62,16 @@ const ( OpcodeSelect Opcode = 0x1b OpcodeTypedSelect Opcode = 0x1c + // Legacy exception handling opcodes (not supported; use wasm-opt --translate-to-exnref) + + OpcodeLegacyDelegate Opcode = 0x18 + OpcodeLegacyCatchAll Opcode = 0x19 + + // Exception handling instructions (toggled with CoreFeaturesExceptionHandling) + + // OpcodeTryTable brackets a sequence of instructions with catch clauses for exception handling. + OpcodeTryTable Opcode = 0x1f + // variable instructions OpcodeLocalGet Opcode = 0x20 @@ -989,6 +1013,8 @@ var instructionNames = [256]string{ OpcodeLoop: OpcodeLoopName, OpcodeIf: OpcodeIfName, OpcodeElse: OpcodeElseName, + OpcodeThrow: OpcodeThrowName, + OpcodeThrowRef: OpcodeThrowRefName, OpcodeEnd: OpcodeEndName, OpcodeBr: OpcodeBrName, OpcodeBrIf: OpcodeBrIfName, @@ -996,6 +1022,7 @@ var instructionNames = [256]string{ OpcodeReturn: OpcodeReturnName, OpcodeCall: OpcodeCallName, OpcodeCallIndirect: OpcodeCallIndirectName, + OpcodeTryTable: OpcodeTryTableName, OpcodeDrop: OpcodeDropName, OpcodeSelect: OpcodeSelectName, OpcodeTypedSelect: OpcodeTypedSelectName, @@ -1889,3 +1916,17 @@ var tailCallInstructionName = map[OpcodeTailCall]string{ func TailCallInstructionName(oc OpcodeTailCall) (ret string) { return tailCallInstructionName[oc] } + +// Catch clause kinds used within try_table encoding. +const ( + CatchKindCatch byte = 0x00 + CatchKindCatchRef byte = 0x01 + CatchKindCatchAll byte = 0x02 + CatchKindCatchAllRef byte = 0x03 +) + +const ( + OpcodeThrowName = "throw" + OpcodeThrowRefName = "throw_ref" + OpcodeTryTableName = "try_table" +) diff --git a/internal/wasm/module.go b/internal/wasm/module.go index fbbe5b713..143d9d305 100644 --- a/internal/wasm/module.go +++ b/internal/wasm/module.go @@ -94,6 +94,19 @@ type Module struct { // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-section%E2%91%A0 MemorySection *Memory + // TagSection contains each tag defined in this module for exception handling. + // + // Tag indexes are offset by any imported tags because the tag index begins with imports, followed by + // ones defined in this module. + // + // Note: In the Binary Format, this is SectionIDTag. + // + // See https://github.com/WebAssembly/exception-handling/blob/main/proposals/exception-handling/Exceptions.md + TagSection []Tag + + // ImportTagCount is the cached count of imported tags set during decoding. + ImportTagCount Index + // GlobalSection contains each global defined in this module. // // Global indexes are offset by any imported globals because the global index begins with imports, followed by @@ -264,7 +277,7 @@ func (m *Module) Validate(enabledFeatures api.CoreFeatures) error { return err } - functions, globals, memory, tables, err := m.AllDeclarations() + functions, globals, memory, tables, tags, err := m.AllDeclarations() if err != nil { return err } @@ -281,12 +294,12 @@ func (m *Module) Validate(enabledFeatures api.CoreFeatures) error { return err } - if err = m.validateExports(enabledFeatures, functions, globals, memory, tables); err != nil { + if err = m.validateExports(enabledFeatures, functions, globals, memory, tables, tags); err != nil { return err } if m.CodeSection != nil { - if err = m.validateFunctions(enabledFeatures, functions, globals, memory, tables, MaximumFunctionIndex); err != nil { + if err = m.validateFunctions(enabledFeatures, functions, globals, memory, tables, tags, MaximumFunctionIndex); err != nil { return err } } // No need to validate host functions as NewHostModule validates @@ -298,6 +311,23 @@ func (m *Module) Validate(enabledFeatures api.CoreFeatures) error { if err = m.validateDataCountSection(); err != nil { return err } + + if err = m.validateTagSection(); err != nil { + return err + } + return nil +} + +func (m *Module) validateTagSection() error { + for i, tag := range m.TagSection { + if tag.Type >= uint32(len(m.TypeSection)) { + return fmt.Errorf("tag[%d] type index out of range", i) + } + ft := &m.TypeSection[tag.Type] + if len(ft.Results) > 0 { + return fmt.Errorf("tag[%d] type must have empty results, got %v", i, ft.Results) + } + } return nil } @@ -334,7 +364,7 @@ func (m *Module) validateGlobals(globals []GlobalType, numFuncts, maxGlobals uin return nil } -func (m *Module) validateFunctions(enabledFeatures api.CoreFeatures, functions []Index, globals []GlobalType, memory *Memory, tables []Table, maximumFunctionIndex uint32) error { +func (m *Module) validateFunctions(enabledFeatures api.CoreFeatures, functions []Index, globals []GlobalType, memory *Memory, tables []Table, tags []Index, maximumFunctionIndex uint32) error { if uint32(len(functions)) > maximumFunctionIndex { return fmt.Errorf("too many functions (%d) in a module", len(functions)) } @@ -368,7 +398,7 @@ func (m *Module) validateFunctions(enabledFeatures api.CoreFeatures, functions [ if c.GoFunc != nil { continue } - if err = m.validateFunction(vs, enabledFeatures, Index(idx), functions, globals, memory, tables, declaredFuncIndexes, br); err != nil { + if err = m.validateFunction(vs, enabledFeatures, Index(idx), functions, globals, memory, tables, tags, declaredFuncIndexes, br); err != nil { return fmt.Errorf("invalid %s: %w", m.funcDesc(SectionIDFunction, Index(idx)), err) } } @@ -506,12 +536,19 @@ func (m *Module) validateImports(enabledFeatures api.CoreFeatures) error { if err := enabledFeatures.RequireEnabled(api.CoreFeatureMutableGlobal); err != nil { return fmt.Errorf("invalid import[%q.%q] global: %w", imp.Module, imp.Name, err) } + case ExternTypeTag: + if int(imp.DescTag) >= len(m.TypeSection) { + return fmt.Errorf("invalid import[%q.%q] tag: type index out of range", imp.Module, imp.Name) + } + if len(m.TypeSection[imp.DescTag].Results) > 0 { + return fmt.Errorf("invalid import[%q.%q] tag: tag types must have no results", imp.Module, imp.Name) + } } } return nil } -func (m *Module) validateExports(enabledFeatures api.CoreFeatures, functions []Index, globals []GlobalType, memory *Memory, tables []Table) error { +func (m *Module) validateExports(enabledFeatures api.CoreFeatures, functions []Index, globals []GlobalType, memory *Memory, tables []Table, tags []Index) error { for i := range m.ExportSection { exp := &m.ExportSection[i] index := exp.Index @@ -538,6 +575,10 @@ func (m *Module) validateExports(enabledFeatures api.CoreFeatures, functions []I if index >= uint32(len(tables)) { return fmt.Errorf("table for export[%q] out of range", exp.Name) } + case ExternTypeTag: + if index >= uint32(len(tags)) { + return fmt.Errorf("tag for export[%q] out of range", exp.Name) + } } } return nil @@ -576,6 +617,16 @@ func (m *Module) validateDataCountSection() (err error) { return } +func (m *ModuleInstance) buildTags(module *Module) { + for i := range module.TagSection { + tag := &module.TagSection[i] + t := &TagInstance{ + Type: &module.TypeSection[tag.Type], + } + m.Tags[i+int(module.ImportTagCount)] = t + } +} + func (m *ModuleInstance) buildGlobals(module *Module, funcRefResolver func(funcIndex Index) Reference) { importedGlobals := m.Globals[:module.ImportGlobalCount] @@ -694,6 +745,13 @@ type FunctionType struct { // ResultsNumInUint64 is the number of uint64 values requires to represent the Wasm result type. ResultNumInUint64 int + + // RecGroupSize is the size of the rec group this type belongs to. + // Standalone types (not in an explicit rec group) have RecGroupSize 1. + RecGroupSize int + + // RecGroupPosition is the 0-based position of this type within its rec group. + RecGroupPosition int } func (f *FunctionType) CacheNumInUint64() { @@ -721,6 +779,15 @@ func (f *FunctionType) EqualsSignature(params []ValueType, results []ValueType) return bytes.Equal(f.Params, params) && bytes.Equal(f.Results, results) } +// EqualsType returns true if the function types are structurally equal AND +// belong to the same rec group position/size (GC proposal type identity). +func (f *FunctionType) EqualsType(other *FunctionType) bool { + if !f.EqualsSignature(other.Params, other.Results) { + return false + } + return f.RecGroupSize == other.RecGroupSize && f.RecGroupPosition == other.RecGroupPosition +} + // key gets or generates the key for Store.typeIDs. e.g. "i32_v" for one i32 parameter and no (void) result. func (f *FunctionType) key() string { if f.string != "" { @@ -741,6 +808,9 @@ func (f *FunctionType) key() string { if len(f.Results) == 0 { ret += "v" } + if f.RecGroupSize > 1 { + ret += fmt.Sprintf("|rec%d/%d", f.RecGroupPosition, f.RecGroupSize) + } f.string = ret return ret } @@ -766,6 +836,8 @@ type Import struct { DescMem *Memory // DescGlobal is the inlined GlobalType when Type equals ExternTypeGlobal DescGlobal GlobalType + // DescTag is the type index when Type equals ExternTypeTag + DescTag Index // IndexPerType has the index of this import per ExternType. IndexPerType Index } @@ -802,6 +874,13 @@ func (m *Memory) Validate(memoryLimitPages uint32) error { return nil } +// Tag represents an exception tag defined in the tag section. +// The Type field is an index into the TypeSection; the referenced function type +// must have empty results (tags carry parameters but produce no results). +type Tag struct { + Type Index +} + type GlobalType struct { ValType ValueType Mutable bool @@ -935,8 +1014,8 @@ type NameMapAssoc struct { NameMap NameMap } -// AllDeclarations returns all declarations for functions, globals, memories and tables in a module including imported ones. -func (m *Module) AllDeclarations() (functions []Index, globals []GlobalType, memory *Memory, tables []Table, err error) { +// AllDeclarations returns all declarations for functions, globals, memories, tables and tags in a module including imported ones. +func (m *Module) AllDeclarations() (functions []Index, globals []GlobalType, memory *Memory, tables []Table, tags []Index, err error) { for i := range m.ImportSection { imp := &m.ImportSection[i] switch imp.Type { @@ -948,6 +1027,8 @@ func (m *Module) AllDeclarations() (functions []Index, globals []GlobalType, mem memory = imp.DescMem case ExternTypeTable: tables = append(tables, imp.DescTable) + case ExternTypeTag: + tags = append(tags, imp.DescTag) } } @@ -956,6 +1037,10 @@ func (m *Module) AllDeclarations() (functions []Index, globals []GlobalType, mem g := &m.GlobalSection[i] globals = append(globals, g.Type) } + for i := range m.TagSection { + t := &m.TagSection[i] + tags = append(tags, t.Type) + } if m.MemorySection != nil { if memory != nil { // shouldn't be possible due to Validate err = errors.New("at most one table allowed in module") @@ -997,6 +1082,11 @@ const ( // See https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/binary/modules.html#data-count-section // See https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/appendix/changes.html#bulk-memory-and-table-instructions SectionIDDataCount + + // SectionIDTag is for exception handling tags. + // + // See https://github.com/WebAssembly/exception-handling/blob/main/proposals/exception-handling/Exceptions.md + SectionIDTag SectionID = 13 ) // SectionIDName returns the canonical name of a module section. @@ -1029,6 +1119,8 @@ func SectionIDName(sectionID SectionID) string { return "data" case SectionIDDataCount: return "data_count" + case SectionIDTag: + return "tag" } return "unknown" } @@ -1046,6 +1138,24 @@ const ( // TODO: ValueTypeFuncref is not exposed in the api pkg yet. ValueTypeFuncref ValueType = 0x70 ValueTypeExternref = api.ValueTypeExternref + // ValueTypeExnref is the exception reference type used in exception handling. + ValueTypeExnref ValueType = 0x69 +) + +const ( + // RefPrefixNullable is the binary encoding prefix for nullable reference types (ref null ). + RefPrefixNullable byte = 0x63 + // RefPrefixNonNullable is the binary encoding prefix for non-nullable reference types (ref ). + RefPrefixNonNullable byte = 0x64 +) + +const ( + // HeapTypeFunc is the abstract heap type for function references. + HeapTypeFunc int64 = -16 + // HeapTypeExtern is the abstract heap type for external references. + HeapTypeExtern int64 = -17 + // HeapTypeExn is the abstract heap type for exception references. + HeapTypeExn int64 = -23 ) // ValueTypeName is an alias of api.ValueTypeName defined to simplify imports. @@ -1054,12 +1164,30 @@ func ValueTypeName(t ValueType) string { return "funcref" } else if t == ValueTypeV128 { return "v128" + } else if t == ValueTypeExnref { + return "exnref" } return api.ValueTypeName(t) } func isReferenceValueType(vt ValueType) bool { - return vt == ValueTypeExternref || vt == ValueTypeFuncref + return vt == ValueTypeExternref || vt == ValueTypeFuncref || vt == ValueTypeExnref +} + +// isRefSubtypeOf returns true if actual is assignment-compatible with expected. +// Currently, non-nullable ref types are desugared to nullable at decode time, +// so this reduces to equality. When non-nullable ref types are properly supported, +// this function should allow non-nullable to match nullable and vice versa. +func isRefSubtypeOf(actual, expected ValueType) bool { + return actual == expected +} + +// isStrictRefSubtypeOf returns true if actual is a strict subtype of expected. +// Currently, non-nullable ref types are desugared to nullable at decode time, +// so this reduces to equality. When non-nullable ref types are properly supported, +// non-nullable should be a subtype of nullable, but NOT vice versa. +func isStrictRefSubtypeOf(actual, expected ValueType) bool { + return actual == expected } // ExternType is an alias of api.ExternType defined to simplify imports. @@ -1074,9 +1202,14 @@ const ( ExternTypeMemoryName = api.ExternTypeMemoryName ExternTypeGlobal = api.ExternTypeGlobal ExternTypeGlobalName = api.ExternTypeGlobalName + ExternTypeTag = ExternType(0x04) + ExternTypeTagName = "tag" ) // ExternTypeName is an alias of api.ExternTypeName defined to simplify imports. func ExternTypeName(t ValueType) string { + if t == ExternTypeTag { + return ExternTypeTagName + } return api.ExternTypeName(t) } diff --git a/internal/wasm/module_test.go b/internal/wasm/module_test.go index 0448430fe..1fad01527 100644 --- a/internal/wasm/module_test.go +++ b/internal/wasm/module_test.go @@ -41,6 +41,17 @@ func TestFunctionType_String(t *testing.T) { } } +func TestIsReferenceValueType(t *testing.T) { + refTypes := []ValueType{ValueTypeFuncref, ValueTypeExternref, ValueTypeExnref} + for _, vt := range refTypes { + require.True(t, isReferenceValueType(vt), "expected %#x to be a reference type", vt) + } + nonRefTypes := []ValueType{ValueTypeI32, ValueTypeI64, ValueTypeF32, ValueTypeF64, ValueTypeV128} + for _, vt := range nonRefTypes { + require.False(t, isReferenceValueType(vt), "expected %#x to not be a reference type", vt) + } +} + func TestSectionIDName(t *testing.T) { tests := []struct { name string @@ -129,6 +140,7 @@ func TestModule_allDeclarations(t *testing.T) { expectedGlobals []GlobalType expectedMemory *Memory expectedTables []Table + expectedTags []Index }{ // Functions. { @@ -196,17 +208,38 @@ func TestModule_allDeclarations(t *testing.T) { }, expectedTables: []Table{{Min: 10}}, }, + // Tags. + { + module: &Module{ + ImportSection: []Import{{Type: ExternTypeTag, DescTag: 5}}, + }, + expectedTags: []Index{5}, + }, + { + module: &Module{ + TagSection: []Tag{{Type: 3}}, + }, + expectedTags: []Index{3}, + }, + { + module: &Module{ + ImportSection: []Import{{Type: ExternTypeTag, DescTag: 5}}, + TagSection: []Tag{{Type: 3}}, + }, + expectedTags: []Index{5, 3}, + }, } for i, tt := range tests { tc := tt t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { - functions, globals, memory, tables, err := tc.module.AllDeclarations() + functions, globals, memory, tables, tags, err := tc.module.AllDeclarations() require.NoError(t, err) require.Equal(t, tc.expectedFunctions, functions) require.Equal(t, tc.expectedGlobals, globals) require.Equal(t, tc.expectedTables, tables) require.Equal(t, tc.expectedMemory, memory) + require.Equal(t, tc.expectedTags, tags) }) } } @@ -461,12 +494,12 @@ func TestModule_validateFunctions(t *testing.T) { FunctionSection: []uint32{0}, CodeSection: []Code{{Body: []byte{OpcodeI32Const, 0, OpcodeDrop, OpcodeEnd}}}, } - err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, MaximumFunctionIndex) + err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, nil, MaximumFunctionIndex) require.NoError(t, err) }) t.Run("too many functions", func(t *testing.T) { m := Module{} - err := m.validateFunctions(api.CoreFeaturesV1, []uint32{1, 2, 3, 4}, nil, nil, nil, 3) + err := m.validateFunctions(api.CoreFeaturesV1, []uint32{1, 2, 3, 4}, nil, nil, nil, nil, 3) require.Error(t, err) require.EqualError(t, err, "too many functions (4) in a module") }) @@ -476,7 +509,7 @@ func TestModule_validateFunctions(t *testing.T) { FunctionSection: []Index{0}, CodeSection: nil, } - err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, MaximumFunctionIndex) + err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, nil, MaximumFunctionIndex) require.Error(t, err) require.EqualError(t, err, "code count (0) != function count (1)") }) @@ -486,7 +519,7 @@ func TestModule_validateFunctions(t *testing.T) { FunctionSection: []Index{1}, CodeSection: []Code{{Body: []byte{OpcodeEnd}}}, } - err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, MaximumFunctionIndex) + err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, nil, MaximumFunctionIndex) require.Error(t, err) require.EqualError(t, err, "invalid function[0]: type section index 1 out of range") }) @@ -496,7 +529,7 @@ func TestModule_validateFunctions(t *testing.T) { FunctionSection: []Index{0}, CodeSection: []Code{{Body: []byte{OpcodeF32Abs}}}, } - err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, MaximumFunctionIndex) + err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, nil, MaximumFunctionIndex) require.Error(t, err) require.Contains(t, err.Error(), "invalid function[0]: cannot pop the 1st f32 operand") }) @@ -507,7 +540,7 @@ func TestModule_validateFunctions(t *testing.T) { CodeSection: []Code{{Body: []byte{OpcodeF32Abs}}}, ExportSection: []Export{{Name: "f1", Type: ExternTypeFunc, Index: 0}}, } - err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, MaximumFunctionIndex) + err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, nil, MaximumFunctionIndex) require.Error(t, err) require.Contains(t, err.Error(), `invalid function[0] export["f1"]: cannot pop the 1st f32`) }) @@ -520,7 +553,7 @@ func TestModule_validateFunctions(t *testing.T) { CodeSection: []Code{{Body: []byte{OpcodeF32Abs}}}, ExportSection: []Export{{Name: "f1", Type: ExternTypeFunc, Index: 1}}, } - err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, MaximumFunctionIndex) + err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, nil, MaximumFunctionIndex) require.Error(t, err) require.Contains(t, err.Error(), `invalid function[0] export["f1"]: cannot pop the 1st f32`) }) @@ -534,7 +567,7 @@ func TestModule_validateFunctions(t *testing.T) { {Name: "f2", Type: ExternTypeFunc, Index: 0}, }, } - err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, MaximumFunctionIndex) + err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, nil, MaximumFunctionIndex) require.Error(t, err) require.Contains(t, err.Error(), `invalid function[0] export["f1","f2"]: cannot pop the 1st f32`) }) @@ -728,7 +761,7 @@ func TestModule_validateExports(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { m := Module{ExportSection: tc.exportSection} - err := m.validateExports(tc.enabledFeatures, tc.functions, tc.globals, tc.memory, tc.tables) + err := m.validateExports(tc.enabledFeatures, tc.functions, tc.globals, tc.memory, tc.tables, nil) if tc.expectedErr != "" { require.EqualError(t, err, tc.expectedErr) } else { diff --git a/internal/wasm/store.go b/internal/wasm/store.go index b707a58a7..b71e4d976 100644 --- a/internal/wasm/store.go +++ b/internal/wasm/store.go @@ -77,6 +77,7 @@ type ( Globals []*GlobalInstance MemoryInstance *MemoryInstance Tables []*TableInstance + Tags []*TagInstance // Engine implements function calls for this module. Engine ModuleEngine @@ -149,6 +150,13 @@ type ( Index Index } + // TagInstance represents an instantiated exception handling tag. + // Tags are compared by identity (pointer equality), not structural type equality. + TagInstance struct { + // Type is the function type of this tag (params only; results must be empty). + Type *FunctionType + } + // FunctionTypeID is a uniquely assigned integer for a function type. // This is wazero specific runtime object and specific to a store, // and used at runtime to do type-checks on indirect function calls. @@ -341,6 +349,7 @@ func (s *Store) instantiate( m.Tables = make([]*TableInstance, int(module.ImportTableCount)+len(module.TableSection)) m.Globals = make([]*GlobalInstance, int(module.ImportGlobalCount)+len(module.GlobalSection)) + m.Tags = make([]*TagInstance, int(module.ImportTagCount)+len(module.TagSection)) m.Engine, err = s.Engine.NewModuleEngine(module, m) if err != nil { return nil, err @@ -360,6 +369,7 @@ func (s *Store) instantiate( allocator, _ := ctx.Value(expctxkeys.MemoryAllocatorKey{}).(experimental.MemoryAllocator) m.buildGlobals(module, m.Engine.FunctionInstanceReference) + m.buildTags(module) m.buildMemory(module, allocator) m.Exports = module.Exports for _, exp := range m.Exports { @@ -502,6 +512,15 @@ func (m *ModuleInstance) resolveImports(ctx context.Context, module *Module) (er return } m.Globals[i.IndexPerType] = importedGlobal + case ExternTypeTag: + expected := &module.TypeSection[i.DescTag] + importedTag := importedModule.Tags[imported.Index] + if !importedTag.Type.EqualsType(expected) { + err = errorInvalidImport(i, fmt.Errorf("tag type mismatch: %s != %s", + expected, importedTag.Type)) + return + } + m.Tags[i.IndexPerType] = importedTag } } } diff --git a/internal/wasm/table_test.go b/internal/wasm/table_test.go index 7f11724ac..fbcee35a1 100644 --- a/internal/wasm/table_test.go +++ b/internal/wasm/table_test.go @@ -373,7 +373,7 @@ func TestModule_validateTable(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - _, _, _, tables, err := tc.input.AllDeclarations() + _, _, _, tables, _, err := tc.input.AllDeclarations() require.NoError(t, err) err = tc.input.validateTable(api.CoreFeaturesV1, tables, maxTableIndex) @@ -682,7 +682,7 @@ func TestModule_validateTable_Errors(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - _, _, _, tables, err := tc.input.AllDeclarations() + _, _, _, tables, _, err := tc.input.AllDeclarations() require.NoError(t, err) err = tc.input.validateTable(api.CoreFeaturesV1, tables, maxTableIndex) require.EqualError(t, err, tc.expectedErr) diff --git a/internal/wasmruntime/errors.go b/internal/wasmruntime/errors.go index 556e5de82..c17e79715 100644 --- a/internal/wasmruntime/errors.go +++ b/internal/wasmruntime/errors.go @@ -33,6 +33,10 @@ var ( ErrRuntimeExpectedSharedMemory = New("expected shared memory") // ErrRuntimeTooManyWaiters indicates that atomic.wait was called with too many waiters. ErrRuntimeTooManyWaiters = New("too many waiters") + // ErrRuntimeUncaughtException indicates that a thrown exception was not caught by any handler. + ErrRuntimeUncaughtException = New("uncaught exception") + // ErrRuntimeNullReference indicates a null reference was used where a non-null reference was expected. + ErrRuntimeNullReference = New("null reference") ) // Error is returned by a wasm.Engine during the execution of Wasm functions, and they indicate that the Wasm runtime