diff --git a/src/cfg/cfg-traversal.h b/src/cfg/cfg-traversal.h index 67e34f0da61..d156c7b1c9e 100644 --- a/src/cfg/cfg-traversal.h +++ b/src/cfg/cfg-traversal.h @@ -453,7 +453,9 @@ struct CFGWalker : public PostWalker { auto handlerBlocks = BranchUtils::getUniqueTargets(*currp); // Add branches to the targets. for (auto target : handlerBlocks) { - self->branches[target].push_back(self->currBasicBlock); + if (target) { + self->branches[target].push_back(self->currBasicBlock); + } } } diff --git a/src/passes/DeadArgumentElimination2.cpp b/src/passes/DeadArgumentElimination2.cpp index 91167d752b4..91a4337a2d2 100644 --- a/src/passes/DeadArgumentElimination2.cpp +++ b/src/passes/DeadArgumentElimination2.cpp @@ -52,6 +52,7 @@ #include "ir/type-updating.h" #include "pass.h" #include "support/index.h" +#include "support/mixed_arena.h" #include "support/utilities.h" #include "wasm-builder.h" #include "wasm-traversal.h" @@ -354,9 +355,25 @@ struct GraphBuilder : public WalkerPass> { } } - void visitResume(Resume* curr) { noteContinuation(curr->cont->type); } + void visitResumeHandlers(const ArenaVector& labels) { + for (Index i = 0; i < labels.size(); ++i) { + if (labels[i]) { + auto* target = findBreakTarget(labels[i]); + assert(target->type.size() >= 1); + auto newContType = target->type[target->type.size() - 1]; + assert(newContType.isContinuation()); + noteContinuation(newContType); + } + } + } + + void visitResume(Resume* curr) { + noteContinuation(curr->cont->type); + visitResumeHandlers(curr->handlerBlocks); + } void visitResumeThrow(ResumeThrow* curr) { noteContinuation(curr->cont->type); + visitResumeHandlers(curr->handlerBlocks); } void visitStackSwitch(StackSwitch* curr) { noteContinuation(curr->cont->type); diff --git a/src/wasm/wasm-ir-builder.cpp b/src/wasm/wasm-ir-builder.cpp index abb47eec1bd..e535c21abb0 100644 --- a/src/wasm/wasm-ir-builder.cpp +++ b/src/wasm/wasm-ir-builder.cpp @@ -1974,6 +1974,9 @@ Result<> IRBuilder::makeRefTest(Type type) { } Result<> IRBuilder::makeRefCast(Type type, bool isDesc) { + if (!type.isCastable()) { + return Err{"ref.cast cannot cast to invalid type"}; + } std::optional descriptor; if (isDesc) { assert(type.isRef()); @@ -2027,6 +2030,9 @@ Result<> IRBuilder::makeBrOn(Index label, curr.op = op; curr.castType = out; curr.desc = nullptr; + if (op != BrOnNull && op != BrOnNonNull && !out.isCastable()) { + return Err{"br_on cannot cast to invalid type"}; + } CHECK_ERR(visitBrOn(&curr)); // Validate type immediates before we forget them. diff --git a/src/wasm/wasm-validator.cpp b/src/wasm/wasm-validator.cpp index 2b1c6cffe25..47fd1de0736 100644 --- a/src/wasm/wasm-validator.cpp +++ b/src/wasm/wasm-validator.cpp @@ -16,7 +16,6 @@ #include #include -#include #include #include @@ -26,12 +25,12 @@ #include "ir/gc-type-utils.h" #include "ir/global-utils.h" #include "ir/intrinsics.h" -#include "ir/local-graph.h" #include "ir/local-structural-dominance.h" #include "ir/module-utils.h" #include "ir/stack-utils.h" #include "ir/utils.h" #include "support/colors.h" +#include "support/mixed_arena.h" #include "wasm-features.h" #include "wasm-type.h" #include "wasm-validator.h" @@ -311,7 +310,15 @@ struct FunctionValidator : public WalkerPass> { // Validate a function. void validate(Function* func) { walkFunction(func); } + // The types sent to each label on branches. std::unordered_map> breakTypes; + // The Tags and continuation result types used to resume at this label. + struct ResumeInfo { + Expression* resume; + Tag* tag; + Type contResult; + }; + std::unordered_map> resumeInfos; std::unordered_set delegateTargetNames; std::unordered_set rethrowTargetNames; @@ -570,6 +577,10 @@ struct FunctionValidator : public WalkerPass> { void visitContNew(ContNew* curr); void visitContBind(ContBind* curr); void visitSuspend(Suspend* curr); + void validateResumeHandlers(Expression* curr, + Type contResults, + const ArenaVector& tags, + const ArenaVector& labels); void visitResume(Resume* curr); void visitResumeThrow(ResumeThrow* curr); void visitStackSwitch(StackSwitch* curr); @@ -759,6 +770,64 @@ void FunctionValidator::visitBlock(Block* curr) { "break type must be a subtype of the target block type"); } breakTypes.erase(iter); + // Check the tags of resume handlers that target this block as well. + if (auto it = resumeInfos.find(curr->name); it != resumeInfos.end()) { + for (const auto& [resume, tag_, contResult] : it->second) { + // TODO: Captured structured references are a C++20 extension. + const auto& tag = tag_; + // The tag's params must match the block results, except for the last + // result, which must be a continuation type whose parameters match the + // tag's results and whose results must be matched by the resumed + // continuation's results. + auto printHandler = [&]() { + if (!info.quiet) { + getStream() << "at (on " << tag->name << ' ' << curr->name << ")\n"; + } + }; + if (!shouldBeEqual(tag->params().size() + 1, + curr->type.size(), + resume, + "mismatched resume handler tag and label arities")) { + printHandler(); + break; + } + for (Index i = 0; i < tag->params().size(); ++i) { + if (!shouldBeSubType(tag->params()[i], + curr->type[i], + resume, + "tag type does not match label type")) { + if (info.quiet) { + getStream() << "at index " << i << "\n"; + } + break; + } + } + Type cont = curr->type[curr->type.size() - 1]; + if (!shouldBeTrue(cont.isContinuation(), + resume, + "resume handler branches to label that does not take " + "a continuation")) { + printHandler(); + break; + } + auto contSig = cont.getHeapType().getContinuation().type.getSignature(); + if (!shouldBeSubType(contSig.params, + tag->results(), + resume, + "new continuation parameters do not match " + "expected suspension results")) { + printHandler(); + } + if (!shouldBeSubType(contResult, + contSig.results, + resume, + "resumed continuation results do not match new " + "continuation results")) { + printHandler(); + } + } + resumeInfos.erase(it); + } } auto* func = getFunction(); @@ -3180,13 +3249,7 @@ void FunctionValidator::visitRefGetDesc(RefGetDesc* curr) { void FunctionValidator::visitBrOn(BrOn* curr) { shouldBeTrue( getModule()->features.hasGC(), curr, "br_on* requires gc [--enable-gc]"); - if (curr->ref->type == Type::unreachable) { - return; - } - if (!shouldBeTrue( - curr->ref->type.isRef(), curr, "br_on* ref must have ref type")) { - return; - } + if (curr->op != BrOnNull && curr->op != BrOnNonNull) { // Common validation for all br_on_cast* if (!shouldBeTrue(curr->castType.isRef(), @@ -3194,11 +3257,23 @@ void FunctionValidator::visitBrOn(BrOn* curr) { "br_on_cast* must have reference cast type")) { return; } - shouldBeEqual( - curr->castType.getHeapType().getBottom(), - curr->ref->type.getHeapType().getBottom(), - curr, - "br_on_cast* target type and ref type must have a common supertype"); + if (curr->ref->type != Type::unreachable) { + shouldBeEqual( + curr->castType.getHeapType().getBottom(), + curr->ref->type.getHeapType().getBottom(), + curr, + "br_on_cast* target type and ref type must have a common supertype"); + } + shouldBeTrue( + curr->castType.isCastable(), curr, "br_on cannot cast to invalid type"); + } + + if (curr->ref->type == Type::unreachable) { + return; + } + if (!shouldBeTrue( + curr->ref->type.isRef(), curr, "br_on* ref must have ref type")) { + return; } switch (curr->op) { case BrOnNull: @@ -3264,8 +3339,6 @@ void FunctionValidator::visitBrOn(BrOn* curr) { } shouldBeTrue( curr->ref->type.isCastable(), curr, "br_on cannot cast invalid type"); - shouldBeTrue( - curr->castType.isCastable(), curr, "br_on cannot cast to invalid type"); break; } } @@ -4157,7 +4230,6 @@ void FunctionValidator::visitStringSliceWTF(StringSliceWTF* curr) { } void FunctionValidator::visitContNew(ContNew* curr) { - // TODO implement actual type-checking shouldBeTrue(!getModule() || getModule()->features.hasStackSwitching(), curr, "cont.new requires stack-switching [--enable-stack-switching]"); @@ -4173,7 +4245,8 @@ void FunctionValidator::visitContNew(ContNew* curr) { } shouldBeTrue(curr->type.isExact(), curr, "cont.new should be exact"); - if (!shouldBeTrue(curr->type.isContinuation(), + if (!shouldBeTrue(curr->type.isRef() && + curr->type.getHeapType().isContinuation(), curr, "cont.new must be annotated with a continuation type")) { return; @@ -4188,84 +4261,239 @@ void FunctionValidator::visitContNew(ContNew* curr) { } void FunctionValidator::visitContBind(ContBind* curr) { - // TODO implement actual type-checking - shouldBeTrue(!getModule() || getModule()->features.hasStackSwitching(), - curr, - "cont.bind requires stack-switching [--enable-stack-switching]"); + if (!shouldBeTrue( + !getModule() || getModule()->features.hasStackSwitching(), + curr, + "cont.bind requires stack-switching [--enable-stack-switching]")) { + return; + } - if (curr->cont->type.isRef() && - curr->cont->type.getHeapType().isMaybeShared(HeapType::nocont)) { + if (curr->cont->type == Type::unreachable || + curr->type == Type::unreachable) { return; } - if (curr->type == Type::unreachable) { + if (!shouldBeTrue( + curr->cont->type.isRef(), curr, "the input type must be a reference")) { return; } + auto inType = curr->cont->type.getHeapType(); - shouldBeTrue( - curr->cont->type.isContinuation() && - curr->cont->type.getHeapType().getContinuation().type.isSignature(), - curr, - "the first type annotation on cont.bind must be a continuation type"); + if (!shouldBeTrue(inType.isMaybeShared(HeapType::nocont) || + inType.isContinuation(), + curr, + "the input type must be a continuation type")) { + return; + } - shouldBeTrue( - curr->type.isContinuation() && - curr->type.getHeapType().getContinuation().type.isSignature(), - curr, - "the second type annotation on cont.bind must be a continuation type"); + if (!shouldBeTrue( + curr->type.isRef() && curr->type.isNonNullable() && + curr->type.isExact(), + curr, + "the output type must be a non-nullable, exact reference")) { + return; + } + auto outType = curr->type.getHeapType(); - if (!shouldBeTrue(curr->type.isNonNullable(), + if (!shouldBeTrue(outType.isContinuation(), curr, - "cont.bind should have a non-nullable reference type")) { + "the output type must be a continuation type")) { return; } - shouldBeTrue(curr->type.isExact(), curr, "cont.bind should be exact"); + + if (inType.isBottom()) { + return; + } + + auto sigIn = inType.getContinuation().type.getSignature(); + auto sigOut = outType.getContinuation().type.getSignature(); + + if (!shouldBeTrue(curr->operands.size() <= sigIn.params.size(), + curr, + "too many arguments for cont.bind")) { + return; + } + + size_t numBound = curr->operands.size(); + size_t numRemaining = sigIn.params.size() - numBound; + + if (!shouldBeEqual(numRemaining, + sigOut.params.size(), + curr, + "result continuation parameter count mismatch")) { + return; + } + + for (size_t i = 0; i < numBound; ++i) { + if (!shouldBeSubType(curr->operands[i]->type, + sigIn.params[i], + curr, + "cont.bind argument type mismatch")) { + if (!info.quiet) { + getStream() << "(at index " << i << ")\n"; + } + } + } + + for (size_t i = 0; i < numRemaining; ++i) { + if (!shouldBeSubType(sigOut.params[i], + sigIn.params[numBound + i], + curr, + "result continuation parameter type mismatch")) { + if (!info.quiet) { + getStream() << "(at index " << i << ")\n"; + } + } + } + + shouldBeSubType(sigIn.results, + sigOut.results, + curr, + "result continuation result type mismatch"); } void FunctionValidator::visitSuspend(Suspend* curr) { - // TODO implement actual type-checking - shouldBeTrue(!getModule() || getModule()->features.hasStackSwitching(), - curr, - "suspend requires stack-switching [--enable-stack-switching]"); + if (!shouldBeTrue( + !getModule() || getModule()->features.hasStackSwitching(), + curr, + "suspend requires stack-switching [--enable-stack-switching]")) { + return; + } + + auto* tag = getModule()->getTagOrNull(curr->tag); + if (!shouldBeTrue(!!tag, curr, "suspend tag must exist")) { + return; + } + + auto sig = tag->type.getSignature(); + if (!shouldBeTrue(curr->operands.size() == sig.params.size(), + curr, + "suspend argument count mismatch")) { + return; + } + + for (size_t i = 0; i < sig.params.size(); ++i) { + if (!shouldBeSubType(curr->operands[i]->type, + sig.params[i], + curr, + "suspend argument type mismatch")) { + if (!info.quiet) { + getStream() << "(at index " << i << ")\n"; + } + } + } + + shouldBeEqualOrFirstIsUnreachable( + curr->type, sig.results, curr, "suspend result type mismatch"); +} + +void FunctionValidator::validateResumeHandlers( + Expression* curr, + Type contResults, + const ArenaVector& tags, + const ArenaVector& labels) { + for (size_t i = 0; i < tags.size(); ++i) { + auto* tag = getModule()->getTagOrNull(tags[i]); + if (!shouldBeTrue(!!tag, curr, "resume handler tag must exist")) { + continue; + } + auto tagSig = tag->type.getSignature(); + + if (labels[i]) { + // (on $tag $label) + // label must accept [t1_tag* (ref $ct_handler)] + // and $ct_handler must be cont [t2_tag*] -> [sig.results] + // But we cannot check this here because we do not know what type the + // block named $label expects. Save the tag to check when we visit the + // block later. + if (!shouldBeTrue(breakTypes.count(labels[i]) != 0, + curr, + "all resume targets must be valid")) { + return; + } + resumeInfos[labels[i]].push_back({curr, tag, contResults}); + } else { + // (on $tag switch) + // tag must be [] -> [t*] where t* are the continuation results. + if (shouldBeTrue(tagSig.params.size() == 0, + curr, + "switch handler tag must have no parameters")) { + // NB: Intentionally not checking subtypes here. + shouldBeEqual(tagSig.results, + contResults, + curr, + "switch handler tag results mismatch"); + } + } + } } void FunctionValidator::visitResume(Resume* curr) { - // TODO implement actual type-checking - shouldBeTrue(!getModule() || getModule()->features.hasStackSwitching(), - curr, - "resume requires stack-switching [--enable-stack-switching]"); + if (!shouldBeTrue( + !getModule() || getModule()->features.hasStackSwitching(), + curr, + "resume requires stack-switching [--enable-stack-switching]")) { + return; + } - shouldBeTrue( - curr->sentTypes.size() == curr->handlerBlocks.size(), - curr, - "sentTypes cache in resume instruction has not been initialized"); + if (curr->cont->type == Type::unreachable) { + return; + } - if (curr->cont->type.isRef() && - curr->cont->type.getHeapType().isMaybeShared(HeapType::nocont)) { + if (!shouldBeTrue(curr->cont->type.isRef(), + curr, + "resume continuation must be a reference")) { return; } - shouldBeTrue( - (curr->cont->type.isContinuation() && - curr->cont->type.getHeapType().getContinuation().type.isSignature()) || - curr->type == Type::unreachable, - curr, - "resume must be annotated with a continuation type"); + auto type = curr->cont->type.getHeapType(); + if (type.isMaybeShared(HeapType::nocont)) { + return; + } + + if (!shouldBeTrue( + type.isContinuation(), + curr, + "resume continuation must have a defined continuation type")) { + return; + } + + auto sig = type.getContinuation().type.getSignature(); + + if (!shouldBeTrue(curr->operands.size() == sig.params.size(), + curr, + "resume argument count mismatch")) { + return; + } + + for (Index i = 0; i < sig.params.size(); ++i) { + if (!shouldBeSubType(curr->operands[i]->type, + sig.params[i], + curr, + "resume argument type mismatch")) { + if (!info.quiet) { + getStream() << "(at index " << i << ")\n"; + } + } + } + + shouldBeEqualOrFirstIsUnreachable( + curr->type, sig.results, curr, "resume result type mismatch"); + + validateResumeHandlers( + curr, sig.results, curr->handlerTags, curr->handlerBlocks); } void FunctionValidator::visitResumeThrow(ResumeThrow* curr) { - // TODO implement actual type-checking - shouldBeTrue( - !getModule() || (getModule()->features.hasExceptionHandling() && - getModule()->features.hasStackSwitching()), - curr, - "resume_throw requires exception handling [--enable-exception-handling] " - "and stack-switching [--enable-stack-switching]"); - - shouldBeTrue( - curr->sentTypes.size() == curr->handlerBlocks.size(), - curr, - "sentTypes cache in resume_throw instruction has not been initialized"); + if (!shouldBeTrue(!getModule() || + (getModule()->features.hasExceptionHandling() && + getModule()->features.hasStackSwitching()), + curr, + "resume_throw requires exception handling " + "[--enable-exception-handling] and stack-switching " + "[--enable-stack-switching]")) { + return; + } if (curr->tag) { // Normal resume_throw @@ -4273,11 +4501,27 @@ void FunctionValidator::visitResumeThrow(ResumeThrow* curr) { 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 + if (!shouldBeTrue(tag->type.getSignature().results == Type::none, + curr, + "resume_throw tag must have no results")) { + return; + } + if (!shouldBeTrue(curr->operands.size() == + tag->type.getSignature().params.size(), + curr, + "resume_throw operand count mismatch")) { + return; + } + for (size_t i = 0; i < tag->type.getSignature().params.size(); ++i) { + if (!shouldBeSubType(curr->operands[i]->type, + tag->type.getSignature().params[i], + curr, + "resume_throw operand type mismatch")) { + if (!info.quiet) { + getStream() << "(at index " << i << ")\n"; + } + } + } } else { // resume_throw_ref Type exnref = Type(HeapType::exn, Nullable); @@ -4292,41 +4536,132 @@ void FunctionValidator::visitResumeThrow(ResumeThrow* curr) { } } - if (curr->cont->type.isRef() && - curr->cont->type.getHeapType().isMaybeShared(HeapType::nocont)) { + if (curr->cont->type == Type::unreachable) { return; } - shouldBeTrue( - (curr->cont->type.isContinuation() && - curr->cont->type.getHeapType().getContinuation().type.isSignature()) || - curr->type == Type::unreachable, - curr, - "resume_throw must be annotated with a continuation type"); + if (!shouldBeTrue(curr->cont->type.isRef(), + curr, + "resume_throw continuation must be a reference")) { + return; + } + + auto type = curr->cont->type.getHeapType(); + if (type.isMaybeShared(HeapType::nocont)) { + return; + } + + if (!shouldBeTrue( + type.isContinuation(), + curr, + "resume_throw continuation must have a defined continuation type")) { + return; + } + + auto sig = type.getContinuation().type.getSignature(); + + shouldBeEqualOrFirstIsUnreachable( + curr->type, sig.results, curr, "resume_throw result type mismatch"); + + validateResumeHandlers( + curr, sig.results, curr->handlerTags, curr->handlerBlocks); } void FunctionValidator::visitStackSwitch(StackSwitch* curr) { - // TODO implement actual type-checking - shouldBeTrue(!getModule() || getModule()->features.hasStackSwitching(), - curr, - "switch requires stack-switching [--enable-stack-switching]"); + if (!shouldBeTrue( + !getModule() || getModule()->features.hasStackSwitching(), + curr, + "switch requires stack-switching [--enable-stack-switching]")) { + return; + } auto* tag = getModule()->getTagOrNull(curr->tag); if (!shouldBeTrue(!!tag, curr, "switch tag must exist")) { return; } - if (curr->cont->type.isRef() && - curr->cont->type.getHeapType().isMaybeShared(HeapType::nocont)) { + if (curr->cont->type == Type::unreachable) { return; } - shouldBeTrue( - (curr->cont->type.isContinuation() && - curr->cont->type.getHeapType().getContinuation().type.isSignature()) || - curr->type == Type::unreachable, - curr, - "switch must be annotated with a continuation type"); + if (!shouldBeTrue(curr->cont->type.isRef(), + curr, + "switch continuation must be a reference")) { + return; + } + + auto type = curr->cont->type.getHeapType(); + if (type.isMaybeShared(HeapType::nocont)) { + return; + } + + if (!shouldBeTrue( + type.isContinuation(), + curr, + "switch continuation must have a defined continuation type")) { + return; + } + + auto sig1 = type.getContinuation().type.getSignature(); + + // sig1 should be [t1* (ref null $ct2)] -> [te1*] + if (!shouldBeTrue(sig1.params.size() >= 1, + curr, + "switch continuation must have at least one parameter (for " + "the next continuation)")) { + return; + } + + Type ct2Type = sig1.params[sig1.params.size() - 1]; + if (!shouldBeTrue( + ct2Type.isContinuation(), + curr, + "the last parameter of the switch continuation must be a continuation " + "type")) { + return; + } + + auto sig2 = ct2Type.getHeapType().getContinuation().type.getSignature(); + + // check operands (t1*) + size_t numT1 = sig1.params.size() - 1; + if (!shouldBeTrue(curr->operands.size() == numT1, + curr, + "switch argument count mismatch")) { + return; + } + for (size_t i = 0; i < numT1; ++i) { + if (!shouldBeSubType(curr->operands[i]->type, + sig1.params[i], + curr, + "switch argument type mismatch")) { + if (!info.quiet) { + getStream() << "(at index " << i << ")\n"; + } + } + } + + auto tagSig = tag->type.getSignature(); + if (!shouldBeTrue(tagSig.params.size() == 0, + curr, + "switch tag must have no parameters")) { + return; + } + + // te1* <: t* + shouldBeSubType(sig1.results, + tagSig.results, + curr, + "switch continuation result type mismatch"); + + // t* <: te2* + shouldBeSubType( + tagSig.results, sig2.results, curr, "switch tag result type mismatch"); + + // curr->type == t2* + // NB: Intentionally not doing a subtype check here. + shouldBeEqualOrFirstIsUnreachable( + curr->type, sig2.params, curr, "switch result type mismatch"); } void FunctionValidator::visitFunction(Function* curr) { @@ -4401,6 +4736,7 @@ void FunctionValidator::visitFunction(Function* curr) { // expressions, and reset the state for next time. Note that we use some of // this state in the above validations, so this must appear last. assert(breakTypes.empty()); + assert(resumeInfos.empty()); assert(delegateTargetNames.empty()); assert(rethrowTargetNames.empty()); labelNames.clear(); diff --git a/test/lit/passes/dae2-stack-switching.wast b/test/lit/passes/dae2-stack-switching.wast index c4902b0c542..3868b775cfe 100644 --- a/test/lit/passes/dae2-stack-switching.wast +++ b/test/lit/passes/dae2-stack-switching.wast @@ -70,7 +70,7 @@ ;; CHECK: (type $2 (func)) - ;; CHECK: (type $f-sent (func)) + ;; CHECK: (type $f-sent (func (param i64))) ;; CHECK: (type $f (func (param i32))) (type $f (func (param i32))) @@ -79,14 +79,14 @@ (type $f-sent (func (param i64))) (type $k-sent (cont $f-sent)) - ;; CHECK: (type $5 (func)) + ;; CHECK: (type $5 (func (result i64))) ;; CHECK: (type $6 (func (param i32 (ref $k)))) ;; CHECK: (elem declare func $f $f-sent) - ;; CHECK: (tag $e (type $5)) - (tag $e) + ;; CHECK: (tag $e (type $5) (result i64)) + (tag $e (result i64)) ;; CHECK: (func $f (type $f) (param $0 i32) ;; CHECK-NEXT: (drop @@ -109,14 +109,14 @@ (nop) ) - ;; CHECK: (func $f-sent (type $f-sent) - ;; CHECK-NEXT: (local $0 i64) + ;; CHECK: (func $f-sent (type $f-sent) (param $0 i64) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (ref.func $f-sent) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $f-sent (type $f-sent) (param i64) - ;; This can be optimized. + ;; This cannot be optimized unless we also optimize out the matching results + ;; from the exception tag TODO. (drop (ref.func $f-sent) ) @@ -134,7 +134,7 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $resume (param $x i32) (param $k (ref $k)) - ;; resume inhibits optimizations for the resumed continuation type, but not + ;; resume inhibits optimizations for the resumed continuation type and also ;; the continuation types it sends. (drop (block $l (result (ref null $k-sent)) @@ -156,7 +156,7 @@ ;; CHECK: (type $2 (func)) - ;; CHECK: (type $f-sent (func)) + ;; CHECK: (type $f-sent (func (param i64))) ;; CHECK: (type $f (func (param i32))) (type $f (func (param i32))) @@ -165,14 +165,18 @@ (type $f-sent (func (param i64))) (type $k-sent (cont $f-sent)) - ;; CHECK: (type $5 (func)) + ;; CHECK: (type $5 (func (result i64))) - ;; CHECK: (type $6 (func (param (ref $k)))) + ;; CHECK: (type $6 (func)) + + ;; CHECK: (type $7 (func (param (ref $k)))) ;; CHECK: (elem declare func $f $f-sent) - ;; CHECK: (tag $e (type $5)) - (tag $e) + ;; CHECK: (tag $throw (type $6)) + (tag $throw) + ;; CHECK: (tag $e (type $5) (result i64)) + (tag $e (result i64)) ;; CHECK: (func $f (type $f) (param $0 i32) ;; CHECK-NEXT: (drop @@ -186,23 +190,23 @@ ) ) - ;; CHECK: (func $f-sent (type $f-sent) - ;; CHECK-NEXT: (local $0 i64) + ;; CHECK: (func $f-sent (type $f-sent) (param $0 i64) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (ref.func $f-sent) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $f-sent (type $f-sent) (param i64) - ;; This can be optimized. + ;; This cannot be optimized unless we also optimize out the matching results + ;; from the exception tag TODO. (drop (ref.func $f-sent) ) ) - ;; CHECK: (func $resume-throw (type $6) (param $k (ref $k)) + ;; CHECK: (func $resume-throw (type $7) (param $k (ref $k)) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block $l (result (ref null $k-sent)) - ;; CHECK-NEXT: (resume_throw $k $e (on $e $l) + ;; CHECK-NEXT: (resume_throw $k $throw (on $e $l) ;; CHECK-NEXT: (local.get $k) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (unreachable) @@ -210,11 +214,11 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $resume-throw (param $k (ref $k)) - ;; resume_throw inhibits optimizations for the resumed continuation type, - ;; but not the continuation types it sends. + ;; resume_throw inhibits optimizations for the resumed continuation type and + ;; also the continuation types it sends. (drop (block $l (result (ref null $k-sent)) - (resume_throw $k $e (on $e $l) + (resume_throw $k $throw (on $e $l) (local.get $k) ) (unreachable) diff --git a/test/lit/passes/gufa-cont.wast b/test/lit/passes/gufa-cont.wast index 2f27193a1d0..d35e77a60ef 100644 --- a/test/lit/passes/gufa-cont.wast +++ b/test/lit/passes/gufa-cont.wast @@ -18,21 +18,25 @@ ;; OPEN_WORLD: (type $cont-i32 (cont $func-i32)) (type $cont-i32 (cont $func-i32)) - ;; CHECK: (type $4 (func (result i32 (ref $cont)))) + ;; CHECK: (type $4 (func (param i32))) + + ;; CHECK: (type $5 (func (result i32 (ref $cont-i32)))) ;; CHECK: (elem declare func $cont $cont-i32) ;; CHECK: (tag $tag (type $func)) - ;; OPEN_WORLD: (type $4 (func (result i32 (ref $cont)))) + ;; OPEN_WORLD: (type $4 (func (param i32))) + + ;; OPEN_WORLD: (type $5 (func (result i32 (ref $cont-i32)))) ;; OPEN_WORLD: (elem declare func $cont $cont-i32) ;; OPEN_WORLD: (tag $tag (type $func)) (tag $tag (type $func)) - ;; CHECK: (tag $tag-i32 (type $func-i32) (result i32)) - ;; OPEN_WORLD: (tag $tag-i32 (type $func-i32) (result i32)) - (tag $tag-i32 (type $func-i32)) + ;; CHECK: (tag $tag-i32 (type $4) (param i32)) + ;; OPEN_WORLD: (tag $tag-i32 (type $4) (param i32)) + (tag $tag-i32 (param i32)) ;; CHECK: (export "resume" (func $resume)) @@ -152,7 +156,7 @@ ;; CHECK: (func $resume-i32 (type $func) ;; CHECK-NEXT: (tuple.drop 2 - ;; CHECK-NEXT: (block $block (type $4) (result i32 (ref $cont)) + ;; CHECK-NEXT: (block $block (type $5) (result i32 (ref $cont-i32)) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (resume $cont-i32 (on $tag-i32 $block) ;; CHECK-NEXT: (cont.new $cont-i32 @@ -166,7 +170,7 @@ ;; CHECK-NEXT: ) ;; OPEN_WORLD: (func $resume-i32 (type $func) ;; OPEN_WORLD-NEXT: (tuple.drop 2 - ;; OPEN_WORLD-NEXT: (block $block (type $4) (result i32 (ref $cont)) + ;; OPEN_WORLD-NEXT: (block $block (type $5) (result i32 (ref $cont-i32)) ;; OPEN_WORLD-NEXT: (drop ;; OPEN_WORLD-NEXT: (resume $cont-i32 (on $tag-i32 $block) ;; OPEN_WORLD-NEXT: (cont.new $cont-i32 @@ -181,7 +185,7 @@ (func $resume-i32 (export "resume-i32") ;; As above, but with more values sent than just the continuation. (tuple.drop 2 - (block $block (result i32 (ref $cont)) + (block $block (result i32 (ref $cont-i32)) (resume $cont-i32 (on $tag-i32 $block) (cont.new $cont-i32 (ref.func $cont-i32) @@ -195,26 +199,27 @@ (module ;; CHECK: (type $func (func (param i32))) - - ;; CHECK: (type $cont (cont $func)) - - ;; CHECK: (type $none (func)) ;; OPEN_WORLD: (type $func (func (param i32))) - - ;; OPEN_WORLD: (type $cont (cont $func)) - - ;; OPEN_WORLD: (type $none (func)) - (type $none (func)) (type $func (func (param i32))) + ;; CHECK: (type $cont (cont $func)) + ;; OPEN_WORLD: (type $cont (cont $func)) (type $cont (cont $func)) + ;; CHECK: (type $2 (func (result i32))) + + ;; CHECK: (type $3 (func)) + ;; CHECK: (elem declare func $func) - ;; CHECK: (tag $tag (type $none)) + ;; CHECK: (tag $tag (type $2) (result i32)) + ;; OPEN_WORLD: (type $2 (func (result i32))) + + ;; OPEN_WORLD: (type $3 (func)) + ;; OPEN_WORLD: (elem declare func $func) - ;; OPEN_WORLD: (tag $tag (type $none)) - (tag $tag (type $none)) + ;; OPEN_WORLD: (tag $tag (type $2) (result i32)) + (tag $tag (result i32)) ;; CHECK: (export "run" (func $run)) @@ -241,7 +246,7 @@ ) ) - ;; CHECK: (func $run (type $none) + ;; CHECK: (func $run (type $3) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block $block (result (ref $cont)) ;; CHECK-NEXT: (resume $cont (on $tag $block) @@ -254,7 +259,7 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; OPEN_WORLD: (func $run (type $none) + ;; OPEN_WORLD: (func $run (type $3) ;; OPEN_WORLD-NEXT: (drop ;; OPEN_WORLD-NEXT: (block $block (result (ref $cont)) ;; OPEN_WORLD-NEXT: (resume $cont (on $tag $block) diff --git a/test/spec/cont.wast b/test/spec/stack-switching/cont.wast similarity index 73% rename from test/spec/cont.wast rename to test/spec/stack-switching/cont.wast index fc48d01ca2f..4df05a2dd36 100644 --- a/test/spec/cont.wast +++ b/test/spec/stack-switching/cont.wast @@ -123,10 +123,11 @@ ) ) - (func (export "null-new") (result (ref null $k1)) + (func (export "null-new") (cont.new $k1 (ref.null $f1) ) + (drop) ) ) @@ -262,6 +263,34 @@ ) "type mismatch") + +;; Test resume_throw used on the very first execution of a continuation (so the +;; code in the continuation function is never reached). +(module + (tag $exn) + + (type $f (func)) + (type $k (cont $f)) + + (func $never + (unreachable) + ) + + (func (export "resume_throw-never") + (block $handle + (try_table (catch $exn $handle) + (resume_throw $k $exn + (cont.new $k (ref.func $never)) + ) + ) + ) + ) + + (elem declare func $never) +) + +(assert_return (invoke "resume_throw-never")) + ;; Simple state example (module $state @@ -314,6 +343,7 @@ (assert_return (invoke "run") (i32.const 19)) + ;; Simple generator example (module $generator @@ -370,7 +400,6 @@ (assert_return (invoke "sum" (i64.const 100) (i64.const 2000)) (i64.const 1_996_050)) - ;; Simple scheduler example (module $scheduler @@ -543,9 +572,10 @@ (assert_return (invoke "run" (i32.const 1) (i32.const 1))) (assert_return (invoke "run" (i32.const 3) (i32.const 4))) + ;; Nested example: generator in a thread -(module $concurrent-generator +(module $concurrent_generator (func $log (import "spectest" "print_i64") (param i64)) (tag $syield (import "scheduler" "yield")) @@ -599,6 +629,80 @@ (assert_return (invoke "sum" (i64.const 10) (i64.const 20)) (i64.const 165)) + +;; cont.bind + +(module + (type $f2 (func (param i32 i32) (result i32 i32 i32 i32 i32 i32))) + (type $f4 (func (param i32 i32 i32 i32) (result i32 i32 i32 i32 i32 i32))) + (type $f6 (func (param i32 i32 i32 i32 i32 i32) (result i32 i32 i32 i32 i32 i32))) + + (type $k2 (cont $f2)) + (type $k4 (cont $f4)) + (type $k6 (cont $f6)) + + (elem declare func $f) + (func $f (param i32 i32 i32 i32 i32 i32) (result i32 i32 i32 i32 i32 i32) + (local.get 0) (local.get 1) (local.get 2) + (local.get 3) (local.get 4) (local.get 5) + ) + + (func (export "run") (result i32 i32 i32 i32 i32 i32) + (local $k6 (ref null $k6)) + (local $k4 (ref null $k4)) + (local $k2 (ref null $k2)) + (local.set $k6 (cont.new $k6 (ref.func $f))) + (local.set $k4 (cont.bind $k6 $k4 (i32.const 1) (i32.const 2) (local.get $k6))) + (local.set $k2 (cont.bind $k4 $k2 (i32.const 3) (i32.const 4) (local.get $k4))) + (resume $k2 (i32.const 5) (i32.const 6) (local.get $k2)) + ) +) + +;; TODO: Finish cont.bind in interpreter. +;; (assert_return (invoke "run") +;; (i32.const 1) (i32.const 2) (i32.const 3) +;; (i32.const 4) (i32.const 5) (i32.const 6) +;; ) + +(module + (tag $e (result i32 i32 i32 i32 i32 i32)) + + (type $f0 (func (result i32 i32 i32 i32 i32 i32 i32))) + (type $f2 (func (param i32 i32) (result i32 i32 i32 i32 i32 i32 i32))) + (type $f4 (func (param i32 i32 i32 i32) (result i32 i32 i32 i32 i32 i32 i32))) + (type $f6 (func (param i32 i32 i32 i32 i32 i32) (result i32 i32 i32 i32 i32 i32 i32))) + + (type $k0 (cont $f0)) + (type $k2 (cont $f2)) + (type $k4 (cont $f4)) + (type $k6 (cont $f6)) + + (elem declare func $f) + (func $f (result i32 i32 i32 i32 i32 i32 i32) + (i32.const 0) (suspend $e) + ) + + (func (export "run") (result i32 i32 i32 i32 i32 i32 i32) + (local $k6 (ref null $k6)) + (local $k4 (ref null $k4)) + (local $k2 (ref null $k2)) + (block $l (result (ref $k6)) + (resume $k0 (on $e $l) (cont.new $k0 (ref.func $f))) + (unreachable) + ) + (local.set $k6) + (local.set $k4 (cont.bind $k6 $k4 (i32.const 1) (i32.const 2) (local.get $k6))) + (local.set $k2 (cont.bind $k4 $k2 (i32.const 3) (i32.const 4) (local.get $k4))) + (resume $k2 (i32.const 5) (i32.const 6) (local.get $k2)) + ) +) + +;; TODO: Finish cont.bind in interpreter. +;; (assert_return (invoke "run") +;; (i32.const 0) (i32.const 1) (i32.const 2) (i32.const 3) +;; (i32.const 4) (i32.const 5) (i32.const 6) +;; ) + ;; Subtyping (module (type $ft1 (func (param i32))) @@ -638,6 +742,89 @@ ) (assert_return (invoke "set-global")) +;; Switch +(module + (rec + (type $ft (func (param (ref null $ct)))) + (type $ct (cont $ft))) + + (func $print-i32 (import "spectest" "print_i32") (param i32)) + + (global $fi (mut i32) (i32.const 0)) + (global $gi (mut i32) (i32.const 1)) + + (tag $swap) + + (func $init (export "init") (result i32) + (resume $ct (on $swap switch) + (cont.new $ct (ref.func $g)) + (cont.new $ct (ref.func $f))) + (return (i32.const 42))) + (func $f (type $ft) + (local $nextk (ref null $ct)) + (local.set $nextk (local.get 0)) + (call $print-i32 (global.get $fi)) + (switch $ct $swap (local.get $nextk)) + (local.set $nextk) + (call $print-i32 (global.get $fi)) + (switch $ct $swap (local.get $nextk)) + (drop)) + (func $g (type $ft) + (local $nextk (ref null $ct)) + (local.set $nextk (local.get 0)) + (call $print-i32 (global.get $gi)) + (switch $ct $swap (local.get $nextk)) + (local.set $nextk) + (call $print-i32 (global.get $gi))) + (elem declare func $f $g) +) + +;; TODO: Fix wasm-shell assertion. +;; (assert_return (invoke "init") (i32.const 42)) + +(module + (rec + (type $ft (func (param i32) (param (ref null $ct)) (result i32))) + (type $ct (cont $ft))) + + (func $print-i32 (import "spectest" "print_i32") (param i32)) + + (tag $swap (result i32)) + + (func $init (export "init") (result i32) + (resume $ct (on $swap switch) + (i32.const 1) + (cont.new $ct (ref.func $g)) + (cont.new $ct (ref.func $f)))) + (func $f (type $ft) + (local $i i32) + (local $nextk (ref null $ct)) + (local.set $i (local.get 0)) + (local.set $nextk (local.get 1)) + (call $print-i32 (local.get $i)) + (switch $ct $swap (i32.add (i32.const 1) (local.get $i)) (local.get $nextk)) + (local.set $nextk) + (local.set $i) + (call $print-i32 (local.get $i)) + (switch $ct $swap (i32.add (i32.const 1) (local.get $i)) (local.get $nextk)) + (unreachable)) + (func $g (type $ft) + (local $i i32) + (local $nextk (ref null $ct)) + (local.set $i (local.get 0)) + (local.set $nextk (local.get 1)) + (call $print-i32 (local.get $i)) + (switch $ct $swap (i32.add (i32.const 1) (local.get $i)) (local.get $nextk)) + (local.set $nextk) + (local.set $i) + (call $print-i32 (local.get $i)) + (return (local.get $i))) + (elem declare func $f $g) +) + +;; TODO: Fix wasm-shell assertion. +;; (assert_return (invoke "init") (i32.const 4)) + (assert_invalid (module (rec @@ -652,6 +839,55 @@ (drop))) "type mismatch") +(assert_invalid + (module + (rec + (type $ft (func (param i32) (param (ref null $ct)))) + (type $ct (cont $ft))) + + (tag $swap) + (func $f (type $ft) + (switch $ct $swap (i64.const 0) (local.get 1)) + (drop) + (drop))) + "type mismatch") + +(module + (type $ft1 (func)) + (type $ct1 (cont $ft1)) + (rec + (type $ft2 (func (param (ref null $ct2)))) + (type $ct2 (cont $ft2))) + + (tag $t) + + (func $suspend (type $ft2) + (suspend $t)) + + (func $switch (type $ft2) + (switch $ct2 $t (local.get 0)) + (drop)) + + (func (export "unhandled-suspend-t") + (resume $ct2 (on $t switch) + (cont.new $ct2 (ref.func $suspend)) + (cont.new $ct2 (ref.func $suspend)))) + (func (export "unhandled-switch-t") + (block $l (result (ref $ct1)) + (resume $ct2 (on $t $l) + (cont.new $ct2 (ref.func $switch)) + (cont.new $ct2 (ref.func $switch))) + (unreachable) + ) + (unreachable)) + + (elem declare func $suspend $switch) +) + +;; TODO: Fix assertion. +;; (assert_suspension (invoke "unhandled-suspend-t") "unhandled tag") +;; (assert_suspension (invoke "unhandled-switch-t") "unhandled tag") + (module (rec (type $ft (func (param (ref null $ct)))) @@ -761,6 +997,39 @@ ) (assert_return (invoke "main") (i32.const 10)) +(module + (type $f1 (func (result i32))) + (type $c1 (cont $f1)) + (type $f2 (func (param (ref null $c1)) (result i32))) + (type $c2 (cont $f2)) + (type $f3 (func (param (ref null $c2)) (result i32))) + (type $c3 (cont $f3)) + (tag $e (result i32)) + + (func $fn_1 (param (ref null $c2)) (result i32) + (local.get 0) + (switch $c2 $e) + (i32.const 24) + ) + (elem declare func $fn_1) + + (func $fn_2 (result i32) + (cont.new $c3 (ref.func $fn_1)) + (switch $c3 $e) + (drop) + (i32.const -1) + ) + (elem declare func $fn_2) + + (func (export "main") (result i32) + (cont.new $c1 (ref.func $fn_2)) + (resume $c1 (on $e switch)) + ) +) + +;; TODO: Fix wasm-shell assertion failure. +;; (assert_return (invoke "main") (i32.const -1)) + ;; Syntax: check unfolded forms (module (type $ft (func)) @@ -901,3 +1170,45 @@ (func (param $k (ref $ct)) (switch $ct $t))) "type mismatch in switch tag") + +;; Synthesized from https://github.com/WebAssembly/stack-switching/issues/117 +(assert_invalid + (module + (type $ft (func)) + (type $ct (cont $ft)) + (tag $t) + + (func + (block $on_t (result (ref cont)) + (resume $ct (on $t $on_t) (cont.new $ct (ref.null $ft))) + (unreachable) + ) + (drop) + )) + "type mismatch: instruction requires concrete continuation reference type but label has [(ref cont)]") + +(assert_invalid + (module + (type $ft (func)) + (type $ct (cont $ft)) + (tag $t) + + (func + (block $on_t (result (ref nocont)) + (resume $ct (on $t $on_t) (cont.new $ct (ref.null $ft))) + (unreachable) + ) + (drop) + )) + "type mismatch: instruction requires concrete continuation reference type but label has [(ref nocont)]") + +;; https://github.com/WebAssembly/stack-switching/issues/117#issuecomment-2908974084 +(module + (type $f (func)) + (type $c (sub (cont $f))) + (tag $e) + (func (param $c (ref $c)) + (local.get $c) + (resume $c) + ) +) diff --git a/test/spec/resume_throw.wast b/test/spec/stack-switching/resume_throw.wast similarity index 94% rename from test/spec/resume_throw.wast rename to test/spec/stack-switching/resume_throw.wast index f480a50430a..2b19f367166 100644 --- a/test/spec/resume_throw.wast +++ b/test/spec/stack-switching/resume_throw.wast @@ -257,18 +257,16 @@ ;; ---- 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 (param i32)) + (func + (i64.const 0) + (resume_throw $ct $exn (ref.null $ct)) ;; null continuation + (unreachable))) + "type mismatch") (assert_invalid (module diff --git a/test/spec/stack-switching/validation.wast b/test/spec/stack-switching/validation.wast new file mode 100644 index 00000000000..116b3d17f18 --- /dev/null +++ b/test/spec/stack-switching/validation.wast @@ -0,0 +1,903 @@ +;; This file tests validation only, without GC types and subtyping. + + +;;;; +;;;; WasmFX types +;;;; + +(module + (type $ft1 (func)) + (type $ct1 (cont $ft1)) + + (type $ft2 (func (param i32) (result i32))) + (type $ct2 (cont $ft2)) + + (func $test + (param $p1 (ref cont)) + (param $p2 (ref nocont)) + (param $p3 (ref $ct1)) + + (local $x1 (ref cont)) + (local $x2 (ref nocont)) + (local $x3 (ref $ct1)) + (local $x4 (ref $ct2)) + (local $x5 (ref null $ct1)) + + ;; nocont <: cont + (local.set $x1 (local.get $p2)) + + ;; nocont <: $ct1 + (local.set $x3 (local.get $p2)) + + ;; $ct1 <: $cont + (local.set $x3 (local.get $p3)) + + ;; (ref $ct1) <: (ref null $cont) + (local.set $x5 (local.get $p3)) + ) +) + +(assert_invalid + (module + (type $ft1 (func)) + (type $ct1 (cont $ft1)) + + (type $ft2 (func (param i32) (result i32))) + (type $ct2 (cont $ft2)) + + (func $test + (param $p1 (ref cont)) + (param $p2 (ref nocont)) + (param $p3 (ref $ct1)) + + (local $x1 (ref cont)) + (local $x2 (ref nocont)) + (local $x3 (ref $ct1)) + (local $x4 (ref $ct2)) + (local $x5 (ref null $ct1)) + + ;; cont