mirror of
https://github.com/wazero/wazero
synced 2026-06-21 14:12:37 +00:00
feat: exception handling spec (#2489)
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 <evacchi@users.noreply.github.com>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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) ./...
|
||||
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
@@ -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]))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
+78
@@ -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 <exception>
|
||||
#include <stdexcept>
|
||||
|
||||
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"
|
||||
BIN
Binary file not shown.
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
`,
|
||||
},
|
||||
} {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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++ {
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 <i>
|
||||
// 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 <i> — 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)
|
||||
}
|
||||
}
|
||||
@@ -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]))
|
||||
}
|
||||
Binary file not shown.
@@ -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
|
||||
)
|
||||
)
|
||||
Binary file not shown.
@@ -0,0 +1,51 @@
|
||||
;; Test: br exiting a try_table must pop the handler (compiler engine).
|
||||
;;
|
||||
;; Without the fix, handler A (stale) incorrectly catches the throw,
|
||||
;; restoring to handler A's checkpoint and branching to $catchA,
|
||||
;; which returns 99.
|
||||
;; With the fix, handler A is popped, and the outer try_table catches
|
||||
;; the throw, returning 1.
|
||||
(module
|
||||
(tag $tag1 (param))
|
||||
(tag $tag2 (param))
|
||||
|
||||
(func (export "test") (result i32)
|
||||
;; Outer: catches $tag1 — CORRECT handler.
|
||||
block $outerCatch
|
||||
try_table (catch $tag1 $outerCatch)
|
||||
|
||||
;; try_table A: catches $tag1 with target $catchA.
|
||||
;; We exit via br, leaving handler A stale.
|
||||
block $catchA
|
||||
block $skipA
|
||||
try_table (catch $tag1 $catchA)
|
||||
br $skipA ;; exits try_table A without End
|
||||
end
|
||||
unreachable
|
||||
end
|
||||
;; After $skipA: handler A still on stack (bug).
|
||||
|
||||
;; try_table B: catches $tag2, but we throw $tag1.
|
||||
block $catchB
|
||||
try_table (catch $tag2 $catchB)
|
||||
throw $tag1
|
||||
end
|
||||
unreachable
|
||||
end
|
||||
;; caught by B (wrong — B catches tag2, not tag1)
|
||||
i32.const 2
|
||||
return
|
||||
end
|
||||
;; $catchA: stale handler A jumped here — WRONG result
|
||||
i32.const 99
|
||||
return
|
||||
|
||||
end
|
||||
;; fell through outer try_table
|
||||
i32.const 3
|
||||
return
|
||||
end
|
||||
;; $outerCatch: outer caught $tag1 — CORRECT
|
||||
i32.const 1
|
||||
)
|
||||
)
|
||||
Binary file not shown.
@@ -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
|
||||
)
|
||||
)
|
||||
Binary file not shown.
@@ -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
|
||||
)
|
||||
)
|
||||
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
;; Test that throw_ref on a null exnref traps as "null reference" not "unreachable".
|
||||
(module
|
||||
(func (export "throw_ref_null") (param exnref)
|
||||
local.get 0
|
||||
throw_ref
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,47 @@
|
||||
package spectest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/tetratelabs/wazero"
|
||||
"github.com/tetratelabs/wazero/api"
|
||||
"github.com/tetratelabs/wazero/experimental"
|
||||
"github.com/tetratelabs/wazero/internal/integration_test/spectest"
|
||||
"github.com/tetratelabs/wazero/internal/platform"
|
||||
)
|
||||
|
||||
//go:embed testdata
|
||||
var testcases embed.FS
|
||||
|
||||
const enabledFeatures = api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling | experimental.CoreFeaturesTailCall
|
||||
|
||||
func TestCompiler(t *testing.T) {
|
||||
if !platform.CompilerSupported() {
|
||||
t.Skip()
|
||||
}
|
||||
ctx := context.Background()
|
||||
config := wazero.NewRuntimeConfigCompiler().WithCoreFeatures(enabledFeatures)
|
||||
runCases(t, ctx, config)
|
||||
}
|
||||
|
||||
func TestInterpreter(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
config := wazero.NewRuntimeConfigInterpreter().WithCoreFeatures(enabledFeatures)
|
||||
runCases(t, ctx, config)
|
||||
}
|
||||
|
||||
func runCases(t *testing.T, ctx context.Context, config wazero.RuntimeConfig) {
|
||||
spectest.RunCase(t, testcases, "throw", ctx, config, -1, 0, math.MaxInt)
|
||||
spectest.RunCase(t, testcases, "throw_ref", ctx, config, -1, 0, math.MaxInt)
|
||||
spectest.RunCase(t, testcases, "tag", ctx, config, -1, 0, math.MaxInt)
|
||||
|
||||
// Run try_table.wast in two ranges, skipping lines 470-495
|
||||
// (two assert_invalid blocks for try_table type validation):
|
||||
// we desugar non-nullable ref types to nullable, so we cannot
|
||||
// detect the type mismatch between (ref null $t) and (ref $t).
|
||||
spectest.RunCase(t, testcases, "try_table", ctx, config, -1, 0, 470)
|
||||
spectest.RunCase(t, testcases, "try_table", ctx, config, -1, 496, math.MaxInt)
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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"}]}
|
||||
@@ -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"
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"source_filename":"./throw.wast","commands":[{"type":"module","line":3,"filename":"throw.0.wasm","module_type":"binary"},{"type":"assert_return","line":38,"action":{"type":"invoke","field":"throw-if","args":[{"type":"i32","value":"0"}]},"expected":[{"type":"i32","value":"0"}]},{"type":"assert_exception","line":39,"action":{"type":"invoke","field":"throw-if","args":[{"type":"i32","value":"10"}]}},{"type":"assert_exception","line":40,"action":{"type":"invoke","field":"throw-if","args":[{"type":"i32","value":"-1"}]}},{"type":"assert_exception","line":42,"action":{"type":"invoke","field":"throw-param-f32","args":[{"type":"f32","value":"1084227584"}]}},{"type":"assert_exception","line":43,"action":{"type":"invoke","field":"throw-param-i64","args":[{"type":"i64","value":"5"}]}},{"type":"assert_exception","line":44,"action":{"type":"invoke","field":"throw-param-f64","args":[{"type":"f64","value":"4617315517961601024"}]}},{"type":"assert_exception","line":46,"action":{"type":"invoke","field":"throw-polymorphic","args":[]}},{"type":"assert_exception","line":47,"action":{"type":"invoke","field":"throw-polymorphic-block","args":[]}},{"type":"assert_return","line":49,"action":{"type":"invoke","field":"test-throw-1-2","args":[]},"expected":[]},{"type":"assert_invalid","line":51,"filename":"throw.1.wasm","module_type":"binary","text":"unknown tag 0"},{"type":"assert_invalid","line":52,"filename":"throw.2.wasm","module_type":"binary","text":"type mismatch: instruction requires [i32] but stack has []"},{"type":"assert_invalid","line":54,"filename":"throw.3.wasm","module_type":"binary","text":"type mismatch: instruction requires [i32] but stack has [i64]"}]}
|
||||
@@ -0,0 +1,55 @@
|
||||
;; Test throw instruction.
|
||||
|
||||
(module
|
||||
(tag $e0)
|
||||
(tag $e-i32 (param i32))
|
||||
(tag $e-f32 (param f32))
|
||||
(tag $e-i64 (param i64))
|
||||
(tag $e-f64 (param f64))
|
||||
(tag $e-i32-i32 (param i32 i32))
|
||||
|
||||
(func $throw-if (export "throw-if") (param i32) (result i32)
|
||||
(local.get 0)
|
||||
(i32.const 0) (if (i32.ne) (then (throw $e0)))
|
||||
(i32.const 0)
|
||||
)
|
||||
|
||||
(func (export "throw-param-f32") (param f32) (local.get 0) (throw $e-f32))
|
||||
|
||||
(func (export "throw-param-i64") (param i64) (local.get 0) (throw $e-i64))
|
||||
|
||||
(func (export "throw-param-f64") (param f64) (local.get 0) (throw $e-f64))
|
||||
|
||||
(func (export "throw-polymorphic") (throw $e0) (throw $e-i32))
|
||||
|
||||
(func (export "throw-polymorphic-block") (block (result i32) (throw $e0)) (throw $e-i32))
|
||||
|
||||
(func $throw-1-2 (i32.const 1) (i32.const 2) (throw $e-i32-i32))
|
||||
(func (export "test-throw-1-2")
|
||||
(block $h (result i32 i32)
|
||||
(try_table (catch $e-i32-i32 $h) (call $throw-1-2))
|
||||
(return)
|
||||
)
|
||||
(if (i32.ne (i32.const 2)) (then (unreachable)))
|
||||
(if (i32.ne (i32.const 1)) (then (unreachable)))
|
||||
)
|
||||
)
|
||||
|
||||
(assert_return (invoke "throw-if" (i32.const 0)) (i32.const 0))
|
||||
(assert_exception (invoke "throw-if" (i32.const 10)))
|
||||
(assert_exception (invoke "throw-if" (i32.const -1)))
|
||||
|
||||
(assert_exception (invoke "throw-param-f32" (f32.const 5.0)))
|
||||
(assert_exception (invoke "throw-param-i64" (i64.const 5)))
|
||||
(assert_exception (invoke "throw-param-f64" (f64.const 5.0)))
|
||||
|
||||
(assert_exception (invoke "throw-polymorphic"))
|
||||
(assert_exception (invoke "throw-polymorphic-block"))
|
||||
|
||||
(assert_return (invoke "test-throw-1-2"))
|
||||
|
||||
(assert_invalid (module (func (throw 0))) "unknown tag 0")
|
||||
(assert_invalid (module (tag (param i32)) (func (throw 0)))
|
||||
"type mismatch: instruction requires [i32] but stack has []")
|
||||
(assert_invalid (module (tag (param i32)) (func (i64.const 5) (throw 0)))
|
||||
"type mismatch: instruction requires [i32] but stack has [i64]")
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
||||
{"source_filename":"./throw_ref.wast","commands":[{"type":"module","line":3,"filename":"throw_ref.0.wasm","module_type":"binary"},{"type":"assert_exception","line":99,"action":{"type":"invoke","field":"catch-throw_ref-0","args":[]}},{"type":"assert_exception","line":101,"action":{"type":"invoke","field":"catch-throw_ref-1","args":[{"type":"i32","value":"0"}]}},{"type":"assert_return","line":102,"action":{"type":"invoke","field":"catch-throw_ref-1","args":[{"type":"i32","value":"1"}]},"expected":[{"type":"i32","value":"23"}]},{"type":"assert_exception","line":104,"action":{"type":"invoke","field":"catchall-throw_ref-0","args":[]}},{"type":"assert_exception","line":106,"action":{"type":"invoke","field":"catchall-throw_ref-1","args":[{"type":"i32","value":"0"}]}},{"type":"assert_return","line":107,"action":{"type":"invoke","field":"catchall-throw_ref-1","args":[{"type":"i32","value":"1"}]},"expected":[{"type":"i32","value":"23"}]},{"type":"assert_exception","line":108,"action":{"type":"invoke","field":"throw_ref-nested","args":[{"type":"i32","value":"0"}]}},{"type":"assert_exception","line":109,"action":{"type":"invoke","field":"throw_ref-nested","args":[{"type":"i32","value":"1"}]}},{"type":"assert_return","line":110,"action":{"type":"invoke","field":"throw_ref-nested","args":[{"type":"i32","value":"2"}]},"expected":[{"type":"i32","value":"23"}]},{"type":"assert_return","line":112,"action":{"type":"invoke","field":"throw_ref-recatch","args":[{"type":"i32","value":"0"}]},"expected":[{"type":"i32","value":"23"}]},{"type":"assert_return","line":113,"action":{"type":"invoke","field":"throw_ref-recatch","args":[{"type":"i32","value":"1"}]},"expected":[{"type":"i32","value":"42"}]},{"type":"assert_exception","line":115,"action":{"type":"invoke","field":"throw_ref-stack-polymorphism","args":[]}},{"type":"assert_invalid","line":117,"filename":"throw_ref.1.wasm","module_type":"binary","text":"type mismatch"},{"type":"assert_invalid","line":118,"filename":"throw_ref.2.wasm","module_type":"binary","text":"type mismatch"}]}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
;; Test throw_ref instruction.
|
||||
|
||||
(module
|
||||
(tag $e0)
|
||||
(tag $e1)
|
||||
|
||||
(func (export "catch-throw_ref-0")
|
||||
(block $h (result exnref)
|
||||
(try_table (catch_ref $e0 $h) (throw $e0))
|
||||
(unreachable)
|
||||
)
|
||||
(throw_ref)
|
||||
)
|
||||
|
||||
(func (export "catch-throw_ref-1") (param i32) (result i32)
|
||||
(block $h (result exnref)
|
||||
(try_table (result i32) (catch_ref $e0 $h) (throw $e0))
|
||||
(return)
|
||||
)
|
||||
(if (param exnref) (i32.eqz (local.get 0))
|
||||
(then (throw_ref))
|
||||
(else (drop))
|
||||
)
|
||||
(i32.const 23)
|
||||
)
|
||||
|
||||
(func (export "catchall-throw_ref-0")
|
||||
(block $h (result exnref)
|
||||
(try_table (result exnref) (catch_all_ref $h) (throw $e0))
|
||||
)
|
||||
(throw_ref)
|
||||
)
|
||||
|
||||
(func (export "catchall-throw_ref-1") (param i32) (result i32)
|
||||
(block $h (result exnref)
|
||||
(try_table (result i32) (catch_all_ref $h) (throw $e0))
|
||||
(return)
|
||||
)
|
||||
(if (param exnref) (i32.eqz (local.get 0))
|
||||
(then (throw_ref))
|
||||
(else (drop))
|
||||
)
|
||||
(i32.const 23)
|
||||
)
|
||||
|
||||
(func (export "throw_ref-nested") (param i32) (result i32)
|
||||
(local $exn1 exnref)
|
||||
(local $exn2 exnref)
|
||||
(block $h1 (result exnref)
|
||||
(try_table (result i32) (catch_ref $e1 $h1) (throw $e1))
|
||||
(return)
|
||||
)
|
||||
(local.set $exn1)
|
||||
(block $h2 (result exnref)
|
||||
(try_table (result i32) (catch_ref $e0 $h2) (throw $e0))
|
||||
(return)
|
||||
)
|
||||
(local.set $exn2)
|
||||
(if (i32.eq (local.get 0) (i32.const 0))
|
||||
(then (throw_ref (local.get $exn1)))
|
||||
)
|
||||
(if (i32.eq (local.get 0) (i32.const 1))
|
||||
(then (throw_ref (local.get $exn2)))
|
||||
)
|
||||
(i32.const 23)
|
||||
)
|
||||
|
||||
(func (export "throw_ref-recatch") (param i32) (result i32)
|
||||
(local $e exnref)
|
||||
(block $h1 (result exnref)
|
||||
(try_table (result i32) (catch_ref $e0 $h1) (throw $e0))
|
||||
(return)
|
||||
)
|
||||
(local.set $e)
|
||||
(block $h2 (result exnref)
|
||||
(try_table (result i32) (catch_ref $e0 $h2)
|
||||
(if (i32.eqz (local.get 0))
|
||||
(then (throw_ref (local.get $e)))
|
||||
)
|
||||
(i32.const 42)
|
||||
)
|
||||
(return)
|
||||
)
|
||||
(drop) (i32.const 23)
|
||||
)
|
||||
|
||||
(func (export "throw_ref-stack-polymorphism")
|
||||
(local $e exnref)
|
||||
(block $h (result exnref)
|
||||
(try_table (result f64) (catch_ref $e0 $h) (throw $e0))
|
||||
(unreachable)
|
||||
)
|
||||
(local.set $e)
|
||||
(i32.const 1)
|
||||
(throw_ref (local.get $e))
|
||||
)
|
||||
)
|
||||
|
||||
(assert_exception (invoke "catch-throw_ref-0"))
|
||||
|
||||
(assert_exception (invoke "catch-throw_ref-1" (i32.const 0)))
|
||||
(assert_return (invoke "catch-throw_ref-1" (i32.const 1)) (i32.const 23))
|
||||
|
||||
(assert_exception (invoke "catchall-throw_ref-0"))
|
||||
|
||||
(assert_exception (invoke "catchall-throw_ref-1" (i32.const 0)))
|
||||
(assert_return (invoke "catchall-throw_ref-1" (i32.const 1)) (i32.const 23))
|
||||
(assert_exception (invoke "throw_ref-nested" (i32.const 0)))
|
||||
(assert_exception (invoke "throw_ref-nested" (i32.const 1)))
|
||||
(assert_return (invoke "throw_ref-nested" (i32.const 2)) (i32.const 23))
|
||||
|
||||
(assert_return (invoke "throw_ref-recatch" (i32.const 0)) (i32.const 23))
|
||||
(assert_return (invoke "throw_ref-recatch" (i32.const 1)) (i32.const 42))
|
||||
|
||||
(assert_exception (invoke "throw_ref-stack-polymorphism"))
|
||||
|
||||
(assert_invalid (module (func (throw_ref))) "type mismatch")
|
||||
(assert_invalid (module (func (block (throw_ref)))) "type mismatch")
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+1
@@ -0,0 +1 @@
|
||||
(module (func (catch_all)))
|
||||
+1
@@ -0,0 +1 @@
|
||||
(module (tag $e) (func (catch $e)))
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
File diff suppressed because one or more lines are too long
+523
@@ -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))
|
||||
@@ -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])))
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)...)
|
||||
}
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
+142
-9
@@ -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 <heaptype>).
|
||||
RefPrefixNullable byte = 0x63
|
||||
// RefPrefixNonNullable is the binary encoding prefix for non-nullable reference types (ref <heaptype>).
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user