diff --git a/src/interp/binary-reader-interp.cc b/src/interp/binary-reader-interp.cc index 4898da90d..eaae9f1b5 100644 --- a/src/interp/binary-reader-interp.cc +++ b/src/interp/binary-reader-interp.cc @@ -48,9 +48,16 @@ SegmentMode ToSegmentMode(uint8_t flags) { } } +// This is only used to distinguish try blocks and all other blocks, +// so there are only two kinds. +enum class LabelKind { Block, Try }; + struct Label { + LabelKind kind; Istream::Offset offset; Istream::Offset fixup_offset; + // Only needs to be set for try blocks. + u32 handler_desc_index; }; struct FixupMap { @@ -106,6 +113,11 @@ class BinaryReaderInterp : public BinaryReaderNop { Index global_index, Type type, bool mutable_) override; + Result OnImportTag(Index import_index, + string_view module_name, + string_view field_name, + Index tag_index, + Index sig_index) override; Result OnFunctionCount(Index count) override; Result OnFunction(Index index, Index sig_index) override; @@ -122,6 +134,9 @@ class BinaryReaderInterp : public BinaryReaderNop { Result BeginGlobal(Index index, Type type, bool mutable_) override; Result EndGlobalInitExpr(Index index) override; + Result OnTagCount(Index count) override; + Result OnTagType(Index index, Index sig_index) override; + Result OnExport(Index index, ExternalKind kind, Index item_index, @@ -162,6 +177,9 @@ class BinaryReaderInterp : public BinaryReaderNop { Index default_target_depth) override; Result OnCallExpr(Index func_index) override; Result OnCallIndirectExpr(Index sig_index, Index table_index) override; + Result OnCatchExpr(Index tag_index) override; + Result OnCatchAllExpr() override; + Result OnDelegateExpr(Index depth) override; Result OnReturnCallExpr(Index func_index) override; Result OnReturnCallIndirectExpr(Index sig_index, Index table_index) override; Result OnCompareExpr(Opcode opcode) override; @@ -194,6 +212,7 @@ class BinaryReaderInterp : public BinaryReaderNop { Result OnRefNullExpr(Type type) override; Result OnRefIsNullExpr() override; Result OnNopExpr() override; + Result OnRethrowExpr(Index depth) override; Result OnReturnExpr() override; Result OnSelectExpr(Index result_count, Type* result_types) override; Result OnStoreExpr(Opcode opcode, @@ -209,6 +228,8 @@ class BinaryReaderInterp : public BinaryReaderNop { Result OnElemDropExpr(Index segment_index) override; Result OnTableInitExpr(Index segment_index, Index table_index) override; Result OnTernaryExpr(Opcode opcode) override; + Result OnThrowExpr(Index tag_index) override; + Result OnTryExpr(Type sig_type) override; Result OnUnreachableExpr() override; Result EndFunctionBody(Index index) override; Result OnSimdLaneOpExpr(Opcode opcode, uint64_t value) override; @@ -259,9 +280,12 @@ class BinaryReaderInterp : public BinaryReaderNop { private: Label* GetLabel(Index depth); + Label* GetNearestTryLabel(Index depth); Label* TopLabel(); - void PushLabel(Istream::Offset offset = Istream::kInvalidOffset, - Istream::Offset fixup_offset = Istream::kInvalidOffset); + void PushLabel(LabelKind label = LabelKind::Block, + Istream::Offset offset = Istream::kInvalidOffset, + Istream::Offset fixup_offset = Istream::kInvalidOffset, + u32 handler_desc_index = kInvalidIndex); void PopLabel(); void PrintError(const char* format, ...); @@ -277,7 +301,10 @@ class BinaryReaderInterp : public BinaryReaderNop { Index keep_extra, Index* out_drop_count, Index* out_keep_count); - void EmitBr(Index depth, Index drop_count, Index keep_count); + void EmitBr(Index depth, + Index drop_count, + Index keep_count, + Index catch_drop_count); void FixupTopLabel(); u32 GetFuncOffset(Index func_index); @@ -348,6 +375,16 @@ Label* BinaryReaderInterp::GetLabel(Index depth) { return &label_stack_[label_stack_.size() - depth - 1]; } +Label* BinaryReaderInterp::GetNearestTryLabel(Index depth) { + for (size_t i = depth; i < label_stack_.size(); i++) { + Label* label = &label_stack_[label_stack_.size() - i - 1]; + if (label->kind == LabelKind::Try) { + return label; + } + } + return nullptr; +} + Label* BinaryReaderInterp::TopLabel() { return GetLabel(0); } @@ -404,8 +441,10 @@ Result BinaryReaderInterp::GetReturnCallDropKeepCount(const FuncType& func_type, void BinaryReaderInterp::EmitBr(Index depth, Index drop_count, - Index keep_count) { + Index keep_count, + Index catch_drop_count) { istream_.EmitDropKeep(drop_count, keep_count); + istream_.EmitCatchDrop(catch_drop_count); Istream::Offset offset = GetLabel(depth)->offset; istream_.Emit(Opcode::Br); if (offset == Istream::kInvalidOffset) { @@ -510,6 +549,20 @@ Result BinaryReaderInterp::OnImportGlobal(Index import_index, return Result::Ok; } +Result BinaryReaderInterp::OnImportTag(Index import_index, + string_view module_name, + string_view field_name, + Index tag_index, + Index sig_index) { + CHECK_RESULT(validator_.OnTag(loc, Var(sig_index))); + FuncType& func_type = module_.func_types[sig_index]; + TagType tag_type{TagAttr::Exception, func_type.params}; + module_.imports.push_back(ImportDesc{ImportType( + module_name.to_string(), field_name.to_string(), tag_type.Clone())}); + tag_types_.push_back(tag_type); + return Result::Ok; +} + Result BinaryReaderInterp::OnFunctionCount(Index count) { module_.funcs.reserve(count); return Result::Ok; @@ -518,7 +571,7 @@ Result BinaryReaderInterp::OnFunctionCount(Index count) { Result BinaryReaderInterp::OnFunction(Index index, Index sig_index) { CHECK_RESULT(validator_.OnFunction(loc, Var(sig_index))); FuncType& func_type = module_.func_types[sig_index]; - module_.funcs.push_back(FuncDesc{func_type, {}, 0}); + module_.funcs.push_back(FuncDesc{func_type, {}, 0, {}}); func_types_.push_back(func_type); return Result::Ok; } @@ -663,6 +716,20 @@ Result BinaryReaderInterp::OnInitExprRefFunc(Index index, Index func_index) { return Result::Ok; } +Result BinaryReaderInterp::OnTagCount(Index count) { + module_.tags.reserve(count); + return Result::Ok; +} + +Result BinaryReaderInterp::OnTagType(Index index, Index sig_index) { + CHECK_RESULT(validator_.OnTag(loc, Var(sig_index))); + FuncType& func_type = module_.func_types[sig_index]; + TagType tag_type{TagAttr::Exception, func_type.params}; + module_.tags.push_back(TagDesc{tag_type}); + tag_types_.push_back(tag_type); + return Result::Ok; +} + Result BinaryReaderInterp::OnExport(Index index, ExternalKind kind, Index item_index, @@ -815,9 +882,11 @@ Result BinaryReaderInterp::OnDataSegmentData(Index index, return Result::Ok; } -void BinaryReaderInterp::PushLabel(Istream::Offset offset, - Istream::Offset fixup_offset) { - label_stack_.push_back(Label{offset, fixup_offset}); +void BinaryReaderInterp::PushLabel(LabelKind kind, + Istream::Offset offset, + Istream::Offset fixup_offset, + u32 handler_desc_index) { + label_stack_.push_back(Label{kind, offset, fixup_offset, handler_desc_index}); } void BinaryReaderInterp::PopLabel() { @@ -837,7 +906,18 @@ Result BinaryReaderInterp::BeginFunctionBody(Index index, Offset size) { CHECK_RESULT(validator_.BeginFunctionBody(loc, index)); // Push implicit func label (equivalent to return). - PushLabel(); + // With exception handling it acts as a catch-less try block, which is + // needed to support delegating to the caller of a function using the + // try-delegate instruction. + PushLabel(LabelKind::Try, Istream::kInvalidOffset, Istream::kInvalidOffset, + func_->handlers.size()); + func_->handlers.push_back(HandlerDesc{HandlerKind::Catch, + istream_.end(), + Istream::kInvalidOffset, + {}, + {Istream::kInvalidOffset}, + static_cast(func_->locals.size()), + 0}); return Result::Ok; } @@ -995,7 +1075,7 @@ Result BinaryReaderInterp::OnBlockExpr(Type sig_type) { Result BinaryReaderInterp::OnLoopExpr(Type sig_type) { CHECK_RESULT(validator_.OnLoop(loc, sig_type)); - PushLabel(istream_.end()); + PushLabel(LabelKind::Block, istream_.end()); return Result::Ok; } @@ -1003,7 +1083,7 @@ Result BinaryReaderInterp::OnIfExpr(Type sig_type) { CHECK_RESULT(validator_.OnIf(loc, sig_type)); istream_.Emit(Opcode::InterpBrUnless); auto fixup = istream_.EmitFixupU32(); - PushLabel(Istream::kInvalidOffset, fixup); + PushLabel(LabelKind::Block, Istream::kInvalidOffset, fixup); return Result::Ok; } @@ -1027,6 +1107,15 @@ Result BinaryReaderInterp::OnEndExpr() { CHECK_RESULT(validator_.OnEnd(loc)); if (label_type == LabelType::If || label_type == LabelType::Else) { istream_.ResolveFixupU32(TopLabel()->fixup_offset); + } else if (label_type == LabelType::Try) { + // Catch-less try blocks need to fill in the handler description + // so that it can trigger an exception rethrow when it's reached. + Label* local_label = TopLabel(); + HandlerDesc& desc = func_->handlers[local_label->handler_desc_index]; + desc.try_end_offset = istream_.end(); + assert(desc.catches.size() == 0); + } else if (label_type == LabelType::Catch) { + istream_.EmitCatchDrop(1); } FixupTopLabel(); PopLabel(); @@ -1034,21 +1123,23 @@ Result BinaryReaderInterp::OnEndExpr() { } Result BinaryReaderInterp::OnBrExpr(Index depth) { - Index drop_count, keep_count; + Index drop_count, keep_count, catch_drop_count; CHECK_RESULT(GetBrDropKeepCount(depth, &drop_count, &keep_count)); + CHECK_RESULT(validator_.GetCatchCount(depth, &catch_drop_count)); CHECK_RESULT(validator_.OnBr(loc, Var(depth))); - EmitBr(depth, drop_count, keep_count); + EmitBr(depth, drop_count, keep_count, catch_drop_count); return Result::Ok; } Result BinaryReaderInterp::OnBrIfExpr(Index depth) { - Index drop_count, keep_count; + Index drop_count, keep_count, catch_drop_count; CHECK_RESULT(validator_.OnBrIf(loc, Var(depth))); CHECK_RESULT(GetBrDropKeepCount(depth, &drop_count, &keep_count)); + CHECK_RESULT(validator_.GetCatchCount(depth, &catch_drop_count)); // Flip the br_if so if is true it can drop values from the stack. istream_.Emit(Opcode::InterpBrUnless); auto fixup = istream_.EmitFixupU32(); - EmitBr(depth, drop_count, keep_count); + EmitBr(depth, drop_count, keep_count, catch_drop_count); istream_.ResolveFixupU32(fixup); return Result::Ok; } @@ -1057,24 +1148,29 @@ Result BinaryReaderInterp::OnBrTableExpr(Index num_targets, Index* target_depths, Index default_target_depth) { CHECK_RESULT(validator_.BeginBrTable(loc)); - Index drop_count, keep_count; + Index drop_count, keep_count, catch_drop_count; istream_.Emit(Opcode::BrTable, num_targets); for (Index i = 0; i < num_targets; ++i) { Index depth = target_depths[i]; CHECK_RESULT(validator_.OnBrTableTarget(loc, Var(depth))); CHECK_RESULT(GetBrDropKeepCount(depth, &drop_count, &keep_count)); + CHECK_RESULT(validator_.GetCatchCount(depth, &catch_drop_count)); // Emit DropKeep directly (instead of using EmitDropKeep) so the - // instruction has a fixed size. + // instruction has a fixed size. Same for CatchDrop as well. istream_.Emit(Opcode::InterpDropKeep, drop_count, keep_count); - EmitBr(depth, 0, 0); + istream_.Emit(Opcode::InterpCatchDrop, catch_drop_count); + EmitBr(depth, 0, 0, 0); } CHECK_RESULT(validator_.OnBrTableTarget(loc, Var(default_target_depth))); CHECK_RESULT( GetBrDropKeepCount(default_target_depth, &drop_count, &keep_count)); + CHECK_RESULT( + validator_.GetCatchCount(default_target_depth, &catch_drop_count)); // The default case doesn't need a fixed size, since it is never jumped over. istream_.EmitDropKeep(drop_count, keep_count); - EmitBr(default_target_depth, 0, 0); + istream_.Emit(Opcode::InterpCatchDrop, catch_drop_count); + EmitBr(default_target_depth, 0, 0, 0); CHECK_RESULT(validator_.EndBrTable(loc)); return Result::Ok; @@ -1103,15 +1199,19 @@ Result BinaryReaderInterp::OnCallIndirectExpr(Index sig_index, Result BinaryReaderInterp::OnReturnCallExpr(Index func_index) { FuncType& func_type = func_types_[func_index]; - Index drop_count, keep_count; + Index drop_count, keep_count, catch_drop_count; CHECK_RESULT( GetReturnCallDropKeepCount(func_type, 0, &drop_count, &keep_count)); + CHECK_RESULT( + validator_.GetCatchCount(label_stack_.size() - 1, &catch_drop_count)); // The validator must be run after we get the drop/keep counts, since it // will change the type stack. CHECK_RESULT(validator_.OnReturnCall(loc, Var(func_index))); istream_.EmitDropKeep(drop_count, keep_count); + istream_.EmitCatchDrop(catch_drop_count); if (func_index >= num_func_imports()) { + istream_.Emit(Opcode::InterpAdjustFrameForReturnCall, func_index); istream_.Emit(Opcode::Br, GetFuncOffset(func_index)); } else { istream_.Emit(Opcode::InterpCallImport, func_index); @@ -1125,15 +1225,18 @@ Result BinaryReaderInterp::OnReturnCallIndirectExpr(Index sig_index, Index table_index) { FuncType& func_type = module_.func_types[sig_index]; - Index drop_count, keep_count; + Index drop_count, keep_count, catch_drop_count; // +1 to include the index of the function. CHECK_RESULT( GetReturnCallDropKeepCount(func_type, +1, &drop_count, &keep_count)); + CHECK_RESULT( + validator_.GetCatchCount(label_stack_.size() - 1, &catch_drop_count)); // The validator must be run after we get the drop/keep counts, since it // changes the type stack. CHECK_RESULT( validator_.OnReturnCallIndirect(loc, Var(sig_index), Var(table_index))); istream_.EmitDropKeep(drop_count, keep_count); + istream_.EmitCatchDrop(catch_drop_count); istream_.Emit(Opcode::ReturnCallIndirect, table_index, sig_index); return Result::Ok; } @@ -1297,10 +1400,13 @@ Result BinaryReaderInterp::OnNopExpr() { } Result BinaryReaderInterp::OnReturnExpr() { - Index drop_count, keep_count; + Index drop_count, keep_count, catch_drop_count; CHECK_RESULT(GetReturnDropKeepCount(&drop_count, &keep_count)); + CHECK_RESULT( + validator_.GetCatchCount(label_stack_.size() - 1, &catch_drop_count)); CHECK_RESULT(validator_.OnReturn(loc)); istream_.EmitDropKeep(drop_count, keep_count); + istream_.EmitCatchDrop(catch_drop_count); istream_.Emit(Opcode::Return); return Result::Ok; } @@ -1397,6 +1503,111 @@ Result BinaryReaderInterp::OnTableInitExpr(Index segment_index, return Result::Ok; } +Result BinaryReaderInterp::OnThrowExpr(Index tag_index) { + CHECK_RESULT(validator_.OnThrow(loc, Var(tag_index))); + istream_.Emit(Opcode::Throw, tag_index); + return Result::Ok; +} + +Result BinaryReaderInterp::OnRethrowExpr(Index depth) { + Index catch_depth; + CHECK_RESULT(validator_.OnRethrow(loc, Var(depth))); + CHECK_RESULT(validator_.GetCatchCount(depth, &catch_depth)); + // The rethrow opcode takes an index into the exception stack rather than + // the number of catch nestings, so we subtract one here. + istream_.Emit(Opcode::Rethrow, catch_depth - 1); + return Result::Ok; +} + +Result BinaryReaderInterp::OnTryExpr(Type sig_type) { + u32 exn_stack_height; + CHECK_RESULT( + validator_.GetCatchCount(label_stack_.size() - 1, &exn_stack_height)); + u32 value_stack_height = validator_.type_stack_size(); + CHECK_RESULT(validator_.OnTry(loc, sig_type)); + // Push a label that tracks mapping of exn -> catch + PushLabel(LabelKind::Try, Istream::kInvalidOffset, Istream::kInvalidOffset, + func_->handlers.size()); + func_->handlers.push_back(HandlerDesc{HandlerKind::Catch, + istream_.end(), + Istream::kInvalidOffset, + {}, + {Istream::kInvalidOffset}, + value_stack_height, + exn_stack_height}); + return Result::Ok; +} + +Result BinaryReaderInterp::OnCatchExpr(Index tag_index) { + CHECK_RESULT(validator_.OnCatch(loc, Var(tag_index), false)); + Label* label = TopLabel(); + HandlerDesc& desc = func_->handlers[label->handler_desc_index]; + desc.kind = HandlerKind::Catch; + // Drop the previous block's exception if it was a catch. + if (label->kind == LabelKind::Block) { + istream_.EmitCatchDrop(1); + } + // Jump to the end of the block at the end of the previous try or catch. + Istream::Offset offset = label->offset; + istream_.Emit(Opcode::Br); + assert(offset == Istream::kInvalidOffset); + depth_fixups_.Append(label_stack_.size() - 1, istream_.end()); + istream_.Emit(offset); + // The offset is only set after the first catch block, as the offset range + // should only cover the try block itself. + if (desc.try_end_offset == Istream::kInvalidOffset) { + desc.try_end_offset = istream_.end(); + } + // The label kind is switched to Block from Try in order to distinguish + // catch blocks from try blocks. This is used to ensure that a try-delegate + // inside this catch will not delegate to the catch, and instead find outer + // try blocks to use as a delegate target. + label->kind = LabelKind::Block; + desc.catches.push_back(CatchDesc{tag_index, istream_.end()}); + return Result::Ok; +} + +Result BinaryReaderInterp::OnCatchAllExpr() { + CHECK_RESULT(validator_.OnCatch(loc, Var(), true)); + Label* label = TopLabel(); + HandlerDesc& desc = func_->handlers[label->handler_desc_index]; + desc.kind = HandlerKind::Catch; + if (label->kind == LabelKind::Block) { + istream_.EmitCatchDrop(1); + } + Istream::Offset offset = label->offset; + istream_.Emit(Opcode::Br); + assert(offset == Istream::kInvalidOffset); + depth_fixups_.Append(label_stack_.size() - 1, istream_.end()); + istream_.Emit(offset); + if (desc.try_end_offset == Istream::kInvalidOffset) { + desc.try_end_offset = istream_.end(); + } + label->kind = LabelKind::Block; + desc.catch_all_offset = istream_.end(); + return Result::Ok; +} + +Result BinaryReaderInterp::OnDelegateExpr(Index depth) { + CHECK_RESULT(validator_.OnDelegate(loc, Var(depth))); + Label* label = TopLabel(); + assert(label->kind == LabelKind::Try); + HandlerDesc& desc = func_->handlers[label->handler_desc_index]; + desc.kind = HandlerKind::Delegate; + Istream::Offset offset = label->offset; + istream_.Emit(Opcode::Br); + assert(offset == Istream::kInvalidOffset); + depth_fixups_.Append(label_stack_.size() - 1, istream_.end()); + istream_.Emit(offset); + desc.try_end_offset = istream_.end(); + Label* target_label = GetNearestTryLabel(depth + 1); + assert(target_label); + desc.delegate_handler_index = target_label->handler_desc_index; + FixupTopLabel(); + PopLabel(); + return Result::Ok; +} + } // namespace Result ReadBinaryInterp(const void* data, diff --git a/src/interp/interp-inl.h b/src/interp/interp-inl.h index 4acccc0ba..2c452f02c 100644 --- a/src/interp/interp-inl.h +++ b/src/interp/interp-inl.h @@ -127,10 +127,16 @@ inline ExportType& ExportType::operator=(const ExportType& other) { //// Frame //// inline Frame::Frame(Ref func, u32 values, + u32 exceptions, u32 offset, Instance* inst, Module* mod) - : func(func), values(values), offset(offset), inst(inst), mod(mod) {} + : func(func), + values(values), + exceptions(exceptions), + offset(offset), + inst(inst), + mod(mod) {} //// FreeList //// template @@ -524,6 +530,25 @@ inline std::string Trap::message() const { return message_; } +//// Exception //// +// static +inline bool Exception::classof(const Object* obj) { + return obj->kind() == skind; +} + +// static +inline Exception::Ptr Exception::New(Store& store, Ref tag, Values& args) { + return store.Alloc(store, tag, args); +} + +inline Ref Exception::tag() const { + return tag_; +} + +inline Values& Exception::args() { + return args_; +} + //// Extern //// // static inline bool Extern::classof(const Object* obj) { diff --git a/src/interp/interp.cc b/src/interp/interp.cc index e5daa6fe7..d1e151f0e 100644 --- a/src/interp/interp.cc +++ b/src/interp/interp.cc @@ -313,6 +313,21 @@ void Trap::Mark(Store& store) { } } +//// Exception //// +Exception::Exception(Store& store, Ref tag, Values& args) + : Object(skind), tag_(tag), args_(args) {} + +void Exception::Mark(Store& store) { + Tag::Ptr tag(store, tag_); + store.Mark(tag_); + ValueTypes params = tag->type().signature; + for (size_t i = 0; i < params.size(); i++) { + if (params[i].IsRef()) { + store.Mark(args_[i].Get()); + } + } +} + //// Extern //// template Result Extern::MatchImpl(Store& store, @@ -386,6 +401,11 @@ Result DefinedFunc::DoCall(Thread& thread, result = thread.Run(out_trap); if (result == RunResult::Trap) { return Result::Error; + } else if (result == RunResult::Exception) { + // While this is not actually a trap, it is a convenient way + // to report an uncaught exception. + *out_trap = Trap::New(thread.store(), "uncaught exception"); + return Result::Error; } thread.PopValues(type_.results, &results); return Result::Ok; @@ -928,6 +948,7 @@ void Thread::Mark(Store& store) { for (auto index: refs_) { store.Mark(values_[index].Get()); } + store.Mark(exceptions_); } void Thread::PushValues(const ValueTypes& types, const Values& values) { @@ -955,7 +976,8 @@ Instance* Thread::GetCallerInstance() { RunResult Thread::PushCall(Ref func, u32 offset, Trap::Ptr* out_trap) { TRAP_IF(frames_.size() == frames_.capacity(), "call stack exhausted"); - frames_.emplace_back(func, values_.size(), offset, inst_, mod_); + frames_.emplace_back(func, values_.size(), exceptions_.size(), offset, inst_, + mod_); return RunResult::Ok; } @@ -963,8 +985,8 @@ RunResult Thread::PushCall(const DefinedFunc& func, Trap::Ptr* out_trap) { TRAP_IF(frames_.size() == frames_.capacity(), "call stack exhausted"); inst_ = store_.UnsafeGet(func.instance()).get(); mod_ = store_.UnsafeGet(inst_->module()).get(); - frames_.emplace_back(func.self(), values_.size(), func.desc().code_offset, - inst_, mod_); + frames_.emplace_back(func.self(), values_.size(), exceptions_.size(), + func.desc().code_offset, inst_, mod_); return RunResult::Ok; } @@ -972,11 +994,15 @@ RunResult Thread::PushCall(const HostFunc& func, Trap::Ptr* out_trap) { TRAP_IF(frames_.size() == frames_.capacity(), "call stack exhausted"); inst_ = nullptr; mod_ = nullptr; - frames_.emplace_back(func.self(), values_.size(), 0, inst_, mod_); + frames_.emplace_back(func.self(), values_.size(), exceptions_.size(), 0, + inst_, mod_); return RunResult::Ok; } RunResult Thread::PopCall() { + // Sanity check that the exception stack was popped correctly. + assert(frames_.back().exceptions == exceptions_.size()); + frames_.pop_back(); if (frames_.empty()) { return RunResult::Return; @@ -1414,6 +1440,24 @@ RunResult Thread::StepInternal(Trap::Ptr* out_trap) { break; } + case O::InterpCatchDrop: { + auto drop = instr.imm_u32; + for (u32 i = 0; i < drop; i++) { + exceptions_.pop_back(); + } + break; + } + + // This operation adjusts the function reference of the reused frame + // after a return_call. This ensures the correct exception handlers are + // used for the call. + case O::InterpAdjustFrameForReturnCall: { + Ref new_func_ref = inst_->funcs()[instr.imm_u32]; + Frame& current_frame = frames_.back(); + current_frame.func = new_func_ref; + break; + } + case O::I32TruncSatF32S: return DoUnop(IntTruncSat); case O::I32TruncSatF32U: return DoUnop(IntTruncSat); case O::I32TruncSatF64S: return DoUnop(IntTruncSat); @@ -1786,6 +1830,22 @@ RunResult Thread::StepInternal(Trap::Ptr* out_trap) { case O::I64AtomicRmw16CmpxchgU: return DoAtomicRmwCmpxchg(instr, out_trap); case O::I64AtomicRmw32CmpxchgU: return DoAtomicRmwCmpxchg(instr, out_trap); + case O::Throw: { + u32 tag_index = instr.imm_u32; + Values params; + Ref tag_ref = inst_->tags()[tag_index]; + Tag::Ptr tag{store_, tag_ref}; + PopValues(tag->type().signature, ¶ms); + Exception::Ptr exn = Exception::New(store_, tag_ref, params); + return DoThrow(exn); + } + case O::Rethrow: { + u32 exn_index = instr.imm_u32; + Exception::Ptr exn{store_, + exceptions_[exceptions_.size() - exn_index - 1]}; + return DoThrow(exn); + } + // The following opcodes are either never generated or should never be // executed. case O::Nop: @@ -1802,8 +1862,6 @@ RunResult Thread::StepInternal(Trap::Ptr* out_trap) { case O::Catch: case O::CatchAll: case O::Delegate: - case O::Throw: - case O::Rethrow: case O::InterpData: case O::Invalid: WABT_UNREACHABLE; @@ -2390,6 +2448,96 @@ RunResult Thread::DoAtomicRmwCmpxchg(Instr instr, Trap::Ptr* out_trap) { return RunResult::Ok; } +RunResult Thread::DoThrow(Exception::Ptr exn) { + Istream::Offset target_offset = Istream::kInvalidOffset; + u32 target_values, target_exceptions; + Tag::Ptr exn_tag{store_, exn->tag()}; + bool popped_frame = false; + bool had_catch_all = false; + + // DoThrow is responsible for unwinding the stack at the point at which an + // exception is thrown, and also branching to the appropriate catch within + // the target try-catch. In a compiler, the tag dispatch might be done in + // generated code in a landing pad, but this is easier for the interpreter. + while (!frames_.empty()) { + const Frame& frame = frames_.back(); + DefinedFunc::Ptr func{store_, frame.func}; + u32 pc = frame.offset; + auto handlers = func->desc().handlers; + + // We iterate in reverse order, in order to traverse handlers from most + // specific (pushed last) to least specific within a nested stack of + // try-catch blocks. + auto iter = handlers.rbegin(); + while (iter != handlers.rend()) { + const HandlerDesc& handler = *iter; + if (pc >= handler.try_start_offset && pc < handler.try_end_offset) { + // For a try-delegate, skip part of the traversal by directly going + // up to an outer handler specified by the delegate depth. + if (handler.kind == HandlerKind::Delegate) { + // Subtract one as we're trying to get a reverse iterator that is + // offset by `delegate_handler_index` from the first item. + iter = handlers.rend() - handler.delegate_handler_index - 1; + continue; + } + // Otherwise, check for a matching catch tag or catch_all. + for (auto _catch : handler.catches) { + // Here we have to be careful to use the target frame's instance + // to look up the tag rather than the throw's instance. + Ref catch_tag_ref = frame.inst->tags()[_catch.tag_index]; + Tag::Ptr catch_tag{store_, catch_tag_ref}; + if (exn_tag == catch_tag) { + target_offset = _catch.offset; + target_values = (*iter).values; + target_exceptions = (*iter).exceptions; + goto found_handler; + } + } + if (handler.catch_all_offset != Istream::kInvalidOffset) { + target_offset = handler.catch_all_offset; + target_values = (*iter).values; + target_exceptions = (*iter).exceptions; + had_catch_all = true; + goto found_handler; + } + } + iter++; + } + frames_.pop_back(); + popped_frame = true; + } + + // If the call frames are empty now, the exception is uncaught. + assert(frames_.empty()); + return RunResult::Exception; + +found_handler: + assert(target_offset != Istream::kInvalidOffset); + + Frame& target_frame = frames_.back(); + // If the throw crosses call frames, we need to reset the state to that + // call frame's values. The stack heights may need to be offset by the + // handler's heights as we may be jumping into the middle of the function + // code after some stack height changes. + if (popped_frame) { + inst_ = target_frame.inst; + mod_ = target_frame.mod; + } + values_.resize(target_frame.values + target_values); + exceptions_.resize(target_frame.exceptions + target_exceptions); + // Jump to the handler. + target_frame.offset = target_offset; + // When an exception is caught, it needs to be tracked in a stack + // to allow for rethrows. This stack is popped on leaving the try-catch + // or by control instructions such as `br`. + exceptions_.push_back(exn.ref()); + // Also push exception payload values if applicable. + if (!had_catch_all) { + PushValues(exn_tag->type().signature, exn->args()); + } + return RunResult::Ok; +} + Thread::TraceSource::TraceSource(Thread* thread) : thread_(thread) {} std::string Thread::TraceSource::Header(Istream::Offset offset) { diff --git a/src/interp/interp.h b/src/interp/interp.h index 2dc3ef29e..34a220c70 100644 --- a/src/interp/interp.h +++ b/src/interp/interp.h @@ -78,6 +78,7 @@ enum class ObjectKind { Null, Foreign, Trap, + Exception, DefinedFunc, HostFunc, Table, @@ -305,6 +306,32 @@ struct LocalDesc { u32 end; }; +// Metadata for representing exception handlers associated with a function's +// code. This is needed to look up exceptions from call frames from interpreter +// instructions. +struct CatchDesc { + Index tag_index; + u32 offset; +}; + +// Handlers for a catch-less `try` or `try-catch` block are included in the +// Catch kind. `try-delegate` instructions create a Delegate handler. +enum class HandlerKind { Catch, Delegate }; + +struct HandlerDesc { + HandlerKind kind; + u32 try_start_offset; + u32 try_end_offset; + std::vector catches; + union { + u32 catch_all_offset; + u32 delegate_handler_index; + }; + // Local stack heights at the handler site that need to be restored. + u32 values; + u32 exceptions; +}; + struct FuncDesc { // Includes params. ValueType GetLocalType(Index) const; @@ -312,6 +339,7 @@ struct FuncDesc { FuncType type; std::vector locals; u32 code_offset; + std::vector handlers; }; struct TableDesc { @@ -378,13 +406,19 @@ struct ModuleDesc { //// Runtime //// struct Frame { - explicit Frame(Ref func, u32 values, u32 offset, Instance*, Module*); + explicit Frame(Ref func, + u32 values, + u32 exceptions, + u32 offset, + Instance*, + Module*); void Mark(Store&); Ref func; u32 values; // Height of the value stack at this activation. - u32 offset; // Istream offset; either the return PC, or the current PC. + u32 exceptions; // Height of the exception stack at this activation. + u32 offset; // Istream offset; either the return PC, or the current PC. // Cached for convenience. Both are null if func is a HostFunc. Instance* inst; @@ -647,6 +681,27 @@ class Trap : public Object { std::vector trace_; }; +class Exception : public Object { + public: + static bool classof(const Object* obj); + static const ObjectKind skind = ObjectKind::Exception; + static const char* GetTypeName() { return "Exception"; } + using Ptr = RefPtr; + + static Exception::Ptr New(Store&, Ref tag, Values& args); + + Ref tag() const; + Values& args(); + + private: + friend Store; + explicit Exception(Store&, Ref, Values&); + void Mark(Store&) override; + + Ref tag_; + Values args_; +}; + class Extern : public Object { public: static bool classof(const Object* obj); @@ -1025,6 +1080,7 @@ enum class RunResult { Ok, Return, Trap, + Exception, }; // TODO: Kinda weird to have a thread as an object, but it makes reference @@ -1183,12 +1239,18 @@ class Thread : public Object { template RunResult DoAtomicRmwCmpxchg(Instr, Trap::Ptr* out_trap); + RunResult DoThrow(Exception::Ptr exn_ref); + RunResult StepInternal(Trap::Ptr* out_trap); std::vector frames_; std::vector values_; std::vector refs_; // Index into values_. + // Exception handling requires tracking a separate stack of caught + // exceptions for catch blocks. + RefVec exceptions_; + // Cached for convenience. Store& store_; Instance* inst_ = nullptr; diff --git a/src/interp/istream.cc b/src/interp/istream.cc index 10a103b3c..301411f50 100644 --- a/src/interp/istream.cc +++ b/src/interp/istream.cc @@ -86,6 +86,12 @@ void Istream::EmitDropKeep(u32 drop, u32 keep) { } } +void Istream::EmitCatchDrop(u32 drop) { + if (drop > 0) { + Emit(Opcode::InterpCatchDrop, drop); + } +} + Istream::Offset Istream::EmitFixupU32() { auto result = end(); EmitInternal(kInvalidOffset); @@ -493,6 +499,8 @@ Instr Istream::Read(Offset* offset) const { case Opcode::DataDrop: case Opcode::ElemDrop: case Opcode::RefFunc: + case Opcode::Throw: + case Opcode::Rethrow: // Index immediate, 0 operands. instr.kind = InstrKind::Imm_Index_Op_0; instr.imm_u32 = ReadAt(offset); @@ -685,6 +693,8 @@ Instr Istream::Read(Offset* offset) const { case Opcode::AtomicFence: case Opcode::I32Const: case Opcode::InterpAlloca: + case Opcode::InterpCatchDrop: + case Opcode::InterpAdjustFrameForReturnCall: // i32/f32 immediate, 0 operands. instr.kind = InstrKind::Imm_I32_Op_0; instr.imm_u32 = ReadAt(offset); @@ -762,8 +772,6 @@ Instr Istream::Read(Offset* offset) const { case Opcode::InterpData: case Opcode::Invalid: case Opcode::Loop: - case Opcode::Rethrow: - case Opcode::Throw: case Opcode::Try: case Opcode::ReturnCall: // Not used. diff --git a/src/interp/istream.h b/src/interp/istream.h index 614a942d3..c86855079 100644 --- a/src/interp/istream.h +++ b/src/interp/istream.h @@ -91,14 +91,15 @@ class Istream { using SerializedOpcode = u32; // TODO: change to u16 using Offset = u32; static const Offset kInvalidOffset = ~0; - // Each br_table entry is made up of two instructions: + // Each br_table entry is made up of three instructions: // // interp_drop_keep $drop $keep + // interp_catch_drop $catches // br $label // // Each opcode is a SerializedOpcode, and each immediate is a u32. static const Offset kBrTableEntrySize = - sizeof(SerializedOpcode) * 2 + 3 * sizeof(u32); + sizeof(SerializedOpcode) * 3 + 4 * sizeof(u32); // Emit API. void Emit(u32); @@ -110,6 +111,7 @@ class Istream { void Emit(Opcode::Enum, u32, u32); void Emit(Opcode::Enum, u32, u32, u8); void EmitDropKeep(u32 drop, u32 keep); + void EmitCatchDrop(u32 drop); Offset EmitFixupU32(); void ResolveFixupU32(Offset); diff --git a/src/opcode.def b/src/opcode.def index 611afd57e..9005418ed 100644 --- a/src/opcode.def +++ b/src/opcode.def @@ -231,6 +231,8 @@ WABT_OPCODE(___, I32, ___, ___, 0, 0, 0xe1, InterpBrUnless, "br_unless", WABT_OPCODE(___, ___, ___, ___, 0, 0, 0xe2, InterpCallImport, "call_import", "") WABT_OPCODE(___, ___, ___, ___, 0, 0, 0xe3, InterpData, "data", "") WABT_OPCODE(___, ___, ___, ___, 0, 0, 0xe4, InterpDropKeep, "drop_keep", "") +WABT_OPCODE(___, ___, ___, ___, 0, 0, 0xe5, InterpCatchDrop, "catch_drop", "") +WABT_OPCODE(___, ___, ___, ___, 0, 0, 0xe6, InterpAdjustFrameForReturnCall, "adjust_frame_for_return_call", "") /* Saturating float-to-int opcodes (--enable-saturating-float-to-int) */ WABT_OPCODE(I32, F32, ___, ___, 0, 0xfc, 0x00, I32TruncSatF32S, "i32.trunc_sat_f32_s", "") diff --git a/src/shared-validator.h b/src/shared-validator.h index 92cc4951d..3bc02ff3d 100644 --- a/src/shared-validator.h +++ b/src/shared-validator.h @@ -51,6 +51,9 @@ class SharedValidator { Result GetLabel(Index depth, Label** out_label) { return typechecker_.GetLabel(depth, out_label); } + Result GetCatchCount(Index depth, Index* out_count) { + return typechecker_.GetCatchCount(depth, out_count); + } Result WABT_PRINTF_FORMAT(3, 4) PrintError(const Location& loc, const char* fmt, ...); diff --git a/src/type-checker.cc b/src/type-checker.cc index 18bedd3eb..7b3cedb22 100644 --- a/src/type-checker.cc +++ b/src/type-checker.cc @@ -100,6 +100,24 @@ Result TypeChecker::GetRethrowLabel(Index depth, Label** out_label) { return Result::Error; } +Result TypeChecker::GetCatchCount(Index depth, Index* out_count) { + Label* unused; + if (Failed(GetLabel(depth, &unused))) { + return Result::Error; + } + + Index catch_count = 0; + for (Index idx = 0; idx <= depth; idx++) { + LabelType type = label_stack_[label_stack_.size() - idx - 1].label_type; + if (type == LabelType::Catch) { + catch_count++; + } + } + *out_count = catch_count; + + return Result::Ok; +} + Result TypeChecker::TopLabel(Label** out_label) { return GetLabel(0, out_label); } diff --git a/src/type-checker.h b/src/type-checker.h index 266f6f18e..01bbcbe50 100644 --- a/src/type-checker.h +++ b/src/type-checker.h @@ -58,6 +58,7 @@ class TypeChecker { bool IsUnreachable(); Result GetLabel(Index depth, Label** out_label); Result GetRethrowLabel(Index depth, Label** out_label); + Result GetCatchCount(Index depth, Index* out_depth); Result BeginFunction(const TypeVector& sig); Result OnAtomicFence(uint32_t consistency_model); diff --git a/test/interp/rethrow-and-br.txt b/test/interp/rethrow-and-br.txt new file mode 100644 index 000000000..3ed2f1eb5 --- /dev/null +++ b/test/interp/rethrow-and-br.txt @@ -0,0 +1,126 @@ +;;; TOOL: run-interp +;;; ARGS*: --enable-exceptions --enable-tail-call +(module + (tag $e1) + (tag $e2) + (type $helper-type (func (result i32))) + (table anyfunc (elem $helper)) + (func (export "rethrow-br") (result i32) + (try (result i32) + (do + (try + (do (throw $e1)) + (catch $e1 + (try $l + (do (throw $e2)) + (catch $e2 + ;; exception stack has two entries + ;; br should reset to height of one + (br $l))) + ;; exception stack has one entry + (rethrow 0))) + (i32.const 0)) + (catch $e1 + (i32.const 1)))) + (func (export "rethrow-br-if") (result i32) + (try (result i32) + (do + (try + (do (throw $e1)) + (catch $e1 + (try $l + (do (throw $e2)) + (catch $e2 + (i32.const 1) + (br_if $l))) + (rethrow 0))) + (i32.const 0)) + (catch $e1 + (i32.const 1)))) + (func (export "rethrow-br-table") (result i32) + (try (result i32) + (do + (try + (do (throw $e1)) + (catch $e1 + (try $l + (do (throw $e2)) + (catch $e2 + (i32.const 1) + (br_table 1 $l 1))) + (rethrow 0))) + (i32.const 0)) + (catch $e1 + (i32.const 1)))) + (func $helper (result i32) + (try (result i32) + (do (throw $e1)) + (catch $e1 + (i32.const 1)))) + (func (export "rethrow-return-call") (result i32) + (try (result i32) + (do (throw $e1)) + (catch $e1 + (try $l + (do (throw $e2)) + (catch $e2 + (return_call $helper))) + (i32.const 0)))) + (func (export "rethrow-return-call-indirect") (result i32) + (try (result i32) + (do (throw $e1)) + (catch $e1 + (try $l + (do (throw $e2)) + (catch $e2 + (i32.const 0) + (return_call_indirect (type $helper-type)))) + (i32.const 0)))) + (func $helper-2 + (try + (do (throw $e2)) + (catch $e2 + return))) + (func (export "rethrow-return") (result i32) + (try (result i32) + (do + (try (result i32) + (do (throw $e1)) + (catch $e1 + (call $helper-2) + (rethrow 0)))) + (catch $e1 + (i32.const 1)) + (catch $e2 + (i32.const 0)))) + ;; test rethrow stack interaction with throw, as if it were a br + (func (export "rethrow-throw") (result i32) + (try (result i32) + (do + (try + (do (throw $e1)) + (catch $e1 + (try + (do + (try + (do (throw $e2)) + (catch $e2 + ;; exception stack has $e1, $e2 + ;; should get unwound on throw to $e1 + (throw $e2)))) + (catch $e2 + ;; should reference $e1 + (rethrow 1))))) + (i32.const 0)) + (catch $e1 (i32.const 1)) + (catch_all (i32.const 0)))) + ) +(;; STDOUT ;;; +rethrow-br() => i32:1 +rethrow-br-if() => i32:1 +rethrow-br-table() => i32:1 +rethrow-return-call() => i32:1 +rethrow-return-call-indirect() => i32:1 +rethrow-return() => i32:1 +rethrow-throw() => i32:1 +;;; STDOUT ;;) diff --git a/test/interp/rethrow.txt b/test/interp/rethrow.txt new file mode 100644 index 000000000..67bd3abbc --- /dev/null +++ b/test/interp/rethrow.txt @@ -0,0 +1,43 @@ +;;; TOOL: run-interp +;;; ARGS*: --enable-exceptions +(module + (tag $e1) + (tag $e2) + (func (export "rethrow-uncaught") + (try + (do + (throw $e1)) + (catch $e1 + (rethrow 0)))) + (func (export "rethrow-1") (result i32) + (try (result i32) + (do + (try + (do + (throw $e1)) + (catch $e1 + (rethrow 0))) + (i32.const 0)) + (catch_all + (i32.const 1)))) + (func (export "rethrow-2") (result i32) + (try (result i32) + (do + (try + (do + (throw $e1)) + (catch $e1 + (try + (do + (throw $e2)) + (catch $e2 + (rethrow 1))))) + (i32.const 0)) + (catch $e1 + (i32.const 1)))) + ) +(;; STDOUT ;;; +rethrow-uncaught() => error: uncaught exception +rethrow-1() => i32:1 +rethrow-2() => i32:1 +;;; STDOUT ;;) diff --git a/test/interp/throw-across-frame.txt b/test/interp/throw-across-frame.txt new file mode 100644 index 000000000..d031e504e --- /dev/null +++ b/test/interp/throw-across-frame.txt @@ -0,0 +1,52 @@ +;;; TOOL: run-interp +;;; ARGS*: --enable-exceptions +(module + (tag $e1) + (tag $e2) + (tag $e3) + (func $thrower + (throw $e1)) + (func $thrower2 + (try + (do (throw $e1)) + (catch $e2))) + (func (export "call-thrower") (result i32) + (try (result i32) + (do + (call $thrower) + (i32.const 0)) + (catch $e1 + (i32.const 1)))) + (func (export "call-thrower2") (result i32) + (try (result i32) + (do + (call $thrower2) + (i32.const 0)) + (catch $e1 + (i32.const 1)))) + (func $helper (result i32) + (try (result i32) + (do + (call $thrower2) + (i32.const 0)) + (catch $e3 + (i32.const 1)))) + (func (export "call-thrower3") (result i32) + (try (result i32) + (do + (call $helper)) + (catch $e1 + (i32.const 1)))) + (func (export "call-thrower4") (result i32) + (try (result i32) + (do + (call $helper)) + (catch $e2 + (i32.const 1)))) + ) +(;; STDOUT ;;; +call-thrower() => i32:1 +call-thrower2() => i32:1 +call-thrower3() => i32:1 +call-thrower4() => error: uncaught exception +;;; STDOUT ;;) diff --git a/test/interp/try-delegate.txt b/test/interp/try-delegate.txt new file mode 100644 index 000000000..d938bb75e --- /dev/null +++ b/test/interp/try-delegate.txt @@ -0,0 +1,81 @@ +;;; TOOL: run-interp +;;; ARGS*: --enable-exceptions +(module + (tag $e1) + (tag $e2) + (func (export "try-delegate") (result i32) + (try $l (result i32) + (do + (try + (do (throw $e1)) + (delegate $l)) + (i32.const 0)) + (catch $e1 + (i32.const 1)))) + (func (export "try-delegate-2") (result i32) + (try $l (result i32) + (do + (try + (do + (try + (do (throw $e1)) + (delegate $l))) + (catch_all)) + (i32.const 0)) + (catch $e1 + (i32.const 1)))) + (func (export "try-delegate-uncaught") (result i32) + (try $l (result i32) + (do + (try + (do (throw $e1)) + (delegate $l)) + (i32.const 0)) + (catch $e2 + (i32.const 0)))) + (func (export "try-delegate-to-caller") (result i32) + (try (result i32) + (do + (try + (do (throw $e1)) + (delegate 1)) + (i32.const 0)) + (catch $e1 + (i32.const 1)))) + (func (export "try-delegate-to-delegate") (result i32) + (try $l1 (result i32) + (do + (try $l2 + (do + (try + (do + (try + (do (throw $e1)) + (delegate $l2))) + (catch_all))) + (delegate $l1)) + (i32.const 0)) + (catch $e1 + (i32.const 1)))) + (func (export "try-delegate-to-block") (result i32) + (try (result i32) + (do + (block $l + (try + (do + (try + (do (throw $e1)) + (delegate $l))) + (catch_all))) + (i32.const 0)) + (catch $e1 + (i32.const 1)))) + ) +(;; STDOUT ;;; +try-delegate() => i32:1 +try-delegate-2() => i32:1 +try-delegate-uncaught() => error: uncaught exception +try-delegate-to-caller() => error: uncaught exception +try-delegate-to-delegate() => i32:1 +try-delegate-to-block() => i32:1 +;;; STDOUT ;;) diff --git a/test/interp/try.txt b/test/interp/try.txt new file mode 100644 index 000000000..2b199ab5c --- /dev/null +++ b/test/interp/try.txt @@ -0,0 +1,142 @@ +;;; TOOL: run-interp +;;; ARGS*: --enable-exceptions +(module + (tag $e1) + (tag $e2) + (tag $e3 (param i32)) + (func (export "throw-uncaught") + (throw $e1)) + (func (export "throw-uncaught-2") + (try + (do + (throw $e1)))) + (func (export "try-catch") (result i32) + (try (result i32) + (do + (throw $e1)) + (catch $e1 + (i32.const 1)))) + (func (export "try-catch-all") (result i32) + (try (result i32) + (do + (throw $e1)) + (catch_all + (i32.const 1)))) + (func (export "try-catch-all-2") + (try + (do + (i32.const 1) + (throw $e3)) + (catch_all))) + (func (export "try-catch-payload") (result i32) + (try (result i32) + (do + (i32.const 42) + (throw $e3)) + (catch $e3))) + (func (export "try-catch-multi") (result i32) + (try (result i32) + (do + (throw $e2)) + (catch $e1 + (i32.const 1)) + (catch $e2 + (i32.const 2)))) + (func (export "try-catch-nested") (result i32) + (try (result i32) + (do + (try (result i32) + (do (throw $e2)) + (catch $e1 + (i32.const 1)))) + (catch $e2 + (i32.const 2)))) + (func (export "try-catch-nested-2") (result i32) + (try (result i32) + (do + (try (result i32) + (do (throw $e1)) + (catch $e1 + (i32.const 1)))) + (catch $e1 + (i32.const 2)))) + (func (export "try-catch-nested-3") (result i32) + (try (result i32) + (do + (try (result i32) + (do + (try (result i32) + (do (throw $e2)) + (catch $e1 + (i32.const 1)))) + (catch $e2 + (i32.const 2)))) + (catch_all + (i32.const 3)))) + (func (export "try-catch-nested-4") (result i32) + (try (result i32) + (do + (try (result i32) + (do + (throw $e1)) + (catch $e1 + (try (do (throw $e2))) + (i32.const 0)) + (catch $e2 + (i32.const 0)))) + (catch $e2 + (i32.const 1)))) + (func (export "try-catch-nested-5") (result i32) + (try (result i32) + (do + (try (result i32) + (do + (throw $e1)) + (catch $e1 + (try (do (throw $e2))) + (i32.const 0)) + (catch_all + (i32.const 0)))) + (catch $e2 + (i32.const 1)))) + (func (export "try-catch-uncaught") (result i32) + (try (result i32) + (do + (throw $e1)) + (catch $e2 + (i32.const 1)))) + (func (export "try-catch-stack-size") (result i32) + (i32.const 1) + (try + (do + (i32.const 0) + (throw $e1)) + (catch $e1)) + ;; here the value stack should have just 1 + ) + (func $helper + (try + (do + (i32.const 0) + (throw $e1)) + (catch $e1))) + (func (export "try-catch-stack-size-2") (result i32) + (i32.const 1) + (call $helper))) +(;; STDOUT ;;; +throw-uncaught() => error: uncaught exception +throw-uncaught-2() => error: uncaught exception +try-catch() => i32:1 +try-catch-all() => i32:1 +try-catch-all-2() => +try-catch-payload() => i32:42 +try-catch-multi() => i32:2 +try-catch-nested() => i32:2 +try-catch-nested-2() => i32:1 +try-catch-nested-3() => i32:2 +try-catch-nested-4() => i32:1 +try-catch-nested-5() => i32:1 +try-catch-uncaught() => error: uncaught exception +try-catch-stack-size() => i32:1 +try-catch-stack-size-2() => i32:1 +;;; STDOUT ;;)