diff --git a/.github/workflows/commit.yaml b/.github/workflows/commit.yaml index 1f2906a88..de5eff2ac 100644 --- a/.github/workflows/commit.yaml +++ b/.github/workflows/commit.yaml @@ -30,11 +30,14 @@ jobs: name: Pre-commit check runs-on: ubuntu-24.04 steps: - - name: Install latest wast2json - run: | # Needed for build.spectest. wabt includes wast2json. + - name: Install latest wast2json and wasm-tools + run: | # Needed for build.spectest. wabt includes wast2json; wasm-tools is used for exception-handling spectests. wabt_version=1.0.39 wabt_url=https://github.com/WebAssembly/wabt/releases/download/${wabt_version}/wabt-${wabt_version}-linux-x64.tar.gz curl -sSL ${wabt_url} | tar --strip-components 2 -C /usr/local/bin -xzf - wabt-${wabt_version}/bin/wast2json + wasm_tools_version=1.245.1 + wasm_tools_url=https://github.com/bytecodealliance/wasm-tools/releases/download/v${wasm_tools_version}/wasm-tools-${wasm_tools_version}-x86_64-linux.tar.gz + curl -sSL ${wasm_tools_url} | tar --strip-components 1 -C /usr/local/bin -xzf - wasm-tools-${wasm_tools_version}-x86_64-linux/wasm-tools - uses: actions/checkout@v6 diff --git a/Makefile b/Makefile index 582fffce9..97943b5a1 100644 --- a/Makefile +++ b/Makefile @@ -128,6 +128,10 @@ spec_version_tail_call := 88e97b0f742f4c3ee01fea683da130f344dd7b02 spectest_extended_const_dir := $(spectest_base_dir)/extended-const spectest_extended_const_testdata_dir := $(spectest_extended_const_dir)/testdata +spectest_exception_handling_dir := $(spectest_base_dir)/exception-handling +spectest_exception_handling_testdata_dir := $(spectest_exception_handling_dir)/testdata +spec_version_exception_handling := 13734f8fb871a5dab939070f893adbd90bffe28c + .PHONY: build.spectest build.spectest: @$(MAKE) build.spectest.v1 @@ -135,6 +139,7 @@ build.spectest: @$(MAKE) build.spectest.threads @$(MAKE) build.spectest.tail_call @$(MAKE) build.spectest.extended_const + @$(MAKE) build.spectest.exception_handling .PHONY: build.spectest.v1 build.spectest.v1: # Note: wabt by default uses >1.0 features, so wast2json flags might drift as they include more. See WebAssembly/wabt#1878 @@ -203,6 +208,17 @@ build.spectest.tail_call: wast2json --enable-tail-call --debug-names $$f; \ done +.PHONY: build.spectest.exception_handling +build.spectest.exception_handling: + @rm -rf $(spectest_exception_handling_testdata_dir) + @mkdir -p $(spectest_exception_handling_testdata_dir) + @cd $(spectest_exception_handling_testdata_dir) \ + && curl -sSL 'https://api.github.com/repos/WebAssembly/spec/contents/test/core/exceptions?ref=$(spec_version_exception_handling)' \ + | jq -r '.[]| .download_url' | grep -E ".wast" | xargs -Iurl curl -sJL url -O + @cd $(spectest_exception_handling_testdata_dir) && for f in `find . -name '*.wast'`; do \ + wasm-tools json-from-wast --wasm-dir . -o $$(basename $$f .wast).json $$f || true; \ + done + .PHONY: test test: @go test $(go_test_options) ./... diff --git a/api/features.go b/api/features.go index c739d3bf7..29405b093 100644 --- a/api/features.go +++ b/api/features.go @@ -209,6 +209,18 @@ func featureName(f CoreFeatures) string { case CoreFeatureSIMD: // match https://github.com/WebAssembly/spec/blob/wg-2.0.draft1/proposals/simd/SIMD.md return "simd" + // The cases below cover features defined in the experimental package + // (experimental.CoreFeaturesThreads, CoreFeaturesTailCall, + // experimental.CoreFeaturesExtendedConst, experimental.CoreFeaturesExceptionHandling). + // They cannot be imported here (circular dependency), so we match by value. + case CoreFeatureSIMD << 1: // experimental.CoreFeaturesThreads + return "threads" + case CoreFeatureSIMD << 2: // experimental.CoreFeaturesTailCall + return "tail-call" + case CoreFeatureSIMD << 3: // experimental.CoreFeaturesExtendedConst + return "extended-const" + case CoreFeatureSIMD << 4: // experimental.CoreFeaturesExceptionHandling + return "exception-handling" } return "" } diff --git a/experimental/exceptions_test.go b/experimental/exceptions_test.go new file mode 100644 index 000000000..088202fdd --- /dev/null +++ b/experimental/exceptions_test.go @@ -0,0 +1,47 @@ +package experimental_test + +import ( + "context" + _ "embed" + "testing" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/experimental" + "github.com/tetratelabs/wazero/internal/testing/require" +) + +//go:embed testdata/cpp_exceptions.wasm +var cppExceptionsWasm []byte + +func TestCppExceptions(t *testing.T) { + ctx := context.Background() + + cfg := wazero.NewRuntimeConfig(). + WithCoreFeatures(api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling) + + r := wazero.NewRuntimeWithConfig(ctx, cfg) + defer r.Close(ctx) + + mod, err := r.InstantiateWithConfig(ctx, cppExceptionsWasm, + wazero.NewModuleConfig().WithStartFunctions("_initialize")) + require.NoError(t, err) + + tests := []struct { + name string + expected int32 + }{ + {"test_no_throw", 42}, + {"test_catch_specific", -1}, + {"test_catch_base", 1}, + {"test_rethrow", -42}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + res, err := mod.ExportedFunction(tc.name).Call(ctx) + require.NoError(t, err) + require.Equal(t, tc.expected, api.DecodeI32(res[0])) + }) + } +} diff --git a/experimental/features.go b/experimental/features.go index c1800b386..4d229fc6e 100644 --- a/experimental/features.go +++ b/experimental/features.go @@ -27,3 +27,8 @@ const CoreFeaturesTailCall = api.CoreFeatureSIMD << 2 // // See https://github.com/WebAssembly/extended-const for further details. const CoreFeaturesExtendedConst = api.CoreFeatureSIMD << 3 + +// CoreFeaturesExceptionHandling enables exception handling instructions. +// +// See https://github.com/WebAssembly/exception-handling for further details. +const CoreFeaturesExceptionHandling = api.CoreFeatureSIMD << 4 diff --git a/experimental/testdata/cpp_exceptions.cpp b/experimental/testdata/cpp_exceptions.cpp new file mode 100644 index 000000000..22661d23d --- /dev/null +++ b/experimental/testdata/cpp_exceptions.cpp @@ -0,0 +1,78 @@ +// Compile with Emscripten (legacy wasm EH), then translate to new EH with Binaryen: +// +// emcc -fwasm-exceptions -o cpp_exceptions_legacy.wasm cpp_exceptions.cpp \ +// -s EXPORTED_FUNCTIONS='["_test_no_throw","_test_catch_specific","_test_catch_base","_test_rethrow"]' \ +// -s ERROR_ON_UNDEFINED_SYMBOLS=0 -s STANDALONE_WASM -O2 --no-entry +// wasm-opt --translate-to-exnref \ +// --enable-exception-handling --enable-reference-types --enable-bulk-memory \ +// --enable-bulk-memory-opt --enable-multivalue --enable-mutable-globals \ +// --enable-sign-ext --enable-nontrapping-float-to-int \ +// -o cpp_exceptions.wasm cpp_exceptions_legacy.wasm + +#include +#include + +class MyException : public std::exception { + int code_; +public: + MyException(int code) : code_(code) {} + int code() const { return code_; } + const char* what() const noexcept override { return "MyException"; } +}; + +static int may_throw(int v) { + if (v < 0) { + throw MyException(v); + } + if (v == 0) { + throw std::runtime_error("zero"); + } + return v * 2; +} + +extern "C" { + +// Returns may_throw(21), expected 42. Returns -1 on unexpected exception. +int test_no_throw() { + try { + return may_throw(21); + } catch (...) { + return -1; + } +} + +// Throws MyException(-1), catches it, returns the code. Expected: -1. +int test_catch_specific() { + try { + may_throw(-1); + return 0; // should not reach + } catch (const MyException& e) { + return e.code(); + } +} + +// Throws std::runtime_error, catches via std::exception base. Returns 1 on match, 0 otherwise. +int test_catch_base() { + try { + may_throw(0); + return 0; + } catch (const std::exception& e) { + return 1; + } +} + +// Throws, catches, rethrows, re-catches. Returns the code from re-catch. Expected: -42. +int test_rethrow() { + try { + try { + may_throw(-42); + } catch (const MyException&) { + throw; + } + } catch (const MyException& e) { + return e.code(); + } + return 0; +} + +} // extern "C" diff --git a/experimental/testdata/cpp_exceptions.wasm b/experimental/testdata/cpp_exceptions.wasm new file mode 100644 index 000000000..bcb3295f9 Binary files /dev/null and b/experimental/testdata/cpp_exceptions.wasm differ diff --git a/internal/engine/interpreter/compiler.go b/internal/engine/interpreter/compiler.go index 4269d237b..569f75eec 100644 --- a/internal/engine/interpreter/compiler.go +++ b/internal/engine/interpreter/compiler.go @@ -21,6 +21,7 @@ const ( controlFrameKindLoop controlFrameKindIfWithElse controlFrameKindIfWithoutElse + controlFrameKindTryTable ) type ( @@ -57,7 +58,8 @@ func (c *controlFrame) asLabel() label { case controlFrameKindFunction: return newLabel(labelKindReturn, 0) case controlFrameKindIfWithElse, - controlFrameKindIfWithoutElse: + controlFrameKindIfWithoutElse, + controlFrameKindTryTable: return newLabel(labelKindContinuation, c.frameID) } panic(fmt.Sprintf("unreachable: a bug in interpreterir implementation: %v", c.kind)) @@ -187,6 +189,8 @@ type compiler struct { funcs []uint32 // globals holds the global types for all declared globals in the module where the target function exists. globals []wasm.GlobalType + // tags holds the type indexes for all declared tags in the module where the target function exists. + tags []uint32 // needSourceOffset is true if this module requires DWARF based stack trace. needSourceOffset bool @@ -254,6 +258,9 @@ type compilationResult struct { LabelCallers map[label]uint32 // UsesMemory is true if this function might use memory. UsesMemory bool + // PendingExceptionTable holds unresolved exception table entries, built during + // compilation. Labels are resolved to final PCs in lowerIR. + PendingExceptionTable []pendingExceptionTableEntry // The following fields are per-module values, not per-function. @@ -276,7 +283,7 @@ type compilationResult struct { // newCompiler returns the new *compiler for the given parameters. // Use compiler.Next function to get compilation result per function. func newCompiler(enabledFeatures api.CoreFeatures, callFrameStackSizeInUint64 int, module *wasm.Module, ensureTermination bool) (*compiler, error) { - functions, globals, mem, tables, err := module.AllDeclarations() + functions, globals, mem, tables, tags, err := module.AllDeclarations() if err != nil { return nil, err } @@ -313,6 +320,7 @@ func newCompiler(enabledFeatures api.CoreFeatures, callFrameStackSizeInUint64 in }, globals: globals, funcs: functions, + tags: tags, types: types, ensureTermination: ensureTermination, br: bytes.NewReader(nil), @@ -336,6 +344,7 @@ func (c *compiler) Next() (*compilationResult, error) { c.result.Operations = c.result.Operations[:0] c.result.IROperationSourceOffsetsInWasmBinary = c.result.IROperationSourceOffsetsInWasmBinary[:0] c.result.UsesMemory = false + c.result.PendingExceptionTable = c.result.PendingExceptionTable[:0] // Clears the existing entries in LabelCallers. for frameID := uint32(0); frameID <= c.currentFrameID; frameID++ { for k := labelKind(0); k < labelKindNum; k++ { @@ -667,7 +676,8 @@ operatorSwitch: // Initiate the continuation. c.emit(newOperationLabel(continuationLabel)) case controlFrameKindBlockWithContinuationLabel, - controlFrameKindIfWithElse: + controlFrameKindIfWithElse, + controlFrameKindTryTable: continuationLabel := newLabel(labelKindContinuation, frame.frameID) c.result.LabelCallers[continuationLabel]++ c.emit(dropOp) @@ -800,6 +810,95 @@ operatorSwitch: // That means subsequent instructions in the current control frame are "unreachable" // and can be safely removed. c.markUnreachable() + case wasm.OpcodeThrow: + if c.unreachableState.on { + break operatorSwitch + } + // Pop the tag's param values from the stack. + if index < uint32(len(c.tags)) { + tagType := &c.types[c.tags[index]] + for i := len(tagType.Params) - 1; i >= 0; i-- { + c.stackPop() + } + } + c.emit(newOperationThrow(index)) + c.markUnreachable() + case wasm.OpcodeThrowRef: + if c.unreachableState.on { + break operatorSwitch + } + // Pop the exnref from the stack. + c.stackPop() + c.emit(newOperationThrowRef()) + c.markUnreachable() + case wasm.OpcodeTryTable: + c.br.Reset(c.body[c.pc+1:]) + bt, num, err := wasm.DecodeBlockType(c.types, c.br, c.enabledFeatures) + if err != nil { + return fmt.Errorf("reading block type for try_table instruction: %w", err) + } + c.pc += num + + if c.unreachableState.on { + c.unreachableState.depth++ + // Still need to skip the catch clause bytes. + c.pc++ + catchCount, catchNum, err := leb128.LoadUint32(c.body[c.pc:]) + if err != nil { + return fmt.Errorf("reading catch count for try_table: %w", err) + } + c.pc += catchNum - 1 + for i := uint32(0); i < catchCount; i++ { + if _, _, _, err := c.parseCatchClause(); err != nil { + return err + } + } + break operatorSwitch + } + + // Read catch clause count. + c.pc++ + catchCount, catchNum, err := leb128.LoadUint32(c.body[c.pc:]) + if err != nil { + return fmt.Errorf("reading catch count for try_table: %w", err) + } + c.pc += catchNum - 1 + + // Parse catch clauses. + var pendingClauses []pendingCatchClause + for i := uint32(0); i < catchCount; i++ { + kind, tagIdx, labelIdx, err := c.parseCatchClause() + if err != nil { + return err + } + // Resolve the label from the control frame stack. + targetFrame := c.controlFrames.get(int(labelIdx)) + targetFrame.ensureContinuation() + targetLabel := targetFrame.asLabel() + c.result.LabelCallers[targetLabel]++ + pendingClauses = append(pendingClauses, pendingCatchClause{ + kind: kind, + tagIndex: tagIdx, + targetLabel: targetLabel, + targetStackDepth: targetFrame.originalStackLenWithoutParamUint64, + }) + } + + // Create a control frame for the try_table block. + frameID := c.nextFrameID() + c.result.PendingExceptionTable = append(c.result.PendingExceptionTable, pendingExceptionTableEntry{ + startOpIndex: len(c.result.Operations), + continuationFrameID: frameID, + clauses: pendingClauses, + }) + frame := controlFrame{ + frameID: frameID, + originalStackLenWithoutParam: len(c.stack) - len(bt.Params), + originalStackLenWithoutParamUint64: c.stackLenInUint64 - bt.ParamNumInUint64, + kind: controlFrameKindTryTable, + blockType: bt, + } + c.controlFrames.push(frame) case wasm.OpcodeCall: c.emit( newOperationCall(index), @@ -3492,7 +3591,9 @@ func (c *compiler) applyToStack(opcode wasm.Opcode) (index uint32, err error) { wasm.OpcodeGlobalSet, // tail-call proposal wasm.OpcodeTailCallReturnCall, - wasm.OpcodeTailCallReturnCallIndirect: + wasm.OpcodeTailCallReturnCallIndirect, + // exception handling - throw reads tag index + wasm.OpcodeThrow: // Assumes that we are at the opcode now so skip it before read immediates. v, num, err := leb128.LoadUint32(c.body[c.pc+1:]) if err != nil { @@ -3605,7 +3706,7 @@ func (c *compiler) emitDefaultValue(t wasm.ValueType) { case wasm.ValueTypeI32: c.stackPush(unsignedTypeI32) c.emit(newOperationConstI32(0)) - case wasm.ValueTypeI64, wasm.ValueTypeExternref, wasm.ValueTypeFuncref: + case wasm.ValueTypeI64, wasm.ValueTypeExternref, wasm.ValueTypeFuncref, wasm.ValueTypeExnref: c.stackPush(unsignedTypeI64) c.emit(newOperationConstI64(0)) case wasm.ValueTypeF32: @@ -3673,3 +3774,30 @@ func (c *compiler) readMemoryArg(tag string) (memoryArg, error) { c.pc += num return memoryArg{Offset: offset, Alignment: alignment}, nil } + +// parseCatchClause parses a single catch clause from the bytecode at c.pc, +// advancing c.pc past the clause. Returns the kind, tag index (0 for catch_all +// variants), and label index. +func (c *compiler) parseCatchClause() (kind byte, tagIdx, labelIdx uint32, err error) { + var n uint64 + c.pc++ + kind = c.body[c.pc] + switch kind { + case wasm.CatchKindCatch, wasm.CatchKindCatchRef: + c.pc++ + tagIdx, n, err = leb128.LoadUint32(c.body[c.pc:]) + if err != nil { + err = fmt.Errorf("reading catch tag index: %w", err) + return + } + c.pc += n - 1 + } + c.pc++ + labelIdx, n, err = leb128.LoadUint32(c.body[c.pc:]) + if err != nil { + err = fmt.Errorf("reading catch label index: %w", err) + return + } + c.pc += n - 1 + return +} diff --git a/internal/engine/interpreter/interpreter.go b/internal/engine/interpreter/interpreter.go index 6b4d5b23f..6e6a51224 100644 --- a/internal/engine/interpreter/interpreter.go +++ b/internal/engine/interpreter/interpreter.go @@ -130,6 +130,39 @@ func (e *moduleEngine) OwnsGlobals() bool { return false } // MemoryGrown implements wasm.ModuleEngine. func (e *moduleEngine) MemoryGrown() {} +// restorable is implemented by panic values that can restore callEngine state. +// Both *snapshot (snapshotter API) and *thrownException (exception handling) +// implement this interface. +type restorable interface { + // canRestore unwinds ce.frames to callerFrameCount and checks whether a + // handler exists at that depth. If no handler is found, the caller + // re-panics and the next outer callWithUnwind unwinds further. + canRestore(ce *callEngine, callerFrameCount int) bool + // doRestore restores the callEngine state to the given stack frame depth. + doRestore(ce *callEngine, callerFrameCount int) +} + +// thrownException is the panic value for wasm exception propagation. +type thrownException struct { + exception *wasm.Exception + // Fields populated by canRestore for doRestore. + clause *exceptionTableCatchClause + values []uint64 +} + +func (t *thrownException) canRestore(ce *callEngine, callerFrameCount int) bool { + ce.frames = ce.frames[:callerFrameCount] + frame := ce.frames[callerFrameCount-1] + t.clause, t.values = searchExceptionTable(t.exception, frame) + return t.clause != nil +} + +func (t *thrownException) doRestore(ce *callEngine, callerFrameCount int) { + frame := ce.frames[callerFrameCount-1] + ce.applyExceptionHandler(frame, t.clause, t.values) + t.clause, t.values = nil, nil +} + // callEngine holds context per moduleEngine.Call, and shared across all the // function calls originating from the same moduleEngine.Call execution. // @@ -151,6 +184,94 @@ type callEngine struct { stackIterator stackIterator } +// matchCatchClause checks whether a single catch clause matches the given exception. +// Returns whether it matched and the values to push onto the stack. +func matchCatchClause(kind byte, clauseTag *wasm.TagInstance, exn *wasm.Exception) (matched bool, values []uint64) { + switch kind { + case wasm.CatchKindCatch: + if exn.Tag == clauseTag { + return true, slices.Clone(exn.Params) + } + case wasm.CatchKindCatchRef: + if exn.Tag == clauseTag { + values = slices.Clone(exn.Params) + values = append(values, uint64(uintptr(unsafe.Pointer(exn)))) + return true, values + } + case wasm.CatchKindCatchAll: + return true, nil + case wasm.CatchKindCatchAllRef: + return true, []uint64{uint64(uintptr(unsafe.Pointer(exn)))} + } + return false, nil +} + +// searchExceptionTable searches the compiled function's static exception table +// for a handler matching the given exception at the current PC. Returns the +// matched clause and catch values, or nil if no handler matches. Searches +// backwards so inner try_tables (which have higher indices) are checked first. +// This function is pure — it does not modify callEngine state. +func searchExceptionTable(exn *wasm.Exception, frame *callFrame) (*exceptionTableCatchClause, []uint64) { + table := frame.f.parent.exceptionTable + pc := frame.pc + for i := len(table) - 1; i >= 0; i-- { + entry := &table[i] + if pc < entry.startPC || pc >= entry.endPC { + continue + } + for j := range entry.clauses { + clause := &entry.clauses[j] + var clauseTag *wasm.TagInstance + if clause.kind == wasm.CatchKindCatch || clause.kind == wasm.CatchKindCatchRef { + clauseTag = frame.f.moduleInstance.Tags[clause.tagIndex] + } + matched, values := matchCatchClause(clause.kind, clauseTag, exn) + if matched { + return clause, values + } + } + } + return nil, nil +} + +// applyExceptionHandler applies a matched exception table clause to the callEngine state. +func (ce *callEngine) applyExceptionHandler(frame *callFrame, clause *exceptionTableCatchClause, values []uint64) { + ce.stack = ce.stack[:frame.base-frame.f.funcType.ParamNumInUint64+clause.targetStackDepth] + ce.stack = append(ce.stack, values...) + frame.pc = clause.targetPC +} + +// callWithUnwind calls the target function with support for stack unwinding +// (exception handling and snapshot restores). Returns true if the frame was +// unwound (caller should refresh frame/body/bodyLen and continue the loop). +// Returns false on normal return (caller should do frame.pc++). +func (ce *callEngine) callWithUnwind(ctx context.Context, m *wasm.ModuleInstance, tf *function) bool { + // Short-circuit: skip defer/recover overhead when neither exception + // handlers nor the snapshotter are active for the calling frame. + frame := ce.frames[len(ce.frames)-1] + if len(frame.f.parent.exceptionTable) == 0 && ctx.Value(expctxkeys.EnableSnapshotterKey{}) == nil { + ce.callFunction(ctx, m, tf) + return false + } + + callerFrameCount := len(ce.frames) + caught := false + func() { + defer func() { + if r := recover(); r != nil { + if v, ok := r.(restorable); ok && v.canRestore(ce, callerFrameCount) { + v.doRestore(ce, callerFrameCount) + caught = true + return + } + panic(r) + } + }() + ce.callFunction(ctx, m, tf) + }() + return caught +} + func (e *moduleEngine) newCallEngine(compiled *function) *callEngine { return &callEngine{f: compiled} } @@ -233,6 +354,7 @@ type callFrame struct { type compiledFunction struct { source *wasm.Module body []unionOperation + exceptionTable []exceptionTableEntry listener experimental.FunctionListener offsetsInWasmBinary []uint64 hostFn interface{} @@ -284,7 +406,11 @@ func (s *snapshot) Restore(ret []uint64) { panic(s) } -func (s *snapshot) doRestore() { +func (s *snapshot) canRestore(ce *callEngine, _ int) bool { + return s.ce == ce +} + +func (s *snapshot) doRestore(_ *callEngine, _ int) { ce := s.ce ce.stack = s.stack @@ -495,6 +621,35 @@ func (e *engine) lowerIR(ir *compilationResult, ret *compiledFunction) error { e.setLabelAddress(&op.Us[1], label(op.Us[1]), labelAddressResolutions) } } + + // Resolve exception table entries (translate labels to PC). + if len(ir.PendingExceptionTable) > 0 { + ret.exceptionTable = make([]exceptionTableEntry, len(ir.PendingExceptionTable)) + for i, pe := range ir.PendingExceptionTable { + contLabel := newLabel(labelKindContinuation, pe.continuationFrameID) + var endPC uint64 + e.setLabelAddress(&endPC, contLabel, labelAddressResolutions) + + clauses := make([]exceptionTableCatchClause, len(pe.clauses)) + for j, clause := range pe.clauses { + var targetPC uint64 + e.setLabelAddress(&targetPC, clause.targetLabel, labelAddressResolutions) + clauses[j] = exceptionTableCatchClause{ + kind: clause.kind, + tagIndex: clause.tagIndex, + targetPC: targetPC, + targetStackDepth: clause.targetStackDepth, + } + } + + ret.exceptionTable[i] = exceptionTableEntry{ + startPC: uint64(pe.startOpIndex), + endPC: endPC, + clauses: clauses, + } + } + } + return nil } @@ -644,6 +799,11 @@ func (ce *callEngine) recoverOnCall(ctx context.Context, m *wasm.ModuleInstance, panic(s) } + // If an exception reached the top level without being caught, convert it to an uncaught exception error. + if _, ok := v.(*thrownException); ok { + v = wasmruntime.ErrRuntimeUncaughtException + } + builder := wasmdebug.NewErrorBuilder() frameCount := len(ce.frames) functionListeners := make([]functionListenerInvocation, 0, 16) @@ -761,30 +921,26 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, m *wasm.ModuleInstance ce.drop(op.Us[v+1]) frame.pc = op.Us[v] case operationKindCall: - func() { - if ctx.Value(expctxkeys.EnableSnapshotterKey{}) != nil { - defer func() { - if r := recover(); r != nil { - if s, ok := r.(*snapshot); ok && s.ce == ce { - s.doRestore() - frame = ce.frames[len(ce.frames)-1] - body = frame.f.parent.body - bodyLen = uint64(len(body)) - } else { - panic(r) - } - } - }() - } - ce.callFunction(ctx, f.moduleInstance, &functions[op.U1]) - }() + frameUnwound := ce.callWithUnwind(ctx, f.moduleInstance, &functions[op.U1]) + if frameUnwound { + frame = ce.frames[len(ce.frames)-1] + body = frame.f.parent.body + bodyLen = uint64(len(body)) + continue + } frame.pc++ case operationKindCallIndirect: offset := ce.popValue() table := tables[op.U2] tf := ce.functionForOffset(table, offset, typeIDs[op.U1]) - ce.callFunction(ctx, f.moduleInstance, tf) + frameUnwound := ce.callWithUnwind(ctx, f.moduleInstance, tf) + if frameUnwound { + frame = ce.frames[len(ce.frames)-1] + body = frame.f.parent.body + bodyLen = uint64(len(body)) + continue + } frame.pc++ case operationKindDrop: ce.drop(op.U1) @@ -4372,6 +4528,35 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, m *wasm.ModuleInstance memoryInst.Mux.Unlock() ce.pushValue(uint64(old)) frame.pc++ + case operationKindThrow: + tagIndex := uint32(op.U1) + tag := moduleInst.Tags[tagIndex] + paramCount := len(tag.Type.Params) + params := make([]uint64, paramCount) + for i := paramCount - 1; i >= 0; i-- { + params[i] = ce.popValue() + } + exn := &wasm.Exception{Tag: tag, Params: params} + if clause, values := searchExceptionTable(exn, frame); clause != nil { + ce.applyExceptionHandler(frame, clause, values) + continue + } + panic(&thrownException{exception: exn}) + + case operationKindThrowRef: + v := ce.popValue() + if v == 0 { + panic(wasmruntime.ErrRuntimeNullReference) // throw_ref on null exnref traps + } + // Read the Exception pointer directly from the uint64 value to avoid + // conversion from uintptr into unsafe.Pointer, which triggers checkptr. + exn := *(**wasm.Exception)(unsafe.Pointer(&v)) + if clause, values := searchExceptionTable(exn, frame); clause != nil { + ce.applyExceptionHandler(frame, clause, values) + continue + } + panic(&thrownException{exception: exn}) + case operationKindTailCallReturnCall: f := &functions[op.U1] ce.dropForTailCall(frame, f) @@ -4387,7 +4572,13 @@ func (ce *callEngine) callNativeFunc(ctx context.Context, m *wasm.ModuleInstance // For details, see internal/engine/RATIONALE.md if tf.moduleInstance != f.moduleInstance { // Revert to a normal call. - ce.callFunction(ctx, f.moduleInstance, tf) + frameUnwound := ce.callWithUnwind(ctx, f.moduleInstance, tf) + if frameUnwound { + frame = ce.frames[len(ce.frames)-1] + body = frame.f.parent.body + bodyLen = uint64(len(body)) + continue + } // Return ce.drop(op.Us[0]) // Jump to the function frame (return) diff --git a/internal/engine/interpreter/operations.go b/internal/engine/interpreter/operations.go index db3cfa250..2fc115462 100644 --- a/internal/engine/interpreter/operations.go +++ b/internal/engine/interpreter/operations.go @@ -449,6 +449,10 @@ func (o operationKind) String() (ret string) { ret = "operationKindTailCallReturnCall" case operationKindTailCallReturnCallIndirect: ret = "operationKindTailCallReturnCallIndirect" + case operationKindThrow: + ret = "operationKindThrow" + case operationKindThrowRef: + ret = "operationKindThrowRef" default: panic(fmt.Errorf("unknown operation %d", o)) } @@ -777,6 +781,11 @@ const ( // operationKindTailCallReturnCallIndirect is the Kind for newOperationKindTailCallReturnCallIndirect. operationKindTailCallReturnCallIndirect + // operationKindThrow is the Kind for throw instruction. + operationKindThrow + // operationKindThrowRef is the Kind for throw_ref instruction. + operationKindThrowRef + // operationKindEnd is always placed at the bottom of this iota definition to be used in the test. operationKindEnd ) @@ -1112,6 +1121,12 @@ func (o unionOperation) String() string { case operationKindTailCallReturnCallIndirect: return fmt.Sprintf("%s %d %d", o.Kind, o.U1, o.U2) + case operationKindThrow: + return fmt.Sprintf("%s %d", o.Kind, o.U1) + + case operationKindThrowRef: + return o.Kind.String() + default: panic(fmt.Sprintf("TODO: %v", o.Kind)) } @@ -2843,3 +2858,46 @@ func newOperationTailCallReturnCall(functionIndex uint32) unionOperation { func newOperationTailCallReturnCallIndirect(typeIndex, tableIndex uint32, dropDepth inclusiveRange, l label) unionOperation { return unionOperation{Kind: operationKindTailCallReturnCallIndirect, U1: uint64(typeIndex), U2: uint64(tableIndex), Us: []uint64{dropDepth.AsU64(), uint64(l)}} } + +// newOperationThrow is a constructor for unionOperation with operationKindThrow. +// U1 stores the tag index. +func newOperationThrow(tagIndex uint32) unionOperation { + return unionOperation{Kind: operationKindThrow, U1: uint64(tagIndex)} +} + +// newOperationThrowRef is a constructor for unionOperation with operationKindThrowRef. +func newOperationThrowRef() unionOperation { + return unionOperation{Kind: operationKindThrowRef} +} + +// exceptionTableEntry represents one try_table's exception handling scope. +// Built at compile time and stored per compiledFunction. +type exceptionTableEntry struct { + startPC uint64 // first PC inside the try_table body + endPC uint64 // PC of continuation label (exclusive) + clauses []exceptionTableCatchClause +} + +// exceptionTableCatchClause is a single catch clause within an exception table entry. +type exceptionTableCatchClause struct { + kind byte // CatchKindCatch, CatchKindCatchRef, CatchKindCatchAll, CatchKindCatchAllRef + tagIndex uint32 // tag index for catch/catch_ref + targetPC uint64 // resolved PC to jump to on match + targetStackDepth int // = targetFrame.originalStackLenWithoutParamUint64 +} + +// pendingExceptionTableEntry is an unresolved exception table entry built during compilation. +// Labels are resolved to final PCs in lowerIR. +type pendingExceptionTableEntry struct { + startOpIndex int // index in Operations[] of the first instruction inside the try_table body + continuationFrameID uint32 + clauses []pendingCatchClause +} + +// pendingCatchClause is an unresolved catch clause within a pending exception table entry. +type pendingCatchClause struct { + kind byte + tagIndex uint32 + targetLabel label // unresolved label, resolved in lowerIR + targetStackDepth int // = targetFrame.originalStackLenWithoutParamUint64 +} diff --git a/internal/engine/interpreter/signature.go b/internal/engine/interpreter/signature.go index da5ca3c15..a579b1069 100644 --- a/internal/engine/interpreter/signature.go +++ b/internal/engine/interpreter/signature.go @@ -268,6 +268,9 @@ func (c *compiler) wasmOpcodeSignature(op wasm.Opcode, index uint32) (*signature return signature_I32_None, nil case wasm.OpcodeElse, wasm.OpcodeEnd, wasm.OpcodeBr: return signature_None_None, nil + case wasm.OpcodeThrow, wasm.OpcodeThrowRef, wasm.OpcodeTryTable: + // Stack manipulation handled dynamically by the compiler. + return signature_None_None, nil case wasm.OpcodeBrIf, wasm.OpcodeBrTable: return signature_I32_None, nil case wasm.OpcodeReturn: @@ -700,7 +703,8 @@ func wasmValueTypeTounsignedType(vt wasm.ValueType) unsignedType { return unsignedTypeI32 case wasm.ValueTypeI64, // From interpreterir layer, ref type values are opaque 64-bit pointers. - wasm.ValueTypeExternref, wasm.ValueTypeFuncref: + wasm.ValueTypeExternref, wasm.ValueTypeFuncref, + wasm.ValueTypeExnref: return unsignedTypeI64 case wasm.ValueTypeF32: return unsignedTypeF32 @@ -718,7 +722,8 @@ func wasmValueTypeToUnsignedOutSignature(vt wasm.ValueType) *signature { return signature_None_I32 case wasm.ValueTypeI64, // From interpreterir layer, ref type values are opaque 64-bit pointers. - wasm.ValueTypeExternref, wasm.ValueTypeFuncref: + wasm.ValueTypeExternref, wasm.ValueTypeFuncref, + wasm.ValueTypeExnref: return signature_None_I64 case wasm.ValueTypeF32: return signature_None_F32 @@ -736,7 +741,8 @@ func wasmValueTypeToUnsignedInSignature(vt wasm.ValueType) *signature { return signature_I32_None case wasm.ValueTypeI64, // From interpreterir layer, ref type values are opaque 64-bit pointers. - wasm.ValueTypeExternref, wasm.ValueTypeFuncref: + wasm.ValueTypeExternref, wasm.ValueTypeFuncref, + wasm.ValueTypeExnref: return signature_I64_None case wasm.ValueTypeF32: return signature_F32_None @@ -754,7 +760,8 @@ func wasmValueTypeToUnsignedInOutSignature(vt wasm.ValueType) *signature { return signature_I32_I32 case wasm.ValueTypeI64, // At interpreterir layer, ref type values are opaque 64-bit pointers. - wasm.ValueTypeExternref, wasm.ValueTypeFuncref: + wasm.ValueTypeExternref, wasm.ValueTypeFuncref, + wasm.ValueTypeExnref: return signature_I64_I64 case wasm.ValueTypeF32: return signature_F32_F32 diff --git a/internal/engine/wazevo/backend/backend_test.go b/internal/engine/wazevo/backend/backend_test.go index 561aeee43..58670c5d6 100644 --- a/internal/engine/wazevo/backend/backend_test.go +++ b/internal/engine/wazevo/backend/backend_test.go @@ -2368,6 +2368,256 @@ L5 (SSA Block: blk5): add sp, sp, #0x10 ldr x30, [sp], #0x10 b f0 +`, + }, + { + name: "try_table_catch_all_empty", m: testcases.TryTableCatchAllEmpty.Module, + afterFinalizeARM64: ` +L0 (SSA Block: blk0): + stp x30, xzr, [sp, #-0x10]! + sub sp, sp, #0x10 + orr x27, xzr, #0x10 + str x27, [sp, #-0x10]! + str x0, [sp, #0x10] + str x1, [sp, #0x18] + str x1, [x0, #0x8] + ldr x8, [x0, #0x4b0] + movz x9, #0x1b, lsl 0 + mov x1, x9 + bl x8 + ldr x8, [sp, #0x10] + ldr x9, [x8, #0x4d0] + orr w10, wzr, #0x1 + subs wzr, w9, w10 + csel w9, w10, w9, hs + br_table_sequence x9, table_index=0 +L4 (SSA Block: blk4): +L1 (SSA Block: blk1): + mov x0, xzr + add sp, sp, #0x10 + add sp, sp, #0x10 + ldr x30, [sp], #0x10 + ret +L3 (SSA Block: blk3): + ldr x9, [sp, #0x18] + str x9, [x8, #0x8] + ldr x9, [x8, #0x4b8] + mov x0, x8 + bl x9 + movz w0, #0x2a, lsl 0 + add sp, sp, #0x10 + add sp, sp, #0x10 + ldr x30, [sp], #0x10 + ret +`, + afterLoweringARM64: ` +L0 (SSA Block: blk0): + mov x128?, x0 + mov x129?, x1 + str x129?, [x128?, #0x8] + ldr x130?, [x128?, #0x4b0] + mov x0, x128? + movz x131?, #0x1b, lsl 0 + mov x1, x131? + bl x130? + ldr x132?, [x128?, #0x4d0] + orr w136?, wzr, #0x1 + subs wzr, w132?, w136? + csel w137?, w136?, w132?, hs + br_table_sequence x137?, table_index=0 +L4 (SSA Block: blk4): +L1 (SSA Block: blk1): + mov x133?, xzr + mov x0, x133? + ret +L3 (SSA Block: blk3): + str x129?, [x128?, #0x8] + ldr x135?, [x128?, #0x4b8] + mov x0, x128? + bl x135? + movz w134?, #0x2a, lsl 0 + mov x0, x134? + ret +`, + }, + { + name: "try_table_catch_all_throw", m: testcases.TryTableCatchAllThrow.Module, + afterFinalizeARM64: ` +L0 (SSA Block: blk0): + stp x30, xzr, [sp, #-0x10]! + sub sp, sp, #0x10 + orr x27, xzr, #0x10 + str x27, [sp, #-0x10]! + str x0, [sp, #0x10] + str x1, [sp, #0x18] + str x1, [x0, #0x8] + ldr x8, [x0, #0x4b0] + movz x9, #0x1b, lsl 0 + mov x1, x9 + bl x8 + ldr x8, [sp, #0x10] + ldr x9, [x8, #0x4d0] + orr w10, wzr, #0x1 + subs wzr, w9, w10 + csel w9, w10, w9, hs + br_table_sequence x9, table_index=0 +L4 (SSA Block: blk4): +L1 (SSA Block: blk1): + movz w0, #0x2a, lsl 0 + add sp, sp, #0x10 + add sp, sp, #0x10 + ldr x30, [sp], #0x10 + ret +L3 (SSA Block: blk3): + ldr x9, [sp, #0x18] + str x9, [x8, #0x8] + ldr x9, [x8, #0x4a0] + mov x0, x8 + mov x1, xzr + bl x9 + mov x1, x0 + ldr x8, [sp, #0x10] + ldr x9, [x8, #0x4a8] + mov x0, x8 + bl x9 + movz x8, #0x3, lsl 0 + ldr x9, [sp, #0x10] + str w8, [x9] + mov x8, sp + str x8, [x9, #0x38] + adr x8, #0x0 + str x8, [x9, #0x30] + exit_sequence x9 +`, + }, + { + // Exercises tags with 5 i32 parameters: verifies that the two-phase + // throw (throwAlloc + throw) correctly passes all 5 params through the + // Exception heap object, and the catch handler reads all 5 via the + // exceptionParamsPtr pointer. + name: "try_table_catch_many_param_throw", m: testcases.TryTableCatchManyParamThrow.Module, + afterFinalizeARM64: ` +L0 (SSA Block: blk0): + stp x30, xzr, [sp, #-0x10]! + sub sp, sp, #0x30 + orr x27, xzr, #0x30 + str x27, [sp, #-0x10]! + str x0, [sp, #0x10] + str x1, [sp, #0x18] + str w2, [sp, #0x20] + str w3, [sp, #0x24] + str w4, [sp, #0x28] + str w5, [sp, #0x2c] + str w6, [sp, #0x30] + str x1, [x0, #0x8] + ldr x8, [x0, #0x4b0] + movz x9, #0x1b, lsl 0 + mov x1, x9 + bl x8 + ldr x8, [sp, #0x10] + ldr x9, [x8, #0x4d0] + orr w10, wzr, #0x1 + subs wzr, w9, w10 + csel w9, w10, w9, hs + br_table_sequence x9, table_index=0 +L4 (SSA Block: blk4): + ldr x8, [x8, #0x4c8] + ldr w9, [x8] + ldr w10, [x8, #0x8] + ldr w11, [x8, #0x10] + ldr w12, [x8, #0x18] + ldr w8, [x8, #0x20] +L1 (SSA Block: blk1): + mov x4, x8 + mov x3, x12 + mov x2, x11 + mov x1, x10 + mov x0, x9 + add sp, sp, #0x10 + add sp, sp, #0x30 + ldr x30, [sp], #0x10 + ret +L3 (SSA Block: blk3): + ldr x9, [sp, #0x18] + str x9, [x8, #0x8] + ldr x9, [x8, #0x4a0] + mov x0, x8 + mov x1, xzr + bl x9 + mov x1, x0 + ldr x8, [sp, #0x10] + ldr x9, [x8, #0x4c8] + ldr w10, [sp, #0x20] + str w10, [x9] + ldr w10, [sp, #0x24] + str w10, [x9, #0x8] + ldr w10, [sp, #0x28] + str w10, [x9, #0x10] + ldr w10, [sp, #0x2c] + str w10, [x9, #0x18] + ldr w10, [sp, #0x30] + str w10, [x9, #0x20] + ldr x9, [x8, #0x4a8] + mov x0, x8 + bl x9 + movz x8, #0x3, lsl 0 + ldr x9, [sp, #0x10] + str w8, [x9] + mov x8, sp + str x8, [x9, #0x38] + adr x8, #0x0 + str x8, [x9, #0x30] + exit_sequence x9 +`, + }, + { + name: "try_table_catch_throw", m: testcases.TryTableCatchThrow.Module, + afterFinalizeARM64: ` +L0 (SSA Block: blk0): + stp x30, xzr, [sp, #-0x10]! + sub sp, sp, #0x10 + orr x27, xzr, #0x10 + str x27, [sp, #-0x10]! + str x0, [sp, #0x10] + str x1, [sp, #0x18] + str x1, [x0, #0x8] + ldr x8, [x0, #0x4b0] + movz x9, #0x1b, lsl 0 + mov x1, x9 + bl x8 + ldr x8, [sp, #0x10] + ldr x9, [x8, #0x4d0] + orr w10, wzr, #0x1 + subs wzr, w9, w10 + csel w9, w10, w9, hs + br_table_sequence x9, table_index=0 +L4 (SSA Block: blk4): +L1 (SSA Block: blk1): + movz w0, #0x17, lsl 0 + add sp, sp, #0x10 + add sp, sp, #0x10 + ldr x30, [sp], #0x10 + ret +L3 (SSA Block: blk3): + ldr x9, [sp, #0x18] + str x9, [x8, #0x8] + ldr x9, [x8, #0x4a0] + mov x0, x8 + mov x1, xzr + bl x9 + mov x1, x0 + ldr x8, [sp, #0x10] + ldr x9, [x8, #0x4a8] + mov x0, x8 + bl x9 + movz x8, #0x3, lsl 0 + ldr x9, [sp, #0x10] + str w8, [x9] + mov x8, sp + str x8, [x9, #0x38] + adr x8, #0x0 + str x8, [x9, #0x30] + exit_sequence x9 `, }, } { @@ -2386,7 +2636,7 @@ L5 (SSA Block: blk5): t.Fail() } - err := tc.m.Validate(api.CoreFeaturesV2 | experimental.CoreFeaturesThreads | experimental.CoreFeaturesTailCall) + err := tc.m.Validate(api.CoreFeaturesV2 | experimental.CoreFeaturesThreads | experimental.CoreFeaturesTailCall | experimental.CoreFeaturesExceptionHandling) require.NoError(t, err) ssab := ssa.NewBuilder() diff --git a/internal/engine/wazevo/backend/isa/arm64/abi_go_call_test.go b/internal/engine/wazevo/backend/isa/arm64/abi_go_call_test.go index ad428d70a..0a1bf6371 100644 --- a/internal/engine/wazevo/backend/isa/arm64/abi_go_call_test.go +++ b/internal/engine/wazevo/backend/isa/arm64/abi_go_call_test.go @@ -442,6 +442,87 @@ func TestMachine_CompileGoFunctionTrampoline(t *testing.T) { ldr x30, [sp], #0x10 ldr w0, [x15], #0x8 ret +`, + }, + { + name: "try_table_enter", + exitCode: wazevoapi.ExitCodeTryTableEnter, + sig: &ssa.Signature{ + Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64}, + Results: []ssa.Type{}, + }, + exp: ` + stp x30, xzr, [sp, #-0x10]! + sub x27, sp, #0x20 + ldr x11, [x0, #0x28] + subs xzr, x27, x11 + b.ge #0x14 + orr x27, xzr, #0x20 + str x27, [x0, #0x40] + ldr x27, [x0, #0x50] + bl x27 + str x19, [x0, #0x60] + str x20, [x0, #0x70] + str x21, [x0, #0x80] + str x22, [x0, #0x90] + str x23, [x0, #0xa0] + str x24, [x0, #0xb0] + str x25, [x0, #0xc0] + str x26, [x0, #0xd0] + str x28, [x0, #0xe0] + str q18, [x0, #0xf0] + str q19, [x0, #0x100] + str q20, [x0, #0x110] + str q21, [x0, #0x120] + str q22, [x0, #0x130] + str q23, [x0, #0x140] + str q24, [x0, #0x150] + str q25, [x0, #0x160] + str q26, [x0, #0x170] + str q27, [x0, #0x180] + str q28, [x0, #0x190] + str q29, [x0, #0x1a0] + str q30, [x0, #0x1b0] + str q31, [x0, #0x1c0] + sub sp, sp, #0x10 + mov x15, sp + str x1, [x15], #0x8 + orr x27, xzr, #0x10 + orr x16, xzr, #0x1 + stp x27, x16, [sp, #-0x10]! + movz w17, #0x1b, lsl 0 + str w17, [x0] + mov x27, sp + str x27, [x0, #0x38] + adr x27, #0x20 + str x27, [x0, #0x30] + exit_sequence x0 + ldr x19, [x0, #0x60] + ldr x20, [x0, #0x70] + ldr x21, [x0, #0x80] + ldr x22, [x0, #0x90] + ldr x23, [x0, #0xa0] + ldr x24, [x0, #0xb0] + ldr x25, [x0, #0xc0] + ldr x26, [x0, #0xd0] + ldr x28, [x0, #0xe0] + ldr q18, [x0, #0xf0] + ldr q19, [x0, #0x100] + ldr q20, [x0, #0x110] + ldr q21, [x0, #0x120] + ldr q22, [x0, #0x130] + ldr q23, [x0, #0x140] + ldr q24, [x0, #0x150] + ldr q25, [x0, #0x160] + ldr q26, [x0, #0x170] + ldr q27, [x0, #0x180] + ldr q28, [x0, #0x190] + ldr q29, [x0, #0x1a0] + ldr q30, [x0, #0x1b0] + ldr q31, [x0, #0x1c0] + add sp, sp, #0x20 + ldr x30, [sp], #0x10 + ret `, }, } { diff --git a/internal/engine/wazevo/call_engine.go b/internal/engine/wazevo/call_engine.go index 639429a63..83140bb42 100644 --- a/internal/engine/wazevo/call_engine.go +++ b/internal/engine/wazevo/call_engine.go @@ -43,6 +43,29 @@ type ( execCtxPtr uintptr numberOfResults int stackIteratorImpl stackIterator + // tryHandlers is the stack of active try_table exception handlers, + // used to match catch clauses when a throw exits to the dispatch loop. + tryHandlers []tryHandler + // pendingException holds the most recently caught exception, so handler + // code can read its params after re-entry. + pendingException *wasm.Exception + } + + // tryHandler records the state at a try_table entry for exception handling. + // On match, we restore the stack to the checkpoint state and re-enter at returnAddress. + tryHandler struct { + // Cloned stack and state from the try_table entry checkpoint, + // using the same approach as experimental.Snapshot. + sp, fp, top uintptr + returnAddress *byte + savedRegisters [64][2]uint64 + stack []byte // cloned stack + // catchClauses describes what exceptions this handler catches. + catchClauses []wazevoapi.CatchClauseInstance + // moduleInstance is the module that set up this try handler. + // Used for tag matching in doHandleException (the tag index in + // catch clauses is relative to this module's tag index space). + moduleInstance *wasm.ModuleInstance } // executionContext is the struct to be read/written by assembly functions. @@ -90,6 +113,29 @@ type ( memoryWait64TrampolineAddress *byte // memoryNotifyTrampolineAddress holds the address of the memory_notify trampoline function. memoryNotifyTrampolineAddress *byte + // throwAllocTrampolineAddress holds the address of the throw-alloc trampoline: + // phase 1 of throw, which allocates the Exception heap object. + throwAllocTrampolineAddress *byte + // throwTrampolineAddress holds the address of the throw/throw_ref trampoline function. + throwTrampolineAddress *byte + // tryTableEnterTrampolineAddress holds the address of the try_table enter trampoline function. + tryTableEnterTrampolineAddress *byte + // tryTableLeaveTrampolineAddress holds the address of the try_table leave trampoline function. + tryTableLeaveTrampolineAddress *byte + // exceptionPtr holds the pointer to the Exception struct, + // used on the throw side (throwAlloc stores the new Exception) + // and on the catch side (catch_ref/catch_all_ref retrieve the exnref). + exceptionPtr uintptr + // exceptionParamsPtr points into exceptionPtr's Params slice + // backing array. On the throw side, throwAlloc sets it so compiled + // code can store params at [ptr + i*8]. On the catch side, compiled + // handler blocks load params from the same pointer. + exceptionParamsPtr uintptr + // caughtExceptionClauseIdx is set by the dispatch loop to -1 on + // TryTableEnter (normal path) or to the matched catch clause index + // when an exception is caught. Compiled code loads this from execCtx + // after the trampoline call to decide which handler to dispatch to. + caughtExceptionClauseIdx int64 } ) @@ -217,6 +263,9 @@ func (c *callEngine) callWithStack(ctx context.Context, paramResultStack []uint6 } } + // Clear any stale try_table handlers from a previous call. + c.tryHandlers = c.tryHandlers[:0] + var paramResultPtr *uint64 if len(paramResultStack) > 0 { paramResultPtr = ¶mResultStack[0] @@ -236,21 +285,25 @@ func (c *callEngine) callWithStack(ctx context.Context, paramResultStack []uint6 var listeners []listenerForAbort builder := wasmdebug.NewErrorBuilder() - def, lsn := c.addFrame(builder, uintptr(unsafe.Pointer(c.execCtx.goCallReturnAddress))) - if lsn != nil { - listeners = append(listeners, listenerForAbort{def, lsn}) - } - returnAddrs := unwindStack( - uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)), - c.execCtx.framePointerBeforeGoCall, - c.stackTop, - nil, - ) - for _, retAddr := range returnAddrs[:len(returnAddrs)-1] { // the last return addr is the trampoline, so we skip it. - def, lsn = c.addFrame(builder, retAddr) + if c.execCtx.stackPointerBeforeGoCall != nil { + def, lsn := c.addFrame(builder, uintptr(unsafe.Pointer(c.execCtx.goCallReturnAddress))) if lsn != nil { listeners = append(listeners, listenerForAbort{def, lsn}) } + returnAddrs := unwindStack( + uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)), + c.execCtx.framePointerBeforeGoCall, + c.stackTop, + nil, + ) + if len(returnAddrs) > 1 { + for _, retAddr := range returnAddrs[:len(returnAddrs)-1] { // the last return addr is the trampoline, so we skip it. + def, lsn = c.addFrame(builder, retAddr) + if lsn != nil { + listeners = append(listeners, listenerForAbort{def, lsn}) + } + } + } } err = builder.FromRecovered(r) @@ -266,6 +319,7 @@ func (c *callEngine) callWithStack(ctx context.Context, paramResultStack []uint6 if err != nil { // Ensures that we can reuse this callEngine even after an error. c.execCtx.exitCode = wazevoapi.ExitCodeOK + c.tryHandlers = c.tryHandlers[:0] } }() @@ -505,12 +559,134 @@ func (c *callEngine) callWithStack(ctx context.Context, paramResultStack []uint6 panic(wasmruntime.ErrRuntimeInvalidConversionToInteger) case wazevoapi.ExitCodeUnalignedAtomic: panic(wasmruntime.ErrRuntimeUnalignedAtomic) + case wazevoapi.ExitCodeThrowAlloc: + // Allocate the Exception heap object sized exactly to the tag's + // param count. Sets exceptionParamsPtr so compiled code can + // store params, and returns the exnref via the stack slot. + s := goCallStackView(c.execCtx.stackPointerBeforeGoCall) + tagIndex := int(s[0]) + mod := c.callerModuleInstance() + tag := mod.Tags[tagIndex] + nParams := len(tag.Type.Params) + exn := &wasm.Exception{Tag: tag, Params: make([]uint64, nParams)} + c.pendingException = exn // GC root: keeps exn alive while compiled code writes params + if nParams > 0 { + c.execCtx.exceptionParamsPtr = uintptr(unsafe.Pointer(&exn.Params[0])) + } + // Return the exnref to compiled code via the stack slot. + s[0] = uint64(uintptr(unsafe.Pointer(exn))) + c.execCtx.exitCode = wazevoapi.ExitCodeOK + afterGoFunctionCallEntrypoint(c.execCtx.goCallReturnAddress, c.execCtxPtr, + uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)), c.execCtx.framePointerBeforeGoCall) + case wazevoapi.ExitCodeThrow: + // Throw trampoline: (execCtx, exnref) → (). + // Reads the exnref from the stack, searches for a matching handler. + s := goCallStackView(c.execCtx.stackPointerBeforeGoCall) + // Read the Exception pointer directly from the uint64 value to avoid + // conversion from uintptr into unsafe.Pointer, which triggers checkptr. + exn := *(**wasm.Exception)(unsafe.Pointer(&s[0])) + if !c.doHandleException(exn) { + panic(wasmruntime.ErrRuntimeUncaughtException) + } + if len(exn.Params) > 0 { + c.execCtx.exceptionParamsPtr = uintptr(unsafe.Pointer(&exn.Params[0])) + } + c.execCtx.exceptionPtr = uintptr(unsafe.Pointer(exn)) + c.execCtx.exitCode = wazevoapi.ExitCodeOK + afterGoFunctionCallEntrypoint(c.execCtx.goCallReturnAddress, c.execCtxPtr, + uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)), c.execCtx.framePointerBeforeGoCall) + case wazevoapi.ExitCodeNullReference: + panic(wasmruntime.ErrRuntimeNullReference) + case wazevoapi.ExitCodeTryTableEnter: + // Save current state as a try handler checkpoint using stack cloning + // (same approach as experimental.Snapshot). + // The encoded exit code (with tryTableID in upper bits) is on the + // Go call stack as the second trampoline argument, not in execCtx.exitCode. + tryTableEnterStack := goCallStackView(c.execCtx.stackPointerBeforeGoCall) + catchClauseTableIdx := wazevoapi.TryTableIDFromExitCode(wazevoapi.ExitCode(tryTableEnterStack[0])) + mod := c.callerModuleInstance() + me := mod.Engine.(*moduleEngine) + clauses := me.parent.catchClauseTable[catchClauseTableIdx] + returnAddress := c.execCtx.goCallReturnAddress + oldTop, oldSp := c.stackTop, uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)) + newSP, newFP, newTop, newStack := c.cloneStack(uintptr(len(c.stack)) + 16) + adjustClonedStack(oldSp, oldTop, newSP, newFP, newTop) + c.tryHandlers = append(c.tryHandlers, tryHandler{ + sp: newSP, + fp: newFP, + top: newTop, + returnAddress: returnAddress, + savedRegisters: c.execCtx.savedRegisters, + stack: newStack, + catchClauses: clauses, + moduleInstance: mod, + }) + // Set clauseIdx = -1 (no exception) in execCtx for the compiled code + // to read after the trampoline returns. + c.execCtx.caughtExceptionClauseIdx = -1 + c.execCtx.exitCode = wazevoapi.ExitCodeOK + afterGoFunctionCallEntrypoint(c.execCtx.goCallReturnAddress, c.execCtxPtr, + uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)), c.execCtx.framePointerBeforeGoCall) + case wazevoapi.ExitCodeTryTableLeave: + // Pop the most recent try handler. + if len(c.tryHandlers) > 0 { + c.tryHandlers = c.tryHandlers[:len(c.tryHandlers)-1] + } + c.execCtx.exitCode = wazevoapi.ExitCodeOK + afterGoFunctionCallEntrypoint(c.execCtx.goCallReturnAddress, c.execCtxPtr, + uintptr(unsafe.Pointer(c.execCtx.stackPointerBeforeGoCall)), c.execCtx.framePointerBeforeGoCall) default: panic("BUG") } } } +// doHandleException tries to match the given exception against active try handlers. +// If a match is found, it restores the execution state to the handler's checkpoint +// (like snapshot.doRestore) and writes the matched clause index as the trampoline +// return value. Returns true if handled. +func (c *callEngine) doHandleException(exn *wasm.Exception) bool { + // Search try handlers from innermost (last) to outermost (first). + for i := len(c.tryHandlers) - 1; i >= 0; i-- { + h := &c.tryHandlers[i] + for clauseIdx, clause := range h.catchClauses { + // Use the module that set up the handler (not the one that threw) + // because clause.TagIndex is relative to that module's tag space. + mod := h.moduleInstance + matched := false + switch clause.Kind { + case wasm.CatchKindCatch, wasm.CatchKindCatchRef: + matched = mod.Tags[clause.TagIndex] == exn.Tag + case wasm.CatchKindCatchAll, wasm.CatchKindCatchAllRef: + matched = true + } + if matched { + // Pop all handlers at and above this one. + c.tryHandlers = c.tryHandlers[:i] + + // Store the caught exception so handler code can read params. + c.pendingException = exn + + // Restore the cloned stack (like snapshot.doRestore). + spp := *(**uint64)(unsafe.Pointer(&h.sp)) + c.stack = h.stack + c.stackTop = h.top + ec := &c.execCtx + ec.stackBottomPtr = &c.stack[0] + ec.stackPointerBeforeGoCall = spp + ec.framePointerBeforeGoCall = h.fp + ec.goCallReturnAddress = h.returnAddress + ec.savedRegisters = h.savedRegisters + + // Set the matched clause index in execCtx for compiled code to read. + ec.caughtExceptionClauseIdx = int64(clauseIdx) + return true + } + } + } + return false +} + func (c *callEngine) callerModuleInstance() *wasm.ModuleInstance { return moduleInstanceFromOpaquePtr(c.execCtx.callerModuleContextPtr) } diff --git a/internal/engine/wazevo/e2e_test.go b/internal/engine/wazevo/e2e_test.go index 215ebe8c8..0dd9b03a1 100644 --- a/internal/engine/wazevo/e2e_test.go +++ b/internal/engine/wazevo/e2e_test.go @@ -896,6 +896,62 @@ func TestE2E(t *testing.T) { {params: []uint64{1, 2, 3, 4, 5, 6, 7}, expResults: []uint64{156}}, }, }, + // ---- Exception Handling ---- + { + name: "throw_only", + m: testcases.ThrowOnly.Module, + features: api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling, + calls: []callCase{{expErr: "uncaught exception"}}, + }, + { + name: "throw_with_param", + m: testcases.ThrowWithParam.Module, + features: api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling, + calls: []callCase{{params: []uint64{42}, expErr: "uncaught exception"}}, + }, + { + name: "try_table_catch_all_empty", + m: testcases.TryTableCatchAllEmpty.Module, + features: api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling, + calls: []callCase{{expResults: []uint64{42}}}, + }, + { + name: "try_table_catch_all_throw", + m: testcases.TryTableCatchAllThrow.Module, + features: api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling, + calls: []callCase{{expResults: []uint64{42}}}, + }, + { + name: "try_table_catch_throw", + m: testcases.TryTableCatchThrow.Module, + features: api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling, + calls: []callCase{{expResults: []uint64{23}}}, + }, + { + name: "try_table_catch_param_throw", + m: testcases.TryTableCatchParamThrow.Module, + features: api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling, + calls: []callCase{{params: []uint64{42}, expResults: []uint64{42}}}, + }, + { + name: "try_table_catch_many_param_throw", + m: testcases.TryTableCatchManyParamThrow.Module, + features: api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling, + calls: []callCase{{params: []uint64{1, 2, 3, 4, 5}, expResults: []uint64{1, 2, 3, 4, 5}}}, + }, + { + // Verifies that removeUntilRet does not accidentally remove the catch handler + // block code that follows the tail-call path in the instruction stream. + // param=0: throw $e is caught → returns 42 (catch handler intact). + // param=1: return_call $target (tail call) → returns 99. + name: "try_table_catch_with_return_call", + m: testcases.TryTableCatchWithReturnCall.Module, + features: api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling | experimental.CoreFeaturesTailCall, + calls: []callCase{ + {params: []uint64{0}, expResults: []uint64{42}}, // throw path: catch handler intact → 42 + {params: []uint64{1}, expResults: []uint64{99}}, // tail-call path: return_call $target → 99 + }, + }, } { tc := tc t.Run(tc.name, func(t *testing.T) { diff --git a/internal/engine/wazevo/engine.go b/internal/engine/wazevo/engine.go index f09dd1791..bee9d2084 100644 --- a/internal/engine/wazevo/engine.go +++ b/internal/engine/wazevo/engine.go @@ -67,7 +67,16 @@ type ( memoryWait64Address *byte // memoryNotifyAddress is the address of memory.notify builtin function memoryNotifyAddress *byte - listenerTrampolines listenerTrampolines + // throwAllocTrampolineAddress is the address of the throw-alloc trampoline: + // phase 1 of throw, which allocates the Exception heap object. + throwAllocTrampolineAddress *byte + // throwTrampolineAddress is the address of the throw/throw_ref trampoline function. + throwTrampolineAddress *byte + // tryTableEnterAddress is the address of try_table enter trampoline. + tryTableEnterAddress *byte + // tryTableLeaveAddress is the address of try_table leave trampoline. + tryTableLeaveAddress *byte + listenerTrampolines listenerTrampolines } listenerTrampolines = map[*wasm.FunctionType]struct { @@ -93,6 +102,9 @@ type ( offsets wazevoapi.ModuleContextOffsetData sharedFunctions *sharedFunctions sourceMap sourceMap + // catchClauseTable stores catch clause info for each try_table, + // indexed by a try_table ID assigned during compilation. + catchClauseTable [][]wazevoapi.CatchClauseInstance } executables struct { @@ -269,6 +281,7 @@ func (e *engine) compileModule(ctx context.Context, module *wasm.Module, listene relocator.appendFunction(fctx, module, cm, i, fidx, body, relsPerFunc, be.SourceOffsetInfo()) } + cm.catchClauseTable = fe.CatchClauseTable() } else { // Compile with N worker goroutines. // Collect compiled functions across workers in a slice, @@ -287,6 +300,10 @@ func (e *engine) compileModule(ctx context.Context, module *wasm.Module, listene ctx, cancel := context.WithCancelCause(ctx) defer cancel(nil) + // Catch clause table IDs are baked into compiled machine code, so all + // workers must share a single table to ensure globally unique IDs. + sharedCCT := frontend.NewSharedCatchClauseTable() + var count atomic.Uint32 var wg sync.WaitGroup wg.Add(workers) @@ -299,7 +316,9 @@ func (e *engine) compileModule(ctx context.Context, module *wasm.Module, listene machine := newMachine() ssaBuilder := ssa.NewBuilder() be := backend.NewCompiler(ctx, machine, ssaBuilder) - fe := frontend.NewFrontendCompiler(module, ssaBuilder, &cm.offsets, ensureTermination, withListener, needSourceInfo) + fe := frontend.NewFrontendCompiler( + module, ssaBuilder, &cm.offsets, ensureTermination, withListener, needSourceInfo). + WithCatchClauseTable(sharedCCT) for { if err := ctx.Err(); err != nil { @@ -346,6 +365,7 @@ func (e *engine) compileModule(ctx context.Context, module *wasm.Module, listene fn := &compiledFuncs[i] relocator.appendFunction(fn.fctx, module, cm, fn.fnum, fn.fidx, fn.body, fn.relsPerFunc, fn.offsPerFunc) } + cm.catchClauseTable = sharedCCT.Table() } // Allocate executable memory and then copy the generated machine code. @@ -733,7 +753,7 @@ func (e *engine) NewModuleEngine(m *wasm.Module, mi *wasm.ModuleInstance) (wasm. } func (e *engine) compileSharedFunctions() { - var sizes [8]int + var sizes [12]int var trampolines []byte addTrampoline := func(i int, buf []byte) { @@ -801,6 +821,38 @@ func (e *engine) compileSharedFunctions() { Results: []ssa.Type{ssa.TypeI32}, }, false)) + e.be.Init() + addTrampoline(8, + e.machine.CompileGoFunctionTrampoline(wazevoapi.ExitCodeThrowAlloc, &ssa.Signature{ + // exec context, tag index → exnref + Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64}, + Results: []ssa.Type{ssa.TypeI64}, + }, false)) + + e.be.Init() + addTrampoline(9, + e.machine.CompileGoFunctionTrampoline(wazevoapi.ExitCodeThrow, &ssa.Signature{ + // exec context, exnref + Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64}, + Results: []ssa.Type{}, + }, false)) + + e.be.Init() + addTrampoline(10, + e.machine.CompileGoFunctionTrampoline(wazevoapi.ExitCodeTryTableEnter, &ssa.Signature{ + // exec context, catch clause info (encoded) + Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64}, + Results: []ssa.Type{}, + }, false)) + + e.be.Init() + addTrampoline(11, + e.machine.CompileGoFunctionTrampoline(wazevoapi.ExitCodeTryTableLeave, &ssa.Signature{ + // exec context + Params: []ssa.Type{ssa.TypeI64}, + Results: []ssa.Type{}, + }, false)) + fns := &sharedFunctions{ executable: mmapExecutable(trampolines), listenerTrampolines: make(listenerTrampolines), @@ -823,6 +875,14 @@ func (e *engine) compileSharedFunctions() { fns.memoryWait64Address = &fns.executable[offset] offset += sizes[6] fns.memoryNotifyAddress = &fns.executable[offset] + offset += sizes[7] + fns.throwAllocTrampolineAddress = &fns.executable[offset] + offset += sizes[8] + fns.throwTrampolineAddress = &fns.executable[offset] + offset += sizes[9] + fns.tryTableEnterAddress = &fns.executable[offset] + offset += sizes[10] + fns.tryTableLeaveAddress = &fns.executable[offset] if wazevoapi.PerfMapEnabled { wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(fns.memoryGrowAddress)), uint64(sizes[0]), "memory_grow_trampoline") @@ -833,6 +893,10 @@ func (e *engine) compileSharedFunctions() { wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(fns.memoryWait32Address)), uint64(sizes[5]), "memory_wait32_trampoline") wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(fns.memoryWait64Address)), uint64(sizes[6]), "memory_wait64_trampoline") wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(fns.memoryNotifyAddress)), uint64(sizes[7]), "memory_notify_trampoline") + wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(fns.throwAllocTrampolineAddress)), uint64(sizes[8]), "throw_alloc_trampoline") + wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(fns.throwTrampolineAddress)), uint64(sizes[9]), "throw_trampoline") + wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(fns.tryTableEnterAddress)), uint64(sizes[10]), "try_table_enter_trampoline") + wazevoapi.PerfMap.AddEntry(uintptr(unsafe.Pointer(fns.tryTableLeaveAddress)), uint64(sizes[11]), "try_table_leave_trampoline") } e.sharedFunctions = fns diff --git a/internal/engine/wazevo/frontend/frontend.go b/internal/engine/wazevo/frontend/frontend.go index 5749e03c7..1e4979f39 100644 --- a/internal/engine/wazevo/frontend/frontend.go +++ b/internal/engine/wazevo/frontend/frontend.go @@ -4,6 +4,7 @@ package frontend import ( "bytes" "math" + "sync" "github.com/tetratelabs/wazero/internal/engine/wazevo/ssa" "github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi" @@ -62,6 +63,20 @@ type Compiler struct { execCtxPtrValue, moduleCtxPtrValue ssa.Value + // throwAllocSig is the signature for the throw-alloc trampoline: + // (execCtx, tagIndex) → (exnref). Allocates the Exception and returns + // its pointer so compiled code can pass it to the throw trampoline. + throwAllocSig ssa.Signature + // throwSig is the signature for the throw/throw_ref trampoline: + // (execCtx, exnref) → (). Searches for a matching handler and restores. + throwSig ssa.Signature + // tryTableEnterSig is the signature for the try_table enter trampoline. + tryTableEnterSig ssa.Signature + // tryTableLeaveSig is the signature for the try_table leave trampoline. + tryTableLeaveSig ssa.Signature + // catchClauseTable accumulates catch clause info for each try_table during compilation. + catchClauseTable catchClauseTable + // Following are reused for the known safe bounds analysis. pointers []int @@ -95,12 +110,75 @@ func NewFrontendCompiler(m *wasm.Module, ssaBuilder ssa.Builder, offset *wazevoa offset: offset, ensureTermination: ensureTermination, needSourceOffsetInfo: sourceInfo, + catchClauseTable: &localCatchClauseTable{}, varLengthKnownSafeBoundWithIDPool: wazevoapi.NewVarLengthPool[knownSafeBoundWithID](), } c.declareSignatures(listenerOn) return c } +// catchClauseTable accumulates catch clause entries during compilation. +type catchClauseTable interface { + Append(clauses []wazevoapi.CatchClauseInstance) int + Table() [][]wazevoapi.CatchClauseInstance +} + +// localCatchClauseTable is the single-threaded implementation. +type localCatchClauseTable struct { + table [][]wazevoapi.CatchClauseInstance +} + +func (t *localCatchClauseTable) Append(clauses []wazevoapi.CatchClauseInstance) int { + id := len(t.table) + t.table = append(t.table, clauses) + return id +} + +func (t *localCatchClauseTable) Table() [][]wazevoapi.CatchClauseInstance { + return t.table +} + +// SharedCatchClauseTable is the thread-safe implementation for parallel compilation. +type SharedCatchClauseTable struct { + mu sync.Mutex + table [][]wazevoapi.CatchClauseInstance + finalized bool +} + +// NewSharedCatchClauseTable creates a new SharedCatchClauseTable. +func NewSharedCatchClauseTable() *SharedCatchClauseTable { + return &SharedCatchClauseTable{} +} + +func (s *SharedCatchClauseTable) Append(clauses []wazevoapi.CatchClauseInstance) int { + if s.finalized { + panic("already finalized") + } + s.mu.Lock() + id := len(s.table) + s.table = append(s.table, clauses) + s.mu.Unlock() + return id +} + +func (s *SharedCatchClauseTable) Table() [][]wazevoapi.CatchClauseInstance { + s.finalized = true + return s.table +} + +// WithCatchClauseTable replaces the catch clause table implementation. +// Used by the parallel compilation path to share a single mutex-protected +// table across workers. +func (c *Compiler) WithCatchClauseTable(t catchClauseTable) *Compiler { + c.catchClauseTable = t + return c +} + +// CatchClauseTable returns the accumulated catch clause table. +func (c *Compiler) CatchClauseTable() [][]wazevoapi.CatchClauseInstance { + return c.catchClauseTable.Table() +} + func (c *Compiler) declareSignatures(listenerOn bool) { m := c.m c.signatures = make(map[*wasm.FunctionType]*ssa.Signature, len(m.TypeSection)+2) @@ -194,6 +272,34 @@ func (c *Compiler) declareSignatures(listenerOn bool) { Results: []ssa.Type{ssa.TypeI32}, } c.ssaBuilder.DeclareSignature(&c.memoryNotifySig) + + c.throwAllocSig = ssa.Signature{ + ID: c.memoryNotifySig.ID + 1, + Params: []ssa.Type{ssa.TypeI64 /* exec context */, ssa.TypeI64 /* tag index */}, + Results: []ssa.Type{ssa.TypeI64 /* exnref */}, + } + c.ssaBuilder.DeclareSignature(&c.throwAllocSig) + + c.throwSig = ssa.Signature{ + ID: c.throwAllocSig.ID + 1, + Params: []ssa.Type{ssa.TypeI64 /* exec context */, ssa.TypeI64 /* exnref */}, + Results: []ssa.Type{}, + } + c.ssaBuilder.DeclareSignature(&c.throwSig) + + c.tryTableEnterSig = ssa.Signature{ + ID: c.throwSig.ID + 1, + Params: []ssa.Type{ssa.TypeI64 /* exec context */, ssa.TypeI64 /* encoded exit code */}, + Results: []ssa.Type{}, + } + c.ssaBuilder.DeclareSignature(&c.tryTableEnterSig) + + c.tryTableLeaveSig = ssa.Signature{ + ID: c.tryTableEnterSig.ID + 1, + Params: []ssa.Type{ssa.TypeI64 /* exec context */}, + Results: []ssa.Type{}, + } + c.ssaBuilder.DeclareSignature(&c.tryTableLeaveSig) } // SignatureForWasmFunctionType returns the ssa.Signature for the given wasm.FunctionType. @@ -234,7 +340,8 @@ func (c *Compiler) Init(idx, typIndex wasm.Index, typ *wasm.FunctionType, localT // Note: this assumes 64-bit platform (I believe we won't have 32-bit backend ;)). const executionContextPtrTyp, moduleContextPtrTyp = ssa.TypeI64, ssa.TypeI64 -// LowerToSSA lowers the current function to SSA function which will be held by ssaBuilder. +// LowerToSSA lowers the current function to SSA IR which will be held by ssaBuilder. +// // After calling this, the caller will be able to access the SSA info in *Compiler.ssaBuilder. // // Note that this only does the naive lowering, and do not do any optimization, instead the caller is expected to do so. @@ -345,8 +452,8 @@ func (c *Compiler) declareWasmGlobal(typ wasm.ValueType, mutable bool) { case wasm.ValueTypeI32: st = ssa.TypeI32 case wasm.ValueTypeI64, - // Both externref and funcref are represented as I64 since we only support 64-bit platforms. - wasm.ValueTypeExternref, wasm.ValueTypeFuncref: + // externref, funcref, and exnref are represented as I64 since we only support 64-bit platforms. + wasm.ValueTypeExternref, wasm.ValueTypeFuncref, wasm.ValueTypeExnref: st = ssa.TypeI64 case wasm.ValueTypeF32: st = ssa.TypeF32 @@ -372,8 +479,9 @@ func WasmTypeToSSAType(vt wasm.ValueType) ssa.Type { case wasm.ValueTypeI32: return ssa.TypeI32 case wasm.ValueTypeI64, - // Both externref and funcref are represented as I64 since we only support 64-bit platforms. - wasm.ValueTypeExternref, wasm.ValueTypeFuncref: + // externref, funcref, and exnref are represented as I64 since we only support 64-bit platforms. + wasm.ValueTypeExternref, wasm.ValueTypeFuncref, + wasm.ValueTypeExnref: return ssa.TypeI64 case wasm.ValueTypeF32: return ssa.TypeF32 diff --git a/internal/engine/wazevo/frontend/frontend_test.go b/internal/engine/wazevo/frontend/frontend_test.go index b982556d8..7921db552 100644 --- a/internal/engine/wazevo/frontend/frontend_test.go +++ b/internal/engine/wazevo/frontend/frontend_test.go @@ -3095,6 +3095,11 @@ func TestCompiler_declareSignatures(t *testing.T) { {ID: 9, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64, ssa.TypeI32, ssa.TypeI64}, Results: []ssa.Type{ssa.TypeI32}}, {ID: 10, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64, ssa.TypeI64, ssa.TypeI64}, Results: []ssa.Type{ssa.TypeI32}}, {ID: 11, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI32, ssa.TypeI64}, Results: []ssa.Type{ssa.TypeI32}}, + // EH signatures. + {ID: 12, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64}, Results: []ssa.Type{ssa.TypeI64}}, + {ID: 13, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64}}, + {ID: 14, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64}}, + {ID: 15, Params: []ssa.Type{ssa.TypeI64}}, } require.Equal(t, len(expected), len(declaredSigs)) @@ -3134,6 +3139,11 @@ func TestCompiler_declareSignatures(t *testing.T) { {ID: 17, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64, ssa.TypeI32, ssa.TypeI64}, Results: []ssa.Type{ssa.TypeI32}}, {ID: 18, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64, ssa.TypeI64, ssa.TypeI64}, Results: []ssa.Type{ssa.TypeI32}}, {ID: 19, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI32, ssa.TypeI64}, Results: []ssa.Type{ssa.TypeI32}}, + // EH signatures. + {ID: 20, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64}, Results: []ssa.Type{ssa.TypeI64}}, + {ID: 21, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64}}, + {ID: 22, Params: []ssa.Type{ssa.TypeI64, ssa.TypeI64}}, + {ID: 23, Params: []ssa.Type{ssa.TypeI64}}, } require.Equal(t, len(expected), len(declaredSigs)) for i := 0; i < len(declaredSigs); i++ { diff --git a/internal/engine/wazevo/frontend/lower.go b/internal/engine/wazevo/frontend/lower.go index 1277db0bf..11224f501 100644 --- a/internal/engine/wazevo/frontend/lower.go +++ b/internal/engine/wazevo/frontend/lower.go @@ -65,6 +65,7 @@ const ( controlFrameKindIfWithElse controlFrameKindIfWithoutElse controlFrameKindBlock + controlFrameKindTryTable ) // String implements fmt.Stringer for debugging. @@ -80,6 +81,8 @@ func (k controlFrameKind) String() string { return "if_without_else" case controlFrameKindBlock: return "block" + case controlFrameKindTryTable: + return "try_table" default: panic(k) } @@ -1443,6 +1446,11 @@ func (c *Compiler) lowerCurrentOpcode() { unreachable := state.unreachable if !unreachable { + // For try_table, emit the leave trampoline before the jump to the following block. + if ctrl.kind == controlFrameKindTryTable { + c.emitTryTableLeave() + } + // Top n-th args will be used as a result of the current control frame. args := c.nPeekDup(len(ctrl.blockType.Results)) @@ -1477,6 +1485,7 @@ func (c *Compiler) lowerCurrentOpcode() { break } + c.emitTryTableLeaves(int(labelIndex)) targetBlk, argNum := state.brTargetArgNumFor(labelIndex) args := c.nPeekDup(argNum) c.insertJumpToBlock(args, targetBlk) @@ -1494,6 +1503,21 @@ func (c *Compiler) lowerCurrentOpcode() { targetBlk, argNum := state.brTargetArgNumFor(labelIndex) args := c.nPeekDup(argNum) var sealTargetBlk bool + + // If the branch exits any try_table frames, emit TryTableLeave + // calls in a trampoline block that only runs on the taken path. + if c.branchExitsTryTable(int(labelIndex)) { + current := builder.CurrentBlock() + trampolineBlk := builder.AllocateBasicBlock() + builder.SetCurrentBlock(trampolineBlk) + c.emitTryTableLeaves(int(labelIndex)) + c.insertJumpToBlock(args, targetBlk) + builder.SetCurrentBlock(current) + targetBlk = trampolineBlk + sealTargetBlk = true + args = ssa.ValuesNil + } + if c.needListener && targetBlk.ReturnBlock() { // In this case, we have to call the listener before returning. // Save the currently active block. current := builder.CurrentBlock() @@ -1559,6 +1583,7 @@ func (c *Compiler) lowerCurrentOpcode() { if state.unreachable { break } + c.emitTryTableLeaves(len(state.controlFrames)) if c.needListener { c.callListenerAfter() } @@ -3399,6 +3424,9 @@ func (c *Compiler) lowerCurrentOpcode() { if state.unreachable { break } + // Per spec, return_call leaves the current frame, so all enclosing + // try_table handlers must be popped before the tail call. + c.emitTryTableLeaves(len(c.state().controlFrames)) _, _ = typeIndex, tableIndex c.lowerTailCallReturnCallIndirect(typeIndex, tableIndex) state.unreachable = true @@ -3408,9 +3436,251 @@ func (c *Compiler) lowerCurrentOpcode() { if state.unreachable { break } + // Per spec, return_call leaves the current frame, so all enclosing + // try_table handlers must be popped before the tail call. + c.emitTryTableLeaves(len(c.state().controlFrames)) c.lowerTailCallReturnCall(fnIndex) state.unreachable = true + case wasm.OpcodeThrow: + tagIndex := c.readI32u() + if state.unreachable { + break + } + tagType := c.resolveTagType(tagIndex) + // Pop the tag's param values from the stack. + var throwParams []ssa.Value + if tagType != nil { + throwParams = make([]ssa.Value, len(tagType.Params)) + for i := len(tagType.Params) - 1; i >= 0; i-- { + throwParams[i] = state.pop() + } + } + + c.storeCallerModuleContext() + + tagIdxVal := builder.AllocateInstruction().AsIconst64(uint64(tagIndex)).Insert(builder).Return() + + // We need to store the throwParams in the exception and then throw it. + // However, each exception might have a variable number of parameters, + // so we let Go allocate the reference on the heap. + // The Go side allocates the Exception object (Params sized to nParams) + // and stores the pointer to the backing-array into execCtx.exceptionParamsPtr. + throwAllocPtr := builder.AllocateInstruction(). + AsLoad(c.execCtxPtrValue, + wazevoapi.ExecutionContextOffsetThrowAllocTrampolineAddress.U32(), + ssa.TypeI64, + ).Insert(builder).Return() + throwAllocArgs := c.allocateVarLengthValues(2, c.execCtxPtrValue, tagIdxVal) + exnref := builder.AllocateInstruction(). + AsCallIndirect(throwAllocPtr, &c.throwAllocSig, throwAllocArgs). + Insert(builder).Return() + + // Reload memory pointers invalidated by the Go call. + c.reloadAfterCall() + + // We can now store each param directly into Exception.Params using the pointer + // stored into execCtx.exceptionParamsPtr. + if len(throwParams) > 0 { + paramsPtr := builder.AllocateInstruction(). + AsLoad(c.execCtxPtrValue, + wazevoapi.ExecutionContextOffsetExceptionParamsPtr.U32(), + ssa.TypeI64, + ).Insert(builder).Return() + for i, v := range throwParams { + switch v.Type() { + case ssa.TypeF32: + v = builder.AllocateInstruction().AsBitcast(v, ssa.TypeI32).Insert(builder).Return() + case ssa.TypeF64: + v = builder.AllocateInstruction().AsBitcast(v, ssa.TypeI64).Insert(builder).Return() + } + builder.AllocateInstruction(). + AsStore(ssa.OpcodeStore, v, paramsPtr, uint32(i)*8). + Insert(builder) + } + } + + // We return again control to Go to search and dispatch to a matching catch clause. + c.emitThrow(exnref) + state.unreachable = true + + case wasm.OpcodeThrowRef: + if state.unreachable { + break + } + exnref := state.pop() + // Check for null exnref. + zero := builder.AllocateInstruction().AsIconst64(0).Insert(builder).Return() + isNull := builder.AllocateInstruction() + isNull.AsIcmp(exnref, zero, ssa.IntegerCmpCondEqual) + builder.InsertInstruction(isNull) + exitIfNull := builder.AllocateInstruction() + exitIfNull.AsExitIfTrueWithCode(c.execCtxPtrValue, isNull.Return(), wazevoapi.ExitCodeNullReference) + builder.InsertInstruction(exitIfNull) + + c.storeCallerModuleContext() + + c.emitThrow(exnref) + state.unreachable = true + + case wasm.OpcodeTryTable: + bt := c.readBlockType() + + if state.unreachable { + state.unreachableDepth++ + // Still need to skip the catch clause bytes in the unreachable case. + c.skipTryTableCatchClauses() + break + } + + // Parse catch clauses. + c.loweringState.pc++ + catchCount, catchNum, _ := leb128.LoadUint32(c.wasmFunctionBody[c.loweringState.pc:]) + c.loweringState.pc += int(catchNum) - 1 + + var catchClauses []catchClause + for i := uint32(0); i < catchCount; i++ { + c.loweringState.pc++ + kind := c.wasmFunctionBody[c.loweringState.pc] + var tagIdx uint32 + switch kind { + case wasm.CatchKindCatch, wasm.CatchKindCatchRef: + c.loweringState.pc++ + var n uint64 + tagIdx, n, _ = leb128.LoadUint32(c.wasmFunctionBody[c.loweringState.pc:]) + c.loweringState.pc += int(n) - 1 + case wasm.CatchKindCatchAll, wasm.CatchKindCatchAllRef: + // No tagIdx for catch_all variants. + } + c.loweringState.pc++ + labelIdx, n, _ := leb128.LoadUint32(c.wasmFunctionBody[c.loweringState.pc:]) + c.loweringState.pc += int(n) - 1 + catchClauses = append(catchClauses, catchClause{kind: kind, tagIndex: tagIdx, labelIdx: labelIdx}) + } + + // Register catch clauses in the table and get the try_table ID. + var clauseInstances []wazevoapi.CatchClauseInstance + for _, cc := range catchClauses { + clauseInstances = append(clauseInstances, wazevoapi.CatchClauseInstance{ + Kind: cc.kind, + TagIndex: cc.tagIndex, + }) + } + tryTableID := c.catchClauseTable.Append(clauseInstances) + + // Allocate the following block (after try_table end) and body block. + followingBlk := builder.AllocateBasicBlock() + c.addBlockParamsFromWasmTypes(bt.Results, followingBlk) + bodyBlk := builder.AllocateBasicBlock() + + if len(catchClauses) > 0 { + // Store the caller module context so the dispatch loop can find the module. + c.storeCallerModuleContext() + + // For each catch clause, create a handler block that loads exception + // params and jumps to the wasm target label. + // NOTE: catch clause label indices do NOT include the try_table itself + // (the try_table is pushed onto the control stack after the catch clauses + // are processed, per the spec). So we resolve labels BEFORE pushing. + varPool := builder.VarLengthPool() + targets := varPool.Allocate(len(catchClauses) + 1) // +1 for bodyBlk + + currentBlk := builder.CurrentBlock() + for _, cc := range catchClauses { + handlerBlk := builder.AllocateBasicBlock() + builder.SetCurrentBlock(handlerBlk) + c.reloadAfterCall() + + // Resolve the wasm target label. + targetBlk, _ := state.brTargetArgNumFor(cc.labelIdx) + + // Load exception params and jump to wasm target. + var brArgs []ssa.Value + switch cc.kind { + case wasm.CatchKindCatch: + if tagType := c.resolveTagType(cc.tagIndex); tagType != nil { + brArgs = c.loadExceptionParams(tagType) + } + case wasm.CatchKindCatchRef: + if tagType := c.resolveTagType(cc.tagIndex); tagType != nil { + brArgs = c.loadExceptionParams(tagType) + } + brArgs = append(brArgs, c.loadExnRef()) + case wasm.CatchKindCatchAll: + // No values. + case wasm.CatchKindCatchAllRef: + brArgs = append(brArgs, c.loadExnRef()) + } + + jmpArgs := c.allocateVarLengthValues(len(brArgs), brArgs...) + c.insertJumpToBlock(jmpArgs, targetBlk) + + targets = targets.Append(varPool, ssa.Value(handlerBlk.ID())) + } + // Last target is the body block (default for clauseIdx == -1 / out of range). + targets = targets.Append(varPool, ssa.Value(bodyBlk.ID())) + + // Back to the original block: call the try_table enter trampoline, + // then dispatch on the caught clause index. + builder.SetCurrentBlock(currentBlk) + encodedExitCode := uint64(wazevoapi.ExitCodeTryTableEnter | wazevoapi.ExitCode(tryTableID<<8)) + + // Load trampoline address from execCtx. + enterPtr := builder.AllocateInstruction(). + AsLoad(c.execCtxPtrValue, + wazevoapi.ExecutionContextOffsetTryTableEnterTrampolineAddress.U32(), + ssa.TypeI64, + ).Insert(builder).Return() + + // Call the trampoline: (execCtx, encodedExitCode) -> (). + exitCodeVal := builder.AllocateInstruction().AsIconst64(encodedExitCode).Insert(builder).Return() + args := c.allocateVarLengthValues(2, c.execCtxPtrValue, exitCodeVal) + builder.AllocateInstruction(). + AsCallIndirect(enterPtr, &c.tryTableEnterSig, args). + Insert(builder) + + // Load the caught clause index written by the dispatch loop. + clauseIdx := builder.AllocateInstruction(). + AsLoad(c.execCtxPtrValue, + wazevoapi.ExecutionContextOffsetCaughtExceptionClauseIdx.U32(), + ssa.TypeI64, + ).Insert(builder).Return() + + // Dispatch to handler blocks or body block via br_table. + brTable := builder.AllocateInstruction() + brTable.AsBrTable(clauseIdx, targets) + builder.InsertInstruction(brTable) + + // Seal handler blocks after BrTable is inserted (so predecessors are registered). + for _, targetID := range targets.View() { + blk := builder.BasicBlock(ssa.BasicBlockID(targetID)) + if !blk.Sealed() { + builder.Seal(blk) + } + } + } else { + // No catch clauses — try_table acts as a plain block. + // Jump directly to body without entering exception handling. + c.insertJumpToBlock(ssa.ValuesNil, bodyBlk) + } + + if !bodyBlk.Sealed() { + builder.Seal(bodyBlk) + } + builder.SetCurrentBlock(bodyBlk) + if len(catchClauses) > 0 { + // Body block is entered after the trampoline call, so we need to reload. + c.reloadAfterCall() + } + + // Push the try_table control frame AFTER resolving catch labels. + state.ctrlPush(controlFrame{ + kind: controlFrameKindTryTable, + originalStackLenWithoutParam: len(state.values) - len(bt.Params), + followingBlock: followingBlk, + blockType: bt, + }) + default: panic("TODO: unsupported in wazevo yet: " + wasm.InstructionName(op)) } @@ -4026,6 +4296,185 @@ func (c *Compiler) storeCallerModuleContext() { builder.InsertInstruction(store) } +// resolveTagType returns the FunctionType for the tag at the given module-local index. +func (c *Compiler) resolveTagType(tagIndex uint32) *wasm.FunctionType { + if tagIndex < c.m.ImportTagCount { + cur := uint32(0) + for i := range c.m.ImportSection { + imp := &c.m.ImportSection[i] + if imp.Type != wasm.ExternTypeTag { + continue + } + if tagIndex == cur { + return &c.m.TypeSection[imp.DescTag] + } + cur++ + } + } else { + tagSectionIdx := tagIndex - c.m.ImportTagCount + if tagSectionIdx < uint32(len(c.m.TagSection)) { + typeIdx := c.m.TagSection[tagSectionIdx].Type + return &c.m.TypeSection[typeIdx] + } + } + return nil +} + +// emitThrow emits a call to the shared throw trampoline with the given exnref, +// followed by an unreachable exit (throw never returns). +func (c *Compiler) emitThrow(exnref ssa.Value) { + builder := c.ssaBuilder + throwPtr := builder.AllocateInstruction(). + AsLoad(c.execCtxPtrValue, + wazevoapi.ExecutionContextOffsetThrowTrampolineAddress.U32(), + ssa.TypeI64, + ).Insert(builder).Return() + throwArgs := c.allocateVarLengthValues(2, c.execCtxPtrValue, exnref) + builder.AllocateInstruction(). + AsCallIndirect(throwPtr, &c.throwSig, throwArgs). + Insert(builder) + + exit := builder.AllocateInstruction() + exit.AsExitWithCode(c.execCtxPtrValue, wazevoapi.ExitCodeUnreachable) + builder.InsertInstruction(exit) +} + +// emitTryTableLeave emits a trampoline call to pop the try handler in the dispatch loop. +func (c *Compiler) emitTryTableLeave() { + builder := c.ssaBuilder + c.storeCallerModuleContext() + + leavePtr := builder.AllocateInstruction(). + AsLoad(c.execCtxPtrValue, + wazevoapi.ExecutionContextOffsetTryTableLeaveTrampolineAddress.U32(), + ssa.TypeI64, + ).Insert(builder).Return() + + args := c.allocateVarLengthValues(1, c.execCtxPtrValue) + builder.AllocateInstruction(). + AsCallIndirect(leavePtr, &c.tryTableLeaveSig, args). + Insert(builder) +} + +// branchExitsTryTable returns true if a branch to the given depth would +// exit at least one try_table frame. +func (c *Compiler) branchExitsTryTable(depth int) bool { + state := c.state() + tail := len(state.controlFrames) - 1 + for i := 0; i < depth; i++ { + if state.controlFrames[tail-i].kind == controlFrameKindTryTable { + return true + } + } + return false +} + +// emitTryTableLeaves emits TryTableLeave calls for try_table frames +// that would be exited by a branch to the given depth. +func (c *Compiler) emitTryTableLeaves(depth int) { + state := c.state() + tail := len(state.controlFrames) - 1 + for i := 0; i < depth; i++ { + if state.controlFrames[tail-i].kind == controlFrameKindTryTable { + c.emitTryTableLeave() + } + } +} + +// catchClause holds a parsed catch clause from a try_table instruction. +type catchClause struct { + kind byte + tagIndex uint32 + labelIdx uint32 +} + +// loadExceptionParams loads the exception params from the caught Exception's +// Params slice. The dispatch loop sets execCtx.exceptionParamsPtr to the +// slice's backing-array pointer after matching a handler. We load that pointer +// and then read each param from [ptr + i*8], mirroring the stores emitted by +// the throw lowering. Float params were bitcast to integers at the throw site, +// so we load as integer and bitcast back to the original type. +func (c *Compiler) loadExceptionParams(tagType *wasm.FunctionType) []ssa.Value { + if len(tagType.Params) == 0 { + return nil + } + builder := c.ssaBuilder + + // Load the pointer to the caught Exception's Params backing array. + paramsPtr := builder.AllocateInstruction(). + AsLoad(c.execCtxPtrValue, + wazevoapi.ExecutionContextOffsetExceptionParamsPtr.U32(), + ssa.TypeI64, + ).Insert(builder).Return() + + var values []ssa.Value + for i, vt := range tagType.Params { + offset := uint32(i) * 8 + ssaType := WasmTypeToSSAType(vt) + switch ssaType { + case ssa.TypeF32: + // Stored as i32 at throw site; bitcast back to f32. + raw := builder.AllocateInstruction(). + AsLoad(paramsPtr, offset, ssa.TypeI32). + Insert(builder).Return() + val := builder.AllocateInstruction().AsBitcast(raw, ssa.TypeF32).Insert(builder).Return() + values = append(values, val) + case ssa.TypeF64: + // Stored as i64 at throw site; bitcast back to f64. + raw := builder.AllocateInstruction(). + AsLoad(paramsPtr, offset, ssa.TypeI64). + Insert(builder).Return() + val := builder.AllocateInstruction().AsBitcast(raw, ssa.TypeF64).Insert(builder).Return() + values = append(values, val) + default: + val := builder.AllocateInstruction(). + AsLoad(paramsPtr, offset, ssaType). + Insert(builder).Return() + values = append(values, val) + } + } + return values +} + +// loadExnRef loads the exnref (pointer to Exception) from the executionContext. +// The dispatch loop writes it to exceptionPtr after matching a handler. +func (c *Compiler) loadExnRef() ssa.Value { + builder := c.ssaBuilder + return builder.AllocateInstruction(). + AsLoad(c.execCtxPtrValue, + wazevoapi.ExecutionContextOffsetExceptionPtr.U32(), + ssa.TypeI64, + ).Insert(builder).Return() +} + +// skipTryTableCatchClauses advances the bytecode PC past the catch clauses +// of a try_table instruction. This is used both in reachable and unreachable states. +func (c *Compiler) skipTryTableCatchClauses() { + c.loweringState.pc++ + catchCount, catchNum, _ := leb128.LoadUint32(c.wasmFunctionBody[c.loweringState.pc:]) + c.loweringState.pc += int(catchNum) - 1 + for i := uint32(0); i < catchCount; i++ { + c.loweringState.pc++ + kind := c.wasmFunctionBody[c.loweringState.pc] + switch kind { + case wasm.CatchKindCatch, wasm.CatchKindCatchRef: + // Read tag index. + c.loweringState.pc++ + _, n, _ := leb128.LoadUint32(c.wasmFunctionBody[c.loweringState.pc:]) + c.loweringState.pc += int(n) - 1 + // Read label index. + c.loweringState.pc++ + _, n, _ = leb128.LoadUint32(c.wasmFunctionBody[c.loweringState.pc:]) + c.loweringState.pc += int(n) - 1 + case wasm.CatchKindCatchAll, wasm.CatchKindCatchAllRef: + // Read label index. + c.loweringState.pc++ + _, n, _ := leb128.LoadUint32(c.wasmFunctionBody[c.loweringState.pc:]) + c.loweringState.pc += int(n) - 1 + } + } +} + func (c *Compiler) readByte() byte { v := c.wasmFunctionBody[c.loweringState.pc+1] c.loweringState.pc++ @@ -4181,6 +4630,7 @@ func (c *Compiler) lowerBrTable(labels []uint32, index ssa.Value) { targetBlk, _ := state.brTargetArgNumFor(l) trampoline := builder.AllocateBasicBlock() builder.SetCurrentBlock(trampoline) + c.emitTryTableLeaves(int(l)) c.insertJumpToBlock(args, targetBlk) trampolineBlockIDs = trampolineBlockIDs.Append(builder.VarLengthPool(), ssa.Value(trampoline.ID())) } diff --git a/internal/engine/wazevo/module_engine.go b/internal/engine/wazevo/module_engine.go index 41c38b86b..e740c988b 100644 --- a/internal/engine/wazevo/module_engine.go +++ b/internal/engine/wazevo/module_engine.go @@ -136,6 +136,14 @@ func (m *moduleEngine) setupOpaque() { } } + if tagOffset := offsets.TagsBegin; tagOffset >= 0 { + for _, tag := range inst.Tags { + binary.LittleEndian.PutUint64(opaque[tagOffset:], + uint64(uintptr(unsafe.Pointer(tag)))) + tagOffset += 8 + } + } + if beforeListenerOffset := offsets.BeforeListenerTrampolines1stElement; beforeListenerOffset >= 0 { binary.LittleEndian.PutUint64(opaque[beforeListenerOffset:], uint64(uintptr(unsafe.Pointer(&m.parent.listenerBeforeTrampolines[0])))) } @@ -208,6 +216,10 @@ func (m *moduleEngine) NewFunction(index wasm.Index) api.Function { ce.execCtx.memoryWait32TrampolineAddress = sharedFunctions.memoryWait32Address ce.execCtx.memoryWait64TrampolineAddress = sharedFunctions.memoryWait64Address ce.execCtx.memoryNotifyTrampolineAddress = sharedFunctions.memoryNotifyAddress + ce.execCtx.throwAllocTrampolineAddress = sharedFunctions.throwAllocTrampolineAddress + ce.execCtx.throwTrampolineAddress = sharedFunctions.throwTrampolineAddress + ce.execCtx.tryTableEnterTrampolineAddress = sharedFunctions.tryTableEnterAddress + ce.execCtx.tryTableLeaveTrampolineAddress = sharedFunctions.tryTableLeaveAddress ce.execCtx.memmoveAddress = memmovPtr ce.init() return ce diff --git a/internal/engine/wazevo/ssa/basic_block.go b/internal/engine/wazevo/ssa/basic_block.go index cf7f14d3b..25bdccdf4 100644 --- a/internal/engine/wazevo/ssa/basic_block.go +++ b/internal/engine/wazevo/ssa/basic_block.go @@ -291,9 +291,9 @@ func (bb *basicBlock) addPred(blk BasicBlock, branch *Instruction) { for i := range bb.preds { existingPred := &bb.preds[i] if existingPred.blk == pred && existingPred.branch != branch { - // If the target is already added, then this must come from the same BrTable, + // If the target is already added, then this must come from the same BrTable or TryTableDispatch, // otherwise such redundant branch should be eliminated by the frontend. (which should be simpler). - panic(fmt.Sprintf("BUG: redundant non BrTable jumps in %s whose targes are the same", bb.Name())) + panic(fmt.Sprintf("BUG: redundant non BrTable/TryTableDispatch jumps in %s whose targets are the same", bb.Name())) } } @@ -344,19 +344,21 @@ func (bb *basicBlock) validate(b *builder) { } } - var exp int - if bb.ReturnBlock() { - exp = len(b.currentSignature.Results) - } else { - exp = len(bb.params.View()) - } + if pred.branch.opcode != OpcodeBrTable { + var exp int + if bb.ReturnBlock() { + exp = len(b.currentSignature.Results) + } else { + exp = len(bb.params.View()) + } - if len(pred.branch.vs.View()) != exp { - panic(fmt.Sprintf( - "BUG: len(argument at %s) != len(params at %s): %d != %d: %s", - pred.blk.Name(), bb.Name(), - len(pred.branch.vs.View()), len(bb.params.View()), pred.branch.Format(b), - )) + if len(pred.branch.vs.View()) != exp { + panic(fmt.Sprintf( + "BUG: len(argument at %s) != len(params at %s): %d != %d: %s", + pred.blk.Name(), bb.Name(), + len(pred.branch.vs.View()), len(bb.params.View()), pred.branch.Format(b), + )) + } } } diff --git a/internal/engine/wazevo/testcases/testcases.go b/internal/engine/wazevo/testcases/testcases.go index 103aa3e5b..22a8daeaf 100644 --- a/internal/engine/wazevo/testcases/testcases.go +++ b/internal/engine/wazevo/testcases/testcases.go @@ -2723,6 +2723,372 @@ func VecShuffleWithLane(lane ...byte) *wasm.Module { } } +// Exception Handling test cases. + +// ThrowOnly: a function that always throws tag 0 (no params). +// Expected: uncaught exception trap. +// +// (module +// (tag $e) +// (func (export "f") (throw $e)) +// ) +var ThrowOnly = TestCase{ + Name: "throw_only", + Module: &wasm.Module{ + TypeSection: []wasm.FunctionType{vv}, + FunctionSection: []wasm.Index{0}, + TagSection: []wasm.Tag{{Type: 0}}, + CodeSection: []wasm.Code{{ + Body: []byte{ + wasm.OpcodeThrow, 0, // throw $e (tag index 0) + wasm.OpcodeEnd, + }, + }}, + ExportSection: []wasm.Export{{Name: ExportedFunctionName, Type: wasm.ExternTypeFunc, Index: 0}}, + }, +} + +// ThrowWithParam: throws tag 0 (i32 param) with the function's i32 parameter. +// Expected: uncaught exception trap. +// +// (module +// (tag $e (param i32)) +// (func (export "f") (param i32) (local.get 0) (throw $e)) +// ) +var ThrowWithParam = TestCase{ + Name: "throw_with_param", + Module: &wasm.Module{ + TypeSection: []wasm.FunctionType{ + {Params: []wasm.ValueType{i32}}, // type 0: tag type (i32) -> () + {Params: []wasm.ValueType{i32}}, // type 1: func type (i32) -> () + }, + FunctionSection: []wasm.Index{1}, + TagSection: []wasm.Tag{{Type: 0}}, + CodeSection: []wasm.Code{{ + Body: []byte{ + wasm.OpcodeLocalGet, 0, + wasm.OpcodeThrow, 0, // throw $e (tag index 0) + wasm.OpcodeEnd, + }, + }}, + ExportSection: []wasm.Export{{Name: ExportedFunctionName, Type: wasm.ExternTypeFunc, Index: 0}}, + }, +} + +// TryTableCatchAllEmpty: try_table with catch_all where the body does not throw. +// The try_table body pushes 42 and returns. catch_all is never triggered. +// +// (module +// (func (export "f") (result i32) +// (block $caught ;; catch_all target, expects 0 values +// (try_table (catch_all $caught) +// (i32.const 42) +// (return) +// ) +// (i32.const 99) +// (return) +// ) +// (i32.const 0) +// ) +// ) +var TryTableCatchAllEmpty = TestCase{ + Name: "try_table_catch_all_empty", + Module: &wasm.Module{ + TypeSection: []wasm.FunctionType{{Results: []wasm.ValueType{i32}}}, + FunctionSection: []wasm.Index{0}, + CodeSection: []wasm.Code{{ + Body: []byte{ + // block $caught -- void block (catch_all delivers 0 values) + wasm.OpcodeBlock, blockSignature_vv, + // try_table (catch_all $caught) -- void block + wasm.OpcodeTryTable, blockSignature_vv, + 1, // catch clause count = 1 + wasm.CatchKindCatchAll, // catch_all + 0, // label index 0 = $caught (try_table not yet on stack) + // body: return 42 directly + wasm.OpcodeI32Const, 42, + wasm.OpcodeReturn, + wasm.OpcodeEnd, // end try_table + // After try_table (catch_all path): return 99 + wasm.OpcodeI32Const, 0xe3, 0x00, // 99 in signed LEB128 + wasm.OpcodeReturn, + wasm.OpcodeEnd, // end block $caught + // Should not reach here. + wasm.OpcodeI32Const, 0, + wasm.OpcodeEnd, // end function + }, + }}, + ExportSection: []wasm.Export{{Name: ExportedFunctionName, Type: wasm.ExternTypeFunc, Index: 0}}, + }, +} + +// TryTableCatchAllThrow: try_table with catch_all where the body throws. +// The catch_all catches the exception, falls through, and returns 42. +// +// (module +// (tag $e) +// (func (export "f") (result i32) +// (block $caught +// (try_table (catch_all $caught) +// (throw $e) +// ) +// (unreachable) +// ) +// (i32.const 42) +// ) +// ) +var TryTableCatchAllThrow = TestCase{ + Name: "try_table_catch_all_throw", + Module: &wasm.Module{ + TypeSection: []wasm.FunctionType{vv, {Results: []wasm.ValueType{i32}}}, + FunctionSection: []wasm.Index{1}, // func type 1: () -> i32 + TagSection: []wasm.Tag{{Type: 0}}, // tag type 0: () -> () + CodeSection: []wasm.Code{{ + Body: []byte{ + wasm.OpcodeBlock, blockSignature_vv, // block $caught + wasm.OpcodeTryTable, blockSignature_vv, // try_table + 1, // 1 catch clause + wasm.CatchKindCatchAll, // catch_all + 0, // label 0 = $caught + wasm.OpcodeThrow, 0, // throw tag 0 + wasm.OpcodeEnd, // end try_table + wasm.OpcodeUnreachable, // after try_table (unreachable) + wasm.OpcodeEnd, // end block $caught + wasm.OpcodeI32Const, 42, + wasm.OpcodeEnd, // end func + }, + }}, + ExportSection: []wasm.Export{{Name: ExportedFunctionName, Type: wasm.ExternTypeFunc, Index: 0}}, + }, +} + +// TryTableCatchThrow: try_table with catch $e0 (specific tag, no params) where +// the body throws. The catch dispatches to the enclosing block. +// +// (module +// (tag $e0) +// (func (export "f") (result i32) +// (block $h ;; label 0 +// (try_table (result i32) (catch $e0 $h) +// (throw $e0) +// (i32.const 42) +// ) +// (return) +// ) +// (i32.const 23) +// ) +// ) +var TryTableCatchThrow = TestCase{ + Name: "try_table_catch_throw", + Module: &wasm.Module{ + TypeSection: []wasm.FunctionType{vv, {Results: []wasm.ValueType{i32}}}, + FunctionSection: []wasm.Index{1}, // func type 1: () -> i32 + TagSection: []wasm.Tag{{Type: 0}}, // tag type 0: () -> () + CodeSection: []wasm.Code{{ + Body: []byte{ + wasm.OpcodeBlock, blockSignature_vv, // block $h (label 0) + wasm.OpcodeTryTable, 0x7F, // try_table (result i32) + 1, // 1 catch clause + wasm.CatchKindCatch, // catch + 0, // tag index 0 ($e0) + 0, // label 0 ($h) + wasm.OpcodeThrow, 0, // throw tag 0 + wasm.OpcodeI32Const, 42, // (unreachable but needed for type) + wasm.OpcodeEnd, // end try_table + wasm.OpcodeReturn, // return try_table result + wasm.OpcodeEnd, // end block $h + wasm.OpcodeI32Const, 23, + wasm.OpcodeEnd, // end func + }, + }}, + ExportSection: []wasm.Export{{Name: ExportedFunctionName, Type: wasm.ExternTypeFunc, Index: 0}}, + }, +} + +// TryTableCatchParamThrow: try_table with catch $e-i32 where the tag has an i32 parameter. +// The exception parameter is forwarded to the catch target block. +// +// (module +// (tag $e-i32 (param i32)) +// (func (export "f") (param i32) (result i32) +// (block $h (result i32) +// (try_table (result i32) (catch $e-i32 $h) +// (throw $e-i32 (local.get 0)) +// (i32.const 2) +// ) +// (return) +// ) +// (return) +// ) +// ) +var TryTableCatchParamThrow = TestCase{ + Name: "try_table_catch_param_throw", + Module: &wasm.Module{ + TypeSection: []wasm.FunctionType{ + {Params: []wasm.ValueType{i32}}, // type 0: (i32) -> () for tag + {Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i32}}, // type 1: (i32) -> (i32) for func + }, + FunctionSection: []wasm.Index{1}, // func type 1 + TagSection: []wasm.Tag{{Type: 0}}, // tag type 0: (i32) -> () + CodeSection: []wasm.Code{{ + Body: []byte{ + wasm.OpcodeBlock, 0x7F, // block $h (result i32) -- label 0 + wasm.OpcodeTryTable, 0x7F, // try_table (result i32) + 1, // 1 catch clause + wasm.CatchKindCatch, // catch + 0, // tag index 0 ($e-i32) + 0, // label 0 ($h) + wasm.OpcodeLocalGet, 0, // local.get 0 + wasm.OpcodeThrow, 0, // throw tag 0 + wasm.OpcodeI32Const, 2, // (unreachable but needed for type) + wasm.OpcodeEnd, // end try_table + wasm.OpcodeReturn, // return try_table result + wasm.OpcodeEnd, // end block $h + wasm.OpcodeReturn, // return + wasm.OpcodeEnd, // end func + }, + }}, + ExportSection: []wasm.Export{{Name: ExportedFunctionName, Type: wasm.ExternTypeFunc, Index: 0}}, + }, +} + +// TryTableCatchManyParamThrow: try_table with catch $e where the tag has 5 i32 parameters. +// This exercises tags with more than 4 parameters, exposing the hardcoded 4-param limit in +// caughtExceptionParams [4]uint64. The 5th (and beyond) parameter will be silently dropped +// under the current implementation. +// +// (module +// (tag $e (param i32 i32 i32 i32 i32)) +// (func (export "f") (param i32 i32 i32 i32 i32) (result i32 i32 i32 i32 i32) +// (block $h (result i32 i32 i32 i32 i32) +// (try_table (result i32 i32 i32 i32 i32) (catch $e $h) +// (local.get 0) +// (local.get 1) +// (local.get 2) +// (local.get 3) +// (local.get 4) +// (throw $e) +// (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0) (i32.const 0) +// ) +// (return) +// ) +// (return) +// ) +// ) +var TryTableCatchManyParamThrow = TestCase{ + Name: "try_table_catch_many_param_throw", + Module: &wasm.Module{ + TypeSection: []wasm.FunctionType{ + {Params: []wasm.ValueType{i32, i32, i32, i32, i32}}, // type 0: (i32x5) -> () for tag + {Params: []wasm.ValueType{i32, i32, i32, i32, i32}, Results: []wasm.ValueType{i32, i32, i32, i32, i32}}, // type 1: (i32x5) -> (i32x5) for func + {Results: []wasm.ValueType{i32, i32, i32, i32, i32}}, // type 2: () -> (i32x5) for block/try_table + }, + FunctionSection: []wasm.Index{1}, // func type 1 + TagSection: []wasm.Tag{{Type: 0}}, // tag type 0: (i32x5) -> () + CodeSection: []wasm.Code{{ + Body: []byte{ + wasm.OpcodeBlock, 0x02, // block $h (result i32 i32 i32 i32 i32) -- type index 2 + wasm.OpcodeTryTable, 0x02, // try_table (result i32 i32 i32 i32 i32) -- type index 2 + 1, // 1 catch clause + wasm.CatchKindCatch, // catch + 0, // tag index 0 ($e) + 0, // label 0 ($h) + wasm.OpcodeLocalGet, 0, // local.get 0 + wasm.OpcodeLocalGet, 1, // local.get 1 + wasm.OpcodeLocalGet, 2, // local.get 2 + wasm.OpcodeLocalGet, 3, // local.get 3 + wasm.OpcodeLocalGet, 4, // local.get 4 + wasm.OpcodeThrow, 0, // throw tag 0 + wasm.OpcodeI32Const, 0, // unreachable dummy results + wasm.OpcodeI32Const, 0, + wasm.OpcodeI32Const, 0, + wasm.OpcodeI32Const, 0, + wasm.OpcodeI32Const, 0, + wasm.OpcodeEnd, // end try_table + wasm.OpcodeReturn, // return try_table result + wasm.OpcodeEnd, // end block $h + wasm.OpcodeReturn, // return + wasm.OpcodeEnd, // end func + }, + }}, + ExportSection: []wasm.Export{{Name: ExportedFunctionName, Type: wasm.ExternTypeFunc, Index: 0}}, + }, +} + +// TryTableCatchWithReturnCall exercises the interaction between try_table and tail calls. +// The try_table body performs a conditional: +// - param=1: TryTableLeave (pops handler) + return_call $target → returns 99 +// - param=0: throw $e (caught by the catch clause) → catch handler returns 42 +// +// This verifies two things: +// +// 1. removeUntilRet does not accidentally remove the catch handler block that follows +// the tail-call path in the instruction stream. +// +// 2. TryTableLeave + return_call correctly pass control to the tail callee. +// +// (module +// (tag $e) +// (func $target (result i32) (i32.const 99)) +// (func (export "f") (param i32) (result i32) +// (block $h +// (try_table (catch $e $h) +// (if (local.get 0) +// (then (return_call $target)) +// (else (throw $e)) +// ) +// (unreachable) +// ) +// (return) +// ) +// (i32.const 42) +// ) +// ) +var TryTableCatchWithReturnCall = TestCase{ + Name: "try_table_catch_with_return_call", + Module: &wasm.Module{ + TypeSection: []wasm.FunctionType{ + vv, // type 0: () -> () for tag $e + {Results: []wasm.ValueType{i32}}, // type 1: () -> i32 for $target + {Params: []wasm.ValueType{i32}, Results: []wasm.ValueType{i32}}, // type 2: (i32) -> i32 for $f + }, + FunctionSection: []wasm.Index{1, 2}, // $target = func 0 (type 1), $f = func 1 (type 2) + TagSection: []wasm.Tag{{Type: 0}}, + CodeSection: []wasm.Code{ + {Body: []byte{wasm.OpcodeI32Const, 0xe3, 0x00, wasm.OpcodeEnd}}, // $target: () -> i32 (99 in signed LEB128) + {Body: []byte{ + // block $h (void) = label 0; catch $e delivers 0 values to label 0. + wasm.OpcodeBlock, blockSignature_vv, + // try_table (result i32): type is declared even though the body is unreachable + // (both if-branches diverge). The result is consumed by the return on the normal + // path (which is also unreachable), satisfying the validator. + wasm.OpcodeTryTable, 0x7F, + 1, // 1 catch clause + wasm.CatchKindCatch, // catch $e (no params) → label 0 ($h) + 0, // tag index 0 + 0, // label 0 ($h) + // body: conditional — both branches diverge. + wasm.OpcodeLocalGet, 0, + wasm.OpcodeIf, blockSignature_vv, // void if — both branches diverge + // then: TryTableLeave (pops handler) + return_call $target (tail call) + wasm.OpcodeTailCallReturnCall, 0, + wasm.OpcodeElse, + // else: throw $e → caught by try_table → branches to end of block $h + wasm.OpcodeThrow, 0, + wasm.OpcodeEnd, // end if + wasm.OpcodeUnreachable, // unreachable + wasm.OpcodeEnd, // end try_table + wasm.OpcodeReturn, // unreachable (never reached) + wasm.OpcodeEnd, // end block $h + // catch handler: reached when $e is caught (jumped to end of block $h) + wasm.OpcodeI32Const, 42, + wasm.OpcodeEnd, // end func + }}, + }, + ExportSection: []wasm.Export{{Name: ExportedFunctionName, Type: wasm.ExternTypeFunc, Index: 1}}, + }, +} + type TestCase struct { Name string Imported, Module *wasm.Module diff --git a/internal/engine/wazevo/wazevo_test.go b/internal/engine/wazevo/wazevo_test.go index e7fa3049d..6242b3b9e 100644 --- a/internal/engine/wazevo/wazevo_test.go +++ b/internal/engine/wazevo/wazevo_test.go @@ -73,4 +73,11 @@ func Test_ExecutionContextOffsets(t *testing.T) { require.Equal(t, wazevoapi.Offset(unsafe.Offsetof(execCtx.memoryWait32TrampolineAddress)), wazevoapi.ExecutionContextOffsetMemoryWait32TrampolineAddress) require.Equal(t, wazevoapi.Offset(unsafe.Offsetof(execCtx.memoryWait64TrampolineAddress)), wazevoapi.ExecutionContextOffsetMemoryWait64TrampolineAddress) require.Equal(t, wazevoapi.Offset(unsafe.Offsetof(execCtx.memoryNotifyTrampolineAddress)), wazevoapi.ExecutionContextOffsetMemoryNotifyTrampolineAddress) + require.Equal(t, wazevoapi.Offset(unsafe.Offsetof(execCtx.throwTrampolineAddress)), wazevoapi.ExecutionContextOffsetThrowTrampolineAddress) + require.Equal(t, wazevoapi.Offset(unsafe.Offsetof(execCtx.tryTableEnterTrampolineAddress)), wazevoapi.ExecutionContextOffsetTryTableEnterTrampolineAddress) + require.Equal(t, wazevoapi.Offset(unsafe.Offsetof(execCtx.tryTableLeaveTrampolineAddress)), wazevoapi.ExecutionContextOffsetTryTableLeaveTrampolineAddress) + require.Equal(t, wazevoapi.Offset(unsafe.Offsetof(execCtx.throwAllocTrampolineAddress)), wazevoapi.ExecutionContextOffsetThrowAllocTrampolineAddress) + require.Equal(t, wazevoapi.Offset(unsafe.Offsetof(execCtx.exceptionPtr)), wazevoapi.ExecutionContextOffsetExceptionPtr) + require.Equal(t, wazevoapi.Offset(unsafe.Offsetof(execCtx.exceptionParamsPtr)), wazevoapi.ExecutionContextOffsetExceptionParamsPtr) + require.Equal(t, wazevoapi.Offset(unsafe.Offsetof(execCtx.caughtExceptionClauseIdx)), wazevoapi.ExecutionContextOffsetCaughtExceptionClauseIdx) } diff --git a/internal/engine/wazevo/wazevoapi/exitcode.go b/internal/engine/wazevo/wazevoapi/exitcode.go index 5ad594982..1055e478a 100644 --- a/internal/engine/wazevo/wazevoapi/exitcode.go +++ b/internal/engine/wazevo/wazevoapi/exitcode.go @@ -30,6 +30,25 @@ const ( ExitCodeMemoryWait64 ExitCodeMemoryNotify ExitCodeUnalignedAtomic + // ExitCodeThrowAlloc is the first phase of wasm throw: Go allocates the + // Exception heap object (with Params sized to the tag's param count) and + // writes its Params data pointer to execCtx.exceptionParamsPtr. + // Compiled code then stores params directly into the Exception.Params slice, + // followed by ExitCodeThrow to search for a matching handler. + ExitCodeThrowAlloc + // ExitCodeThrow is the shared throw/throw_ref exit code. + // The exnref is passed on the stack. The handler searches for a + // matching catch clause and restores the stack checkpoint. + ExitCodeThrow + // ExitCodeNullReference is an exit code for a null reference trap (throw_ref with null exnref). + ExitCodeNullReference + // ExitCodeTryTableEnter is an exit code for entering a try_table block. + // The catch clause info is encoded in the upper bits. The dispatch loop + // saves the current SP/FP/returnAddress as a try handler checkpoint. + ExitCodeTryTableEnter + // ExitCodeTryTableLeave is an exit code for leaving a try_table block. + // The dispatch loop pops the most recent try handler. + ExitCodeTryTableLeave exitCodeMax ) @@ -86,6 +105,16 @@ func (e ExitCode) String() string { return "memory_wait64" case ExitCodeMemoryNotify: return "memory_notify" + case ExitCodeThrowAlloc: + return "throw_alloc" + case ExitCodeThrow: + return "throw" + case ExitCodeNullReference: + return "null_reference" + case ExitCodeTryTableEnter: + return "try_table_enter" + case ExitCodeTryTableLeave: + return "try_table_leave" } panic("TODO") } @@ -107,3 +136,15 @@ func ExitCodeCallGoFunctionWithIndex(index int, withListener bool) ExitCode { func GoFunctionIndexFromExitCode(exitCode ExitCode) int { return int(exitCode >> 8) } + +// TryTableIDFromExitCode extracts the try-table ID from an ExitCodeTryTableEnter +// exit code. Uses the same encoding as GoFunctionIndexFromExitCode (upper 24 bits). +func TryTableIDFromExitCode(exitCode ExitCode) int { + return GoFunctionIndexFromExitCode(exitCode) +} + +// CatchClauseInstance is a runtime catch clause with resolved tag index. +type CatchClauseInstance struct { + Kind byte // wasm.CatchKindCatch, etc. + TagIndex uint32 // module-local tag index +} diff --git a/internal/engine/wazevo/wazevoapi/offsetdata.go b/internal/engine/wazevo/wazevoapi/offsetdata.go index fe6161b04..854365f90 100644 --- a/internal/engine/wazevo/wazevoapi/offsetdata.go +++ b/internal/engine/wazevo/wazevoapi/offsetdata.go @@ -53,6 +53,23 @@ const ( ExecutionContextOffsetMemoryWait32TrampolineAddress Offset = 1160 ExecutionContextOffsetMemoryWait64TrampolineAddress Offset = 1168 ExecutionContextOffsetMemoryNotifyTrampolineAddress Offset = 1176 + // ExecutionContextOffsetThrowAllocTrampolineAddress is the address of the + // throw-alloc trampoline, which allocates the Exception heap object, + // sets exceptionParamsPtr, and returns the exnref. + ExecutionContextOffsetThrowAllocTrampolineAddress Offset = 1184 + ExecutionContextOffsetThrowTrampolineAddress Offset = 1192 + ExecutionContextOffsetTryTableEnterTrampolineAddress Offset = 1200 + ExecutionContextOffsetTryTableLeaveTrampolineAddress Offset = 1208 + // ExecutionContextOffsetExceptionPtr holds the pointer to the Exception struct, + // used on the throw side and by catch_ref/catch_all_ref handlers. + ExecutionContextOffsetExceptionPtr Offset = 1216 + // ExecutionContextOffsetExceptionParamsPtr points into the Exception's + // Params slice backing array. Used by both throw (store params) and + // catch (load params) sides. + ExecutionContextOffsetExceptionParamsPtr Offset = 1224 + // ExecutionContextOffsetCaughtExceptionClauseIdx is the matched catch clause index + // written by handleException and read by compiled handler dispatch code. + ExecutionContextOffsetCaughtExceptionClauseIdx Offset = 1232 ) // ModuleContextOffsetData allows the compilers to get the information about offsets to the fields of wazevo.moduleContextOpaque, @@ -66,6 +83,7 @@ type ModuleContextOffsetData struct { GlobalsBegin, TypeIDs1stElement, TablesBegin, + TagsBegin, BeforeListenerTrampolines1stElement, AfterListenerTrampolines1stElement, DataInstances1stElement, @@ -122,6 +140,11 @@ func (m *ModuleContextOffsetData) TableOffset(tableIndex int) Offset { return m.TablesBegin + Offset(tableIndex)*8 } +// TagOffset returns an offset of the i-th tag instance pointer. +func (m *ModuleContextOffsetData) TagOffset(tagIndex int) Offset { + return m.TagsBegin + Offset(tagIndex)*8 +} + // NewModuleContextOffsetData creates a ModuleContextOffsetData determining the structure of moduleContextOpaque for the given Module. // The structure is described in the comment of wazevo.moduleContextOpaque. func NewModuleContextOffsetData(m *wasm.Module, withListener bool) ModuleContextOffsetData { @@ -185,6 +208,15 @@ func NewModuleContextOffsetData(m *wasm.Module, withListener bool) ModuleContext ret.TablesBegin = -1 } + if tags := int(m.ImportTagCount) + len(m.TagSection); tags > 0 { + offset = align8(offset) + ret.TagsBegin = offset + // Pointers to *wasm.TagInstance. + offset += Offset(tags) * 8 + } else { + ret.TagsBegin = -1 + } + if withListener { offset = align8(offset) ret.BeforeListenerTrampolines1stElement = offset diff --git a/internal/engine/wazevo/wazevoapi/offsetdata_test.go b/internal/engine/wazevo/wazevoapi/offsetdata_test.go index 09cf6b712..23751b690 100644 --- a/internal/engine/wazevo/wazevoapi/offsetdata_test.go +++ b/internal/engine/wazevo/wazevoapi/offsetdata_test.go @@ -24,6 +24,7 @@ func TestNewModuleContextOffsetData(t *testing.T) { GlobalsBegin: -1, TypeIDs1stElement: -1, TablesBegin: -1, + TagsBegin: -1, BeforeListenerTrampolines1stElement: -1, AfterListenerTrampolines1stElement: -1, DataInstances1stElement: 8, @@ -41,6 +42,7 @@ func TestNewModuleContextOffsetData(t *testing.T) { GlobalsBegin: -1, TypeIDs1stElement: -1, TablesBegin: -1, + TagsBegin: -1, BeforeListenerTrampolines1stElement: -1, AfterListenerTrampolines1stElement: -1, DataInstances1stElement: 24, @@ -58,6 +60,7 @@ func TestNewModuleContextOffsetData(t *testing.T) { GlobalsBegin: -1, TypeIDs1stElement: -1, TablesBegin: -1, + TagsBegin: -1, BeforeListenerTrampolines1stElement: -1, AfterListenerTrampolines1stElement: -1, DataInstances1stElement: 24, @@ -75,6 +78,7 @@ func TestNewModuleContextOffsetData(t *testing.T) { GlobalsBegin: -1, TypeIDs1stElement: -1, TablesBegin: -1, + TagsBegin: -1, BeforeListenerTrampolines1stElement: -1, AfterListenerTrampolines1stElement: -1, DataInstances1stElement: 10*FunctionInstanceSize + 8, @@ -92,6 +96,7 @@ func TestNewModuleContextOffsetData(t *testing.T) { GlobalsBegin: -1, TypeIDs1stElement: -1, TablesBegin: -1, + TagsBegin: -1, BeforeListenerTrampolines1stElement: -1, AfterListenerTrampolines1stElement: -1, DataInstances1stElement: 10*FunctionInstanceSize + 24, @@ -117,6 +122,7 @@ func TestNewModuleContextOffsetData(t *testing.T) { GlobalsBegin: 32 + 10*FunctionInstanceSize, TypeIDs1stElement: 32 + 10*FunctionInstanceSize + 16*30, TablesBegin: 32 + 10*FunctionInstanceSize + 16*30 + 8, + TagsBegin: -1, BeforeListenerTrampolines1stElement: -1, AfterListenerTrampolines1stElement: -1, DataInstances1stElement: 32 + 10*FunctionInstanceSize + 16*30 + 8 + 8*15, @@ -143,6 +149,7 @@ func TestNewModuleContextOffsetData(t *testing.T) { GlobalsBegin: 32 + 10*FunctionInstanceSize, TypeIDs1stElement: 32 + 10*FunctionInstanceSize + 16*30, TablesBegin: 32 + 10*FunctionInstanceSize + 16*30 + 8, + TagsBegin: -1, BeforeListenerTrampolines1stElement: 32 + 10*FunctionInstanceSize + 16*30 + 8 + 8*15, AfterListenerTrampolines1stElement: 32 + 10*FunctionInstanceSize + 16*30 + 8 + 8*15 + 8, DataInstances1stElement: 32 + 10*FunctionInstanceSize + 16*30 + 8 + 8*15 + 16, diff --git a/internal/integration_test/engine/eh_hammer_test.go b/internal/integration_test/engine/eh_hammer_test.go new file mode 100644 index 000000000..c72347abf --- /dev/null +++ b/internal/integration_test/engine/eh_hammer_test.go @@ -0,0 +1,215 @@ +package adhoc + +import ( + "context" + "fmt" + "testing" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/experimental" + "github.com/tetratelabs/wazero/internal/platform" + "github.com/tetratelabs/wazero/internal/testing/require" + "github.com/tetratelabs/wazero/internal/wasm" +) + +// TestEHParallelCompilation is a hammer test that exercises parallel compilation +// of a module with many functions, each containing a try_table. Without proper +// synchronization of catch clause table IDs, parallel workers will assign +// conflicting IDs, causing runtime panics or wrong handler dispatch. +func TestEHParallelCompilation(t *testing.T) { + if !platform.CompilerSupported() { + t.Skip() + } + + const numFuncs = 32 + bin := buildEHModule(numFuncs) + + for _, workers := range []int{2, 4, 8} { + workers := workers + t.Run(fmt.Sprintf("workers=%d", workers), func(t *testing.T) { + ctx := experimental.WithCompilationWorkers(context.Background(), workers) + cfg := wazero.NewRuntimeConfigCompiler(). + WithCoreFeatures(api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling) + r := wazero.NewRuntimeWithConfig(ctx, cfg) + defer r.Close(ctx) + + mod, err := r.InstantiateWithConfig(ctx, bin, + wazero.NewModuleConfig().WithStartFunctions()) + require.NoError(t, err) + + for i := 0; i < numFuncs; i++ { + name := fmt.Sprintf("f%d", i) + res, err := mod.ExportedFunction(name).Call(ctx) + require.NoError(t, err, "function %s", name) + require.Equal(t, uint64(i), res[0], "function %s returned wrong value", name) + } + }) + } +} + +// buildEHModule constructs a wasm binary with `n` exported functions, each +// containing a try_table that catches a throw and returns the function index. +// +// Each function looks like: +// +// (func (export "fN") (result i32) +// block $caught +// try_table (catch_all $caught) +// throw $tag +// end +// i32.const -1 ;; unreachable +// return +// end +// i32.const N ;; caught: return function index +// ) +func buildEHModule(n int) []byte { + m := &wasm.Module{ + // One type: () -> (i32) + TypeSection: []wasm.FunctionType{ + {Results: []wasm.ValueType{wasm.ValueTypeI32}}, + {}, // tag type: () -> () + }, + // One tag with type 1 (empty params, empty results) + TagSection: []wasm.Tag{{Type: 1}}, + } + + for i := 0; i < n; i++ { + m.FunctionSection = append(m.FunctionSection, 0) // type 0: () -> (i32) + m.ExportSection = append(m.ExportSection, wasm.Export{ + Type: wasm.ExternTypeFunc, + Name: fmt.Sprintf("f%d", i), + Index: wasm.Index(i), + }) + + // Build the function body: + // block void ;; label 0 = $caught + // try_table (catch_all 0) + // throw 0 + // end + // i32.const -1 + // return + // end + // i32.const + // end + body := []byte{ + wasm.OpcodeBlock, 0x40, // block void + wasm.OpcodeTryTable, 0x40, // try_table void + 1, // catch count = 1 + wasm.CatchKindCatchAll, // catch_all + 0, // label index 0 (the enclosing block) + wasm.OpcodeThrow, 0, // throw tag 0 + wasm.OpcodeEnd, // end try_table + wasm.OpcodeI32Const, 0x7f, // i32.const -1 (signed LEB128) + wasm.OpcodeReturn, + wasm.OpcodeEnd, // end block + } + // i32.const — encode i as signed LEB128 + body = append(body, wasm.OpcodeI32Const) + body = appendSleb128(body, int32(i)) + body = append(body, wasm.OpcodeEnd) // end function + + m.CodeSection = append(m.CodeSection, wasm.Code{Body: body}) + } + + return encodeModule(m) +} + +// appendSleb128 appends a signed LEB128 encoding of v to b. +func appendSleb128(b []byte, v int32) []byte { + for { + c := byte(v & 0x7f) + v >>= 7 + if (v == 0 && c&0x40 == 0) || (v == -1 && c&0x40 != 0) { + return append(b, c) + } + b = append(b, c|0x80) + } +} + +// encodeModule encodes the wasm.Module into a valid wasm binary. +// This is a minimal encoder that handles the sections used by buildEHModule. +func encodeModule(m *wasm.Module) []byte { + var buf []byte + // Magic + version + buf = append(buf, 0x00, 0x61, 0x73, 0x6d) // \0asm + buf = append(buf, 0x01, 0x00, 0x00, 0x00) // version 1 + + // Type section (id=1) + buf = appendSection(buf, 1, func(s []byte) []byte { + s = appendUleb128(s, uint32(len(m.TypeSection))) + for _, ft := range m.TypeSection { + s = append(s, 0x60) // func type + s = appendUleb128(s, uint32(len(ft.Params))) + s = append(s, ft.Params...) + s = appendUleb128(s, uint32(len(ft.Results))) + s = append(s, ft.Results...) + } + return s + }) + + // Function section (id=3) + buf = appendSection(buf, 3, func(s []byte) []byte { + s = appendUleb128(s, uint32(len(m.FunctionSection))) + for _, idx := range m.FunctionSection { + s = appendUleb128(s, idx) + } + return s + }) + + // Tag section (id=13) + buf = appendSection(buf, 13, func(s []byte) []byte { + s = appendUleb128(s, uint32(len(m.TagSection))) + for _, tag := range m.TagSection { + s = append(s, 0x00) // attribute byte (must be 0) + s = appendUleb128(s, tag.Type) + } + return s + }) + + // Export section (id=7) + buf = appendSection(buf, 7, func(s []byte) []byte { + s = appendUleb128(s, uint32(len(m.ExportSection))) + for _, exp := range m.ExportSection { + s = appendUleb128(s, uint32(len(exp.Name))) + s = append(s, exp.Name...) + s = append(s, exp.Type) + s = appendUleb128(s, exp.Index) + } + return s + }) + + // Code section (id=10) + buf = appendSection(buf, 10, func(s []byte) []byte { + s = appendUleb128(s, uint32(len(m.CodeSection))) + for _, code := range m.CodeSection { + // Each code entry: size + locals_count(0) + body + funcBody := appendUleb128(nil, 0) // 0 locals + funcBody = append(funcBody, code.Body...) + s = appendUleb128(s, uint32(len(funcBody))) + s = append(s, funcBody...) + } + return s + }) + + return buf +} + +func appendSection(buf []byte, id byte, buildContent func([]byte) []byte) []byte { + content := buildContent(nil) + buf = append(buf, id) + buf = appendUleb128(buf, uint32(len(content))) + buf = append(buf, content...) + return buf +} + +func appendUleb128(b []byte, v uint32) []byte { + for { + c := byte(v & 0x7f) + v >>= 7 + if v == 0 { + return append(b, c) + } + b = append(b, c|0x80) + } +} diff --git a/internal/integration_test/engine/exceptions_test.go b/internal/integration_test/engine/exceptions_test.go new file mode 100644 index 000000000..a53f20957 --- /dev/null +++ b/internal/integration_test/engine/exceptions_test.go @@ -0,0 +1,187 @@ +package adhoc + +// Exception handling integration tests for the interpreter engine. +// +// Background: these tests cover the Emscripten/pdfium-style EH pattern where +// exceptions propagate across multiple function call frames, each of which may +// have its own try_table handler (catch_all_ref + throw_ref cleanup pattern). +// +// The interpreter bug fixed here: when an inner callWithUnwind (e.g., in a +// "child" function) recovered a *thrownException whose matching try_table +// handler belonged to an outer (grandparent) callNativeFunc invocation, +// doRestore incorrectly restored grandparent's frame while still inside +// child's callNativeFunc. The child then started executing grandparent's +// body, eventually calling popTryHandler on an already-empty slice and +// panicking with "slice bounds out of range [:-1]". +// +// The fix: callWithUnwind only handles handlers whose savedFrames length is >= +// the caller's frame count (i.e., handlers set up at the current or deeper +// call depth). Handlers from outer invocations are re-panicked so that the +// correct outer callWithUnwind catches them. + +import ( + "context" + _ "embed" + "testing" + + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/experimental" + "github.com/tetratelabs/wazero/internal/platform" + "github.com/tetratelabs/wazero/internal/testing/require" + "github.com/tetratelabs/wazero/internal/wasmruntime" +) + +//go:embed testdata/eh_cross_callnative.wasm +var ehCrossCallnativeWasm []byte + +//go:embed testdata/eh_pdfium.wasm +var ehPdfiumWasm []byte + +//go:embed testdata/eh_throw_ref_null.wasm +var ehThrowRefNullWasm []byte + +//go:embed testdata/eh_br_orphan.wasm +var ehBrOrphanWasm []byte + +//go:embed testdata/eh_br_stale_handler.wasm +var ehBrStaleHandlerWasm []byte + +// TestExceptionHandlingInterpreter runs EH tests only for the interpreter. +func TestExceptionHandlingInterpreter(t *testing.T) { + cfg := wazero.NewRuntimeConfigInterpreter(). + WithCoreFeatures(api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling) + runEHTests(t, cfg) +} + +// TestExceptionHandlingCompiler runs EH tests for the compiler where supported. +func TestExceptionHandlingCompiler(t *testing.T) { + if !platform.CompilerSupported() { + t.Skip() + } + cfg := wazero.NewRuntimeConfigCompiler(). + WithCoreFeatures(api.CoreFeaturesV2 | experimental.CoreFeaturesExceptionHandling) + runEHTests(t, cfg) +} + +func runEHTests(t *testing.T, cfg wazero.RuntimeConfig) { + t.Run("cross_frame_catch", func(t *testing.T) { + testEHCrossFrameCatch(t, cfg) + }) + t.Run("pdfium_rethrow_pattern", func(t *testing.T) { + testEHPdfiumRethrow(t, cfg) + }) + t.Run("throw_ref_null", func(t *testing.T) { + testThrowRefNull(t, cfg) + }) + t.Run("br_exits_try_table", func(t *testing.T) { + testBrExitsTryTable(t, cfg) + }) + t.Run("br_stale_handler", func(t *testing.T) { + testBrStaleHandler(t, cfg) + }) +} + +// testEHCrossFrameCatch is the core reproducer for the interpreter bug: +// try_table in grandparent, exception thrown in grandchild, +// propagating through child which has no handler of its own. +// The grandparent's handler must catch it correctly. +func testEHCrossFrameCatch(t *testing.T, cfg wazero.RuntimeConfig) { + ctx := context.Background() + r := wazero.NewRuntimeWithConfig(ctx, cfg) + defer r.Close(ctx) + + mod, err := r.InstantiateWithConfig(ctx, ehCrossCallnativeWasm, + wazero.NewModuleConfig().WithStartFunctions()) + require.NoError(t, err) + + // grandparent has a try_table, calls child, child calls grandchild which throws. + // Grandparent's handler must catch via cross-frame propagation. + res, err := mod.ExportedFunction("test_cross_frame_catch").Call(ctx) + require.NoError(t, err) + require.Equal(t, int32(1), api.DecodeI32(res[0])) + + // Rethrow pattern: child has catch_all_ref + throw_ref, grandparent catches the rethrow. + res, err = mod.ExportedFunction("test_rethrow_cross_frame").Call(ctx) + require.NoError(t, err) + require.Equal(t, int32(2), api.DecodeI32(res[0])) +} + +// testEHPdfiumRethrow tests the Emscripten destructor-cleanup pattern: +// catch_all_ref captures the exnref, runs cleanup, then rethrows via throw_ref. +// This pattern appears in pdfium.wasm for C++ exception handling. +func testEHPdfiumRethrow(t *testing.T, cfg wazero.RuntimeConfig) { + ctx := context.Background() + r := wazero.NewRuntimeWithConfig(ctx, cfg) + defer r.Close(ctx) + + mod, err := r.InstantiateWithConfig(ctx, ehPdfiumWasm, + wazero.NewModuleConfig().WithStartFunctions()) + require.NoError(t, err) + + // One-level: leaf throws, level2 catches + rethrows, outer catches. + res, err := mod.ExportedFunction("test_one_level_rethrow").Call(ctx) + require.NoError(t, err) + require.Equal(t, int32(1), api.DecodeI32(res[0])) + + // Two-level: throw → catch_all_ref + throw_ref → catch_all_ref + throw_ref → catch. + res, err = mod.ExportedFunction("test_two_level_rethrow").Call(ctx) + require.NoError(t, err) + require.Equal(t, int32(1), api.DecodeI32(res[0])) +} + +// testThrowRefNull verifies that throw_ref on a null exnref traps with +// "null reference" (not "unreachable"). This was a bug where the interpreter +// used ErrRuntimeUnreachable instead of ErrRuntimeNullReference. +func testThrowRefNull(t *testing.T, cfg wazero.RuntimeConfig) { + ctx := context.Background() + r := wazero.NewRuntimeWithConfig(ctx, cfg) + defer r.Close(ctx) + + mod, err := r.InstantiateWithConfig(ctx, ehThrowRefNullWasm, + wazero.NewModuleConfig().WithStartFunctions()) + require.NoError(t, err) + + // Call with null exnref (0) — should trap as "null reference". + _, err = mod.ExportedFunction("throw_ref_null").Call(ctx, 0) + require.ErrorIs(t, err, wasmruntime.ErrRuntimeNullReference) +} + +// testBrExitsTryTable verifies that br/br_if that exits a try_table block +// correctly pops the try handler. Without the fix, orphaned handlers would +// cause a popTryHandler underflow panic ("slice bounds out of range [:-1]"). +func testBrExitsTryTable(t *testing.T, cfg wazero.RuntimeConfig) { + ctx := context.Background() + r := wazero.NewRuntimeWithConfig(ctx, cfg) + defer r.Close(ctx) + + mod, err := r.InstantiateWithConfig(ctx, ehBrOrphanWasm, + wazero.NewModuleConfig().WithStartFunctions()) + require.NoError(t, err) + + // The function calls loop_with_try (which exits try_table via br_if), + // then catches a throw in its own try_table. Should return 1. + res, err := mod.ExportedFunction("test").Call(ctx) + require.NoError(t, err) + require.Equal(t, int32(1), api.DecodeI32(res[0])) +} + +// testBrStaleHandler verifies that br exiting a try_table pops the handler +// so it doesn't interfere with later exception dispatch. Without the fix, +// the stale handler from try_table A incorrectly catches a throw meant for +// the outer handler, returning 99 instead of 1. +func testBrStaleHandler(t *testing.T, cfg wazero.RuntimeConfig) { + ctx := context.Background() + r := wazero.NewRuntimeWithConfig(ctx, cfg) + defer r.Close(ctx) + + mod, err := r.InstantiateWithConfig(ctx, ehBrStaleHandlerWasm, + wazero.NewModuleConfig().WithStartFunctions()) + require.NoError(t, err) + + // Without fix: stale handler A catches $tag1 -> wrong checkpoint restore. + // With fix: outer handler catches $tag1 -> returns 1. + res, err := mod.ExportedFunction("test").Call(ctx) + require.NoError(t, err) + require.Equal(t, int32(1), api.DecodeI32(res[0])) +} diff --git a/internal/integration_test/engine/testdata/eh_br_orphan.wasm b/internal/integration_test/engine/testdata/eh_br_orphan.wasm new file mode 100644 index 000000000..475227966 Binary files /dev/null and b/internal/integration_test/engine/testdata/eh_br_orphan.wasm differ diff --git a/internal/integration_test/engine/testdata/eh_br_orphan.wat b/internal/integration_test/engine/testdata/eh_br_orphan.wat new file mode 100644 index 000000000..a10243237 --- /dev/null +++ b/internal/integration_test/engine/testdata/eh_br_orphan.wat @@ -0,0 +1,54 @@ +;; Hypothesis: br_if inside try_table body that exits the try_table block +;; skips the popTryHandler at the continuation label, leaving an orphaned handler. +;; The orphaned handler then incorrectly catches a later exception. +(module + (tag $e0) + + ;; This function has a try_table wrapping a loop. + ;; When br_if fires to exit the loop, it also exits the try_table + ;; without going through try_table's `end` (so popTryHandler is skipped). + (func $loop_with_try (param $n i32) + block $outer ;; $outer is the catch target + try_table (catch_all $outer) ;; handler pushed, targets $outer end + loop $loop + local.get $n + i32.eqz + br_if $outer ;; exits try_table body — skips popTryHandler! + local.get $n + i32.const 1 + i32.sub + local.set $n + br $loop + end + ;; only reached if n was 0 from the start; popTryHandler runs + end ;; try_table end: emits popTryHandler (only on fall-through path) + end ;; $outer + ) + + (func $throw + throw $e0 + ) + + (func (export "test") (result i32) + ;; Call loop_with_try — leaves orphaned handler in ce.tryHandlers + i32.const 3 + call $loop_with_try + + ;; Now throw inside a proper try_table. + ;; Correct: caught by the inner try_table → returns 1. + ;; Bug: caught by the orphaned handler from loop_with_try → wrong result. + block $done + block $catch + try_table (catch_all $catch) + call $throw + end + br $done + end + ;; exception caught — expected path + i32.const 1 + return + end + ;; no exception — shouldn't happen + i32.const 0 + ) +) diff --git a/internal/integration_test/engine/testdata/eh_br_stale_handler.wasm b/internal/integration_test/engine/testdata/eh_br_stale_handler.wasm new file mode 100644 index 000000000..c235c8769 Binary files /dev/null and b/internal/integration_test/engine/testdata/eh_br_stale_handler.wasm differ diff --git a/internal/integration_test/engine/testdata/eh_br_stale_handler.wat b/internal/integration_test/engine/testdata/eh_br_stale_handler.wat new file mode 100644 index 000000000..bf9876baa --- /dev/null +++ b/internal/integration_test/engine/testdata/eh_br_stale_handler.wat @@ -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 + ) +) diff --git a/internal/integration_test/engine/testdata/eh_cross_callnative.wasm b/internal/integration_test/engine/testdata/eh_cross_callnative.wasm new file mode 100644 index 000000000..66549e11f Binary files /dev/null and b/internal/integration_test/engine/testdata/eh_cross_callnative.wasm differ diff --git a/internal/integration_test/engine/testdata/eh_cross_callnative.wat b/internal/integration_test/engine/testdata/eh_cross_callnative.wat new file mode 100644 index 000000000..9108925e5 --- /dev/null +++ b/internal/integration_test/engine/testdata/eh_cross_callnative.wat @@ -0,0 +1,75 @@ +;; Reproducer: try_table in grandparent, exception thrown in grandchild, +;; caught by grandparent's handler via cross-frame doRestore. +;; The `eh` branch interpreter bug: after doRestore removes the parent frame, +;; parent's callNativeFunc wrongly continues executing grandparent's body. +(module + (tag $e0 (param)) + + ;; grandchild: just throws + (func $grandchild + throw $e0 + ) + + ;; child: calls grandchild (no try_table of its own) + ;; exception propagates out of child to grandparent's handler + (func $child + call $grandchild + ) + + ;; grandparent: has a try_table, calls child + ;; When child's exception is caught by grandparent's handler, + ;; doRestore removes child's frame from ce.frames. + ;; child's callNativeFunc (running in callWithUnwind) must return, + ;; not continue executing grandparent's body. + (func $grandparent (result i32) + block $caught + try_table (catch_all $caught) + call $child + end + ;; No exception (unreachable in this test) + i32.const -1 + return + end + ;; Exception caught: return 1 + i32.const 1 + ) + + (func (export "test_cross_frame_catch") (result i32) + call $grandparent + ) + + ;; More realistic pdfium-style: grandparent has catch_all_ref, + ;; child has inner catch_all_ref + throw_ref, exception re-caught by grandparent + (func $child_rethrow + (local $x exnref) + block $done + block $catch (result exnref) + try_table (catch_all_ref $catch) + call $grandchild + end + br $done + end + local.set $x + local.get $x + throw_ref ;; rethrow: grandparent's handler must catch this + end + ) + + (func $grandparent_catches_rethrow (result i32) + (local $x exnref) + block $done + block $catch (result exnref) + try_table (catch_all_ref $catch) + call $child_rethrow + end + br $done + end + local.set $x + end + i32.const 2 ;; caught the rethrow + ) + + (func (export "test_rethrow_cross_frame") (result i32) + call $grandparent_catches_rethrow + ) +) diff --git a/internal/integration_test/engine/testdata/eh_pdfium.wasm b/internal/integration_test/engine/testdata/eh_pdfium.wasm new file mode 100644 index 000000000..bc1876dca Binary files /dev/null and b/internal/integration_test/engine/testdata/eh_pdfium.wasm differ diff --git a/internal/integration_test/engine/testdata/eh_pdfium.wat b/internal/integration_test/engine/testdata/eh_pdfium.wat new file mode 100644 index 000000000..ce8fbe3fa --- /dev/null +++ b/internal/integration_test/engine/testdata/eh_pdfium.wat @@ -0,0 +1,71 @@ +;; Tests pdfium/Emscripten EH pattern: +;; - throw in leaf +;; - catch_all_ref + throw_ref (Emscripten cleanup/rethrow pattern) at multiple call levels +;; - cross-frame exception propagation +(module + (tag $e0 (param)) + + ;; leaf: throws + (func $leaf + throw $e0 + ) + + ;; level2: catch_all_ref, cleanup, rethrow — like pdfium's inner destructor wrapper + (func $level2 + (local $x exnref) + block $done + block $catch (result exnref) + try_table (catch_all_ref $catch) + call $leaf + end + br $done + end + local.set $x + ;; cleanup ... + local.get $x + throw_ref + end + ) + + ;; level1: outer catch_all_ref, calls level2, then itself rethrows + ;; This tests: inner catch_all_ref -> throw_ref -> outer catch_all_ref -> throw_ref + (func $level1 + (local $x exnref) + block $done + block $catch (result exnref) + try_table (catch_all_ref $catch) + call $level2 + end + br $done + end + local.set $x + ;; outer cleanup ... + local.get $x + throw_ref + end + ) + + ;; top: calls level1 and catches with catch_all + (func (export "test_two_level_rethrow") (result i32) + block $caught + try_table (catch_all $caught) + call $level1 + end + i32.const -1 + return + end + i32.const 1 + ) + + ;; Verify the leaf throws are caught correctly at each level + (func (export "test_one_level_rethrow") (result i32) + block $caught + try_table (catch_all $caught) + call $level2 + end + i32.const -1 + return + end + i32.const 1 + ) +) diff --git a/internal/integration_test/engine/testdata/eh_throw_ref_null.wasm b/internal/integration_test/engine/testdata/eh_throw_ref_null.wasm new file mode 100644 index 000000000..be7b7c31b Binary files /dev/null and b/internal/integration_test/engine/testdata/eh_throw_ref_null.wasm differ diff --git a/internal/integration_test/engine/testdata/eh_throw_ref_null.wat b/internal/integration_test/engine/testdata/eh_throw_ref_null.wat new file mode 100644 index 000000000..a7add575b --- /dev/null +++ b/internal/integration_test/engine/testdata/eh_throw_ref_null.wat @@ -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 + ) +) diff --git a/internal/integration_test/spectest/exception-handling/spec_test.go b/internal/integration_test/spectest/exception-handling/spec_test.go new file mode 100644 index 000000000..c77e3df61 --- /dev/null +++ b/internal/integration_test/spectest/exception-handling/spec_test.go @@ -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) +} diff --git a/internal/integration_test/spectest/exception-handling/testdata/tag.0.wasm b/internal/integration_test/spectest/exception-handling/testdata/tag.0.wasm new file mode 100644 index 000000000..47a521e66 Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/tag.0.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/tag.1.wasm b/internal/integration_test/spectest/exception-handling/testdata/tag.1.wasm new file mode 100644 index 000000000..4a1a7f147 Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/tag.1.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/tag.2.wasm b/internal/integration_test/spectest/exception-handling/testdata/tag.2.wasm new file mode 100644 index 000000000..a223c5155 Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/tag.2.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/tag.3.wasm b/internal/integration_test/spectest/exception-handling/testdata/tag.3.wasm new file mode 100644 index 000000000..c3c1d6fe0 Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/tag.3.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/tag.4.wasm b/internal/integration_test/spectest/exception-handling/testdata/tag.4.wasm new file mode 100644 index 000000000..d277b404b Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/tag.4.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/tag.5.wasm b/internal/integration_test/spectest/exception-handling/testdata/tag.5.wasm new file mode 100644 index 000000000..5cef55b25 Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/tag.5.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/tag.6.wasm b/internal/integration_test/spectest/exception-handling/testdata/tag.6.wasm new file mode 100644 index 000000000..d0c2c860c Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/tag.6.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/tag.7.wasm b/internal/integration_test/spectest/exception-handling/testdata/tag.7.wasm new file mode 100644 index 000000000..5aab07164 Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/tag.7.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/tag.json b/internal/integration_test/spectest/exception-handling/testdata/tag.json new file mode 100644 index 000000000..66cc76514 --- /dev/null +++ b/internal/integration_test/spectest/exception-handling/testdata/tag.json @@ -0,0 +1 @@ +{"source_filename":"./tag.wast","commands":[{"type":"module","line":3,"filename":"tag.0.wasm","module_type":"binary"},{"type":"register","line":11,"as":"test"},{"type":"module","line":13,"filename":"tag.1.wasm","module_type":"binary"},{"type":"assert_invalid","line":19,"filename":"tag.2.wasm","module_type":"binary","text":"non-empty tag result type"},{"type":"assert_invalid","line":23,"filename":"tag.3.wasm","module_type":"binary","text":"non-empty tag result type"},{"type":"module","line":30,"filename":"tag.4.wasm","module_type":"binary"},{"type":"register","line":38,"as":"M"},{"type":"module","line":40,"filename":"tag.5.wasm","module_type":"binary"},{"type":"assert_unlinkable","line":49,"filename":"tag.6.wasm","module_type":"binary","text":"incompatible import type"},{"type":"assert_unlinkable","line":60,"filename":"tag.7.wasm","module_type":"binary","text":"incompatible import type"}]} \ No newline at end of file diff --git a/internal/integration_test/spectest/exception-handling/testdata/tag.wast b/internal/integration_test/spectest/exception-handling/testdata/tag.wast new file mode 100644 index 000000000..b2ec2864f --- /dev/null +++ b/internal/integration_test/spectest/exception-handling/testdata/tag.wast @@ -0,0 +1,65 @@ +;; Test tag section + +(module + (tag) + (tag (param i32)) + (tag (export "t2") (param i32)) + (tag $t3 (param i32 f32)) + (export "t3" (tag 3)) +) + +(register "test") + +(module + (tag $t0 (import "test" "t2") (param i32)) + (import "test" "t3" (tag $t1 (param i32 f32))) +) + +(assert_invalid + (module (tag (result i32))) + "non-empty tag result type" +) +(assert_invalid + (module (import "" "" (tag (result i32)))) + "non-empty tag result type" +) + + +;; Link-time typing + +(module + (rec + (type $t1 (func)) + (type $t2 (func)) + ) + (tag (export "tag") (type $t1)) +) + +(register "M") + +(module + (rec + (type $t1 (func)) + (type $t2 (func)) + ) + (tag (import "M" "tag") (type $t1)) +) + +(assert_unlinkable + (module + (rec + (type $t1 (func)) + (type $t2 (func)) + ) + (tag (import "M" "tag") (type $t2)) + ) + "incompatible import type" +) + +(assert_unlinkable + (module + (type $t (func)) + (tag (import "M" "tag") (type $t)) + ) + "incompatible import type" +) diff --git a/internal/integration_test/spectest/exception-handling/testdata/throw.0.wasm b/internal/integration_test/spectest/exception-handling/testdata/throw.0.wasm new file mode 100644 index 000000000..7f5c17d77 Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/throw.0.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/throw.1.wasm b/internal/integration_test/spectest/exception-handling/testdata/throw.1.wasm new file mode 100644 index 000000000..9d56f8a9b Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/throw.1.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/throw.2.wasm b/internal/integration_test/spectest/exception-handling/testdata/throw.2.wasm new file mode 100644 index 000000000..b22d59e6a Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/throw.2.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/throw.3.wasm b/internal/integration_test/spectest/exception-handling/testdata/throw.3.wasm new file mode 100644 index 000000000..03f9b9165 Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/throw.3.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/throw.json b/internal/integration_test/spectest/exception-handling/testdata/throw.json new file mode 100644 index 000000000..fb328fde8 --- /dev/null +++ b/internal/integration_test/spectest/exception-handling/testdata/throw.json @@ -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]"}]} \ No newline at end of file diff --git a/internal/integration_test/spectest/exception-handling/testdata/throw.wast b/internal/integration_test/spectest/exception-handling/testdata/throw.wast new file mode 100644 index 000000000..dc1aa4a98 --- /dev/null +++ b/internal/integration_test/spectest/exception-handling/testdata/throw.wast @@ -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]") diff --git a/internal/integration_test/spectest/exception-handling/testdata/throw_ref.0.wasm b/internal/integration_test/spectest/exception-handling/testdata/throw_ref.0.wasm new file mode 100644 index 000000000..77f045cb6 Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/throw_ref.0.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/throw_ref.1.wasm b/internal/integration_test/spectest/exception-handling/testdata/throw_ref.1.wasm new file mode 100644 index 000000000..42b8a076a Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/throw_ref.1.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/throw_ref.2.wasm b/internal/integration_test/spectest/exception-handling/testdata/throw_ref.2.wasm new file mode 100644 index 000000000..f4eb79305 Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/throw_ref.2.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/throw_ref.json b/internal/integration_test/spectest/exception-handling/testdata/throw_ref.json new file mode 100644 index 000000000..1ef35a138 --- /dev/null +++ b/internal/integration_test/spectest/exception-handling/testdata/throw_ref.json @@ -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"}]} \ No newline at end of file diff --git a/internal/integration_test/spectest/exception-handling/testdata/throw_ref.wast b/internal/integration_test/spectest/exception-handling/testdata/throw_ref.wast new file mode 100644 index 000000000..f59710a1f --- /dev/null +++ b/internal/integration_test/spectest/exception-handling/testdata/throw_ref.wast @@ -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") diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.0.wasm b/internal/integration_test/spectest/exception-handling/testdata/try_table.0.wasm new file mode 100644 index 000000000..c0164f131 Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/try_table.0.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.1.wasm b/internal/integration_test/spectest/exception-handling/testdata/try_table.1.wasm new file mode 100644 index 000000000..d7b4476e8 Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/try_table.1.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.10.wasm b/internal/integration_test/spectest/exception-handling/testdata/try_table.10.wasm new file mode 100644 index 000000000..d8aff845f Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/try_table.10.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.11.wasm b/internal/integration_test/spectest/exception-handling/testdata/try_table.11.wasm new file mode 100644 index 000000000..1d0b4d697 Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/try_table.11.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.12.wasm b/internal/integration_test/spectest/exception-handling/testdata/try_table.12.wasm new file mode 100644 index 000000000..b9a0dd659 Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/try_table.12.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.13.wasm b/internal/integration_test/spectest/exception-handling/testdata/try_table.13.wasm new file mode 100644 index 000000000..5e2d61019 Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/try_table.13.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.14.wasm b/internal/integration_test/spectest/exception-handling/testdata/try_table.14.wasm new file mode 100644 index 000000000..f1db9c1cb Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/try_table.14.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.15.wasm b/internal/integration_test/spectest/exception-handling/testdata/try_table.15.wasm new file mode 100644 index 000000000..753e3287c Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/try_table.15.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.16.wasm b/internal/integration_test/spectest/exception-handling/testdata/try_table.16.wasm new file mode 100644 index 000000000..75ad15761 Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/try_table.16.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.2.wasm b/internal/integration_test/spectest/exception-handling/testdata/try_table.2.wasm new file mode 100644 index 000000000..c84fc13e9 Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/try_table.2.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.3.wat b/internal/integration_test/spectest/exception-handling/testdata/try_table.3.wat new file mode 100644 index 000000000..d8fcc08e3 --- /dev/null +++ b/internal/integration_test/spectest/exception-handling/testdata/try_table.3.wat @@ -0,0 +1 @@ +(module (func (catch_all))) \ No newline at end of file diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.4.wat b/internal/integration_test/spectest/exception-handling/testdata/try_table.4.wat new file mode 100644 index 000000000..7c13d41dd --- /dev/null +++ b/internal/integration_test/spectest/exception-handling/testdata/try_table.4.wat @@ -0,0 +1 @@ +(module (tag $e) (func (catch $e))) \ No newline at end of file diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.5.wasm b/internal/integration_test/spectest/exception-handling/testdata/try_table.5.wasm new file mode 100644 index 000000000..726e3f601 Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/try_table.5.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.6.wasm b/internal/integration_test/spectest/exception-handling/testdata/try_table.6.wasm new file mode 100644 index 000000000..a2f98051b Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/try_table.6.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.7.wasm b/internal/integration_test/spectest/exception-handling/testdata/try_table.7.wasm new file mode 100644 index 000000000..501903cfa Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/try_table.7.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.8.wasm b/internal/integration_test/spectest/exception-handling/testdata/try_table.8.wasm new file mode 100644 index 000000000..6f506f45c Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/try_table.8.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.9.wasm b/internal/integration_test/spectest/exception-handling/testdata/try_table.9.wasm new file mode 100644 index 000000000..c7ff46b19 Binary files /dev/null and b/internal/integration_test/spectest/exception-handling/testdata/try_table.9.wasm differ diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.json b/internal/integration_test/spectest/exception-handling/testdata/try_table.json new file mode 100644 index 000000000..6072f023f --- /dev/null +++ b/internal/integration_test/spectest/exception-handling/testdata/try_table.json @@ -0,0 +1 @@ +{"source_filename":"./try_table.wast","commands":[{"type":"module","line":3,"filename":"try_table.0.wasm","module_type":"binary"},{"type":"register","line":8,"as":"test"},{"type":"module","line":10,"filename":"try_table.1.wasm","module_type":"binary"},{"type":"assert_return","line":282,"action":{"type":"invoke","field":"simple-throw-catch","args":[{"type":"i32","value":"0"}]},"expected":[{"type":"i32","value":"23"}]},{"type":"assert_return","line":283,"action":{"type":"invoke","field":"simple-throw-catch","args":[{"type":"i32","value":"1"}]},"expected":[{"type":"i32","value":"42"}]},{"type":"assert_trap","line":285,"action":{"type":"invoke","field":"unreachable-not-caught","args":[]},"text":"unreachable"},{"type":"assert_return","line":287,"action":{"type":"invoke","field":"trap-in-callee","args":[{"type":"i32","value":"7"},{"type":"i32","value":"2"}]},"expected":[{"type":"i32","value":"3"}]},{"type":"assert_trap","line":288,"action":{"type":"invoke","field":"trap-in-callee","args":[{"type":"i32","value":"1"},{"type":"i32","value":"0"}]},"text":"integer divide by zero"},{"type":"assert_return","line":290,"action":{"type":"invoke","field":"catch-complex-1","args":[{"type":"i32","value":"0"}]},"expected":[{"type":"i32","value":"3"}]},{"type":"assert_return","line":291,"action":{"type":"invoke","field":"catch-complex-1","args":[{"type":"i32","value":"1"}]},"expected":[{"type":"i32","value":"4"}]},{"type":"assert_exception","line":292,"action":{"type":"invoke","field":"catch-complex-1","args":[{"type":"i32","value":"2"}]}},{"type":"assert_return","line":294,"action":{"type":"invoke","field":"catch-complex-2","args":[{"type":"i32","value":"0"}]},"expected":[{"type":"i32","value":"3"}]},{"type":"assert_return","line":295,"action":{"type":"invoke","field":"catch-complex-2","args":[{"type":"i32","value":"1"}]},"expected":[{"type":"i32","value":"4"}]},{"type":"assert_exception","line":296,"action":{"type":"invoke","field":"catch-complex-2","args":[{"type":"i32","value":"2"}]}},{"type":"assert_return","line":298,"action":{"type":"invoke","field":"throw-catch-param-i32","args":[{"type":"i32","value":"0"}]},"expected":[{"type":"i32","value":"0"}]},{"type":"assert_return","line":299,"action":{"type":"invoke","field":"throw-catch-param-i32","args":[{"type":"i32","value":"1"}]},"expected":[{"type":"i32","value":"1"}]},{"type":"assert_return","line":300,"action":{"type":"invoke","field":"throw-catch-param-i32","args":[{"type":"i32","value":"10"}]},"expected":[{"type":"i32","value":"10"}]},{"type":"assert_return","line":302,"action":{"type":"invoke","field":"throw-catch-param-f32","args":[{"type":"f32","value":"1084227584"}]},"expected":[{"type":"f32","value":"1084227584"}]},{"type":"assert_return","line":303,"action":{"type":"invoke","field":"throw-catch-param-f32","args":[{"type":"f32","value":"1093140480"}]},"expected":[{"type":"f32","value":"1093140480"}]},{"type":"assert_return","line":305,"action":{"type":"invoke","field":"throw-catch-param-i64","args":[{"type":"i64","value":"5"}]},"expected":[{"type":"i64","value":"5"}]},{"type":"assert_return","line":306,"action":{"type":"invoke","field":"throw-catch-param-i64","args":[{"type":"i64","value":"0"}]},"expected":[{"type":"i64","value":"0"}]},{"type":"assert_return","line":307,"action":{"type":"invoke","field":"throw-catch-param-i64","args":[{"type":"i64","value":"-1"}]},"expected":[{"type":"i64","value":"-1"}]},{"type":"assert_return","line":309,"action":{"type":"invoke","field":"throw-catch-param-f64","args":[{"type":"f64","value":"4617315517961601024"}]},"expected":[{"type":"f64","value":"4617315517961601024"}]},{"type":"assert_return","line":310,"action":{"type":"invoke","field":"throw-catch-param-f64","args":[{"type":"f64","value":"4622100592565682176"}]},"expected":[{"type":"f64","value":"4622100592565682176"}]},{"type":"assert_return","line":312,"action":{"type":"invoke","field":"throw-catch_ref-param-i32","args":[{"type":"i32","value":"0"}]},"expected":[{"type":"i32","value":"0"}]},{"type":"assert_return","line":313,"action":{"type":"invoke","field":"throw-catch_ref-param-i32","args":[{"type":"i32","value":"1"}]},"expected":[{"type":"i32","value":"1"}]},{"type":"assert_return","line":314,"action":{"type":"invoke","field":"throw-catch_ref-param-i32","args":[{"type":"i32","value":"10"}]},"expected":[{"type":"i32","value":"10"}]},{"type":"assert_return","line":316,"action":{"type":"invoke","field":"throw-catch_ref-param-f32","args":[{"type":"f32","value":"1084227584"}]},"expected":[{"type":"f32","value":"1084227584"}]},{"type":"assert_return","line":317,"action":{"type":"invoke","field":"throw-catch_ref-param-f32","args":[{"type":"f32","value":"1093140480"}]},"expected":[{"type":"f32","value":"1093140480"}]},{"type":"assert_return","line":319,"action":{"type":"invoke","field":"throw-catch_ref-param-i64","args":[{"type":"i64","value":"5"}]},"expected":[{"type":"i64","value":"5"}]},{"type":"assert_return","line":320,"action":{"type":"invoke","field":"throw-catch_ref-param-i64","args":[{"type":"i64","value":"0"}]},"expected":[{"type":"i64","value":"0"}]},{"type":"assert_return","line":321,"action":{"type":"invoke","field":"throw-catch_ref-param-i64","args":[{"type":"i64","value":"-1"}]},"expected":[{"type":"i64","value":"-1"}]},{"type":"assert_return","line":323,"action":{"type":"invoke","field":"throw-catch_ref-param-f64","args":[{"type":"f64","value":"4617315517961601024"}]},"expected":[{"type":"f64","value":"4617315517961601024"}]},{"type":"assert_return","line":324,"action":{"type":"invoke","field":"throw-catch_ref-param-f64","args":[{"type":"f64","value":"4622100592565682176"}]},"expected":[{"type":"f64","value":"4622100592565682176"}]},{"type":"assert_return","line":326,"action":{"type":"invoke","field":"catch-param-i32","args":[{"type":"i32","value":"5"}]},"expected":[{"type":"i32","value":"5"}]},{"type":"assert_return","line":328,"action":{"type":"invoke","field":"catch-imported","args":[]},"expected":[{"type":"i32","value":"2"}]},{"type":"assert_return","line":329,"action":{"type":"invoke","field":"catch-imported-alias","args":[]},"expected":[{"type":"i32","value":"2"}]},{"type":"assert_return","line":331,"action":{"type":"invoke","field":"catchless-try","args":[{"type":"i32","value":"0"}]},"expected":[{"type":"i32","value":"0"}]},{"type":"assert_return","line":332,"action":{"type":"invoke","field":"catchless-try","args":[{"type":"i32","value":"1"}]},"expected":[{"type":"i32","value":"1"}]},{"type":"assert_exception","line":334,"action":{"type":"invoke","field":"return-call-in-try-catch","args":[]}},{"type":"assert_exception","line":335,"action":{"type":"invoke","field":"return-call-indirect-in-try-catch","args":[]}},{"type":"assert_return","line":337,"action":{"type":"invoke","field":"try-with-param","args":[]},"expected":[]},{"type":"assert_return","line":339,"action":{"type":"invoke","field":"duplicated-catches","args":[]},"expected":[{"type":"i32","value":"2"}]},{"type":"assert_return","line":340,"action":{"type":"invoke","field":"catch-all-before-catch","args":[]},"expected":[{"type":"i32","value":"2"}]},{"type":"module","line":342,"filename":"try_table.2.wasm","module_type":"binary"},{"type":"assert_return","line":364,"action":{"type":"invoke","field":"imported-mismatch","args":[]},"expected":[{"type":"i32","value":"3"}]},{"type":"assert_malformed","line":367,"filename":"try_table.3.wat","module_type":"text","text":"unexpected token"},{"type":"assert_malformed","line":372,"filename":"try_table.4.wat","module_type":"text","text":"unexpected token"},{"type":"module","line":376,"filename":"try_table.5.wasm","module_type":"binary"},{"type":"assert_invalid","line":387,"filename":"try_table.6.wasm","module_type":"binary","text":"type mismatch"},{"type":"assert_invalid","line":391,"filename":"try_table.7.wasm","module_type":"binary","text":"type mismatch"},{"type":"assert_invalid","line":396,"filename":"try_table.8.wasm","module_type":"binary","text":"type mismatch"},{"type":"assert_invalid","line":400,"filename":"try_table.9.wasm","module_type":"binary","text":"type mismatch"},{"type":"assert_invalid","line":404,"filename":"try_table.10.wasm","module_type":"binary","text":"type mismatch"},{"type":"assert_invalid","line":408,"filename":"try_table.11.wasm","module_type":"binary","text":"type mismatch"},{"type":"assert_invalid","line":412,"filename":"try_table.12.wasm","module_type":"binary","text":"type mismatch"},{"type":"module","line":420,"filename":"try_table.13.wasm","module_type":"binary"},{"type":"assert_return","line":464,"action":{"type":"invoke","field":"catch","args":[]},"expected":[{"type":"funcref"}]},{"type":"assert_return","line":465,"action":{"type":"invoke","field":"catch_ref1","args":[]},"expected":[{"type":"funcref"}]},{"type":"assert_return","line":466,"action":{"type":"invoke","field":"catch_ref2","args":[]},"expected":[{"type":"funcref"}]},{"type":"assert_return","line":467,"action":{"type":"invoke","field":"catch_all_ref1","args":[]},"expected":[]},{"type":"assert_return","line":468,"action":{"type":"invoke","field":"catch_all_ref2","args":[]},"expected":[]},{"type":"assert_invalid","line":471,"filename":"try_table.14.wasm","module_type":"binary","text":"type mismatch"},{"type":"assert_invalid","line":484,"filename":"try_table.15.wasm","module_type":"binary","text":"type mismatch"},{"type":"module","line":499,"filename":"try_table.16.wasm","module_type":"binary"},{"type":"assert_return","line":522,"action":{"type":"invoke","field":"as-br-target","args":[]},"expected":[{"type":"i32","value":"111"}]},{"type":"assert_return","line":523,"action":{"type":"invoke","field":"as-value-provider","args":[]},"expected":[{"type":"i32","value":"333"}]}]} \ No newline at end of file diff --git a/internal/integration_test/spectest/exception-handling/testdata/try_table.wast b/internal/integration_test/spectest/exception-handling/testdata/try_table.wast new file mode 100644 index 000000000..f402ffb4a --- /dev/null +++ b/internal/integration_test/spectest/exception-handling/testdata/try_table.wast @@ -0,0 +1,523 @@ +;; Test try-catch blocks. + +(module + (tag $e0 (export "e0")) + (func (export "throw") (throw $e0)) +) + +(register "test") + +(module + (tag $imported-e0 (import "test" "e0")) + (tag $imported-e0-alias (import "test" "e0")) + (func $imported-throw (import "test" "throw")) + (tag $e0) + (tag $e1) + (tag $e2) + (tag $e-i32 (param i32)) + (tag $e-f32 (param f32)) + (tag $e-i64 (param i64)) + (tag $e-f64 (param f64)) + + (func $throw-if (param i32) (result i32) + (local.get 0) + (i32.const 0) (if (i32.ne) (then (throw $e0))) + (i32.const 0) + ) + + (func (export "simple-throw-catch") (param i32) (result i32) + (block $h + (try_table (result i32) (catch $e0 $h) + (if (i32.eqz (local.get 0)) (then (throw $e0)) (else)) + (i32.const 42) + ) + (return) + ) + (i32.const 23) + ) + + (func (export "unreachable-not-caught") + (block $h + (try_table (catch_all $h) (unreachable)) + (return) + ) + ) + + (func $div (param i32 i32) (result i32) + (local.get 0) (local.get 1) (i32.div_u) + ) + (func (export "trap-in-callee") (param i32 i32) (result i32) + (block $h + (try_table (result i32) (catch_all $h) + (call $div (local.get 0) (local.get 1)) + ) + (return) + ) + (i32.const 11) + ) + + (func (export "catch-complex-1") (param i32) (result i32) + (block $h1 + (try_table (result i32) (catch $e1 $h1) + (block $h0 + (try_table (result i32) (catch $e0 $h0) + (if (i32.eqz (local.get 0)) + (then (throw $e0)) + (else + (if (i32.eq (local.get 0) (i32.const 1)) + (then (throw $e1)) + (else (throw $e2)) + ) + ) + ) + (i32.const 2) + ) + (br 1) + ) + (i32.const 3) + ) + (return) + ) + (i32.const 4) + ) + + (func (export "catch-complex-2") (param i32) (result i32) + (block $h0 + (block $h1 + (try_table (result i32) (catch $e0 $h0) (catch $e1 $h1) + (if (i32.eqz (local.get 0)) + (then (throw $e0)) + (else + (if (i32.eq (local.get 0) (i32.const 1)) + (then (throw $e1)) + (else (throw $e2)) + ) + ) + ) + (i32.const 2) + ) + (return) + ) + (return (i32.const 4)) + ) + (i32.const 3) + ) + + (func (export "throw-catch-param-i32") (param i32) (result i32) + (block $h (result i32) + (try_table (result i32) (catch $e-i32 $h) + (throw $e-i32 (local.get 0)) + (i32.const 2) + ) + (return) + ) + (return) + ) + + (func (export "throw-catch-param-f32") (param f32) (result f32) + (block $h (result f32) + (try_table (result f32) (catch $e-f32 $h) + (throw $e-f32 (local.get 0)) + (f32.const 0) + ) + (return) + ) + (return) + ) + + (func (export "throw-catch-param-i64") (param i64) (result i64) + (block $h (result i64) + (try_table (result i64) (catch $e-i64 $h) + (throw $e-i64 (local.get 0)) + (i64.const 2) + ) + (return) + ) + (return) + ) + + (func (export "throw-catch-param-f64") (param f64) (result f64) + (block $h (result f64) + (try_table (result f64) (catch $e-f64 $h) + (throw $e-f64 (local.get 0)) + (f64.const 0) + ) + (return) + ) + (return) + ) + + (func (export "throw-catch_ref-param-i32") (param i32) (result i32) + (block $h (result i32 exnref) + (try_table (result i32) (catch_ref $e-i32 $h) + (throw $e-i32 (local.get 0)) + (i32.const 2) + ) + (return) + ) + (drop) (return) + ) + + (func (export "throw-catch_ref-param-f32") (param f32) (result f32) + (block $h (result f32 exnref) + (try_table (result f32) (catch_ref $e-f32 $h) + (throw $e-f32 (local.get 0)) + (f32.const 0) + ) + (return) + ) + (drop) (return) + ) + + (func (export "throw-catch_ref-param-i64") (param i64) (result i64) + (block $h (result i64 exnref) + (try_table (result i64) (catch_ref $e-i64 $h) + (throw $e-i64 (local.get 0)) + (i64.const 2) + ) + (return) + ) + (drop) (return) + ) + + (func (export "throw-catch_ref-param-f64") (param f64) (result f64) + (block $h (result f64 exnref) + (try_table (result f64) (catch_ref $e-f64 $h) + (throw $e-f64 (local.get 0)) + (f64.const 0) + ) + (return) + ) + (drop) (return) + ) + + (func $throw-param-i32 (param i32) (throw $e-i32 (local.get 0))) + (func (export "catch-param-i32") (param i32) (result i32) + (block $h (result i32) + (try_table (result i32) (catch $e-i32 $h) + (i32.const 0) + (call $throw-param-i32 (local.get 0)) + ) + (return) + ) + ) + + (func (export "catch-imported") (result i32) + (block $h + (try_table (result i32) (catch $imported-e0 $h) + (call $imported-throw (i32.const 1)) + ) + (return) + ) + (i32.const 2) + ) + + (func (export "catch-imported-alias") (result i32) + (block $h + (try_table (result i32) (catch $imported-e0 $h) + (throw $imported-e0-alias (i32.const 1)) + ) + (return) + ) + (i32.const 2) + ) + + (func (export "catchless-try") (param i32) (result i32) + (block $h + (try_table (result i32) (catch $e0 $h) + (try_table (result i32) (call $throw-if (local.get 0))) + ) + (return) + ) + (i32.const 1) + ) + + (func $throw-void (throw $e0)) + (func (export "return-call-in-try-catch") + (block $h + (try_table (catch $e0 $h) + (return_call $throw-void) + ) + ) + ) + + (table funcref (elem $throw-void)) + (func (export "return-call-indirect-in-try-catch") + (block $h + (try_table (catch $e0 $h) + (return_call_indirect (i32.const 0)) + ) + ) + ) + + (func (export "try-with-param") + (i32.const 0) (try_table (param i32) (drop)) + ) + + (func (export "duplicated-catches") (result i32) + (block + (block + (try_table (catch $e0 0) (catch $e0 1) + (throw $e0) + ) + ) + (return (i32.const 2)) + ) + (return (i32.const 3)) + ) + + (func (export "catch-all-before-catch") (result i32) + (block + (block + (try_table (catch_all 0) (catch $e0 1) + (throw $e0) + ) + ) + (return (i32.const 2)) + ) + (return (i32.const 3)) + ) +) + +(assert_return (invoke "simple-throw-catch" (i32.const 0)) (i32.const 23)) +(assert_return (invoke "simple-throw-catch" (i32.const 1)) (i32.const 42)) + +(assert_trap (invoke "unreachable-not-caught") "unreachable") + +(assert_return (invoke "trap-in-callee" (i32.const 7) (i32.const 2)) (i32.const 3)) +(assert_trap (invoke "trap-in-callee" (i32.const 1) (i32.const 0)) "integer divide by zero") + +(assert_return (invoke "catch-complex-1" (i32.const 0)) (i32.const 3)) +(assert_return (invoke "catch-complex-1" (i32.const 1)) (i32.const 4)) +(assert_exception (invoke "catch-complex-1" (i32.const 2))) + +(assert_return (invoke "catch-complex-2" (i32.const 0)) (i32.const 3)) +(assert_return (invoke "catch-complex-2" (i32.const 1)) (i32.const 4)) +(assert_exception (invoke "catch-complex-2" (i32.const 2))) + +(assert_return (invoke "throw-catch-param-i32" (i32.const 0)) (i32.const 0)) +(assert_return (invoke "throw-catch-param-i32" (i32.const 1)) (i32.const 1)) +(assert_return (invoke "throw-catch-param-i32" (i32.const 10)) (i32.const 10)) + +(assert_return (invoke "throw-catch-param-f32" (f32.const 5.0)) (f32.const 5.0)) +(assert_return (invoke "throw-catch-param-f32" (f32.const 10.5)) (f32.const 10.5)) + +(assert_return (invoke "throw-catch-param-i64" (i64.const 5)) (i64.const 5)) +(assert_return (invoke "throw-catch-param-i64" (i64.const 0)) (i64.const 0)) +(assert_return (invoke "throw-catch-param-i64" (i64.const -1)) (i64.const -1)) + +(assert_return (invoke "throw-catch-param-f64" (f64.const 5.0)) (f64.const 5.0)) +(assert_return (invoke "throw-catch-param-f64" (f64.const 10.5)) (f64.const 10.5)) + +(assert_return (invoke "throw-catch_ref-param-i32" (i32.const 0)) (i32.const 0)) +(assert_return (invoke "throw-catch_ref-param-i32" (i32.const 1)) (i32.const 1)) +(assert_return (invoke "throw-catch_ref-param-i32" (i32.const 10)) (i32.const 10)) + +(assert_return (invoke "throw-catch_ref-param-f32" (f32.const 5.0)) (f32.const 5.0)) +(assert_return (invoke "throw-catch_ref-param-f32" (f32.const 10.5)) (f32.const 10.5)) + +(assert_return (invoke "throw-catch_ref-param-i64" (i64.const 5)) (i64.const 5)) +(assert_return (invoke "throw-catch_ref-param-i64" (i64.const 0)) (i64.const 0)) +(assert_return (invoke "throw-catch_ref-param-i64" (i64.const -1)) (i64.const -1)) + +(assert_return (invoke "throw-catch_ref-param-f64" (f64.const 5.0)) (f64.const 5.0)) +(assert_return (invoke "throw-catch_ref-param-f64" (f64.const 10.5)) (f64.const 10.5)) + +(assert_return (invoke "catch-param-i32" (i32.const 5)) (i32.const 5)) + +(assert_return (invoke "catch-imported") (i32.const 2)) +(assert_return (invoke "catch-imported-alias") (i32.const 2)) + +(assert_return (invoke "catchless-try" (i32.const 0)) (i32.const 0)) +(assert_return (invoke "catchless-try" (i32.const 1)) (i32.const 1)) + +(assert_exception (invoke "return-call-in-try-catch")) +(assert_exception (invoke "return-call-indirect-in-try-catch")) + +(assert_return (invoke "try-with-param")) + +(assert_return (invoke "duplicated-catches") (i32.const 2)) +(assert_return (invoke "catch-all-before-catch") (i32.const 2)) + +(module + (func $imported-throw (import "test" "throw")) + (tag $e0) + + (func (export "imported-mismatch") (result i32) + (block $h + (try_table (result i32) (catch_all $h) + (block $h0 + (try_table (result i32) (catch $e0 $h0) + (i32.const 1) + (call $imported-throw) + ) + (return) + ) + (i32.const 2) + ) + (return) + ) + (i32.const 3) + ) +) + +(assert_return (invoke "imported-mismatch") (i32.const 3)) + +(assert_malformed + (module quote "(module (func (catch_all)))") + "unexpected token" +) + +(assert_malformed + (module quote "(module (tag $e) (func (catch $e)))") + "unexpected token" +) + +(module + (tag $e) + (func (try_table (catch $e 0) (catch $e 0))) + (func (try_table (catch_all 0) (catch $e 0))) + (func (try_table (catch_all 0) (catch_all 0))) + (func (result exnref) (try_table (catch_ref $e 0) (catch_ref $e 0)) (unreachable)) + (func (result exnref) (try_table (catch_all_ref 0) (catch_ref $e 0)) (unreachable)) + (func (result exnref) (try_table (catch_all_ref 0) (catch_all_ref 0)) (unreachable)) +) + +(assert_invalid + (module (func (result i32) (try_table (result i32)))) + "type mismatch" +) +(assert_invalid + (module (func (result i32) (try_table (result i32) (i64.const 42)))) + "type mismatch" +) + +(assert_invalid + (module (tag) (func (try_table (catch_ref 0 0)))) + "type mismatch" +) +(assert_invalid + (module (tag) (func (result exnref) (try_table (catch 0 0)) (unreachable))) + "type mismatch" +) +(assert_invalid + (module (func (try_table (catch_all_ref 0)))) + "type mismatch" +) +(assert_invalid + (module (func (result exnref) (try_table (catch_all 0)) (unreachable))) + "type mismatch" +) +(assert_invalid + (module + (tag (param i64)) + (func (result i32 exnref) (try_table (result i32) (catch_ref 0 0) (i32.const 42))) + ) + "type mismatch" +) + + +(module + (type $t (func)) + (func $dummy) + (elem declare func $dummy) + + (tag $e (param (ref $t))) + (func $throw (throw $e (ref.func $dummy))) + + (func (export "catch") (result (ref null $t)) + (block $l (result (ref null $t)) + (try_table (catch $e $l) (call $throw)) + (unreachable) + ) + ) + (func (export "catch_ref1") (result (ref null $t)) + (block $l (result (ref null $t) (ref exn)) + (try_table (catch_ref $e $l) (call $throw)) + (unreachable) + ) + (drop) + ) + (func (export "catch_ref2") (result (ref null $t)) + (block $l (result (ref null $t) (ref null exn)) + (try_table (catch_ref $e $l) (call $throw)) + (unreachable) + ) + (drop) + ) + (func (export "catch_all_ref1") + (block $l (result (ref exn)) + (try_table (catch_all_ref $l) (call $throw)) + (unreachable) + ) + (drop) + ) + (func (export "catch_all_ref2") + (block $l (result (ref null exn)) + (try_table (catch_all_ref $l) (call $throw)) + (unreachable) + ) + (drop) + ) +) + +(assert_return (invoke "catch") (ref.func)) +(assert_return (invoke "catch_ref1") (ref.func)) +(assert_return (invoke "catch_ref2") (ref.func)) +(assert_return (invoke "catch_all_ref1")) +(assert_return (invoke "catch_all_ref2")) + +(assert_invalid + (module + (type $t (func)) + (tag $e (param (ref null $t))) + (func (export "catch") (result (ref $t)) + (block $l (result (ref $t)) + (try_table (catch $e $l)) + (unreachable) + ) + ) + ) + "type mismatch" +) +(assert_invalid + (module + (type $t (func)) + (tag $e (param (ref null $t))) + (func (export "catch_ref") (result (ref $t)) + (block $l (result (ref $t) (ref exn)) + (try_table (catch_ref $e $l)) + (unreachable) + ) + ) + ) + "type mismatch" +) + +;; try_table acts a regular block for br, etc. + +(module + (func (export "as-br-target") (result i32) + (block + (try_table + (br 0) + (unreachable) + ) + (return (i32.const 111)) + ) + (i32.const 222) + ) + + (func (export "as-value-provider") (result i32) + (block + (try_table (result i32) + (br 0 (i32.const 333)) + ) + (return) + ) + (unreachable) + ) +) + +(assert_return (invoke "as-br-target") (i32.const 111)) +(assert_return (invoke "as-value-provider") (i32.const 333)) diff --git a/internal/integration_test/spectest/spectest.go b/internal/integration_test/spectest/spectest.go index 0638883ef..694ed341b 100644 --- a/internal/integration_test/spectest/spectest.go +++ b/internal/integration_test/spectest/spectest.go @@ -114,6 +114,8 @@ func (c commandActionVal) String() string { case "funcref": // All the in and out funcref params are null in spectest (cannot represent non-null as it depends on runtime impl). v = "null" + case "exnref": + v = "null" case "v128": simdValues, ok := c.Value.([]interface{}) if !ok { @@ -260,6 +262,9 @@ func getNaNBits(strValue string, is32bit bool) (ret uint64) { } func (c commandActionVal) toUint64() (ret uint64) { + if c.Value == nil || c.Value == "null" { + return 0 + } strValue := c.Value.(string) if strings.Contains(strValue, "nan") { ret = getNaNBits(strValue, c.ValType == "f32") @@ -273,9 +278,20 @@ func (c commandActionVal) toUint64() (ret uint64) { ret = original + 1 } } else if strings.Contains(c.ValType, "32") { - ret, _ = strconv.ParseUint(strValue, 10, 32) + // wasm-tools may output signed decimals (e.g. "-1"); handle both. + if strings.HasPrefix(strValue, "-") { + v, _ := strconv.ParseInt(strValue, 10, 32) + ret = uint64(uint32(int32(v))) + } else { + ret, _ = strconv.ParseUint(strValue, 10, 32) + } } else { - ret, _ = strconv.ParseUint(strValue, 10, 64) + if strings.HasPrefix(strValue, "-") { + v, _ := strconv.ParseInt(strValue, 10, 64) + ret = uint64(v) + } else { + ret, _ = strconv.ParseUint(strValue, 10, 64) + } } return } @@ -305,6 +321,8 @@ func (c command) expectedError() (err error) { err = wasmruntime.ErrRuntimeUnalignedAtomic case "unreachable": err = wasmruntime.ErrRuntimeUnreachable + case "uncaught exception": + err = wasmruntime.ErrRuntimeUncaughtException default: if strings.HasPrefix(c.Text, "uninitialized") { err = wasmruntime.ErrRuntimeInvalidTableAccess @@ -419,12 +437,17 @@ func RunCase(t *testing.T, testDataFS embed.FS, f string, ctx context.Context, c require.NoError(t, err, msg) require.Equal(t, len(exps), len(results), msg) laneTypes := map[int]string{} + skipIndices := map[int]bool{} for i, expV := range c.Exps { if expV.ValType == "v128" { laneTypes[i] = expV.LaneType } + // When value is nil for ref types, it means "any ref" — skip comparison. + if expV.Value == nil && (expV.ValType == "funcref" || expV.ValType == "externref" || expV.ValType == "exnref") { + skipIndices[i] = true + } } - matched, valuesMsg := valuesEq(results, exps, fn.Definition().ResultTypes(), laneTypes) + matched, valuesMsg := valuesEq(results, exps, fn.Definition().ResultTypes(), laneTypes, skipIndices) require.True(t, matched, msg+"\n"+valuesMsg) case "get": _, exps := c.getAssertReturnArgsExps() @@ -495,6 +518,23 @@ func RunCase(t *testing.T, testDataFS embed.FS, f string, ctx context.Context, c require.NoError(t, err, msg) _, err = r.InstantiateWithConfig(ctx, buf, wazero.NewModuleConfig()) require.Error(t, err, msg) + case "assert_exception": + m := lastInstantiatedModule + if c.Action.Module != "" { + m = modules[c.Action.Module] + } + switch c.Action.ActionType { + case "invoke": + args := c.getAssertReturnArgs() + msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args) + if c.Action.Module != "" { + msg += " in module " + c.Action.Module + } + _, err := m.ExportedFunction(c.Action.Field).Call(ctx, args...) + require.ErrorIs(t, err, wasmruntime.ErrRuntimeUncaughtException, msg) + default: + t.Fatalf("unsupported action type type: %v", c) + } case "assert_uninstantiable": buf, err := testDataFS.ReadFile(testdataPath(c.Filename)) require.NoError(t, err, msg) @@ -539,12 +579,23 @@ func testdataPath(filename string) string { // - laneTypes maps the index of valueTypes to laneType if valueTypes[i] == wasm.ValueTypeV128. // // Also, if matched == false this returns non-empty valuesMsg which can be used to augment the test failure message. -func valuesEq(actual, exps []uint64, valTypes []wasm.ValueType, laneTypes map[int]laneType) (matched bool, valuesMsg string) { +func valuesEq(actual, exps []uint64, valTypes []wasm.ValueType, laneTypes map[int]laneType, skipIndices map[int]bool) (matched bool, valuesMsg string) { matched = true var msgExpValuesStrs, msgActualValuesStrs []string var uint64RepPos int // the index to actual and exps slice. for i, tp := range valTypes { + if skipIndices[i] { + // Skip comparison for this result (e.g., "any funcref"). + if tp == wasm.ValueTypeV128 { + uint64RepPos += 2 + } else { + uint64RepPos++ + } + msgExpValuesStrs = append(msgExpValuesStrs, "*") + msgActualValuesStrs = append(msgActualValuesStrs, "*") + continue + } switch tp { case wasm.ValueTypeI32: msgExpValuesStrs = append(msgExpValuesStrs, fmt.Sprintf("%d", uint32(exps[uint64RepPos]))) diff --git a/internal/integration_test/spectest/spectest_test.go b/internal/integration_test/spectest/spectest_test.go index ed8b3b8d7..45b98ca40 100644 --- a/internal/integration_test/spectest/spectest_test.go +++ b/internal/integration_test/spectest/spectest_test.go @@ -456,7 +456,7 @@ func Test_valuesEq(t *testing.T) { for _, tc := range tests { tc := tc t.Run(tc.name, func(t *testing.T) { - actualMatched, actualValuesMsg := valuesEq(tc.actual, tc.exps, tc.valueTypes, tc.laneTypes) + actualMatched, actualValuesMsg := valuesEq(tc.actual, tc.exps, tc.valueTypes, tc.laneTypes, nil) require.Equal(t, tc.expMatched, actualMatched) require.Equal(t, tc.expValuesMsg, actualValuesMsg) }) diff --git a/internal/logging/logging.go b/internal/logging/logging.go index 577efff19..c66aa9240 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -25,6 +25,7 @@ const ( ValueTypeV128 ValueType = 0x7b // same as wasm.ValueTypeV128 ValueTypeFuncref ValueType = 0x70 // same as wasm.ValueTypeFuncref ValueTypeExternref = api.ValueTypeExternref + ValueTypeExnref ValueType = 0x69 // same as wasm.ValueTypeExnref // ValueTypeMemI32 is a non-standard type which writes ValueTypeI32 from the memory offset. ValueTypeMemI32 = 0xfd @@ -211,7 +212,7 @@ func ValWriterForType(vt ValueType) ValWriter { return writeF64 case ValueTypeV128: return writeV128 - case ValueTypeExternref, ValueTypeFuncref: + case ValueTypeExternref, ValueTypeFuncref, ValueTypeExnref: return writeRef case ValueTypeMemI32: return writeMemI32 diff --git a/internal/testing/binaryencoding/encoder.go b/internal/testing/binaryencoding/encoder.go index 4314a9533..82b435314 100644 --- a/internal/testing/binaryencoding/encoder.go +++ b/internal/testing/binaryencoding/encoder.go @@ -27,7 +27,9 @@ func EncodeModule(m *wasm.Module) (bytes []byte) { if m.SectionElementCount(wasm.SectionIDMemory) > 0 { bytes = append(bytes, encodeMemorySection(m.MemorySection)...) } - // wasm.SectionIDTag (used for Exceptions) would be here. + if m.SectionElementCount(wasm.SectionIDTag) > 0 { + bytes = append(bytes, encodeTagSection(m.TagSection)...) + } if m.SectionElementCount(wasm.SectionIDGlobal) > 0 { bytes = append(bytes, encodeGlobalSection(m.GlobalSection)...) } diff --git a/internal/testing/binaryencoding/import.go b/internal/testing/binaryencoding/import.go index 8abffde20..c4b82c4f7 100644 --- a/internal/testing/binaryencoding/import.go +++ b/internal/testing/binaryencoding/import.go @@ -33,6 +33,9 @@ func EncodeImport(i *wasm.Import) []byte { mutable = 1 } data = append(data, g.ValType, mutable) + case wasm.ExternTypeTag: + data = append(data, 0x00) // attribute byte + data = append(data, leb128.EncodeUint32(i.DescTag)...) default: panic(fmt.Errorf("invalid externtype: %s", wasm.ExternTypeName(i.Type))) } diff --git a/internal/testing/binaryencoding/section.go b/internal/testing/binaryencoding/section.go index 531c6fcef..90d7856a7 100644 --- a/internal/testing/binaryencoding/section.go +++ b/internal/testing/binaryencoding/section.go @@ -89,6 +89,16 @@ func encodeMemorySection(memory *wasm.Memory) []byte { return encodeSection(wasm.SectionIDMemory, contents) } +// encodeTagSection encodes a wasm.SectionIDTag for the given tags. +func encodeTagSection(tags []wasm.Tag) []byte { + contents := leb128.EncodeUint32(uint32(len(tags))) + for _, tag := range tags { + contents = append(contents, 0x00) // attribute byte (always 0) + contents = append(contents, leb128.EncodeUint32(tag.Type)...) + } + return encodeSection(wasm.SectionIDTag, contents) +} + // encodeGlobalSection encodes a wasm.SectionIDGlobal for the given globals in WebAssembly 1.0 (20191205) Binary // Format. // diff --git a/internal/wasm/binary/code.go b/internal/wasm/binary/code.go index 2fac9196c..66988fd1b 100644 --- a/internal/wasm/binary/code.go +++ b/internal/wasm/binary/code.go @@ -47,7 +47,8 @@ func decodeCode(r *bytes.Reader, codeSectionStart uint64, ret *wasm.Code) (err e bytesRead += n + 1 switch vt := b; vt { case wasm.ValueTypeI32, wasm.ValueTypeF32, wasm.ValueTypeI64, wasm.ValueTypeF64, - wasm.ValueTypeFuncref, wasm.ValueTypeExternref, wasm.ValueTypeV128: + wasm.ValueTypeFuncref, wasm.ValueTypeExternref, wasm.ValueTypeV128, + wasm.ValueTypeExnref: default: return fmt.Errorf("invalid local type: 0x%x", vt) } diff --git a/internal/wasm/binary/decoder.go b/internal/wasm/binary/decoder.go index 8eca1d149..b794ce626 100644 --- a/internal/wasm/binary/decoder.go +++ b/internal/wasm/binary/decoder.go @@ -8,6 +8,7 @@ import ( "io" "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/experimental" "github.com/tetratelabs/wazero/internal/leb128" "github.com/tetratelabs/wazero/internal/wasm" "github.com/tetratelabs/wazero/internal/wasmdebug" @@ -113,7 +114,7 @@ func DecodeModule( case wasm.SectionIDType: m.TypeSection, err = decodeTypeSection(enabledFeatures, r) case wasm.SectionIDImport: - m.ImportSection, m.ImportPerModule, m.ImportFunctionCount, m.ImportGlobalCount, m.ImportMemoryCount, m.ImportTableCount, err = decodeImportSection(r, memSizer, memoryLimitPages, enabledFeatures) + m.ImportSection, m.ImportPerModule, m.ImportFunctionCount, m.ImportGlobalCount, m.ImportMemoryCount, m.ImportTableCount, m.ImportTagCount, err = decodeImportSection(r, memSizer, memoryLimitPages, enabledFeatures) if err != nil { return nil, err // avoid re-wrapping the error. } @@ -123,6 +124,11 @@ func DecodeModule( m.TableSection, err = decodeTableSection(r, enabledFeatures) case wasm.SectionIDMemory: m.MemorySection, err = decodeMemorySection(r, enabledFeatures, memSizer, memoryLimitPages) + case wasm.SectionIDTag: + if err := enabledFeatures.RequireEnabled(experimental.CoreFeaturesExceptionHandling); err != nil { + return nil, fmt.Errorf("tag section not supported as %v", err) + } + m.TagSection, err = decodeTagSection(r) case wasm.SectionIDGlobal: if m.GlobalSection, err = decodeGlobalSection(r, enabledFeatures); err != nil { return nil, err // avoid re-wrapping the error. @@ -176,8 +182,15 @@ func checkSectionOrder(current, previous wasm.SectionID) (byte, bool) { return previous, true } - // DataCount was introduced in Wasm 2.0, - // and it's the maximum we support so far. + // Tag section (ID 13) must come after Memory (5) and before Global (6). + if current == wasm.SectionIDTag { + return current, previous <= wasm.SectionIDMemory + } + if previous == wasm.SectionIDTag { + return current, current >= wasm.SectionIDGlobal + } + + // DataCount was introduced in Wasm 2.0. // It must come after Element and before Code. if current > wasm.SectionIDDataCount { return current, false @@ -189,9 +202,6 @@ func checkSectionOrder(current, previous wasm.SectionID) (byte, bool) { return current, current >= wasm.SectionIDCode } - // Tag will be introduced in Wasm 3.0. - // It must come after Memory and before Global. - // Otherwise, strictly increasing order. return current, current > previous } diff --git a/internal/wasm/binary/export.go b/internal/wasm/binary/export.go index 925e9c499..76f222a04 100644 --- a/internal/wasm/binary/export.go +++ b/internal/wasm/binary/export.go @@ -21,7 +21,7 @@ func decodeExport(r *bytes.Reader, ret *wasm.Export) (err error) { ret.Type = b switch ret.Type { - case wasm.ExternTypeFunc, wasm.ExternTypeTable, wasm.ExternTypeMemory, wasm.ExternTypeGlobal: + case wasm.ExternTypeFunc, wasm.ExternTypeTable, wasm.ExternTypeMemory, wasm.ExternTypeGlobal, wasm.ExternTypeTag: if ret.Index, _, err = leb128.DecodeUint32(r); err != nil { err = fmt.Errorf("error decoding export index: %w", err) } diff --git a/internal/wasm/binary/import.go b/internal/wasm/binary/import.go index 39d310c55..1e830cc4d 100644 --- a/internal/wasm/binary/import.go +++ b/internal/wasm/binary/import.go @@ -42,6 +42,22 @@ func decodeImport( ret.DescMem, err = decodeMemory(r, enabledFeatures, memorySizer, memoryLimitPages) case wasm.ExternTypeGlobal: ret.DescGlobal, err = decodeGlobalType(r) + case wasm.ExternTypeTag: + if err = enabledFeatures.RequireEnabled(api.CoreFeatureSIMD << 4); err != nil { // CoreFeaturesExceptionHandling + err = fmt.Errorf("tag imports require exception handling feature: %w", err) + break + } + // Tag import: read attribute byte (must be 0x00) then type index. + var attr byte + attr, err = r.ReadByte() + if err != nil { + break + } + if attr != 0x00 { + err = fmt.Errorf("invalid tag attribute: %#x", attr) + break + } + ret.DescTag, _, err = leb128.DecodeUint32(r) default: err = fmt.Errorf("%w: invalid byte for importdesc: %#x", ErrInvalidByte, b) } diff --git a/internal/wasm/binary/section.go b/internal/wasm/binary/section.go index 622ee5923..1ec4b105a 100644 --- a/internal/wasm/binary/section.go +++ b/internal/wasm/binary/section.go @@ -16,11 +16,39 @@ func decodeTypeSection(enabledFeatures api.CoreFeatures, r *bytes.Reader) ([]was return nil, fmt.Errorf("get size of vector: %w", err) } - result := make([]wasm.FunctionType, vs) + var result []wasm.FunctionType for i := uint32(0); i < vs; i++ { - if err = decodeFunctionType(enabledFeatures, r, &result[i]); err != nil { + // Peek at the leading byte to check for rec group (0x4e, GC proposal). + b, err := r.ReadByte() + if err != nil { return nil, fmt.Errorf("read %d-th type: %v", i, err) } + if b == 0x4e { + // Rec group: contains multiple types. + recCount, _, err := leb128.DecodeUint32(r) + if err != nil { + return nil, fmt.Errorf("read rec group count: %v", err) + } + for j := uint32(0); j < recCount; j++ { + var ft wasm.FunctionType + if err = decodeFunctionType(enabledFeatures, r, &ft); err != nil { + return nil, fmt.Errorf("read %d-th type in rec group: %v", j, err) + } + ft.RecGroupSize = int(recCount) + ft.RecGroupPosition = int(j) + result = append(result, ft) + } + } else { + // Put back the byte and decode as a regular function type. + if err := r.UnreadByte(); err != nil { + return nil, err + } + var ft wasm.FunctionType + if err = decodeFunctionType(enabledFeatures, r, &ft); err != nil { + return nil, fmt.Errorf("read %d-th type: %v", i, err) + } + result = append(result, ft) + } } return result, nil } @@ -33,7 +61,7 @@ func decodeImportSection( enabledFeatures api.CoreFeatures, ) (result []wasm.Import, perModule map[string][]*wasm.Import, - funcCount, globalCount, memoryCount, tableCount wasm.Index, err error, + funcCount, globalCount, memoryCount, tableCount, tagCount wasm.Index, err error, ) { vs, _, err := leb128.DecodeUint32(r) if err != nil { @@ -61,6 +89,9 @@ func decodeImportSection( case wasm.ExternTypeTable: imp.IndexPerType = tableCount tableCount++ + case wasm.ExternTypeTag: + imp.IndexPerType = tagCount + tagCount++ } perModule[imp.Module] = append(perModule[imp.Module], imp) } @@ -216,6 +247,31 @@ func decodeDataSection(r *bytes.Reader, enabledFeatures api.CoreFeatures) ([]was return result, nil } +func decodeTagSection(r *bytes.Reader) ([]wasm.Tag, error) { + vs, _, err := leb128.DecodeUint32(r) + if err != nil { + return nil, fmt.Errorf("get size of vector: %w", err) + } + + result := make([]wasm.Tag, vs) + for i := uint32(0); i < vs; i++ { + // Read attribute byte (must be 0x00 per spec). + attr, err := r.ReadByte() + if err != nil { + return nil, fmt.Errorf("read tag[%d] attribute: %w", i, err) + } + if attr != 0x00 { + return nil, fmt.Errorf("tag[%d] has invalid attribute: %#x", i, attr) + } + // Read type index. + result[i].Type, _, err = leb128.DecodeUint32(r) + if err != nil { + return nil, fmt.Errorf("read tag[%d] type index: %w", i, err) + } + } + return result, nil +} + func decodeDataCountSection(r *bytes.Reader) (count *uint32, err error) { v, _, err := leb128.DecodeUint32(r) if err != nil && err != io.EOF { diff --git a/internal/wasm/binary/value.go b/internal/wasm/binary/value.go index dfc4417ed..d2f9675e7 100644 --- a/internal/wasm/binary/value.go +++ b/internal/wasm/binary/value.go @@ -16,18 +16,42 @@ func decodeValueTypes(r *bytes.Reader, num uint32) ([]wasm.ValueType, error) { return nil, nil } - ret := make([]wasm.ValueType, num) - _, err := io.ReadFull(r, ret) - if err != nil { - return nil, err - } - - for _, v := range ret { - switch v { + ret := make([]wasm.ValueType, 0, num) + for i := uint32(0); i < num; i++ { + b, err := r.ReadByte() + if err != nil { + return nil, err + } + switch b { case wasm.ValueTypeI32, wasm.ValueTypeF32, wasm.ValueTypeI64, wasm.ValueTypeF64, - wasm.ValueTypeExternref, wasm.ValueTypeFuncref, wasm.ValueTypeV128: + wasm.ValueTypeExternref, wasm.ValueTypeFuncref, wasm.ValueTypeV128, + wasm.ValueTypeExnref: + ret = append(ret, b) + case wasm.RefPrefixNullable, wasm.RefPrefixNonNullable: + ht, _, err := leb128.DecodeInt33AsInt64(r) + if err != nil { + return nil, fmt.Errorf("read ref heap type: %w", err) + } + // The following nullable refs are an alternative representation of the corresponding ref types: + // - (ref null exn) is equivalent to exnref + // - (ref null func) is equivalent to funcref + // - (ref null extern) is equivalent to externref + // See https://webassembly.github.io/gc/core/syntax/types.html#reference-types + // Current limitation: we desugar NON-NULLABLE types to NULLABLE types internally. + // This technically breaks type-checking in some cases, but we will fix this + // when we introduce proper ref types. + switch ht { + case wasm.HeapTypeExn: + ret = append(ret, wasm.ValueTypeExnref) + case wasm.HeapTypeFunc: + ret = append(ret, wasm.ValueTypeFuncref) + case wasm.HeapTypeExtern: + ret = append(ret, wasm.ValueTypeExternref) + default: // concrete type index — treat as nullable funcref + ret = append(ret, wasm.ValueTypeFuncref) + } default: - return nil, fmt.Errorf("invalid value type: %d", v) + return nil, fmt.Errorf("invalid value type: %d", b) } } return ret, nil diff --git a/internal/wasm/counts.go b/internal/wasm/counts.go index 685a40941..39e87272d 100644 --- a/internal/wasm/counts.go +++ b/internal/wasm/counts.go @@ -45,6 +45,8 @@ func (m *Module) SectionElementCount(sectionID SectionID) uint32 { // element as return uint32(len(m.CodeSection)) case SectionIDData: return uint32(len(m.DataSection)) + case SectionIDTag: + return uint32(len(m.TagSection)) default: panic(fmt.Errorf("BUG: unknown section: %d", sectionID)) } diff --git a/internal/wasm/exception.go b/internal/wasm/exception.go new file mode 100644 index 000000000..d47c44f6c --- /dev/null +++ b/internal/wasm/exception.go @@ -0,0 +1,9 @@ +package wasm + +// Exception represents a thrown WebAssembly exception. +type Exception struct { + // Tag is the tag instance that was thrown. + Tag *TagInstance + // Params holds the argument values matching the tag's function type params. + Params []uint64 +} diff --git a/internal/wasm/func_validation.go b/internal/wasm/func_validation.go index b0e7f7cfb..798546a0a 100644 --- a/internal/wasm/func_validation.go +++ b/internal/wasm/func_validation.go @@ -29,9 +29,9 @@ const maximumValuesOnStack = 1 << 27 // Returns an error if the instruction sequence is not valid, // or potentially it can exceed the maximum number of values on the stack. func (m *Module) validateFunction(sts *stacks, enabledFeatures api.CoreFeatures, idx Index, functions []Index, - globals []GlobalType, memory *Memory, tables []Table, declaredFunctionIndexes map[Index]struct{}, br *bytes.Reader, + globals []GlobalType, memory *Memory, tables []Table, tags []Index, declaredFunctionIndexes map[Index]struct{}, br *bytes.Reader, ) error { - return m.validateFunctionWithMaxStackValues(sts, enabledFeatures, idx, functions, globals, memory, tables, maximumValuesOnStack, declaredFunctionIndexes, br) + return m.validateFunctionWithMaxStackValues(sts, enabledFeatures, idx, functions, globals, memory, tables, tags, maximumValuesOnStack, declaredFunctionIndexes, br) } func readMemArg(pc uint64, body []byte) (align, offset uint32, read uint64, err error) { @@ -69,6 +69,7 @@ func (m *Module) validateFunctionWithMaxStackValues( globals []GlobalType, memory *Memory, tables []Table, + tags []Index, maxStackValues int, declaredFunctionIndexes map[Index]struct{}, br *bytes.Reader, @@ -538,6 +539,143 @@ func (m *Module) validateFunctionWithMaxStackValues( // br_table instruction is stack-polymorphic. valueTypeStack.unreachable() + } else if op == OpcodeThrow { + if err := enabledFeatures.RequireEnabled(experimental.CoreFeaturesExceptionHandling); err != nil { + return fmt.Errorf("%s invalid as %v", OpcodeThrowName, err) + } + pc++ + tagIndex, num, err := leb128.LoadUint32(body[pc:]) + if err != nil { + return fmt.Errorf("read immediate: %v", err) + } + pc += num - 1 + if tagIndex >= uint32(len(tags)) { + return fmt.Errorf("invalid tag index for %s: %d", OpcodeThrowName, tagIndex) + } + tagType := &m.TypeSection[tags[tagIndex]] + // Pop values matching the tag's params in reverse order. + for i := len(tagType.Params) - 1; i >= 0; i-- { + if err := valueTypeStack.popAndVerifyType(tagType.Params[i]); err != nil { + return fmt.Errorf("type mismatch on %s operation: %v", OpcodeThrowName, err) + } + } + // throw is stack-polymorphic (never returns). + valueTypeStack.unreachable() + } else if op == OpcodeThrowRef { + if err := enabledFeatures.RequireEnabled(experimental.CoreFeaturesExceptionHandling); err != nil { + return fmt.Errorf("%s invalid as %v", OpcodeThrowRefName, err) + } + if err := valueTypeStack.popAndVerifyType(ValueTypeExnref); err != nil { + return fmt.Errorf("type mismatch on %s: %v", OpcodeThrowRefName, err) + } + // throw_ref is stack-polymorphic (never returns). + valueTypeStack.unreachable() + } else if op == OpcodeTryTable { + if err := enabledFeatures.RequireEnabled(experimental.CoreFeaturesExceptionHandling); err != nil { + return fmt.Errorf("%s invalid as %v", OpcodeTryTableName, err) + } + br.Reset(body[pc+1:]) + bt, num, err := DecodeBlockType(m.TypeSection, br, enabledFeatures) + if err != nil { + return fmt.Errorf("read block: %w", err) + } + pc += num + + // Read catch clause count. + catchCount, catchNum, err := leb128.LoadUint32(body[pc+1:]) + if err != nil { + return fmt.Errorf("read catch count: %v", err) + } + pc += catchNum + + // Validate each catch clause. + for i := uint32(0); i < catchCount; i++ { + pc++ + catchKind := body[pc] + switch catchKind { + case CatchKindCatch, CatchKindCatchRef: + pc++ + tagIdx, tagNum, err := leb128.LoadUint32(body[pc:]) + if err != nil { + return fmt.Errorf("read catch tag index: %v", err) + } + pc += tagNum - 1 + if tagIdx >= uint32(len(tags)) { + return fmt.Errorf("invalid tag index in catch clause: %d", tagIdx) + } + tagType := &m.TypeSection[tags[tagIdx]] + pc++ + labelIdx, labelNum, err := leb128.LoadUint32(body[pc:]) + if err != nil { + return fmt.Errorf("read catch label index: %v", err) + } + pc += labelNum - 1 + if int(labelIdx) >= len(controlBlockStack.stack) { + return fmt.Errorf("invalid label index in catch clause: %d", labelIdx) + } + // Validate that the target label can accept the catch values. + target := &controlBlockStack.stack[len(controlBlockStack.stack)-int(labelIdx)-1] + var expectedTypes []ValueType + if target.op == OpcodeLoop { + expectedTypes = target.blockType.Params + } else { + expectedTypes = target.blockType.Results + } + var catchTypes []ValueType + catchTypes = append(catchTypes, tagType.Params...) + if catchKind == CatchKindCatchRef { + catchTypes = append(catchTypes, ValueTypeExnref) + } + if len(catchTypes) != len(expectedTypes) { + return fmt.Errorf("catch clause type mismatch: catch delivers %d values but label expects %d", len(catchTypes), len(expectedTypes)) + } + for j := range catchTypes { + if !isStrictRefSubtypeOf(catchTypes[j], expectedTypes[j]) { + return fmt.Errorf("catch clause type mismatch at index %d: %v is not a subtype of %v", j, catchTypes[j], expectedTypes[j]) + } + } + case CatchKindCatchAll, CatchKindCatchAllRef: + pc++ + labelIdx, labelNum, err := leb128.LoadUint32(body[pc:]) + if err != nil { + return fmt.Errorf("read catch_all label index: %v", err) + } + pc += labelNum - 1 + if int(labelIdx) >= len(controlBlockStack.stack) { + return fmt.Errorf("invalid label index in catch_all clause: %d", labelIdx) + } + target := &controlBlockStack.stack[len(controlBlockStack.stack)-int(labelIdx)-1] + var expectedTypes []ValueType + if target.op == OpcodeLoop { + expectedTypes = target.blockType.Params + } else { + expectedTypes = target.blockType.Results + } + var catchTypes []ValueType + if catchKind == CatchKindCatchAllRef { + catchTypes = append(catchTypes, ValueTypeExnref) + } + if len(catchTypes) != len(expectedTypes) { + return fmt.Errorf("catch_all clause type mismatch: catch delivers %d values but label expects %d", len(catchTypes), len(expectedTypes)) + } + for j := range catchTypes { + if catchTypes[j] != expectedTypes[j] { + return fmt.Errorf("catch_all clause type mismatch at index %d", j) + } + } + default: + return fmt.Errorf("invalid catch kind: %#x", catchKind) + } + } + + controlBlockStack.push(pc, 0, 0, bt, 0, op) + if err = valueTypeStack.popParams(op, bt.Params, false); err != nil { + return err + } + for _, p := range bt.Params { + valueTypeStack.push(p) + } + valueTypeStack.pushStackLimit(len(bt.Params)) } else if op == OpcodeCall || op == OpcodeTailCallReturnCall { pc++ index, num, err := leb128.LoadUint32(body[pc:]) @@ -885,6 +1023,7 @@ func (m *Module) validateFunctionWithMaxStackValues( return fmt.Errorf("undeclared function index %d for ref.func", index) } pc += num - 1 + // ref.func always produces a non-null reference. valueTypeStack.push(ValueTypeFuncref) } } else if op == OpcodeTableGet || op == OpcodeTableSet { @@ -1973,6 +2112,10 @@ func (m *Module) validateFunctionWithMaxStackValues( // unreachable instruction is stack-polymorphic. valueTypeStack.unreachable() } else if op == OpcodeNop { + } else if enabledFeatures.IsEnabled(experimental.CoreFeaturesExceptionHandling) && + (op == OpcodeLegacyTry || op == OpcodeLegacyCatch || op == OpcodeLegacyRethrow || + op == OpcodeLegacyDelegate || op == OpcodeLegacyCatchAll) { + return fmt.Errorf("legacy exception handling instruction 0x%x not supported; recompile with wasm-opt --translate-to-exnref", op) } else { return fmt.Errorf("invalid instruction 0x%x", op) } @@ -2125,7 +2268,7 @@ func (s *valueTypeStack) popAndVerifyType(expected ValueType) error { if !ok { return fmt.Errorf("%s missing", ValueTypeName(expected)) } - if have != expected && have != valueTypeUnknown && expected != valueTypeUnknown { + if have != expected && have != valueTypeUnknown && expected != valueTypeUnknown && !isRefSubtypeOf(have, expected) { return fmt.Errorf("type mismatch: expected %s, but was %s", ValueTypeName(expected), ValueTypeName(have)) } return nil @@ -2207,7 +2350,7 @@ func (s *valueTypeStack) requireStackValues( // Finally, check the types of the values: for i, v := range s.requireStackValuesTmp { nextWant := want[countWanted-i-1] // have is in reverse order (stack) - if v != nextWant && v != valueTypeUnknown && nextWant != valueTypeUnknown { + if v != nextWant && v != valueTypeUnknown && nextWant != valueTypeUnknown && !isRefSubtypeOf(v, nextWant) { return typeMismatchError(isParam, context, v, nextWant, i) } } @@ -2336,6 +2479,36 @@ func DecodeBlockType(types []FunctionType, r *bytes.Reader, enabledFeatures api. ret = blockType_v_funcref case -17: // 0x6f in original byte = externref ret = blockType_v_externref + case -23: // 0x69 in original byte = exnref + ret = blockType_v_exnref + case -29: // 0x63 = ref null (nullable) — GC proposal + ht, htNum, err := leb128.DecodeInt33AsInt64(r) + if err != nil { + return nil, 0, fmt.Errorf("read ref heap type in block: %w", err) + } + num += htNum + switch ht { + case -23: // exn + ret = blockType_v_exnref + case -16: // func + ret = blockType_v_funcref + case -17: // extern + ret = blockType_v_externref + default: // concrete type index — treat as nullable funcref + ret = blockType_v_funcref + } + case -28: // 0x64 = ref (non-nullable) — GC proposal + ht, htNum, err := leb128.DecodeInt33AsInt64(r) + if err != nil { + return nil, 0, fmt.Errorf("read ref heap type in block: %w", err) + } + num += htNum + switch ht { + case -23: // exn + ret = blockType_v_exnref // TODO: non-null exnref + default: + ret = blockType_v_funcref + } default: if err = enabledFeatures.RequireEnabled(api.CoreFeatureMultiValue); err != nil { return nil, num, fmt.Errorf("block with function type return invalid as %v", err) @@ -2358,6 +2531,7 @@ var ( blockType_v_v128 = &FunctionType{Results: []ValueType{ValueTypeV128}, ResultNumInUint64: 2} blockType_v_funcref = &FunctionType{Results: []ValueType{ValueTypeFuncref}, ResultNumInUint64: 1} blockType_v_externref = &FunctionType{Results: []ValueType{ValueTypeExternref}, ResultNumInUint64: 1} + blockType_v_exnref = &FunctionType{Results: []ValueType{ValueTypeExnref}, ResultNumInUint64: 1} ) // SplitCallStack returns the input stack resliced to the count of params and diff --git a/internal/wasm/func_validation_test.go b/internal/wasm/func_validation_test.go index 453ae4571..94cec87cb 100644 --- a/internal/wasm/func_validation_test.go +++ b/internal/wasm/func_validation_test.go @@ -37,12 +37,12 @@ func TestModule_ValidateFunction_validateFunctionWithMaxStackValues(t *testing.T t.Run("not exceed", func(t *testing.T) { err := m.validateFunctionWithMaxStackValues(&stacks{}, api.CoreFeaturesV1, - 0, []Index{0}, nil, nil, nil, max+1, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, nil, nil, nil, max+1, nil, bytes.NewReader(nil)) require.NoError(t, err) }) t.Run("exceed", func(t *testing.T) { err := m.validateFunctionWithMaxStackValues(&stacks{}, api.CoreFeaturesV1, - 0, []Index{0}, nil, nil, nil, max, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, nil, nil, nil, max, nil, bytes.NewReader(nil)) require.Error(t, err) expMsg := fmt.Sprintf("function may have %d stack values, which exceeds limit %d", valuesNum, max) require.Equal(t, expMsg, err.Error()) @@ -86,7 +86,7 @@ func TestModule_ValidateFunction_SignExtensionOps(t *testing.T) { CodeSection: []Code{{Body: []byte{tc.input}}}, } err := m.validateFunction(&stacks{}, api.CoreFeaturesV1, - 0, []Index{0}, nil, nil, nil, nil, + 0, []Index{0}, nil, nil, nil, nil, nil, bytes.NewReader(nil)) require.EqualError(t, err, tc.expectedErrOnDisable) }) @@ -105,7 +105,7 @@ func TestModule_ValidateFunction_SignExtensionOps(t *testing.T) { CodeSection: []Code{{Body: body}}, } err := m.validateFunction(&stacks{}, api.CoreFeatureSignExtensionOps, - 0, []Index{0}, nil, nil, nil, + 0, []Index{0}, nil, nil, nil, nil, nil, bytes.NewReader(nil)) require.NoError(t, err) }) @@ -162,7 +162,7 @@ func TestModule_ValidateFunction_NonTrappingFloatToIntConversion(t *testing.T) { CodeSection: []Code{{Body: []byte{OpcodeMiscPrefix, tc.input}}}, } err := m.validateFunction(&stacks{}, api.CoreFeaturesV1, - 0, []Index{0}, nil, nil, nil, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, nil, nil, nil, nil, bytes.NewReader(nil)) require.EqualError(t, err, tc.expectedErrOnDisable) }) t.Run("enabled", func(t *testing.T) { @@ -181,7 +181,7 @@ func TestModule_ValidateFunction_NonTrappingFloatToIntConversion(t *testing.T) { CodeSection: []Code{{Body: body}}, } err := m.validateFunction(&stacks{}, api.CoreFeatureNonTrappingFloatToIntConversion, - 0, []Index{0}, nil, nil, nil, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, nil, nil, nil, nil, bytes.NewReader(nil)) require.NoError(t, err) }) }) @@ -259,12 +259,12 @@ func TestModule_ValidateFunction_MultiValue(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Run("disabled", func(t *testing.T) { err := tc.module.validateFunction(&stacks{}, api.CoreFeaturesV1, - 0, []Index{0}, nil, nil, nil, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, nil, nil, nil, nil, bytes.NewReader(nil)) require.EqualError(t, err, tc.expectedErrOnDisable) }) t.Run("enabled", func(t *testing.T) { err := tc.module.validateFunction(&stacks{}, api.CoreFeatureMultiValue, - 0, []Index{0}, nil, nil, nil, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, nil, nil, nil, nil, bytes.NewReader(nil)) require.NoError(t, err) }) }) @@ -302,7 +302,7 @@ func TestModule_ValidateFunction_BulkMemoryOperations(t *testing.T) { DataCountSection: &c, } err := m.validateFunction(&stacks{}, api.CoreFeatureBulkMemoryOperations, - 0, []Index{0}, nil, &Memory{}, []Table{{}, {}}, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, &Memory{}, []Table{{}, {}}, nil, nil, bytes.NewReader(nil)) require.NoError(t, err) }) } @@ -666,7 +666,7 @@ func TestModule_ValidateFunction_BulkMemoryOperations(t *testing.T) { c := uint32(0) m.DataCountSection = &c } - err := m.validateFunction(&stacks{}, tc.flag, 0, []Index{0}, nil, tc.memory, tc.tables, nil, bytes.NewReader(nil)) + err := m.validateFunction(&stacks{}, tc.flag, 0, []Index{0}, nil, tc.memory, tc.tables, nil, nil, bytes.NewReader(nil)) require.EqualError(t, err, tc.expectedErr) }) } @@ -2203,7 +2203,7 @@ func TestModule_ValidateFunction_MultiValue_TypeMismatch(t *testing.T) { t.Run(tc.name, func(t *testing.T) { err := tc.module.validateFunction(&stacks{}, api.CoreFeatureMultiValue, - 0, []Index{0}, nil, nil, nil, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, nil, nil, nil, nil, bytes.NewReader(nil)) require.EqualError(t, err, tc.expectedErr) }) } @@ -2221,7 +2221,7 @@ func TestModule_funcValidation_CallIndirect(t *testing.T) { }}}, } err := m.validateFunction(&stacks{}, api.CoreFeatureReferenceTypes, - 0, []Index{0}, nil, &Memory{}, []Table{{Type: RefTypeFuncref}}, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, &Memory{}, []Table{{Type: RefTypeFuncref}}, nil, nil, bytes.NewReader(nil)) require.NoError(t, err) }) t.Run("non zero table index", func(t *testing.T) { @@ -2236,12 +2236,12 @@ func TestModule_funcValidation_CallIndirect(t *testing.T) { } t.Run("disabled", func(t *testing.T) { err := m.validateFunction(&stacks{}, api.CoreFeaturesV1, - 0, []Index{0}, nil, &Memory{}, []Table{{}, {}}, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, &Memory{}, []Table{{}, {}}, nil, nil, bytes.NewReader(nil)) require.EqualError(t, err, "table index must be zero but was 100: feature \"reference-types\" is disabled") }) t.Run("enabled but out of range", func(t *testing.T) { err := m.validateFunction(&stacks{}, api.CoreFeatureReferenceTypes, - 0, []Index{0}, nil, &Memory{}, []Table{{}, {}}, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, &Memory{}, []Table{{}, {}}, nil, nil, bytes.NewReader(nil)) require.EqualError(t, err, "unknown table index: 100") }) }) @@ -2256,7 +2256,7 @@ func TestModule_funcValidation_CallIndirect(t *testing.T) { }}}, } err := m.validateFunction(&stacks{}, api.CoreFeatureReferenceTypes, - 0, []Index{0}, nil, &Memory{}, []Table{{Type: RefTypeExternref}}, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, &Memory{}, []Table{{Type: RefTypeExternref}}, nil, nil, bytes.NewReader(nil)) require.EqualError(t, err, "table is not funcref type but was externref for call_indirect") }) } @@ -2352,7 +2352,7 @@ func TestModule_funcValidation_RefTypes(t *testing.T) { CodeSection: []Code{{Body: tc.body}}, } err := m.validateFunction(&stacks{}, tc.flag, - 0, []Index{0}, nil, nil, nil, tc.declaredFunctionIndexes, bytes.NewReader(nil)) + 0, []Index{0}, nil, nil, nil, nil, tc.declaredFunctionIndexes, bytes.NewReader(nil)) if tc.expectedErr != "" { require.EqualError(t, err, tc.expectedErr) } else { @@ -2521,7 +2521,7 @@ func TestModule_funcValidation_TableGrowSizeFill(t *testing.T) { CodeSection: []Code{{Body: tc.body}}, } err := m.validateFunction(&stacks{}, tc.flag, - 0, []Index{0}, nil, nil, tables, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, nil, tables, nil, nil, bytes.NewReader(nil)) if tc.expectedErr != "" { require.EqualError(t, err, tc.expectedErr) } else { @@ -2634,7 +2634,7 @@ func TestModule_funcValidation_TableGetSet(t *testing.T) { CodeSection: []Code{{Body: tc.body}}, } err := m.validateFunction(&stacks{}, tc.flag, - 0, []Index{0}, nil, nil, tables, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, nil, tables, nil, nil, bytes.NewReader(nil)) if tc.expectedErr != "" { require.EqualError(t, err, tc.expectedErr) } else { @@ -2692,7 +2692,7 @@ func TestModule_funcValidation_Select_error(t *testing.T) { CodeSection: []Code{{Body: tc.body}}, } err := m.validateFunction(&stacks{}, tc.flag, - 0, []Index{0}, nil, nil, nil, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, nil, nil, nil, nil, bytes.NewReader(nil)) require.EqualError(t, err, tc.expectedErr) }) } @@ -3178,7 +3178,7 @@ func TestModule_funcValidation_SIMD(t *testing.T) { CodeSection: []Code{{Body: tc.body}}, } err := m.validateFunction(&stacks{}, api.CoreFeatureSIMD, - 0, []Index{0}, nil, &Memory{}, nil, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, &Memory{}, nil, nil, nil, bytes.NewReader(nil)) require.NoError(t, err) }) } @@ -3324,7 +3324,7 @@ func TestModule_funcValidation_SIMD_error(t *testing.T) { CodeSection: []Code{{Body: tc.body}}, } err := m.validateFunction(&stacks{}, tc.flag, - 0, []Index{0}, nil, &Memory{}, nil, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, &Memory{}, nil, nil, nil, bytes.NewReader(nil)) require.EqualError(t, err, tc.expectedErr) }) } @@ -3442,7 +3442,7 @@ func TestFuncValidation_UnreachableBrTable_NotModifyTypes(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { err := tc.m.validateFunction(&stacks{}, api.CoreFeaturesV2, - 0, nil, nil, nil, nil, nil, bytes.NewReader(nil)) + 0, nil, nil, nil, nil, nil, nil, bytes.NewReader(nil)) require.NoError(t, err) // Ensures that funcType has remained intact. @@ -3567,7 +3567,7 @@ func TestModule_funcValidation_loopWithParams(t *testing.T) { CodeSection: []Code{{Body: tc.body}}, } err := m.validateFunction(&stacks{}, api.CoreFeatureMultiValue, - 0, []Index{0}, nil, nil, nil, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, nil, nil, nil, nil, bytes.NewReader(nil)) if tc.expErr != "" { require.EqualError(t, err, tc.expErr) } else { @@ -3585,7 +3585,7 @@ func TestFunctionValidation_redundantEnd(t *testing.T) { CodeSection: []Code{{Body: []byte{OpcodeEnd, OpcodeEnd}}}, } err := m.validateFunction(&stacks{}, api.CoreFeaturesV2, - 0, nil, nil, nil, nil, nil, bytes.NewReader(nil)) + 0, nil, nil, nil, nil, nil, nil, bytes.NewReader(nil)) require.EqualError(t, err, "unexpected end of function at pc=0x1") } @@ -3618,7 +3618,7 @@ func TestFunctionValidation_redundantElse(t *testing.T) { t.Run(tc.expErr, func(t *testing.T) { m := &Module{TypeSection: []FunctionType{{}}, FunctionSection: []Index{0}, CodeSection: []Code{{Body: tc.body}}} err := m.validateFunction(&stacks{}, api.CoreFeaturesV2, - 0, nil, nil, nil, nil, nil, bytes.NewReader(nil)) + 0, nil, nil, nil, nil, nil, nil, bytes.NewReader(nil)) require.EqualError(t, err, tc.expErr) }) } @@ -4272,13 +4272,13 @@ func TestModule_funcValidation_Atomic(t *testing.T) { t.Run("with memory", func(t *testing.T) { err := m.validateFunction(&stacks{}, experimental.CoreFeaturesThreads, - 0, []Index{0}, nil, &Memory{}, []Table{}, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, &Memory{}, []Table{}, nil, nil, bytes.NewReader(nil)) require.NoError(t, err) }) t.Run("without memory", func(t *testing.T) { err := m.validateFunction(&stacks{}, experimental.CoreFeaturesThreads, - 0, []Index{0}, nil, nil, []Table{}, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, nil, []Table{}, nil, nil, bytes.NewReader(nil)) // Only fence doesn't require memory if tc.name == "memory.atomic.fence" { require.NoError(t, err) @@ -4301,7 +4301,7 @@ func TestModule_funcValidation_Atomic(t *testing.T) { CodeSection: []Code{{Body: body}}, } err := m.validateFunction(&stacks{}, experimental.CoreFeaturesThreads, - 0, []Index{0}, nil, &Memory{}, []Table{}, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, &Memory{}, []Table{}, nil, nil, bytes.NewReader(nil)) require.Error(t, err, "invalid immediate value for atomic.fence") }) @@ -4864,9 +4864,43 @@ func TestModule_funcValidation_Atomic(t *testing.T) { CodeSection: []Code{{Body: body}}, } err := m.validateFunction(&stacks{}, experimental.CoreFeaturesThreads, - 0, []Index{0}, nil, &Memory{}, []Table{}, nil, bytes.NewReader(nil)) + 0, []Index{0}, nil, &Memory{}, []Table{}, nil, nil, bytes.NewReader(nil)) require.Error(t, err, "invalid memory alignment") }) } }) } + +func TestValidation_LegacyExceptionHandlingOpcodes(t *testing.T) { + for _, tc := range []struct { + opcode byte + name string + }{ + {OpcodeLegacyTry, "try"}, + {OpcodeLegacyCatch, "catch"}, + {OpcodeLegacyRethrow, "rethrow"}, + {OpcodeLegacyDelegate, "delegate"}, + {OpcodeLegacyCatchAll, "catch_all"}, + } { + t.Run(tc.name, func(t *testing.T) { + m := &Module{ + TypeSection: []FunctionType{v_v}, + FunctionSection: []Index{0}, + CodeSection: []Code{{Body: []byte{tc.opcode, OpcodeEnd}}}, + } + t.Run("with EH enabled", func(t *testing.T) { + err := m.validateFunction(&stacks{}, api.CoreFeaturesV2|experimental.CoreFeaturesExceptionHandling, + 0, []Index{0}, nil, nil, nil, nil, nil, bytes.NewReader(nil)) + require.Error(t, err) + require.Contains(t, err.Error(), "legacy exception handling instruction") + require.Contains(t, err.Error(), "wasm-opt --translate-to-exnref") + }) + t.Run("without EH enabled", func(t *testing.T) { + err := m.validateFunction(&stacks{}, api.CoreFeaturesV2, + 0, []Index{0}, nil, nil, nil, nil, nil, bytes.NewReader(nil)) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid instruction") + }) + }) + } +} diff --git a/internal/wasm/instruction.go b/internal/wasm/instruction.go index 7f76b1e52..06222b76e 100644 --- a/internal/wasm/instruction.go +++ b/internal/wasm/instruction.go @@ -20,6 +20,20 @@ const ( // OpcodeElse brackets a sequence of instructions enclosed by an OpcodeIf. A branch instruction on a then label // breaks out to after the OpcodeEnd on the enclosing OpcodeIf. OpcodeElse Opcode = 0x05 + + // Exception handling instructions (toggled with CoreFeaturesExceptionHandling) + + // OpcodeThrow throws an exception with the given tag. + OpcodeThrow Opcode = 0x08 + // OpcodeThrowRef re-throws the exception referenced by an exnref value. + OpcodeThrowRef Opcode = 0x0a + + // Legacy exception handling opcodes (not supported; use wasm-opt --translate-to-exnref) + + OpcodeLegacyTry Opcode = 0x06 + OpcodeLegacyCatch Opcode = 0x07 + OpcodeLegacyRethrow Opcode = 0x09 + // OpcodeEnd terminates a control instruction OpcodeBlock, OpcodeLoop or OpcodeIf. OpcodeEnd Opcode = 0x0b @@ -48,6 +62,16 @@ const ( OpcodeSelect Opcode = 0x1b OpcodeTypedSelect Opcode = 0x1c + // Legacy exception handling opcodes (not supported; use wasm-opt --translate-to-exnref) + + OpcodeLegacyDelegate Opcode = 0x18 + OpcodeLegacyCatchAll Opcode = 0x19 + + // Exception handling instructions (toggled with CoreFeaturesExceptionHandling) + + // OpcodeTryTable brackets a sequence of instructions with catch clauses for exception handling. + OpcodeTryTable Opcode = 0x1f + // variable instructions OpcodeLocalGet Opcode = 0x20 @@ -989,6 +1013,8 @@ var instructionNames = [256]string{ OpcodeLoop: OpcodeLoopName, OpcodeIf: OpcodeIfName, OpcodeElse: OpcodeElseName, + OpcodeThrow: OpcodeThrowName, + OpcodeThrowRef: OpcodeThrowRefName, OpcodeEnd: OpcodeEndName, OpcodeBr: OpcodeBrName, OpcodeBrIf: OpcodeBrIfName, @@ -996,6 +1022,7 @@ var instructionNames = [256]string{ OpcodeReturn: OpcodeReturnName, OpcodeCall: OpcodeCallName, OpcodeCallIndirect: OpcodeCallIndirectName, + OpcodeTryTable: OpcodeTryTableName, OpcodeDrop: OpcodeDropName, OpcodeSelect: OpcodeSelectName, OpcodeTypedSelect: OpcodeTypedSelectName, @@ -1889,3 +1916,17 @@ var tailCallInstructionName = map[OpcodeTailCall]string{ func TailCallInstructionName(oc OpcodeTailCall) (ret string) { return tailCallInstructionName[oc] } + +// Catch clause kinds used within try_table encoding. +const ( + CatchKindCatch byte = 0x00 + CatchKindCatchRef byte = 0x01 + CatchKindCatchAll byte = 0x02 + CatchKindCatchAllRef byte = 0x03 +) + +const ( + OpcodeThrowName = "throw" + OpcodeThrowRefName = "throw_ref" + OpcodeTryTableName = "try_table" +) diff --git a/internal/wasm/module.go b/internal/wasm/module.go index fbbe5b713..143d9d305 100644 --- a/internal/wasm/module.go +++ b/internal/wasm/module.go @@ -94,6 +94,19 @@ type Module struct { // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#memory-section%E2%91%A0 MemorySection *Memory + // TagSection contains each tag defined in this module for exception handling. + // + // Tag indexes are offset by any imported tags because the tag index begins with imports, followed by + // ones defined in this module. + // + // Note: In the Binary Format, this is SectionIDTag. + // + // See https://github.com/WebAssembly/exception-handling/blob/main/proposals/exception-handling/Exceptions.md + TagSection []Tag + + // ImportTagCount is the cached count of imported tags set during decoding. + ImportTagCount Index + // GlobalSection contains each global defined in this module. // // Global indexes are offset by any imported globals because the global index begins with imports, followed by @@ -264,7 +277,7 @@ func (m *Module) Validate(enabledFeatures api.CoreFeatures) error { return err } - functions, globals, memory, tables, err := m.AllDeclarations() + functions, globals, memory, tables, tags, err := m.AllDeclarations() if err != nil { return err } @@ -281,12 +294,12 @@ func (m *Module) Validate(enabledFeatures api.CoreFeatures) error { return err } - if err = m.validateExports(enabledFeatures, functions, globals, memory, tables); err != nil { + if err = m.validateExports(enabledFeatures, functions, globals, memory, tables, tags); err != nil { return err } if m.CodeSection != nil { - if err = m.validateFunctions(enabledFeatures, functions, globals, memory, tables, MaximumFunctionIndex); err != nil { + if err = m.validateFunctions(enabledFeatures, functions, globals, memory, tables, tags, MaximumFunctionIndex); err != nil { return err } } // No need to validate host functions as NewHostModule validates @@ -298,6 +311,23 @@ func (m *Module) Validate(enabledFeatures api.CoreFeatures) error { if err = m.validateDataCountSection(); err != nil { return err } + + if err = m.validateTagSection(); err != nil { + return err + } + return nil +} + +func (m *Module) validateTagSection() error { + for i, tag := range m.TagSection { + if tag.Type >= uint32(len(m.TypeSection)) { + return fmt.Errorf("tag[%d] type index out of range", i) + } + ft := &m.TypeSection[tag.Type] + if len(ft.Results) > 0 { + return fmt.Errorf("tag[%d] type must have empty results, got %v", i, ft.Results) + } + } return nil } @@ -334,7 +364,7 @@ func (m *Module) validateGlobals(globals []GlobalType, numFuncts, maxGlobals uin return nil } -func (m *Module) validateFunctions(enabledFeatures api.CoreFeatures, functions []Index, globals []GlobalType, memory *Memory, tables []Table, maximumFunctionIndex uint32) error { +func (m *Module) validateFunctions(enabledFeatures api.CoreFeatures, functions []Index, globals []GlobalType, memory *Memory, tables []Table, tags []Index, maximumFunctionIndex uint32) error { if uint32(len(functions)) > maximumFunctionIndex { return fmt.Errorf("too many functions (%d) in a module", len(functions)) } @@ -368,7 +398,7 @@ func (m *Module) validateFunctions(enabledFeatures api.CoreFeatures, functions [ if c.GoFunc != nil { continue } - if err = m.validateFunction(vs, enabledFeatures, Index(idx), functions, globals, memory, tables, declaredFuncIndexes, br); err != nil { + if err = m.validateFunction(vs, enabledFeatures, Index(idx), functions, globals, memory, tables, tags, declaredFuncIndexes, br); err != nil { return fmt.Errorf("invalid %s: %w", m.funcDesc(SectionIDFunction, Index(idx)), err) } } @@ -506,12 +536,19 @@ func (m *Module) validateImports(enabledFeatures api.CoreFeatures) error { if err := enabledFeatures.RequireEnabled(api.CoreFeatureMutableGlobal); err != nil { return fmt.Errorf("invalid import[%q.%q] global: %w", imp.Module, imp.Name, err) } + case ExternTypeTag: + if int(imp.DescTag) >= len(m.TypeSection) { + return fmt.Errorf("invalid import[%q.%q] tag: type index out of range", imp.Module, imp.Name) + } + if len(m.TypeSection[imp.DescTag].Results) > 0 { + return fmt.Errorf("invalid import[%q.%q] tag: tag types must have no results", imp.Module, imp.Name) + } } } return nil } -func (m *Module) validateExports(enabledFeatures api.CoreFeatures, functions []Index, globals []GlobalType, memory *Memory, tables []Table) error { +func (m *Module) validateExports(enabledFeatures api.CoreFeatures, functions []Index, globals []GlobalType, memory *Memory, tables []Table, tags []Index) error { for i := range m.ExportSection { exp := &m.ExportSection[i] index := exp.Index @@ -538,6 +575,10 @@ func (m *Module) validateExports(enabledFeatures api.CoreFeatures, functions []I if index >= uint32(len(tables)) { return fmt.Errorf("table for export[%q] out of range", exp.Name) } + case ExternTypeTag: + if index >= uint32(len(tags)) { + return fmt.Errorf("tag for export[%q] out of range", exp.Name) + } } } return nil @@ -576,6 +617,16 @@ func (m *Module) validateDataCountSection() (err error) { return } +func (m *ModuleInstance) buildTags(module *Module) { + for i := range module.TagSection { + tag := &module.TagSection[i] + t := &TagInstance{ + Type: &module.TypeSection[tag.Type], + } + m.Tags[i+int(module.ImportTagCount)] = t + } +} + func (m *ModuleInstance) buildGlobals(module *Module, funcRefResolver func(funcIndex Index) Reference) { importedGlobals := m.Globals[:module.ImportGlobalCount] @@ -694,6 +745,13 @@ type FunctionType struct { // ResultsNumInUint64 is the number of uint64 values requires to represent the Wasm result type. ResultNumInUint64 int + + // RecGroupSize is the size of the rec group this type belongs to. + // Standalone types (not in an explicit rec group) have RecGroupSize 1. + RecGroupSize int + + // RecGroupPosition is the 0-based position of this type within its rec group. + RecGroupPosition int } func (f *FunctionType) CacheNumInUint64() { @@ -721,6 +779,15 @@ func (f *FunctionType) EqualsSignature(params []ValueType, results []ValueType) return bytes.Equal(f.Params, params) && bytes.Equal(f.Results, results) } +// EqualsType returns true if the function types are structurally equal AND +// belong to the same rec group position/size (GC proposal type identity). +func (f *FunctionType) EqualsType(other *FunctionType) bool { + if !f.EqualsSignature(other.Params, other.Results) { + return false + } + return f.RecGroupSize == other.RecGroupSize && f.RecGroupPosition == other.RecGroupPosition +} + // key gets or generates the key for Store.typeIDs. e.g. "i32_v" for one i32 parameter and no (void) result. func (f *FunctionType) key() string { if f.string != "" { @@ -741,6 +808,9 @@ func (f *FunctionType) key() string { if len(f.Results) == 0 { ret += "v" } + if f.RecGroupSize > 1 { + ret += fmt.Sprintf("|rec%d/%d", f.RecGroupPosition, f.RecGroupSize) + } f.string = ret return ret } @@ -766,6 +836,8 @@ type Import struct { DescMem *Memory // DescGlobal is the inlined GlobalType when Type equals ExternTypeGlobal DescGlobal GlobalType + // DescTag is the type index when Type equals ExternTypeTag + DescTag Index // IndexPerType has the index of this import per ExternType. IndexPerType Index } @@ -802,6 +874,13 @@ func (m *Memory) Validate(memoryLimitPages uint32) error { return nil } +// Tag represents an exception tag defined in the tag section. +// The Type field is an index into the TypeSection; the referenced function type +// must have empty results (tags carry parameters but produce no results). +type Tag struct { + Type Index +} + type GlobalType struct { ValType ValueType Mutable bool @@ -935,8 +1014,8 @@ type NameMapAssoc struct { NameMap NameMap } -// AllDeclarations returns all declarations for functions, globals, memories and tables in a module including imported ones. -func (m *Module) AllDeclarations() (functions []Index, globals []GlobalType, memory *Memory, tables []Table, err error) { +// AllDeclarations returns all declarations for functions, globals, memories, tables and tags in a module including imported ones. +func (m *Module) AllDeclarations() (functions []Index, globals []GlobalType, memory *Memory, tables []Table, tags []Index, err error) { for i := range m.ImportSection { imp := &m.ImportSection[i] switch imp.Type { @@ -948,6 +1027,8 @@ func (m *Module) AllDeclarations() (functions []Index, globals []GlobalType, mem memory = imp.DescMem case ExternTypeTable: tables = append(tables, imp.DescTable) + case ExternTypeTag: + tags = append(tags, imp.DescTag) } } @@ -956,6 +1037,10 @@ func (m *Module) AllDeclarations() (functions []Index, globals []GlobalType, mem g := &m.GlobalSection[i] globals = append(globals, g.Type) } + for i := range m.TagSection { + t := &m.TagSection[i] + tags = append(tags, t.Type) + } if m.MemorySection != nil { if memory != nil { // shouldn't be possible due to Validate err = errors.New("at most one table allowed in module") @@ -997,6 +1082,11 @@ const ( // See https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/binary/modules.html#data-count-section // See https://www.w3.org/TR/2022/WD-wasm-core-2-20220419/appendix/changes.html#bulk-memory-and-table-instructions SectionIDDataCount + + // SectionIDTag is for exception handling tags. + // + // See https://github.com/WebAssembly/exception-handling/blob/main/proposals/exception-handling/Exceptions.md + SectionIDTag SectionID = 13 ) // SectionIDName returns the canonical name of a module section. @@ -1029,6 +1119,8 @@ func SectionIDName(sectionID SectionID) string { return "data" case SectionIDDataCount: return "data_count" + case SectionIDTag: + return "tag" } return "unknown" } @@ -1046,6 +1138,24 @@ const ( // TODO: ValueTypeFuncref is not exposed in the api pkg yet. ValueTypeFuncref ValueType = 0x70 ValueTypeExternref = api.ValueTypeExternref + // ValueTypeExnref is the exception reference type used in exception handling. + ValueTypeExnref ValueType = 0x69 +) + +const ( + // RefPrefixNullable is the binary encoding prefix for nullable reference types (ref null ). + RefPrefixNullable byte = 0x63 + // RefPrefixNonNullable is the binary encoding prefix for non-nullable reference types (ref ). + RefPrefixNonNullable byte = 0x64 +) + +const ( + // HeapTypeFunc is the abstract heap type for function references. + HeapTypeFunc int64 = -16 + // HeapTypeExtern is the abstract heap type for external references. + HeapTypeExtern int64 = -17 + // HeapTypeExn is the abstract heap type for exception references. + HeapTypeExn int64 = -23 ) // ValueTypeName is an alias of api.ValueTypeName defined to simplify imports. @@ -1054,12 +1164,30 @@ func ValueTypeName(t ValueType) string { return "funcref" } else if t == ValueTypeV128 { return "v128" + } else if t == ValueTypeExnref { + return "exnref" } return api.ValueTypeName(t) } func isReferenceValueType(vt ValueType) bool { - return vt == ValueTypeExternref || vt == ValueTypeFuncref + return vt == ValueTypeExternref || vt == ValueTypeFuncref || vt == ValueTypeExnref +} + +// isRefSubtypeOf returns true if actual is assignment-compatible with expected. +// Currently, non-nullable ref types are desugared to nullable at decode time, +// so this reduces to equality. When non-nullable ref types are properly supported, +// this function should allow non-nullable to match nullable and vice versa. +func isRefSubtypeOf(actual, expected ValueType) bool { + return actual == expected +} + +// isStrictRefSubtypeOf returns true if actual is a strict subtype of expected. +// Currently, non-nullable ref types are desugared to nullable at decode time, +// so this reduces to equality. When non-nullable ref types are properly supported, +// non-nullable should be a subtype of nullable, but NOT vice versa. +func isStrictRefSubtypeOf(actual, expected ValueType) bool { + return actual == expected } // ExternType is an alias of api.ExternType defined to simplify imports. @@ -1074,9 +1202,14 @@ const ( ExternTypeMemoryName = api.ExternTypeMemoryName ExternTypeGlobal = api.ExternTypeGlobal ExternTypeGlobalName = api.ExternTypeGlobalName + ExternTypeTag = ExternType(0x04) + ExternTypeTagName = "tag" ) // ExternTypeName is an alias of api.ExternTypeName defined to simplify imports. func ExternTypeName(t ValueType) string { + if t == ExternTypeTag { + return ExternTypeTagName + } return api.ExternTypeName(t) } diff --git a/internal/wasm/module_test.go b/internal/wasm/module_test.go index 0448430fe..1fad01527 100644 --- a/internal/wasm/module_test.go +++ b/internal/wasm/module_test.go @@ -41,6 +41,17 @@ func TestFunctionType_String(t *testing.T) { } } +func TestIsReferenceValueType(t *testing.T) { + refTypes := []ValueType{ValueTypeFuncref, ValueTypeExternref, ValueTypeExnref} + for _, vt := range refTypes { + require.True(t, isReferenceValueType(vt), "expected %#x to be a reference type", vt) + } + nonRefTypes := []ValueType{ValueTypeI32, ValueTypeI64, ValueTypeF32, ValueTypeF64, ValueTypeV128} + for _, vt := range nonRefTypes { + require.False(t, isReferenceValueType(vt), "expected %#x to not be a reference type", vt) + } +} + func TestSectionIDName(t *testing.T) { tests := []struct { name string @@ -129,6 +140,7 @@ func TestModule_allDeclarations(t *testing.T) { expectedGlobals []GlobalType expectedMemory *Memory expectedTables []Table + expectedTags []Index }{ // Functions. { @@ -196,17 +208,38 @@ func TestModule_allDeclarations(t *testing.T) { }, expectedTables: []Table{{Min: 10}}, }, + // Tags. + { + module: &Module{ + ImportSection: []Import{{Type: ExternTypeTag, DescTag: 5}}, + }, + expectedTags: []Index{5}, + }, + { + module: &Module{ + TagSection: []Tag{{Type: 3}}, + }, + expectedTags: []Index{3}, + }, + { + module: &Module{ + ImportSection: []Import{{Type: ExternTypeTag, DescTag: 5}}, + TagSection: []Tag{{Type: 3}}, + }, + expectedTags: []Index{5, 3}, + }, } for i, tt := range tests { tc := tt t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { - functions, globals, memory, tables, err := tc.module.AllDeclarations() + functions, globals, memory, tables, tags, err := tc.module.AllDeclarations() require.NoError(t, err) require.Equal(t, tc.expectedFunctions, functions) require.Equal(t, tc.expectedGlobals, globals) require.Equal(t, tc.expectedTables, tables) require.Equal(t, tc.expectedMemory, memory) + require.Equal(t, tc.expectedTags, tags) }) } } @@ -461,12 +494,12 @@ func TestModule_validateFunctions(t *testing.T) { FunctionSection: []uint32{0}, CodeSection: []Code{{Body: []byte{OpcodeI32Const, 0, OpcodeDrop, OpcodeEnd}}}, } - err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, MaximumFunctionIndex) + err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, nil, MaximumFunctionIndex) require.NoError(t, err) }) t.Run("too many functions", func(t *testing.T) { m := Module{} - err := m.validateFunctions(api.CoreFeaturesV1, []uint32{1, 2, 3, 4}, nil, nil, nil, 3) + err := m.validateFunctions(api.CoreFeaturesV1, []uint32{1, 2, 3, 4}, nil, nil, nil, nil, 3) require.Error(t, err) require.EqualError(t, err, "too many functions (4) in a module") }) @@ -476,7 +509,7 @@ func TestModule_validateFunctions(t *testing.T) { FunctionSection: []Index{0}, CodeSection: nil, } - err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, MaximumFunctionIndex) + err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, nil, MaximumFunctionIndex) require.Error(t, err) require.EqualError(t, err, "code count (0) != function count (1)") }) @@ -486,7 +519,7 @@ func TestModule_validateFunctions(t *testing.T) { FunctionSection: []Index{1}, CodeSection: []Code{{Body: []byte{OpcodeEnd}}}, } - err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, MaximumFunctionIndex) + err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, nil, MaximumFunctionIndex) require.Error(t, err) require.EqualError(t, err, "invalid function[0]: type section index 1 out of range") }) @@ -496,7 +529,7 @@ func TestModule_validateFunctions(t *testing.T) { FunctionSection: []Index{0}, CodeSection: []Code{{Body: []byte{OpcodeF32Abs}}}, } - err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, MaximumFunctionIndex) + err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, nil, MaximumFunctionIndex) require.Error(t, err) require.Contains(t, err.Error(), "invalid function[0]: cannot pop the 1st f32 operand") }) @@ -507,7 +540,7 @@ func TestModule_validateFunctions(t *testing.T) { CodeSection: []Code{{Body: []byte{OpcodeF32Abs}}}, ExportSection: []Export{{Name: "f1", Type: ExternTypeFunc, Index: 0}}, } - err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, MaximumFunctionIndex) + err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, nil, MaximumFunctionIndex) require.Error(t, err) require.Contains(t, err.Error(), `invalid function[0] export["f1"]: cannot pop the 1st f32`) }) @@ -520,7 +553,7 @@ func TestModule_validateFunctions(t *testing.T) { CodeSection: []Code{{Body: []byte{OpcodeF32Abs}}}, ExportSection: []Export{{Name: "f1", Type: ExternTypeFunc, Index: 1}}, } - err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, MaximumFunctionIndex) + err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, nil, MaximumFunctionIndex) require.Error(t, err) require.Contains(t, err.Error(), `invalid function[0] export["f1"]: cannot pop the 1st f32`) }) @@ -534,7 +567,7 @@ func TestModule_validateFunctions(t *testing.T) { {Name: "f2", Type: ExternTypeFunc, Index: 0}, }, } - err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, MaximumFunctionIndex) + err := m.validateFunctions(api.CoreFeaturesV1, nil, nil, nil, nil, nil, MaximumFunctionIndex) require.Error(t, err) require.Contains(t, err.Error(), `invalid function[0] export["f1","f2"]: cannot pop the 1st f32`) }) @@ -728,7 +761,7 @@ func TestModule_validateExports(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { m := Module{ExportSection: tc.exportSection} - err := m.validateExports(tc.enabledFeatures, tc.functions, tc.globals, tc.memory, tc.tables) + err := m.validateExports(tc.enabledFeatures, tc.functions, tc.globals, tc.memory, tc.tables, nil) if tc.expectedErr != "" { require.EqualError(t, err, tc.expectedErr) } else { diff --git a/internal/wasm/store.go b/internal/wasm/store.go index b707a58a7..b71e4d976 100644 --- a/internal/wasm/store.go +++ b/internal/wasm/store.go @@ -77,6 +77,7 @@ type ( Globals []*GlobalInstance MemoryInstance *MemoryInstance Tables []*TableInstance + Tags []*TagInstance // Engine implements function calls for this module. Engine ModuleEngine @@ -149,6 +150,13 @@ type ( Index Index } + // TagInstance represents an instantiated exception handling tag. + // Tags are compared by identity (pointer equality), not structural type equality. + TagInstance struct { + // Type is the function type of this tag (params only; results must be empty). + Type *FunctionType + } + // FunctionTypeID is a uniquely assigned integer for a function type. // This is wazero specific runtime object and specific to a store, // and used at runtime to do type-checks on indirect function calls. @@ -341,6 +349,7 @@ func (s *Store) instantiate( m.Tables = make([]*TableInstance, int(module.ImportTableCount)+len(module.TableSection)) m.Globals = make([]*GlobalInstance, int(module.ImportGlobalCount)+len(module.GlobalSection)) + m.Tags = make([]*TagInstance, int(module.ImportTagCount)+len(module.TagSection)) m.Engine, err = s.Engine.NewModuleEngine(module, m) if err != nil { return nil, err @@ -360,6 +369,7 @@ func (s *Store) instantiate( allocator, _ := ctx.Value(expctxkeys.MemoryAllocatorKey{}).(experimental.MemoryAllocator) m.buildGlobals(module, m.Engine.FunctionInstanceReference) + m.buildTags(module) m.buildMemory(module, allocator) m.Exports = module.Exports for _, exp := range m.Exports { @@ -502,6 +512,15 @@ func (m *ModuleInstance) resolveImports(ctx context.Context, module *Module) (er return } m.Globals[i.IndexPerType] = importedGlobal + case ExternTypeTag: + expected := &module.TypeSection[i.DescTag] + importedTag := importedModule.Tags[imported.Index] + if !importedTag.Type.EqualsType(expected) { + err = errorInvalidImport(i, fmt.Errorf("tag type mismatch: %s != %s", + expected, importedTag.Type)) + return + } + m.Tags[i.IndexPerType] = importedTag } } } diff --git a/internal/wasm/table_test.go b/internal/wasm/table_test.go index 7f11724ac..fbcee35a1 100644 --- a/internal/wasm/table_test.go +++ b/internal/wasm/table_test.go @@ -373,7 +373,7 @@ func TestModule_validateTable(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - _, _, _, tables, err := tc.input.AllDeclarations() + _, _, _, tables, _, err := tc.input.AllDeclarations() require.NoError(t, err) err = tc.input.validateTable(api.CoreFeaturesV1, tables, maxTableIndex) @@ -682,7 +682,7 @@ func TestModule_validateTable_Errors(t *testing.T) { tc := tt t.Run(tc.name, func(t *testing.T) { - _, _, _, tables, err := tc.input.AllDeclarations() + _, _, _, tables, _, err := tc.input.AllDeclarations() require.NoError(t, err) err = tc.input.validateTable(api.CoreFeaturesV1, tables, maxTableIndex) require.EqualError(t, err, tc.expectedErr) diff --git a/internal/wasmruntime/errors.go b/internal/wasmruntime/errors.go index 556e5de82..c17e79715 100644 --- a/internal/wasmruntime/errors.go +++ b/internal/wasmruntime/errors.go @@ -33,6 +33,10 @@ var ( ErrRuntimeExpectedSharedMemory = New("expected shared memory") // ErrRuntimeTooManyWaiters indicates that atomic.wait was called with too many waiters. ErrRuntimeTooManyWaiters = New("too many waiters") + // ErrRuntimeUncaughtException indicates that a thrown exception was not caught by any handler. + ErrRuntimeUncaughtException = New("uncaught exception") + // ErrRuntimeNullReference indicates a null reference was used where a non-null reference was expected. + ErrRuntimeNullReference = New("null reference") ) // Error is returned by a wasm.Engine during the execution of Wasm functions, and they indicate that the Wasm runtime