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:
Edoardo Vacchi
2026-04-26 18:12:51 +02:00
committed by GitHub
parent 5cb4bb3ec0
commit bfb20e0ba7
101 changed files with 4452 additions and 148 deletions
+5 -2
View File
@@ -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
+16
View File
@@ -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) ./...
+12
View File
@@ -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 ""
}
+47
View File
@@ -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]))
})
}
}
+5
View File
@@ -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
View File
@@ -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"
Binary file not shown.
+133 -5
View File
@@ -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
}
+211 -20
View File
@@ -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)
+58
View File
@@ -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
}
+11 -4
View File
@@ -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
+251 -1
View File
@@ -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
`,
},
} {
+188 -12
View File
@@ -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 = &paramResultStack[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)
}
+56
View File
@@ -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 -3
View File
@@ -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
+113 -5
View File
@@ -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++ {
+450
View File
@@ -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()))
}
+12
View File
@@ -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
+16 -14
View File
@@ -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
+7
View File
@@ -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
)
)
@@ -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
)
)
@@ -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.
+71
View File
@@ -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)
}
@@ -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"
)
@@ -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]")
@@ -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"}]}
@@ -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")
@@ -0,0 +1 @@
(module (func (catch_all)))
@@ -0,0 +1 @@
(module (tag $e) (func (catch $e)))
File diff suppressed because one or more lines are too long
@@ -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))
+55 -4
View File
@@ -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)
})
+2 -1
View File
@@ -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
+3 -1
View File
@@ -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.
//
+2 -1
View File
@@ -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)
}
+16 -6
View File
@@ -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
}
+1 -1
View File
@@ -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)
}
+16
View File
@@ -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)
}
+59 -3
View File
@@ -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 {
+34 -10
View File
@@ -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
+2
View File
@@ -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))
}
+9
View File
@@ -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
}
+178 -4
View File
@@ -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
+63 -29
View File
@@ -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")
})
})
}
}
+41
View File
@@ -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
View File
@@ -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)
}
+43 -10
View File
@@ -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 {
+19
View File
@@ -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
}
}
}
+2 -2
View File
@@ -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