From fa4bd9a17c24053f5159e953d399975d7540e3da Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 12 Mar 2026 21:23:24 -0700 Subject: [PATCH] Stack switching validation Now that we want to fuzz stack switching against a real implementation in V8, add proper validation. Copy in the stack switching tests, commenting out parts we do not handle for other reasons, to test that the new validation logic is correct. Fix DAE2 and a couple of stack switching tests that were not actually valid to begin with. --- src/cfg/cfg-traversal.h | 4 +- src/passes/DeadArgumentElimination2.cpp | 19 +- src/wasm/wasm-ir-builder.cpp | 6 + src/wasm/wasm-validator.cpp | 526 ++++++++-- test/lit/passes/dae2-stack-switching.wast | 46 +- test/lit/passes/gufa-cont.wast | 49 +- test/spec/{ => stack-switching}/cont.wast | 317 +++++- .../{ => stack-switching}/resume_throw.wast | 22 +- test/spec/stack-switching/validation.wast | 903 ++++++++++++++++++ test/spec/stack-switching/validation_gc.wast | 320 +++++++ 10 files changed, 2057 insertions(+), 155 deletions(-) rename test/spec/{ => stack-switching}/cont.wast (73%) rename test/spec/{ => stack-switching}/resume_throw.wast (94%) create mode 100644 test/spec/stack-switching/validation.wast create mode 100644 test/spec/stack-switching/validation_gc.wast 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