From 46ef4dd4d58e4b77f01b30b4d0643f447f712c11 Mon Sep 17 00:00:00 2001 From: Asumu Takikawa Date: Wed, 25 Aug 2021 15:32:04 -0700 Subject: [PATCH] Add interpreter support for exception handling Details about the implementation approach: * Try blocks generate metadata tracking the instruction ranges for the handlers and which exception tags are handled (or if a `catch_all` is present). The metadata is stored in a function's `FuncDesc`, and is transferred into the `Frame` when a function call is executed. * The stack is unwound when a `throw` is executed. This unwinding also handles tag dispatch to the appropriate catch. The metadata to find the matching handler is looked up in the call `Frame` stack. * If a `try-delegate` is present, it is used in the stack unwinding process to skip over to the relevant handler. * A separate `exceptions_` stack in call frames tracks caught exceptions that can be accessed via a `rethrow`. The stack is popped on exit from a try block or when exiting via control instructions like `br`. * Because stack unwinding relies on finding metadata in the call frame, `return_call` needs to be modified slightly to adjust the current frame when executing the call, rather than re-using the frame completely as-is. --- src/interp/binary-reader-interp.cc | 218 ++++++++++++++++++++++++++--- src/interp/interp-inl.h | 27 +++- src/interp/interp.cc | 144 ++++++++++++++++++- src/interp/interp.h | 61 +++++++- src/interp/istream.cc | 12 +- src/interp/istream.h | 3 +- src/opcode.def | 2 + src/shared-validator.h | 3 + src/type-checker.cc | 18 +++ src/type-checker.h | 1 + test/interp/rethrow-and-br.txt | 73 ++++++++++ test/interp/rethrow.txt | 43 ++++++ test/interp/throw-across-frame.txt | 52 +++++++ test/interp/try-delegate.txt | 81 +++++++++++ test/interp/try.txt | 95 +++++++++++++ 15 files changed, 804 insertions(+), 29 deletions(-) create mode 100644 test/interp/rethrow-and-br.txt create mode 100644 test/interp/rethrow.txt create mode 100644 test/interp/throw-across-frame.txt create mode 100644 test/interp/try-delegate.txt create mode 100644 test/interp/try.txt diff --git a/src/interp/binary-reader-interp.cc b/src/interp/binary-reader-interp.cc index 53a996f5a..79f454e06 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 = 0); 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,14 @@ 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. + PushLabel(LabelKind::Try, Istream::kInvalidOffset, Istream::kInvalidOffset, + func_->handlers.size()); + func_->handlers.push_back(HandlerDesc{HandlerKind::Catch, + istream_.end(), + Istream::kInvalidOffset, + {}, + {Istream::kInvalidOffset}}); return Result::Ok; } @@ -995,7 +1071,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 +1079,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; } @@ -1024,6 +1100,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.end_offset = istream_.end(); + assert(desc.catches.size() == 0); + } else if (label_type == LabelType::Catch) { + istream_.EmitCatchDrop(1); } FixupTopLabel(); PopLabel(); @@ -1031,21 +1116,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_.GetCatchDepth(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_.GetCatchDepth(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; } @@ -1054,24 +1141,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_.GetCatchDepth(depth, &catch_drop_count)); // Emit DropKeep directly (instead of using EmitDropKeep) so the // instruction has a fixed size. 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_.GetCatchDepth(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; @@ -1100,15 +1192,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_.GetCatchDepth(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); @@ -1394,6 +1490,90 @@ 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_.GetCatchDepth(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) { + 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}}); + 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; + // 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); + if (offset == Istream::kInvalidOffset) { + depth_fixups_.Append(label_stack_.size() - 1, istream_.end()); + } + istream_.Emit(offset); + desc.end_offset = istream_.end(); + 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; + Istream::Offset offset = label->offset; + istream_.Emit(Opcode::Br); + if (offset == Istream::kInvalidOffset) { + depth_fixups_.Append(label_stack_.size() - 1, istream_.end()); + } + istream_.Emit(offset); + desc.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(); + HandlerDesc& desc = func_->handlers[label->handler_desc_index]; + desc.kind = HandlerKind::Delegate; + Istream::Offset offset = label->offset; + istream_.Emit(Opcode::Br); + if (offset == Istream::kInvalidOffset) { + depth_fixups_.Append(label_stack_.size() - 1, istream_.end()); + } + istream_.Emit(offset); + desc.end_offset = istream_.end(); + Label* target_label = GetNearestTryLabel(depth + 1); + assert(target_label); + desc.delegate_offset = 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..726e0fc2e 100644 --- a/src/interp/interp-inl.h +++ b/src/interp/interp-inl.h @@ -128,9 +128,15 @@ inline ExportType& ExportType::operator=(const ExportType& other) { inline Frame::Frame(Ref func, u32 values, u32 offset, + u32 exceptions, Instance* inst, Module* mod) - : func(func), values(values), offset(offset), inst(inst), mod(mod) {} + : func(func), + values(values), + offset(offset), + exceptions(exceptions), + 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..41ad92692 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; @@ -955,7 +975,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(), offset, exceptions_.size(), inst_, + mod_); return RunResult::Ok; } @@ -964,7 +985,7 @@ RunResult Thread::PushCall(const DefinedFunc& func, Trap::Ptr* out_trap) { 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_); + exceptions_.size(), inst_, mod_); return RunResult::Ok; } @@ -972,7 +993,8 @@ 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(), 0, exceptions_.size(), + inst_, mod_); return RunResult::Ok; } @@ -1414,6 +1436,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 +1826,21 @@ 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); + Ref exn = Exception::New(store_, tag_ref, params).ref(); + return DoThrow(exn); + } + case O::Rethrow: { + u32 exn_index = instr.imm_u32; + Ref exn = 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 +1857,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 +2443,87 @@ RunResult Thread::DoAtomicRmwCmpxchg(Instr instr, Trap::Ptr* out_trap) { return RunResult::Ok; } +RunResult Thread::DoThrow(Ref exn_ref) { + Istream::Offset target_offset = Istream::kInvalidOffset; + Exception::Ptr exn{store_, exn_ref}; + 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.start_offset && pc < handler.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) { + iter = handlers.rend() - handler.delegate_offset - 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_ref = frame.inst->tags()[_catch.tag_index]; + Tag::Ptr catch_tag{store_, catch_ref}; + if (exn_tag == catch_tag) { + target_offset = _catch.offset; + goto found_handler; + } + } + if (handler.catch_all_offset != Istream::kInvalidOffset) { + target_offset = handler.catch_all_offset; + 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. + if (popped_frame) { + values_.resize(target_frame.values); + exceptions_.resize(target_frame.exceptions); + inst_ = target_frame.inst; + mod_ = target_frame.mod; + } + // Jump to the handler. + target_frame.offset = target_offset; + if (!had_catch_all) { + PushValues(exn_tag->type().signature, exn->args()); + } + // 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); + 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..3598839ba 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,29 @@ 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; +}; + +// The Try kind is for catch-less try blocks, which have a HandlerDesc that +// is ignored as the block cannot ever catch an exception. +enum class HandlerKind { Catch, Delegate }; + +struct HandlerDesc { + HandlerKind kind; + u32 start_offset; + u32 end_offset; + std::vector catches; + union { + u32 catch_all_offset; + u32 delegate_offset; + }; +}; + struct FuncDesc { // Includes params. ValueType GetLocalType(Index) const; @@ -312,6 +336,7 @@ struct FuncDesc { FuncType type; std::vector locals; u32 code_offset; + std::vector handlers; }; struct TableDesc { @@ -378,13 +403,19 @@ struct ModuleDesc { //// Runtime //// struct Frame { - explicit Frame(Ref func, u32 values, u32 offset, Instance*, Module*); + explicit Frame(Ref func, + u32 values, + u32 offset, + u32 exceptions, + 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. // Cached for convenience. Both are null if func is a HostFunc. Instance* inst; @@ -647,6 +678,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 +1077,7 @@ enum class RunResult { Ok, Return, Trap, + Exception, }; // TODO: Kinda weird to have a thread as an object, but it makes reference @@ -1183,12 +1236,18 @@ class Thread : public Object { template RunResult DoAtomicRmwCmpxchg(Instr, Trap::Ptr* out_trap); + RunResult DoThrow(Ref 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..a81e56395 100644 --- a/src/interp/istream.h +++ b/src/interp/istream.h @@ -98,7 +98,7 @@ class Istream { // // 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 +110,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 415e484ea..6a11f4112 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 GetCatchDepth(Index depth, Index* out_depth) { + return typechecker_.GetCatchDepth(depth, out_depth); + } 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 f7f381824..73959c23a 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::GetCatchDepth(Index depth, Index* out_depth) { + Label* unused; + if (Failed(GetLabel(depth, &unused))) { + return Result::Error; + } + + Index catch_depth = 0; + for (Index idx = 0; idx <= depth; idx++) { + LabelType type = label_stack_[label_stack_.size() - idx - 1].label_type; + if (type == LabelType::Catch) { + catch_depth++; + } + } + *out_depth = catch_depth; + + 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 b7cdf82dd..a6d924a68 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 GetCatchDepth(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..ac376d551 --- /dev/null +++ b/test/interp/rethrow-and-br.txt @@ -0,0 +1,73 @@ +;;; TOOL: run-interp +;;; ARGS*: --enable-exceptions --enable-tail-call +(module + (tag $e1) + (tag $e2) + (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)))) + ) +(;; STDOUT ;;; +rethrow-br() => i32:1 +rethrow-br-if() => i32:1 +rethrow-br-table() => i32:1 +rethrow-return-call() => 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..cb8faefa3 --- /dev/null +++ b/test/interp/try.txt @@ -0,0 +1,95 @@ +;;; 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-uncaught") (result i32) + (try (result i32) + (do + (throw $e1)) + (catch $e2 + (i32.const 1)))) + ) +(;; 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-uncaught() => error: uncaught exception +;;; STDOUT ;;)