diff --git a/src/passes/GUFA.cpp b/src/passes/GUFA.cpp index 21f9aa8521d..c5d0bbbe3a7 100644 --- a/src/passes/GUFA.cpp +++ b/src/passes/GUFA.cpp @@ -371,8 +371,8 @@ struct GUFAOptimizer bool optimized = false; void visitExpression(Expression* curr) { - if (!curr->type.isRef()) { - // Ignore anything we cannot infer a type for. + // Ignore anything we cannot emit a cast for. + if (!curr->type.isCastable()) { return; } diff --git a/src/tools/fuzzing.h b/src/tools/fuzzing.h index d5078511de9..664af3e2e1c 100644 --- a/src/tools/fuzzing.h +++ b/src/tools/fuzzing.h @@ -542,6 +542,7 @@ class TranslateToFuzzReader { // Getters for Types Type getSingleConcreteType(); Type getReferenceType(); + Type getCastableReferenceType(); Type getEqReferenceType(); Type getMVPType(); Type getTupleType(); diff --git a/src/tools/fuzzing/fuzzing.cpp b/src/tools/fuzzing/fuzzing.cpp index 109fb2d80d0..93939b8c667 100644 --- a/src/tools/fuzzing/fuzzing.cpp +++ b/src/tools/fuzzing/fuzzing.cpp @@ -2414,10 +2414,12 @@ Expression* TranslateToFuzzReader::_makeConcrete(Type type) { options.add(FeatureSet::ReferenceTypes | FeatureSet::GC, &Self::makeCompoundRef); } - // Exact casts are only allowed with custom descriptors enabled. - if (type.isInexact() || wasm.features.hasCustomDescriptors()) { - options.add(FeatureSet::ReferenceTypes | FeatureSet::GC, - &Self::makeRefCast); + if (type.isCastable()) { + // Exact casts are only allowed with custom descriptors enabled. + if (type.isInexact() || wasm.features.hasCustomDescriptors()) { + options.add(FeatureSet::ReferenceTypes | FeatureSet::GC, + &Self::makeRefCast); + } } if (heapType.getDescribedType()) { options.add(FeatureSet::ReferenceTypes | FeatureSet::GC, @@ -5044,8 +5046,8 @@ Expression* TranslateToFuzzReader::makeRefTest(Type type) { switch (upTo(3)) { case 0: // Totally random. - refType = getReferenceType(); - castType = getReferenceType(); + refType = getCastableReferenceType(); + castType = getCastableReferenceType(); // They must share a bottom type in order to validate. if (refType.getHeapType().getBottom() == castType.getHeapType().getBottom()) { @@ -5056,12 +5058,12 @@ Expression* TranslateToFuzzReader::makeRefTest(Type type) { [[fallthrough]]; case 1: // Cast is a subtype of ref. - refType = getReferenceType(); + refType = getCastableReferenceType(); castType = getSubType(refType); break; case 2: // Ref is a subtype of cast. - castType = getReferenceType(); + castType = getCastableReferenceType(); refType = getSubType(castType); break; default: @@ -5085,7 +5087,7 @@ Expression* TranslateToFuzzReader::makeRefCast(Type type) { switch (upTo(3)) { case 0: // Totally random. - refType = getReferenceType(); + refType = getCastableReferenceType(); // They must share a bottom type in order to validate. if (refType.getHeapType().getBottom() == type.getHeapType().getBottom()) { break; @@ -5190,7 +5192,11 @@ Expression* TranslateToFuzzReader::makeBrOn(Type type) { // We are sending a reference type to the target. All other BrOn variants can // do that. assert(targetType.isRef()); - auto op = pick(BrOnNonNull, BrOnCast, BrOnCastFail); + // BrOnNonNull can handle sending any reference. The casts are more limited. + auto op = BrOnNonNull; + if (targetType.isCastable()) { + op = pick(BrOnNonNull, BrOnCast, BrOnCastFail); + } Type castType = Type::none; Type refType; switch (op) { @@ -5635,6 +5641,26 @@ Type TranslateToFuzzReader::getReferenceType() { Type(HeapType::string, NonNullable))); } +Type TranslateToFuzzReader::getCastableReferenceType() { + int tries = fuzzParams->TRIES; + while (tries-- > 0) { + auto type = getReferenceType(); + if (type.isCastable()) { + return type; + } + } + // We failed to find a type using fair sampling. Do something simple that must + // work. + Type type; + if (oneIn(4)) { + type = getSubType(Type(HeapType::func, Nullable)); + } else { + type = getSubType(Type(HeapType::any, Nullable)); + } + assert(type.isCastable()); + return type; +} + Type TranslateToFuzzReader::getEqReferenceType() { if (oneIn(2) && !interestingHeapTypes.empty()) { // Try to find an interesting eq-compatible type. diff --git a/src/wasm-stack.h b/src/wasm-stack.h index f97c9c7acc7..41118d1391e 100644 --- a/src/wasm-stack.h +++ b/src/wasm-stack.h @@ -167,16 +167,11 @@ class BinaryInstWriter : public OverriddenVisitor { // when they have a value that is more refined than the wasm type system // allows atm (and they are not dropped, in which case the type would not // matter). See https://github.com/WebAssembly/binaryen/pull/6390 for more on - // the difference. As a result of the difference, we will insert extra casts - // to ensure validation in the wasm spec. The wasm spec will hopefully improve - // to use the more refined type as well, which would remove the need for this - // hack. - // - // Each br_if present as a key here is mapped to the unrefined type for it. - // That is, the br_if has a type in Binaryen IR that is too refined, and the - // map contains the unrefined one (which we need to know the local types, as - // we'll stash the unrefined values and then cast them). - std::unordered_map brIfsNeedingHandling; + // the difference. As a result of the difference, we must fix things up for + // the spec. (The wasm spec might - hopefully - improve to use the more + // refined type as well, which would remove the need for this hack, and + // improve code size in general.) + std::unordered_set brIfsNeedingHandling; }; // Takes binaryen IR and converts it to something else (binary or stack IR) diff --git a/src/wasm-type.h b/src/wasm-type.h index d90385b1c67..d6dc254734a 100644 --- a/src/wasm-type.h +++ b/src/wasm-type.h @@ -184,6 +184,8 @@ class HeapType { return isBasic() && getBasic(Unshared) == type; } + bool isCastable(); + Signature getSignature() const; Continuation getContinuation() const; @@ -415,6 +417,7 @@ class Type { return isRef() && getHeapType().isContinuation(); } bool isDefaultable() const; + bool isCastable(); // TODO: Allow this only for reference types. Nullability getNullability() const { diff --git a/src/wasm/wasm-stack.cpp b/src/wasm/wasm-stack.cpp index 529bb1db19c..33c3d7e3973 100644 --- a/src/wasm/wasm-stack.cpp +++ b/src/wasm/wasm-stack.cpp @@ -63,56 +63,88 @@ void BinaryInstWriter::visitLoop(Loop* curr) { } void BinaryInstWriter::visitBreak(Break* curr) { + auto type = curr->type; + + // See comment on |brIfsNeedingHandling| for the extra handling we need to + // emit here for certain br_ifs. If we need that handling, we either use a + // cast in simple cases, or scratch locals otherwise. We use the scratch + // locals to stash the stack before the br_if (which contains the refined + // types), then restore it later from those locals. + bool needScratchLocals = false; + // If we need locals, we must track how many we've used from each type as we + // go, as a type might appear multiple times in the tuple. We know we have + // enough of a range allocated for them, so we just increment as we go. + std::unordered_map scratchTypeUses; + // Logic to stash and restore the stack, given a vector of types we are + // stashing/restoring. We will first stash the entire stack, including the i32 + // condition, and after the br_if, restore the value (without the condition). + auto stashStack = [&](const std::vector& types) { + for (Index i = 0; i < types.size(); i++) { + auto t = types[types.size() - i - 1]; + assert(scratchLocals.find(t) != scratchLocals.end()); + auto localIndex = scratchLocals[t] + scratchTypeUses[t]++; + o << int8_t(BinaryConsts::LocalSet) << U32LEB(localIndex); + } + }; + auto restoreStack = [&](const std::vector& types) { + // Use a copy of this data, as we will restore twice. + auto currScratchTypeUses = scratchTypeUses; + for (Index i = 0; i < types.size(); i++) { + auto t = types[i]; + auto localIndex = scratchLocals[t] + --currScratchTypeUses[t]; + o << int8_t(BinaryConsts::LocalGet) << U32LEB(localIndex); + } + }; + + // The types on the stack before the br_if. We need this if we use locals to + // stash the stack. + std::vector typesOnStack; + + auto needHandling = brIfsNeedingHandling.count(curr); + if (needHandling) { + // Tuples always need scratch locals. Uncastable types do as well, we we + // can't fix them up below with a simple cast. + needScratchLocals = type.isTuple() || !type.isCastable(); + if (needScratchLocals) { + // Stash all the values on the stack to those locals, then reload them for + // the br_if to consume. Later, we can reload the refined values after the + // br_if, for its parent to consume. + + typesOnStack = std::vector(type.begin(), type.end()); + typesOnStack.push_back(Type::i32); + + stashStack(typesOnStack); + restoreStack(typesOnStack); + // The stack is now in the same state as before, but we have copies in + // locals for later. + } + } + o << int8_t(curr->condition ? BinaryConsts::BrIf : BinaryConsts::Br) << U32LEB(getBreakIndex(curr->name)); - // See comment on |brIfsNeedingHandling| for the extra casts we need to emit - // here for certain br_ifs. - auto iter = brIfsNeedingHandling.find(curr); - if (iter != brIfsNeedingHandling.end()) { - auto unrefinedType = iter->second; - auto type = curr->type; - assert(type.size() == unrefinedType.size()); + if (needHandling) { + if (!needScratchLocals) { + // We can just cast here, avoiding scratch locals. (Casting adds overhead, + // but this is very rare, and it avoids adding locals, which would keep + // growing the wasm with each roundtrip.) - assert(curr->type.hasRef()); - - auto emitCast = [&](Type to) { // Shim a tiny bit of IR, just enough to get visitRefCast to see what we // are casting, and to emit the proper thing. RefCast cast; - cast.type = to; + cast.type = type; cast.ref = cast.desc = nullptr; visitRefCast(&cast); - }; - - if (!type.isTuple()) { - // Simple: Just emit a cast, and then the type matches Binaryen IR's. - emitCast(type); } else { - // Tuples are trickier to handle, and we need to use scratch locals. Stash - // all the values on the stack to those locals, then reload them, casting - // as we go. - // - // We must track how many scratch locals we've used from each type as we - // go, as a type might appear multiple times in the tuple. We allocated - // enough for each, in a contiguous range, so we just increment as we go. - std::unordered_map scratchTypeUses; - for (Index i = 0; i < unrefinedType.size(); i++) { - auto t = unrefinedType[unrefinedType.size() - i - 1]; - assert(scratchLocals.find(t) != scratchLocals.end()); - auto localIndex = scratchLocals[t] + scratchTypeUses[t]++; - o << int8_t(BinaryConsts::LocalSet) << U32LEB(localIndex); - } - for (Index i = 0; i < unrefinedType.size(); i++) { - auto t = unrefinedType[i]; - auto localIndex = scratchLocals[t] + --scratchTypeUses[t]; - o << int8_t(BinaryConsts::LocalGet) << U32LEB(localIndex); - if (t.isRef()) { - // Note that we cast all types here, when perhaps only some of the - // tuple's lanes need that. This is simpler. - emitCast(type[i]); - } + // We need locals. Earlier we stashed the stack, so we just need to + // restore the value from there (note we don't restore the condition), + // after dropping the br_if's unrefined values. + for (Index i = 0; i < type.size(); ++i) { + o << int8_t(BinaryConsts::Drop); } + assert(typesOnStack.back() == Type::i32); + typesOnStack.pop_back(); + restoreStack(typesOnStack); } } } @@ -3094,8 +3126,9 @@ InsertOrderedMap BinaryInstWriter::countScratchLocals() { : writer(writer), finder(finder) {} void visitBreak(Break* curr) { + auto type = curr->type; // See if this is one of the dangerous br_ifs we must handle. - if (!curr->type.hasRef()) { + if (!type.hasRef()) { // Not even a reference. return; } @@ -3106,7 +3139,7 @@ InsertOrderedMap BinaryInstWriter::countScratchLocals() { return; } if (auto* cast = parent->dynCast()) { - if (Type::isSubType(cast->type, curr->type)) { + if (Type::isSubType(cast->type, type)) { // It is cast to the same type or a better one. In particular this // handles the case of repeated roundtripping: After the first // roundtrip we emit a cast that we'll identify here, and not emit @@ -3117,23 +3150,30 @@ InsertOrderedMap BinaryInstWriter::countScratchLocals() { } auto* breakTarget = findBreakTarget(curr->name); auto unrefinedType = breakTarget->type; - if (unrefinedType == curr->type) { + if (unrefinedType == type) { // It has the proper type anyhow. return; } // Mark the br_if as needing handling, and add the type to the set of // types we need scratch tuple locals for (if relevant). - writer.brIfsNeedingHandling[curr] = unrefinedType; - - if (unrefinedType.isTuple()) { - // We must allocate enough scratch locals for this tuple. Note that we - // may need more than one per type in the tuple, if a type appears more - // than once, so we count their appearances. + writer.brIfsNeedingHandling.insert(curr); + + // Simple cases can be handled by a cast. However, tuples and uncastable + // types require us to use locals too. + if (type.isTuple() || !type.isCastable()) { + // We must allocate enough scratch locals for this tuple, plus the i32 + // of the condition, as we will stash it all so that we can restore the + // fully refined value after the br_if. + // + // Note that we may need more than one per type in the tuple, if a type + // appears more than once, so we count their appearances. InsertOrderedMap scratchTypeUses; - for (auto t : unrefinedType) { + for (auto t : type) { scratchTypeUses[t]++; } + // The condition. + scratchTypeUses[Type::i32]++; for (auto& [type, uses] : scratchTypeUses) { auto& count = finder.scratches[type]; count = std::max(count, uses); diff --git a/src/wasm/wasm-type.cpp b/src/wasm/wasm-type.cpp index 795bf410cbf..097f3b2284f 100644 --- a/src/wasm/wasm-type.cpp +++ b/src/wasm/wasm-type.cpp @@ -623,6 +623,8 @@ bool Type::isDefaultable() const { return isConcrete() && !isNonNullable(); } +bool Type::isCastable() { return isRef() && getHeapType().isCastable(); } + unsigned Type::getByteSize() const { // TODO: alignment? auto getSingleByteSize = [](Type t) { @@ -889,6 +891,11 @@ Shareability HeapType::getShared() const { } } +bool HeapType::isCastable() { + return !isContinuation() && !isMaybeShared(HeapType::cont) && + !isMaybeShared(HeapType::nocont); +} + Signature HeapType::getSignature() const { assert(isSignature()); return getHeapTypeInfo(*this)->signature; diff --git a/src/wasm/wasm-validator.cpp b/src/wasm/wasm-validator.cpp index c8466b8c792..7394f1ac722 100644 --- a/src/wasm/wasm-validator.cpp +++ b/src/wasm/wasm-validator.cpp @@ -2911,6 +2911,10 @@ void FunctionValidator::visitI31Get(I31Get* curr) { void FunctionValidator::visitRefTest(RefTest* curr) { shouldBeTrue( getModule()->features.hasGC(), curr, "ref.test requires gc [--enable-gc]"); + + shouldBeTrue( + curr->castType.isCastable(), curr, "ref.test cannot cast to invalid type"); + if (curr->ref->type == Type::unreachable) { return; } @@ -2940,6 +2944,9 @@ void FunctionValidator::visitRefTest(RefTest* curr) { "ref.test of exact type requires custom descriptors " "[--enable-custom-descriptors]"); } + + shouldBeTrue( + curr->ref->type.isCastable(), curr, "ref.test cannot cast invalid type"); } void FunctionValidator::visitRefCast(RefCast* curr) { @@ -3000,6 +3007,11 @@ void FunctionValidator::visitRefCast(RefCast* curr) { "[--enable-custom-descriptors]"); } + shouldBeTrue( + curr->ref->type.isCastable(), curr, "ref.cast cannot cast invalid type"); + shouldBeTrue( + curr->type.isCastable(), curr, "ref.cast cannot cast to invalid type"); + if (!curr->desc) { return; } @@ -3123,6 +3135,10 @@ void FunctionValidator::visitBrOn(BrOn* curr) { "br_on_cast* to exact type requires custom descriptors " "[--enable-custom-descriptors]"); } + 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; } } diff --git a/test/lit/br_if_cont_uncastable.wast b/test/lit/br_if_cont_uncastable.wast new file mode 100644 index 00000000000..7b323426126 --- /dev/null +++ b/test/lit/br_if_cont_uncastable.wast @@ -0,0 +1,252 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. + +;; Test that our hack for br_if output types works with continuation types, +;; which are not castable. + +;; RUN: wasm-opt %s -all --roundtrip -S -o - | filecheck %s + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $cont (cont $none)) + (type $cont (cont $none)) + ;; CHECK: (type $none (func)) + (type $none (func)) + ) + + ;; CHECK: (func $suspend (type $none) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $suspend (type $none) + (nop) + ) + + ;; CHECK: (func $br_if_gc (type $4) (param $ref (ref any)) (result anyref) + ;; CHECK-NEXT: (block $block (result anyref) + ;; CHECK-NEXT: (call $receive_any + ;; CHECK-NEXT: (ref.cast (ref any) + ;; CHECK-NEXT: (br_if $block + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br_if_gc (param $ref (ref any)) (result anyref) + ;; After the roundtrip here we should see a cast, and no added locals. + (block $label (result anyref) + (call $receive_any ;; ensure the value flowing out is fully refined + (br_if $label + (local.get $ref) + (i32.const 0) + ) + ) + (unreachable) + ) + ) + + ;; CHECK: (func $br_if_cont (type $5) (result contref) + ;; CHECK-NEXT: (local $0 (ref (exact $cont))) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (local $scratch (ref (exact $cont))) + ;; CHECK-NEXT: (block $block (result contref) + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (block (result (ref (exact $cont))) + ;; CHECK-NEXT: (local.set $scratch + ;; CHECK-NEXT: (cont.new $cont + ;; CHECK-NEXT: (ref.func $suspend) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (br_if $block + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $receive_cont + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br_if_cont (result contref) + ;; After the roundtrip here we should see stashing of the continuation into a + ;; local, rather than a cast (as continuations cannot be cast). + (block $label (result contref) + (call $receive_cont + (br_if $label + (cont.new $cont + (ref.func $suspend) + ) + (i32.const 0) + ) + ) + (unreachable) + ) + ) + + ;; CHECK: (func $receive_any (type $6) (param $0 (ref any)) + ;; CHECK-NEXT: ) + (func $receive_any (param (ref any)) + ) + + ;; CHECK: (func $receive_cont (type $7) (param $0 (ref $cont)) + ;; CHECK-NEXT: ) + (func $receive_cont (param (ref $cont)) + ) + + ;; As above, but with tuples. + + ;; CHECK: (func $br_if_gc_tuple (type $8) (param $ref (ref any)) (result anyref i32) + ;; CHECK-NEXT: (local $1 (ref any)) + ;; CHECK-NEXT: (local $2 i32) + ;; CHECK-NEXT: (local $3 i32) + ;; CHECK-NEXT: (local $scratch i32) + ;; CHECK-NEXT: (local $scratch_5 (ref any)) + ;; CHECK-NEXT: (local $scratch_6 (tuple (ref any) i32)) + ;; CHECK-NEXT: (local $scratch_7 (ref any)) + ;; CHECK-NEXT: (block $block (type $3) (result anyref i32) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (block (result (ref any)) + ;; CHECK-NEXT: (local.set $scratch_5 + ;; CHECK-NEXT: (local.get $ref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $3 + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (local.set $scratch + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $scratch_5) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result (ref any)) + ;; CHECK-NEXT: (local.set $scratch_7 + ;; CHECK-NEXT: (tuple.extract 2 0 + ;; CHECK-NEXT: (local.tee $scratch_6 + ;; CHECK-NEXT: (br_if $block + ;; CHECK-NEXT: (tuple.make 2 + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (tuple.extract 2 1 + ;; CHECK-NEXT: (local.get $scratch_6) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $scratch_7) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (tuple.make 2 + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br_if_gc_tuple (param $ref (ref any)) (result anyref i32) + ;; We use locals here, avoiding casts - since we are using locals anyhow, we + ;; don't need any casts (we stash the refined values). + (block $label (result anyref i32) + (br_if $label + (tuple.make 2 + (local.get $ref) + (i32.const 1) + ) + (i32.const 0) + ) + ) + ) + + ;; CHECK: (func $br_if_cont_tuple (type $2) (result contref i32) + ;; CHECK-NEXT: (local $0 (ref (exact $cont))) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (local $2 i32) + ;; CHECK-NEXT: (local $scratch i32) + ;; CHECK-NEXT: (local $scratch_4 (ref (exact $cont))) + ;; CHECK-NEXT: (local $scratch_5 (tuple (ref (exact $cont)) i32)) + ;; CHECK-NEXT: (local $scratch_6 (ref (exact $cont))) + ;; CHECK-NEXT: (block $block (type $2) (result contref i32) + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (block (result (ref (exact $cont))) + ;; CHECK-NEXT: (local.set $scratch_4 + ;; CHECK-NEXT: (cont.new $cont + ;; CHECK-NEXT: (ref.func $suspend) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (local.set $scratch + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $scratch_4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result (ref (exact $cont))) + ;; CHECK-NEXT: (local.set $scratch_6 + ;; CHECK-NEXT: (tuple.extract 2 0 + ;; CHECK-NEXT: (local.tee $scratch_5 + ;; CHECK-NEXT: (br_if $block + ;; CHECK-NEXT: (tuple.make 2 + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (tuple.extract 2 1 + ;; CHECK-NEXT: (local.get $scratch_5) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $scratch_6) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (tuple.make 2 + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br_if_cont_tuple (result contref i32) + ;; As above, locals are used (and here, casts would also be invalid). + (block $label (result contref i32) + (br_if $label + (tuple.make 2 + (cont.new $cont + (ref.func $suspend) + ) + (i32.const 1) + ) + (i32.const 0) + ) + ) + ) +) diff --git a/test/lit/cast-and-recast-tuple.wast b/test/lit/cast-and-recast-tuple.wast index 6ceefe7d605..5bc4b19ccfe 100644 --- a/test/lit/cast-and-recast-tuple.wast +++ b/test/lit/cast-and-recast-tuple.wast @@ -167,47 +167,67 @@ ;; CHECK: (func $test-local-tuple-4-bad (type $3) (param $B (ref $B)) (param $x i32) (result anyref i32) ;; CHECK-NEXT: (local $temp (ref $B)) - ;; CHECK-NEXT: (local $3 (ref $A)) + ;; CHECK-NEXT: (local $3 (ref $B)) ;; CHECK-NEXT: (local $4 i32) ;; CHECK-NEXT: (local $5 i32) - ;; CHECK-NEXT: (local $scratch (tuple (ref $B) i32)) - ;; CHECK-NEXT: (local $scratch_7 (ref $B)) + ;; CHECK-NEXT: (local $6 i32) + ;; CHECK-NEXT: (local $scratch i32) ;; CHECK-NEXT: (local $scratch_8 (ref $B)) + ;; CHECK-NEXT: (local $scratch_9 (tuple (ref $B) i32)) + ;; CHECK-NEXT: (local $scratch_10 (ref $B)) + ;; CHECK-NEXT: (local $scratch_11 (ref $B)) ;; CHECK-NEXT: (block $block (type $2) (result (ref $A) i32) ;; CHECK-NEXT: (local.set $3 ;; CHECK-NEXT: (block (result (ref $B)) - ;; CHECK-NEXT: (local.set $scratch_7 + ;; CHECK-NEXT: (local.set $scratch_8 + ;; CHECK-NEXT: (local.get $B) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $6 + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (local.set $scratch + ;; CHECK-NEXT: (i32.const 3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $5 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $scratch_8) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result (ref $B)) + ;; CHECK-NEXT: (local.set $scratch_10 ;; CHECK-NEXT: (tuple.extract 2 0 - ;; CHECK-NEXT: (local.tee $scratch + ;; CHECK-NEXT: (local.tee $scratch_9 ;; CHECK-NEXT: (br_if $block ;; CHECK-NEXT: (tuple.make 2 - ;; CHECK-NEXT: (local.get $B) - ;; CHECK-NEXT: (i32.const 3) + ;; CHECK-NEXT: (local.get $3) + ;; CHECK-NEXT: (local.get $6) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (local.get $5) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $5 + ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (tuple.extract 2 1 - ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: (local.get $scratch_9) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $scratch_7) + ;; CHECK-NEXT: (local.get $scratch_10) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (local.set $temp ;; CHECK-NEXT: (block (result (ref $B)) - ;; CHECK-NEXT: (local.set $scratch_8 - ;; CHECK-NEXT: (ref.cast (ref $B) - ;; CHECK-NEXT: (local.get $3) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch_11 + ;; CHECK-NEXT: (local.get $3) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (local.set $4 - ;; CHECK-NEXT: (local.get $5) + ;; CHECK-NEXT: (local.get $6) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $scratch_8) + ;; CHECK-NEXT: (local.get $scratch_11) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (unreachable) @@ -218,10 +238,10 @@ ;; As above, but none of the mitigating circumstances happens: we have a ;; tuple with a reference that is refined compared to the break target. As a ;; result we must fix this up, which we do by adding locals, saving the - ;; br_if's output to them, and then loading from those locals and casting. + ;; br_if's refined input to them, reloading those values for the br_if, then + ;; reloading them again afterwards. ;; - ;; Comparing to $test-local-tuple-4, we end up with 3 more locals, and also - ;; there is now a ref.cast. + ;; Comparing to $test-local-tuple-4, we end up with 6 more locals. (block $out (result (ref $A) i32) (local.set $temp (br_if $out @@ -239,85 +259,114 @@ ;; CHECK: (func $test-local-tuple-4-bad-dupes (type $8) (param $B (ref $B)) (param $x i32) (result i32 anyref i32) ;; CHECK-NEXT: (local $temp (ref $B)) ;; CHECK-NEXT: (local $3 (ref $B)) - ;; CHECK-NEXT: (local $4 (ref $A)) + ;; CHECK-NEXT: (local $4 (ref $B)) ;; CHECK-NEXT: (local $5 i32) ;; CHECK-NEXT: (local $scratch i32) ;; CHECK-NEXT: (local $7 i32) ;; CHECK-NEXT: (local $8 i32) ;; CHECK-NEXT: (local $9 i32) - ;; CHECK-NEXT: (local $scratch_10 (tuple i32 (ref $B) i32)) - ;; CHECK-NEXT: (local $scratch_11 (ref $B)) - ;; CHECK-NEXT: (local $scratch_12 i32) - ;; CHECK-NEXT: (local $scratch_13 (ref $B)) - ;; CHECK-NEXT: (local $scratch_14 i32) + ;; CHECK-NEXT: (local $10 i32) + ;; CHECK-NEXT: (local $scratch_11 i32) + ;; CHECK-NEXT: (local $scratch_12 (ref $B)) + ;; CHECK-NEXT: (local $scratch_13 i32) + ;; CHECK-NEXT: (local $scratch_14 (tuple i32 (ref $B) i32)) ;; CHECK-NEXT: (local $scratch_15 (ref $B)) + ;; CHECK-NEXT: (local $scratch_16 i32) + ;; CHECK-NEXT: (local $scratch_17 (ref $B)) + ;; CHECK-NEXT: (local $scratch_18 i32) + ;; CHECK-NEXT: (local $scratch_19 (ref $B)) ;; CHECK-NEXT: (block $block (type $6) (result i32 (ref $A) i32) - ;; CHECK-NEXT: (local.set $9 + ;; CHECK-NEXT: (local.set $10 ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (local.set $scratch_12 + ;; CHECK-NEXT: (local.set $scratch_13 + ;; CHECK-NEXT: (i32.const -3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $4 + ;; CHECK-NEXT: (block (result (ref $B)) + ;; CHECK-NEXT: (local.set $scratch_12 + ;; CHECK-NEXT: (local.get $B) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $9 + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (local.set $scratch_11 + ;; CHECK-NEXT: (i32.const 3) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $8 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $scratch_11) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $scratch_12) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $scratch_13) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (local.set $scratch_16 ;; CHECK-NEXT: (tuple.extract 3 0 - ;; CHECK-NEXT: (local.tee $scratch_10 + ;; CHECK-NEXT: (local.tee $scratch_14 ;; CHECK-NEXT: (br_if $block ;; CHECK-NEXT: (tuple.make 3 - ;; CHECK-NEXT: (i32.const -3) - ;; CHECK-NEXT: (local.get $B) - ;; CHECK-NEXT: (i32.const 3) + ;; CHECK-NEXT: (local.get $10) + ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: (local.get $9) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (local.get $8) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $4 + ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block (result (ref $B)) - ;; CHECK-NEXT: (local.set $scratch_11 + ;; CHECK-NEXT: (local.set $scratch_15 ;; CHECK-NEXT: (tuple.extract 3 1 - ;; CHECK-NEXT: (local.get $scratch_10) + ;; CHECK-NEXT: (local.get $scratch_14) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $8 + ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (tuple.extract 3 2 - ;; CHECK-NEXT: (local.get $scratch_10) + ;; CHECK-NEXT: (local.get $scratch_14) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $scratch_11) + ;; CHECK-NEXT: (local.get $scratch_15) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $scratch_12) + ;; CHECK-NEXT: (local.get $scratch_16) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (local.tee $scratch ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (local.set $scratch_14 - ;; CHECK-NEXT: (local.get $9) + ;; CHECK-NEXT: (local.set $scratch_18 + ;; CHECK-NEXT: (local.get $10) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (local.set $3 ;; CHECK-NEXT: (block (result (ref $B)) - ;; CHECK-NEXT: (local.set $scratch_13 - ;; CHECK-NEXT: (ref.cast (ref $B) - ;; CHECK-NEXT: (local.get $4) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $scratch_17 + ;; CHECK-NEXT: (local.get $4) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (local.set $7 - ;; CHECK-NEXT: (local.get $8) + ;; CHECK-NEXT: (local.get $9) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $scratch_13) + ;; CHECK-NEXT: (local.get $scratch_17) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $scratch_14) + ;; CHECK-NEXT: (local.get $scratch_18) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (local.set $temp ;; CHECK-NEXT: (block (result (ref $B)) - ;; CHECK-NEXT: (local.set $scratch_15 + ;; CHECK-NEXT: (local.set $scratch_19 ;; CHECK-NEXT: (local.get $3) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (local.set $5 ;; CHECK-NEXT: (local.get $7) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $scratch_15) + ;; CHECK-NEXT: (local.get $scratch_19) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (unreachable) @@ -330,28 +379,36 @@ ;; that the tuple.extracts use different locals for the first and last i32. ;; For easier reading, here is the wami output of the binary: ;; - ;; (func $func4 (param $var0 (ref $type1)) (param $var1 i32) (result i32) (result anyref) (result i32) + ;; (func $func0 (param $var0 (ref $type1)) (param $var1 i32) (result i32) (result anyref) (result i32) ;; (local $var2 (ref $type1)) ;; (local $var3 (ref $type1)) - ;; (local $var4 (ref $type0)) + ;; (local $var4 (ref $type1)) ;; (local $var5 i32) ;; (local $var6 i32) ;; (local $var7 i32) ;; (local $var8 i32) ;; (local $var9 i32) + ;; (local $var10 i32) ;; block $label0 (result i32) (result (ref $type0)) (result i32) ;; i32.const -3 ;; local.get $var0 ;; i32.const 3 ;; local.get $var1 + ;; local.set $var8 ;; saves condition + ;; local.set $var9 ;; saves 3 + ;; local.set $var4 ;; saves ref + ;; local.set $var10 ;; saves -3 + ;; local.get $var10 ;; gets -3 + ;; local.get $var4 ;; gets ref + ;; local.get $var9 ;; gets 3 + ;; local.get $var8 ;; gets condition ;; br_if $label0 - ;; local.set $var8 ;; saves 3 - ;; local.set $var4 ;; saves the ref - ;; local.set $var9 ;; saves -3 - ;; local.get $var9 ;; gets -3 - ;; local.get $var4 ;; gets the ref - ;; ref.cast $type1 ;; casts the ref - ;; local.get $var8 ;; gets 3 + ;; drop + ;; drop + ;; drop + ;; local.get $var10 ;; gets -3 + ;; local.get $var4 ;; gets ref + ;; local.get $var9 ;; gets 3 ;; local.set $var7 ;; local.set $var3 ;; local.tee $var6 diff --git a/test/lit/passes/gufa-cast-all.wast b/test/lit/passes/gufa-cast-all.wast index d8f01441d2b..812ba58f7f2 100644 --- a/test/lit/passes/gufa-cast-all.wast +++ b/test/lit/passes/gufa-cast-all.wast @@ -354,3 +354,41 @@ ) ) +;; Do not refine uncastable types. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $cont (cont $none)) + (type $cont (cont $none)) + ;; CHECK: (type $none (func)) + (type $none (func)) + ) + + ;; CHECK: (type $2 (func (result contref))) + + ;; CHECK: (elem declare func $suspend) + + ;; CHECK: (func $suspend (type $none) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $suspend (type $none) + (nop) + ) + + ;; CHECK: (func $unrefine (type $2) (result contref) + ;; CHECK-NEXT: (block $label (result contref) + ;; CHECK-NEXT: (cont.new $cont + ;; CHECK-NEXT: (ref.func $suspend) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $unrefine (result contref) + ;; No cast should be added here. + (block $label (result contref) + (cont.new $cont + (ref.func $suspend) + ) + ) + ) +) + diff --git a/test/spec/cont-validation.wast b/test/spec/cont-validation.wast new file mode 100644 index 00000000000..47d5df15eac --- /dev/null +++ b/test/spec/cont-validation.wast @@ -0,0 +1,25 @@ +;; Part of stack-switching/validation.wast + +;; Illegal casts + +(assert_invalid + (module + (func (drop (ref.test contref (unreachable)))) + ) + "invalid cast" +) +(assert_invalid + (module + (func (drop (ref.test nullcontref (unreachable)))) + ) + "invalid cast" +) +(assert_invalid + (module + (type $f (func)) + (type $c (cont $f)) + (func (drop (ref.test (ref $c) (unreachable)))) + ) + "invalid cast" +) +