diff --git a/scripts/gen-s-parser.py b/scripts/gen-s-parser.py index 0ac80c64e31..b83259b219c 100755 --- a/scripts/gen-s-parser.py +++ b/scripts/gen-s-parser.py @@ -609,6 +609,7 @@ ("suspend", "makeSuspend()"), ("resume", "makeResume()"), ("resume_throw", "makeResumeThrow()"), + ("resume_throw_ref", "makeResumeThrowRef()"), ("switch", "makeStackSwitch()"), # GC ("ref.i31", "makeRefI31(Unshared)"), diff --git a/src/gen-s-parser.inc b/src/gen-s-parser.inc index 301d87c73e2..081b92199b8 100644 --- a/src/gen-s-parser.inc +++ b/src/gen-s-parser.inc @@ -4972,12 +4972,23 @@ switch (buf[0]) { return Ok{}; } goto parse_error; - case '_': - if (op == "resume_throw"sv) { - CHECK_ERR(makeResumeThrow(ctx, pos, annotations)); - return Ok{}; + case '_': { + switch (buf[12]) { + case '\0': + if (op == "resume_throw"sv) { + CHECK_ERR(makeResumeThrow(ctx, pos, annotations)); + return Ok{}; + } + goto parse_error; + case '_': + if (op == "resume_throw_ref"sv) { + CHECK_ERR(makeResumeThrowRef(ctx, pos, annotations)); + return Ok{}; + } + goto parse_error; + default: goto parse_error; } - goto parse_error; + } default: goto parse_error; } } diff --git a/src/ir/child-typer.h b/src/ir/child-typer.h index 649f7192783..d93c9c05f0f 100644 --- a/src/ir/child-typer.h +++ b/src/ir/child-typer.h @@ -1325,10 +1325,16 @@ template struct ChildTyper : OverriddenVisitor { ct = curr->cont->type.getHeapType(); } assert(ct->isContinuation()); - auto params = wasm.getTag(curr->tag)->params(); - assert(params.size() == curr->operands.size()); - for (size_t i = 0; i < params.size(); ++i) { - note(&curr->operands[i], params[i]); + if (curr->tag) { + // resume_throw + auto params = wasm.getTag(curr->tag)->params(); + assert(params.size() == curr->operands.size()); + for (size_t i = 0; i < params.size(); ++i) { + note(&curr->operands[i], params[i]); + } + } else { + // resume_throw_ref + note(&curr->operands[0], Type(HeapType::exn, Nullable)); } note(&curr->cont, Type(*ct, Nullable)); } diff --git a/src/parser/contexts.h b/src/parser/contexts.h index 3a8ff98fd91..474a49eaf38 100644 --- a/src/parser/contexts.h +++ b/src/parser/contexts.h @@ -935,6 +935,13 @@ struct NullInstrParserCtx { return Ok{}; } template + Result<> makeResumeThrowRef(Index, + const std::vector&, + HeapTypeT, + const TagLabelListT&) { + return Ok{}; + } + template Result<> makeStackSwitch(Index, const std::vector&, HeapTypeT, TagIdxT) { return Ok{}; @@ -2889,24 +2896,41 @@ struct ParseDefsCtx : TypeParserCtx, AnnotationParserCtx { return withLoc(pos, irBuilder.makeResume(type, tags, labels)); } + struct ResumeThrowData { + std::vector tags; + std::vector> labels; + + ResumeThrowData(const std::vector& resumetable) { + tags.reserve(resumetable.size()); + labels.reserve(resumetable.size()); + for (const OnClauseInfo& info : resumetable) { + tags.push_back(info.tag); + if (info.isOnSwitch) { + labels.push_back(std::nullopt); + } else { + labels.push_back(std::optional(info.label)); + } + } + } + }; + Result<> makeResumeThrow(Index pos, const std::vector& annotations, HeapType type, Name tag, const std::vector& resumetable) { - std::vector tags; - std::vector> labels; - tags.reserve(resumetable.size()); - labels.reserve(resumetable.size()); - for (const OnClauseInfo& info : resumetable) { - tags.push_back(info.tag); - if (info.isOnSwitch) { - labels.push_back(std::nullopt); - } else { - labels.push_back(std::optional(info.label)); - } - } - return withLoc(pos, irBuilder.makeResumeThrow(type, tag, tags, labels)); + ResumeThrowData data(resumetable); + return withLoc( + pos, irBuilder.makeResumeThrow(type, tag, data.tags, data.labels)); + } + + Result<> makeResumeThrowRef(Index pos, + const std::vector& annotations, + HeapType type, + const std::vector& resumetable) { + ResumeThrowData data(resumetable); + return withLoc(pos, + irBuilder.makeResumeThrowRef(type, data.tags, data.labels)); } Result<> makeStackSwitch(Index pos, diff --git a/src/parser/parsers.h b/src/parser/parsers.h index 360878848c5..a8f1495097d 100644 --- a/src/parser/parsers.h +++ b/src/parser/parsers.h @@ -2743,6 +2743,21 @@ Result<> makeResumeThrow(Ctx& ctx, return ctx.makeResumeThrow(pos, annotations, *type, *exnTag, *resumetable); } +// resume_throw_ref ::= 'resume_throw' typeidx ('(' 'on' tagidx labelidx | +// 'on' tagidx switch ')')* +template +Result<> makeResumeThrowRef(Ctx& ctx, + Index pos, + const std::vector& annotations) { + auto type = typeidx(ctx); + CHECK_ERR(type); + + auto resumetable = makeResumeTable(ctx); + CHECK_ERR(resumetable); + + return ctx.makeResumeThrowRef(pos, annotations, *type, *resumetable); +} + // switch ::= 'switch' typeidx tagidx template Result<> makeStackSwitch(Ctx& ctx, diff --git a/src/passes/Print.cpp b/src/passes/Print.cpp index 39573372d78..96fd3c3051f 100644 --- a/src/passes/Print.cpp +++ b/src/passes/Print.cpp @@ -2623,11 +2623,15 @@ struct PrintExpressionContents void visitResumeThrow(ResumeThrow* curr) { assert(curr->cont->type.isContinuation()); printMedium(o, "resume_throw"); - + if (!curr->tag) { + printMedium(o, "_ref"); + } o << ' '; printHeapTypeName(curr->cont->type.getHeapType()); - o << ' '; - curr->tag.print(o); + if (curr->tag) { + o << ' '; + curr->tag.print(o); + } handleResumeTable(o, curr); } diff --git a/src/wasm-binary.h b/src/wasm-binary.h index eb6f1ee267c..f7a20190e5a 100644 --- a/src/wasm-binary.h +++ b/src/wasm-binary.h @@ -1246,7 +1246,8 @@ enum ASTNodes { Suspend = 0xe2, Resume = 0xe3, ResumeThrow = 0xe4, - Switch = 0xe5, // NOTE(dhil): the internal class is known as + ResumeThrowRef = 0xe5, + Switch = 0xe6, // NOTE(dhil): the internal class is known as // StackSwitch to avoid conflict with the existing // 'switch table'. OnLabel = 0x00, // (on $tag $label) diff --git a/src/wasm-interpreter.h b/src/wasm-interpreter.h index 5b153b3203f..3dad2780203 100644 --- a/src/wasm-interpreter.h +++ b/src/wasm-interpreter.h @@ -273,9 +273,14 @@ struct ContData { // suspend). Literals resumeArguments; - // If set, this is the exception to be thrown at the resume point. + // If set, this is the tag for an exception to be thrown at the resume point + // (from resume_throw). Tag* exceptionTag = nullptr; + // If set, this is the exception ref to be thrown at the resume point (from + // resume_throw_ref). + Literal exception; + // Whether we executed. Continuations are one-shot, so they may not be // executed a second time. bool executed = false; @@ -4480,7 +4485,6 @@ class ModuleRunnerBase : public ExpressionRunner { } } Flow visitTryTable(TryTable* curr) { - assert(!self()->isResuming()); // TODO try { return self()->visit(curr->body); } catch (const WasmException& e) { @@ -4557,6 +4561,22 @@ class ModuleRunnerBase : public ExpressionRunner { old->executed = true; return Literal(std::make_shared(newData)); } + + void maybeThrowAfterResuming(std::shared_ptr& currContinuation) { + // We may throw by creating a tag, or an exnref. + auto* tag = currContinuation->exceptionTag; + auto exnref = currContinuation->exception.type != Type::none; + assert(!(tag && exnref)); + if (tag) { + // resume_throw + throwException(WasmException{ + self()->makeExnData(tag, currContinuation->resumeArguments)}); + } else if (exnref) { + // resume_throw_ref + throwException(WasmException{currContinuation->exception}); + } + } + Flow visitSuspend(Suspend* curr) { // Process the arguments, whether or not we are resuming. If we are resuming // then we don't need these values (we sent them as part of the suspension), @@ -4579,11 +4599,7 @@ class ModuleRunnerBase : public ExpressionRunner { // restoredValues map. assert(currContinuation->resumeInfo.empty()); assert(self()->restoredValuesMap.empty()); - // Throw, if we were resumed by resume_throw; - if (auto* tag = currContinuation->exceptionTag) { - throwException(WasmException{ - self()->makeExnData(tag, currContinuation->resumeArguments)}); - } + maybeThrowAfterResuming(currContinuation); return currContinuation->resumeArguments; } @@ -4616,7 +4632,7 @@ class ModuleRunnerBase : public ExpressionRunner { new_->resumeExpr = curr; return Flow(SUSPEND_FLOW, tag, std::move(arguments)); } - template Flow doResume(T* curr, Tag* exceptionTag = nullptr) { + template Flow doResume(T* curr) { Literals arguments; VISIT_ARGUMENTS(flow, curr->operands, arguments) VISIT_REUSE(flow, curr->cont); @@ -4636,13 +4652,26 @@ class ModuleRunnerBase : public ExpressionRunner { trap("continuation already executed"); } contData->executed = true; + if (contData->resumeArguments.empty()) { // The continuation has no bound arguments. For now, we just handle the // simple case of binding all of them, so that means we can just use all // the immediate ones here. TODO contData->resumeArguments = arguments; } - contData->exceptionTag = exceptionTag; + // Fill in the continuation data. How we do this depends on whether we + // are resume or resume_throw*. + if (auto* resumeThrow = curr->template dynCast()) { + if (resumeThrow->tag) { + // resume_throw + contData->exceptionTag = + self()->getModule()->getTag(resumeThrow->tag); + } else { + // resume_throw_ref + contData->exception = arguments[0]; + } + } + self()->pushCurrContinuation(contData); self()->continuationStore->resuming = true; #if WASM_INTERPRETER_DEBUG @@ -4709,10 +4738,7 @@ class ModuleRunnerBase : public ExpressionRunner { return ret; } Flow visitResume(Resume* curr) { return doResume(curr); } - Flow visitResumeThrow(ResumeThrow* curr) { - // TODO: should the Resume and ResumeThrow classes be merged? - return doResume(curr, self()->getModule()->getTag(curr->tag)); - } + Flow visitResumeThrow(ResumeThrow* curr) { return doResume(curr); } Flow visitStackSwitch(StackSwitch* curr) { return Flow(NONCONSTANT_FLOW); } void trap(const char* why) override { @@ -4784,10 +4810,7 @@ class ModuleRunnerBase : public ExpressionRunner { // to do is just start calling this function (with the arguments we've // set), so resuming is done. (And throw, if resume_throw.) self()->continuationStore->resuming = false; - if (auto* tag = currContinuation->exceptionTag) { - throwException(WasmException{ - self()->makeExnData(tag, currContinuation->resumeArguments)}); - } + maybeThrowAfterResuming(currContinuation); } } diff --git a/src/wasm-ir-builder.h b/src/wasm-ir-builder.h index 76deda05ae3..e8bed5bc4fb 100644 --- a/src/wasm-ir-builder.h +++ b/src/wasm-ir-builder.h @@ -273,6 +273,12 @@ class IRBuilder : public UnifiedExpressionVisitor> { Name tag, const std::vector& tags, const std::vector>& labels); + Result<> makeResumeThrowRef(HeapType ct, + const std::vector& tags, + const std::vector>& labels) { + // resume_throw_ref has an empty tag. + return makeResumeThrow(ct, Name(), tags, labels); + } Result<> makeStackSwitch(HeapType ct, Name tag); // Private functions that must be public for technical reasons. diff --git a/src/wasm.h b/src/wasm.h index 605c7025395..c7677d01412 100644 --- a/src/wasm.h +++ b/src/wasm.h @@ -2114,6 +2114,9 @@ class ResumeThrow : public SpecificExpression { : handlerTags(allocator), handlerBlocks(allocator), operands(allocator), sentTypes(allocator) {} + // If tag is set to a non-null Name, this is a resume_throw and |operands| + // contains the values to be set in an exception of that tag. If tag is null, + // this is resume_throw_ref and |operands| contains a single item, the exnref. Name tag; // See the comment on `Resume` above. ArenaVector handlerTags; diff --git a/src/wasm/wasm-binary.cpp b/src/wasm/wasm-binary.cpp index df8b6a28df5..8259b02c4a1 100644 --- a/src/wasm/wasm-binary.cpp +++ b/src/wasm/wasm-binary.cpp @@ -3374,9 +3374,13 @@ Result<> WasmBinaryReader::readInst() { } return builder.makeResume(type, tags, labels); } - case BinaryConsts::ResumeThrow: { + case BinaryConsts::ResumeThrow: + case BinaryConsts::ResumeThrowRef: { auto type = getIndexedHeapType(); - auto tag = getTagName(getU32LEB()); + Name tag; + if (code == BinaryConsts::ResumeThrow) { + tag = getTagName(getU32LEB()); + } auto numHandlers = getU32LEB(); std::vector tags; std::vector> labels; diff --git a/src/wasm/wasm-ir-builder.cpp b/src/wasm/wasm-ir-builder.cpp index 0bb72a8a62a..a60dd6edc57 100644 --- a/src/wasm/wasm-ir-builder.cpp +++ b/src/wasm/wasm-ir-builder.cpp @@ -2586,7 +2586,13 @@ IRBuilder::makeResumeThrow(HeapType ct, ResumeThrow curr(wasm.allocator); curr.tag = tag; - curr.operands.resize(wasm.getTag(tag)->params().size()); + if (tag) { + // This is a normal resume_throw. + curr.operands.resize(wasm.getTag(tag)->params().size()); + } else { + // This is a resume_throw_ref. + curr.operands.resize(1); + } Result resumetable = makeResumeTable( labels, diff --git a/src/wasm/wasm-stack.cpp b/src/wasm/wasm-stack.cpp index c7eff7d9b73..f1d26774680 100644 --- a/src/wasm/wasm-stack.cpp +++ b/src/wasm/wasm-stack.cpp @@ -2852,9 +2852,12 @@ void BinaryInstWriter::visitResumeThrow(ResumeThrow* curr) { emitUnreachable(); return; } - o << int8_t(BinaryConsts::ResumeThrow); + o << int8_t(curr->tag ? BinaryConsts::ResumeThrow + : BinaryConsts::ResumeThrowRef); parent.writeIndexedHeapType(curr->cont->type.getHeapType()); - o << U32LEB(parent.getTagIndex(curr->tag)); + if (curr->tag) { + o << U32LEB(parent.getTagIndex(curr->tag)); + } size_t handlerNum = curr->handlerTags.size(); o << U32LEB(handlerNum); diff --git a/src/wasm/wasm-validator.cpp b/src/wasm/wasm-validator.cpp index ebb77a548e9..62efe49f007 100644 --- a/src/wasm/wasm-validator.cpp +++ b/src/wasm/wasm-validator.cpp @@ -4096,9 +4096,29 @@ void FunctionValidator::visitResumeThrow(ResumeThrow* curr) { curr, "sentTypes cache in resume_throw instruction has not been initialized"); - auto* tag = getModule()->getTagOrNull(curr->tag); - if (!shouldBeTrue(!!tag, curr, "resume_throw exception tag must exist")) { - return; + if (curr->tag) { + // Normal resume_throw + auto* tag = getModule()->getTagOrNull(curr->tag); + if (!shouldBeTrue(!!tag, curr, "resume_throw exception tag must exist")) { + return; + } + shouldBeEqual(curr->operands.size(), + tag->params().size(), + curr, + "resume_throw num operands must match the tag"); + // TODO: validate operand types as well + } else { + // resume_throw_ref + Type exnref = Type(HeapType::exn, Nullable); + if (shouldBeEqual(curr->operands.size(), + size_t(1), + curr, + "resume_throw_ref must have a single exnref operand")) { + shouldBeSubType(curr->operands[0]->type, + exnref, + curr, + "resume_throw_ref must receive exnref"); + } } if (curr->cont->type.isRef() && diff --git a/test/spec/resume_throw.wast b/test/spec/resume_throw.wast new file mode 100644 index 00000000000..f480a50430a --- /dev/null +++ b/test/spec/resume_throw.wast @@ -0,0 +1,314 @@ +;; Tests for resume_throw + +;; Test resume_throw on a continuation that is never resumed. +(module + (tag $exn) + + (type $f (func)) + (type $k (cont $f)) + + (func $never (unreachable)) + (elem declare func $never) + + (func (export "throw_never") + (block $h + (try_table (catch $exn $h) + (cont.new $k (ref.func $never)) + (resume_throw $k $exn) + (unreachable) + ) + ) + ) +) +(assert_return (invoke "throw_never")) + +;; Test resume_throw with a value type argument. +(module + (tag $exn_i32 (param i32)) + + (type $f (func)) + (type $k (cont $f)) + + (func $never (unreachable)) + (elem declare func $never) + + (func (export "throw_never_i32") (result i32) + (block $h (result i32) + (try_table (result i32) (catch $exn_i32 $h) + (i32.const 42) + (cont.new $k (ref.func $never)) + (resume_throw $k $exn_i32) + (unreachable) + ) + ) + ) +) +(assert_return (invoke "throw_never_i32") (i32.const 42)) + +;; Test resume_throw with a reference type argument. +(module + (tag $exn_ref (param externref)) + + (type $f (func)) + (type $k (cont $f)) + + (func $never (unreachable)) + (elem declare func $never) + + (func (export "throw_never_ref") (param $val externref) (result externref) + (block $h (result externref) + (try_table (result externref) (catch $exn_ref $h) + (local.get $val) + (cont.new $k (ref.func $never)) + (resume_throw $k $exn_ref) + (unreachable) + ) + ) + ) +) +(assert_return (invoke "throw_never_ref" (ref.extern 1)) (ref.extern 1)) + +;; Test resume_throw where the continuation handles the exception. +(module + (tag $exn) + (tag $e1) + + (type $f (func)) + (type $k (cont $f)) + + (func $handler + (block $h + (try_table (catch $exn $h) + (suspend $e1) + ) + ) + ) + (elem declare func $handler) + + (func (export "throw_handled") + (block $h (result (ref $k)) + (resume $k (on $e1 $h) (cont.new $k (ref.func $handler))) + (unreachable) + ) + (resume_throw $k $exn) + ) +) +(assert_return (invoke "throw_handled")) + +;; Test resume_throw where the continuation does not handle the exception. +(module + (tag $exn) + (tag $e1) + + (type $f (func)) + (type $k (cont $f)) + + (func $no_handler + (suspend $e1) + ) + (elem declare func $no_handler) + + (func (export "throw_unhandled") + (block $h (result (ref $k)) + (resume $k (on $e1 $h) (cont.new $k (ref.func $no_handler))) + (unreachable) + ) + (resume_throw $k $exn) + ) +) +(assert_exception (invoke "throw_unhandled")) + +;; Test resume_throw on a consumed continuation. +(module + (tag $exn) + + (type $f (func)) + (type $k (cont $f)) + + (func $f1) + (elem declare func $f1) + + (func (export "throw_consumed") + (local $k_ref (ref $k)) + (local.set $k_ref (cont.new $k (ref.func $f1))) + (resume $k (local.get $k_ref)) ;; consume it + (resume_throw $k $exn (local.get $k_ref)) ;; should trap + ) +) +(assert_trap (invoke "throw_consumed") "continuation already consumed") + +;; Test resume_throw on a null continuation reference. +(module + (tag $exn) + (type $f (func)) + (type $k (cont $f)) + (func (export "throw_null") + (resume_throw $k $exn (ref.null $k)) + ) +) +(assert_trap (invoke "throw_null") "null continuation reference") + +;; Test resume_throw_ref where the continuation handles the exception. +(module + (tag $e0 (param i32)) + (tag $yield) + + (type $f (func (result i32))) + (type $k (cont $f)) + + (func (export "throw_handled_ref") (result i32) + (local $k_ref (ref $k)) + (local.set $k_ref (cont.new $k (ref.func $yield42))) + + (block $y (result (ref $k)) + (resume $k (on $yield $y) + (local.get $k_ref)) + (unreachable)) + (local.set $k_ref) + + (block $h (result i32 exnref) + (try_table (catch_ref $e0 $h) + (i32.const 42) + (throw $e0)) + (unreachable) + ) + + (resume_throw_ref $k (local.get $k_ref)) + (return) + ) + + (func $yield42 (result i32) + (block $h (result i32) + (try_table (result i32) (catch $e0 $h) + (suspend $yield) + (unreachable) + ) + ) + ) + (elem declare func $yield42) +) +(assert_return (invoke "throw_handled_ref") (i32.const 42)) + + +;; Test resume_throw_ref where the continuation does not handle the exception. +(module + (tag $e0) + + (type $f (func)) + (type $k (cont $f)) + + (func $no_handler + (unreachable) ;; We only throw into this function + ) + (elem declare func $no_handler) + + (func (export "throw_unhandled_ref") + (block $h (result exnref) + (try_table (catch_ref $e0 $h) (throw $e0)) + (unreachable) + ) + (resume_throw_ref $k (cont.new $k (ref.func $no_handler))) + ) +) +(assert_exception (invoke "throw_unhandled_ref")) + +;; Test resume_throw_ref on a consumed continuation. +(module + (tag $e0) + + (type $f (func)) + (type $k (cont $f)) + + (func $f1) + (elem declare func $f1) + + (func (export "throw_consumed_ref") + (local $k_ref (ref $k)) + (local.set $k_ref (cont.new $k (ref.func $f1))) + (resume $k (local.get $k_ref)) ;; consume it + + (block $h (result exnref) + (try_table (result exnref) (catch_ref $e0 $h) + (throw $e0) + ) + ) + (local.get $k_ref) + + (resume_throw_ref $k) ;; should trap + ) +) +(assert_trap (invoke "throw_consumed_ref") "continuation already consumed") + +;; Test resume_throw_ref on a null continuation reference. +(module + (tag $e0) + (type $f (func)) + (type $k (cont $f)) + (func (export "throw_null_ref") + (block $h (result exnref) + (try_table (catch_ref $e0 $h) + (throw $e0)) + (unreachable) + ) + (resume_throw_ref $k (ref.null $k)) + ) +) +(assert_trap (invoke "throw_null_ref") "null continuation reference") + +;; ---- Validation ---- + +;; TODO(Binaryen): validate here, even though the continuation is null and we +;; don't have the info in the IR. +;;(assert_invalid +;; (module +;; (type $ft (func)) +;; (type $ct (cont $ft)) +;; (tag $exn (param i32)) +;; (func +;; (i64.const 0) +;; (resume_throw $ct $exn (ref.null $ct)) ;; null continuation +;; (unreachable))) +;; "type mismatch") + +(assert_invalid + (module + (type $ft (func)) + (type $ct (cont $ft)) + (tag $exn) + (func + (ref.null $ct) + (i32.const 0) + (resume_throw $ct $exn) ;; exception tag does not take paramter + (unreachable))) + "type mismatch") + +(assert_invalid + (module + (type $ft (func)) + (type $ct (cont $ft)) + (tag $exn (param i32)) + (func + (resume_throw $ct $exn (ref.null $ct)) ;; missing exception payload + (unreachable))) + "type mismatch") + + +(assert_invalid + (module + (type $ft (func)) + (type $ct (cont $ft)) + (tag $exn (param externref)) + (func + (i64.const 0) + (resume_throw_ref $ct (ref.null $ct)) ;; expecting an exception ref + (unreachable))) + "type mismatch") + +(assert_invalid + (module + (type $ft (func)) + (type $ct (cont $ft)) + (func + (resume_throw_ref $ct (ref.null $ct)) ;; expecting an exception ref + (unreachable))) + "type mismatch")