diff --git a/src/ir/subtype-exprs.h b/src/ir/subtype-exprs.h index cf39a7253d1..bc99156e0fb 100644 --- a/src/ir/subtype-exprs.h +++ b/src/ir/subtype-exprs.h @@ -311,8 +311,16 @@ struct SubtypingDiscoverer : public OverriddenVisitor { void visitRefCast(RefCast* curr) { self()->noteCast(curr->ref, curr); } void visitRefGetDesc(RefGetDesc* curr) {} void visitBrOn(BrOn* curr) { - if (curr->op == BrOnCast || curr->op == BrOnCastFail) { - self()->noteCast(curr->ref, curr->castType); + switch (curr->op) { + case BrOnNull: + case BrOnNonNull: + break; + case BrOnCast: + case BrOnCastFail: + case BrOnCastDesc: + case BrOnCastDescFail: + self()->noteCast(curr->ref, curr->castType); + break; } self()->noteSubtype(curr->getSentType(), self()->findBreakTarget(curr->name)); diff --git a/src/passes/Unsubtyping.cpp b/src/passes/Unsubtyping.cpp index 57a9e41d607..2e45cdba316 100644 --- a/src/passes/Unsubtyping.cpp +++ b/src/passes/Unsubtyping.cpp @@ -17,13 +17,18 @@ #define UNSUBTYPING_DEBUG 0 #include +#include +#include #if !UNSUBTYPING_DEBUG #include #include #endif +#include "ir/effects.h" +#include "ir/localize.h" #include "ir/module-utils.h" +#include "ir/names.h" #include "ir/subtype-exprs.h" #include "ir/type-updating.h" #include "ir/utils.h" @@ -43,18 +48,19 @@ #define DBG(x) #endif -// Compute and use the minimal subtype relation required to maintain module -// validity and behavior. This minimal relation will be a subset of the original -// subtype relation. Start by walking the IR and collecting pairs of types that -// need to be in the subtype relation for each expression to validate. For +// Compute and use the minimal subtype (and descriptor) relations required to +// maintain module validity and behavior. This minimal relation will be a subset +// of the original subtype (and descriptor) relations. Start by walking the IR +// and collecting pairs of types that need to be in the subtype relation for +// each expression to validate (or require a type to have a descriptor). For // example, a local.set requires that the type of its operand be a subtype of // the local's type. Casts do not generate subtypings at this point because it // is not necessary for the cast target to be a subtype of the cast source for // the cast to validate. // -// From that initial subtype relation, we then start finding new subtypings that -// are required by the subtypings we have found already. These transitively -// required subtypings come from two sources. +// From that initial subtype relation, we then start finding new subtypings (and +// descriptors) that are required by the subtypings we have found already. These +// transitively required subtypings (and descriptors) come from three sources. // // The first source is type definitions. Consider these type definitions: // @@ -111,11 +117,28 @@ // types of values that can flow into casts as we learn about new subtypes of // cast sources. // -// Starting with the initial subtype relation determined by walking the IR, -// repeatedly search for new subtypings by analyzing type definitions and casts -// until we reach a fixed point. This is the minimal subtype relation that -// preserves module validity and behavior that can be found without a more -// precise analysis of types that might flow into each cast. +// The third source of transitive subtyping requirements is the discovery of +// required descriptors (and vice versa). Subtyping and descriptors combine to +// form this diagram, where rightward arrows mean "described by": +// +// A -> A.desc +// ^ ^ +// | | +// B -> B.desc +// +// If any three of these types exist in these relations with the others, then +// the validation rules require that the fourth type also exist and be in these +// relations. The only exception is that A.desc is allowed to be missing. This +// complex and recursive relationship between subtyping and descriptor relations +// is why we optimize out unneeded descriptors in this pass rather than e.g. +// GlobalTypeOptimization. +// +// Starting with the initial subtype and descriptor relations determined by +// walking the IR, repeatedly search for new subtypings and descriptors by +// analyzing type definitions and casts until we reach a fixed point. This is +// the minimal subtype/descriptor relation that preserves module validity and +// behavior that can be found without a more precise analysis of types that +// might flow into each cast. namespace wasm { @@ -144,6 +167,9 @@ struct TypeTree { Index indexInParent = 0; // The indices of the children (subtypes) in the list of nodes. std::vector children; + // The index of the described and descriptor types, if they are necessary. + std::optional described; + std::optional descriptor; Node(HeapType type, Index index) : type(type), parent(index) {} }; @@ -175,15 +201,52 @@ struct TypeTree { parentNode.children.push_back(childIndex); } - std::optional getSupertype(HeapType type) { - auto index = getIndex(type); - auto parentIndex = nodes[index].parent; - if (parentIndex == index) { + std::optional getSupertype(HeapType type) const { + auto index = maybeGetIndex(type); + if (!index) { + return std::nullopt; + } + auto parentIndex = nodes[*index].parent; + if (parentIndex == *index) { return std::nullopt; } return nodes[parentIndex].type; } + void setDescriptor(HeapType described, HeapType descriptor) { + auto describedIndex = getIndex(described); + auto descriptorIndex = getIndex(descriptor); + auto& describedNode = nodes[describedIndex]; + auto& descriptorNode = nodes[descriptorIndex]; + // We only ever set the descriptor once. + assert(!describedNode.descriptor); + assert(!descriptorNode.described); + describedNode.descriptor = descriptorIndex; + descriptorNode.described = describedIndex; + } + + std::optional getDescriptor(HeapType type) const { + auto index = maybeGetIndex(type); + if (!index) { + return std::nullopt; + } + if (auto descIndex = nodes[*index].descriptor) { + return nodes[*descIndex].type; + } + return std::nullopt; + } + + std::optional getDescribed(HeapType type) const { + auto index = maybeGetIndex(type); + if (!index) { + return std::nullopt; + } + if (auto descIndex = nodes[*index].described) { + return nodes[*descIndex].type; + } + return std::nullopt; + } + struct SupertypeIterator { using value_type = const HeapType; using difference_type = std::ptrdiff_t; @@ -194,10 +257,10 @@ struct TypeTree { TypeTree* parent; std::optional index; - bool operator==(const SupertypeIterator& other) { + bool operator==(const SupertypeIterator& other) const { return index == other.index; } - bool operator!=(const SupertypeIterator& other) { + bool operator!=(const SupertypeIterator& other) const { return !(*this == other); } const HeapType& operator*() const { return parent->nodes[*index].type; } @@ -227,6 +290,50 @@ struct TypeTree { Supertypes supertypes(HeapType type) { return {this, getIndex(type)}; } + struct ImmediateSubtypeIterator { + using value_type = const HeapType; + using difference_type = std::ptrdiff_t; + using reference = const HeapType&; + using pointer = const HeapType*; + using iterator_category = std::input_iterator_tag; + + TypeTree* parent; + std::vector::const_iterator child; + + bool operator==(const ImmediateSubtypeIterator& other) const { + return child == other.child; + } + bool operator!=(const ImmediateSubtypeIterator& other) const { + return !(*this == other); + } + const HeapType& operator*() const { return parent->nodes[*child].type; } + const HeapType* operator->() const { return &*(*this); } + ImmediateSubtypeIterator& operator++() { + ++child; + return *this; + } + ImmediateSubtypeIterator operator++(int) { + auto it = *this; + ++(*this); + return it; + } + }; + + struct ImmediateSubtypes { + TypeTree* parent; + Index index; + ImmediateSubtypeIterator begin() { + return {parent, parent->nodes[index].children.begin()}; + } + ImmediateSubtypeIterator end() { + return {parent, parent->nodes[index].children.end()}; + } + }; + + ImmediateSubtypes immediateSubtypes(HeapType type) { + return {this, getIndex(type)}; + } + struct SubtypeIterator { using value_type = const HeapType; using difference_type = std::ptrdiff_t; @@ -286,6 +393,12 @@ struct TypeTree { if (auto super = getSupertype(node.type)) { std::cerr << " <: " << ModuleHeapType(wasm, *super); } + if (auto desc = getDescribed(node.type)) { + std::cerr << ", describes " << ModuleHeapType(wasm, *desc); + } + if (auto desc = getDescriptor(node.type)) { + std::cerr << ", descriptor " << ModuleHeapType(wasm, *desc); + } std::cerr << ", children:"; for (auto child : node.children) { std::cerr << " " << ModuleHeapType(wasm, nodes[child].type); @@ -303,11 +416,20 @@ struct TypeTree { } return it->second; } + + std::optional maybeGetIndex(HeapType type) const { + if (auto it = indices.find(type); it != indices.end()) { + return it->second; + } + return std::nullopt; + } }; struct Unsubtyping : Pass { + // The kind of work to process. + enum class Kind { Subtype, Descriptor }; // (sub, super) pairs that we have discovered but not yet processed. - std::vector> work; + std::vector> work; // Record the type tree with supertype and subtype relations in such a way // that we can add new supertype relationships in constant time. @@ -331,12 +453,23 @@ struct Unsubtyping : Pass { // Find further subtypings and iterate to a fixed point. while (!work.empty()) { - auto [sub, super] = work.back(); + auto [kind, a, b] = work.back(); work.pop_back(); - process(sub, super); + switch (kind) { + case Kind::Subtype: + processSubtype(a, b); + break; + case Kind::Descriptor: + processDescriptor(a, b); + break; + } } DBG(types.dump(*wasm)); + // If we removed a descriptor from a type, we may need to update its + // allocation sites accordingly. + fixupAllocations(*wasm); + rewriteTypes(*wasm); // Cast types may be refinable if their source and target types are no @@ -353,7 +486,7 @@ struct Unsubtyping : Pass { } DBG(std::cerr << "noting " << ModuleHeapType(*wasm, sub) << " <: " << ModuleHeapType(*wasm, super) << '\n'); - work.push_back({sub, super}); + work.push_back({Kind::Subtype, sub, super}); } void noteSubtype(Type sub, Type super) { @@ -370,12 +503,21 @@ struct Unsubtyping : Pass { noteSubtype(sub.getHeapType(), super.getHeapType()); } + void noteDescriptor(HeapType described, HeapType descriptor) { + DBG(std::cerr << "noting " << ModuleHeapType(*wasm, described) << " -> " + << ModuleHeapType(*wasm, descriptor) << '\n'); + work.push_back({Kind::Descriptor, described, descriptor}); + } + void analyzePublicTypes(Module& wasm) { // We cannot change supertypes for anything public. for (auto type : ModuleUtils::getPublicHeapTypes(wasm)) { if (auto super = type.getDeclaredSuperType()) { noteSubtype(type, *super); } + if (auto desc = type.getDescriptorType()) { + noteDescriptor(type, *desc); + } } } @@ -386,12 +528,22 @@ struct Unsubtyping : Pass { // Observed (sub, super) subtype constraints. Set> subtypings; + + // Observed (described, descriptor) requirements. + Set> descriptors; }; struct Collector : ControlFlowWalker> { + using Super = + ControlFlowWalker>; + Info& info; - Collector(Info& info) : info(info) {} + bool trapsNeverHappen; + + Collector(Info& info, bool trapsNeverHappen) + : info(info), trapsNeverHappen(trapsNeverHappen) {} + void noteSubtype(Type sub, Type super) { if (sub.isTuple()) { assert(super.isTuple() && sub.size() == super.size()); @@ -473,13 +625,69 @@ struct Unsubtyping : Pass { noteCast(src->type.getHeapType(), dst->type.getHeapType()); } } + + // Visitors for finding required descriptors. + void noteDescribed(HeapType type) { + auto desc = type.getDescriptorType(); + assert(desc); + info.descriptors.insert({type, *desc}); + } + void noteDescriptor(HeapType type) { + auto desc = type.getDescribedType(); + assert(desc); + info.descriptors.insert({*desc, type}); + } + void visitRefGetDesc(RefGetDesc* curr) { + Super::visitRefGetDesc(curr); + if (!curr->ref->type.isStruct()) { + return; + } + noteDescribed(curr->ref->type.getHeapType()); + } + void visitRefCast(RefCast* curr) { + Super::visitRefCast(curr); + if (!curr->desc || !curr->desc->type.isStruct()) { + return; + } + noteDescriptor(curr->desc->type.getHeapType()); + } + void visitBrOn(BrOn* curr) { + Super::visitBrOn(curr); + if (!curr->desc || !curr->desc->type.isStruct()) { + return; + } + noteDescriptor(curr->desc->type.getHeapType()); + } + void visitStructNew(StructNew* curr) { + Super::visitStructNew(curr); + if (curr->type == Type::unreachable || !curr->desc) { + return; + } + // Normally we do not treat struct.new as requiring a descriptor, even + // if it has one. We are happy to optimize out descriptors that are set + // in allocations and then never used. But if the descriptor is nullable + // and outside a function context and we assume it may be null and cause + // a trap, then we have no way to preserve that trap without keeping the + // descriptor around. + if (trapsNeverHappen || getFunction() || + curr->desc->type.isNonNullable()) { + return; + } + // We must preserve the potential trap. When we update the instructions + // later we will move this allocation to a new global if necessary to + // preserve the potential trap even if a parent of the current + // expression is removed. + noteDescribed(curr->type.getHeapType()); + } }; + bool trapsNeverHappen = getPassOptions().trapsNeverHappen; + // Collect subtyping constraints and casts from functions in parallel. ModuleUtils::ParallelFunctionAnalysis analysis( wasm, [&](Function* func, Info& info) { if (!func->imported()) { - Collector(info).walkFunctionInModule(func, &wasm); + Collector(info, trapsNeverHappen).walkFunctionInModule(func, &wasm); } }); @@ -488,10 +696,12 @@ struct Unsubtyping : Pass { collectedInfo.casts.insert(info.casts.begin(), info.casts.end()); collectedInfo.subtypings.insert(info.subtypings.begin(), info.subtypings.end()); + collectedInfo.descriptors.insert(info.descriptors.begin(), + info.descriptors.end()); } // Collect constraints from module-level code as well. - Collector collector(collectedInfo); + Collector collector(collectedInfo, trapsNeverHappen); collector.walkModuleCode(&wasm); collector.setModule(&wasm); for (auto& global : wasm.globals) { @@ -508,9 +718,12 @@ struct Unsubtyping : Pass { for (auto [src, dst] : collectedInfo.casts) { casts[src].push_back(dst); } + for (auto [described, descriptor] : collectedInfo.descriptors) { + noteDescriptor(described, descriptor); + } } - void process(HeapType sub, HeapType super) { + void processSubtype(HeapType sub, HeapType super) { DBG(std::cerr << "processing " << ModuleHeapType(*wasm, sub) << " <: " << ModuleHeapType(*wasm, super) << '\n'); assert(HeapType::isSubType(sub, super)); @@ -534,17 +747,54 @@ struct Unsubtyping : Pass { // super will already be in the same tree when we process them below, so // when we process casts we will know that we only need to process up to // oldSuper. - process(super, *oldSuper); + processSubtype(super, *oldSuper); } types.setSupertype(sub, super); - // We have a new supertype. Find the implied subtypings from the type - // definitions and casts. + // Complete the descriptor squares to the left and right of the new + // subtyping edge if those squares can possibly exist based on the original + // types. + if (super.getDescribedType()) { + completeDescriptorSquare( + types.getDescribed(super), super, types.getDescribed(sub), sub); + } + if (super.getDescriptorType()) { + completeDescriptorSquare( + super, types.getDescriptor(super), sub, types.getDescriptor(sub)); + } + + // Find the implied subtypings from the type definitions and casts. processDefinitions(sub, super); processCasts(sub, super, oldSuper); } + void processDescriptor(HeapType described, HeapType descriptor) { + DBG(std::cerr << "processing " << ModuleHeapType(*wasm, described) << " -> " + << ModuleHeapType(*wasm, descriptor) << '\n'); + assert(described.getDescriptorType() && + *described.getDescriptorType() == descriptor); + if (auto oldDesc = types.getDescriptor(described)) { + // We already know about this descriptor. + assert(*oldDesc == descriptor); + return; + } + + types.setDescriptor(described, descriptor); + + // Complete the descriptor squares above and below the new descriptor edge. + completeDescriptorSquare( + std::nullopt, types.getSupertype(descriptor), described, descriptor); + for (auto sub : types.immediateSubtypes(described)) { + completeDescriptorSquare( + described, descriptor, sub, types.getDescriptor(sub)); + } + for (auto subDesc : types.immediateSubtypes(descriptor)) { + completeDescriptorSquare( + described, descriptor, types.getDescribed(subDesc), subDesc); + } + } + void processDefinitions(HeapType sub, HeapType super) { if (super.isBasic()) { return; @@ -575,19 +825,6 @@ struct Unsubtyping : Pass { case HeapTypeKind::Basic: WASM_UNREACHABLE("unexpected kind"); } - if (auto desc = sub.getDescriptorType()) { - if (auto superDesc = super.getDescriptorType()) { - noteSubtype(*desc, *superDesc); - } - } - if (auto desc = sub.getDescribedType()) { - if (auto superDesc = super.getDescribedType()) { - // We will be able to assume this once we fix the validation rules. - if (HeapType::isSubType(*desc, *superDesc)) { - noteSubtype(*desc, *superDesc); - } - } - } } void @@ -614,6 +851,45 @@ struct Unsubtyping : Pass { } } + void completeDescriptorSquare(std::optional super, + std::optional superDesc, + std::optional sub, + std::optional subDesc) { + if ((super && super->isBasic()) || (superDesc && superDesc->isBasic())) { + // Basic types do not have descriptors or described types, so do not form + // descriptor squares. + return; + } + if (bool(super) + bool(superDesc) + bool(sub) + bool(subDesc) < 3) { + // We must have two adjacent edges (involving at least 3 types) for there + // to be any further requirements. + return; + } + // There may be up to one missing type. Look it up using its original + // descriptor relation with the present types and add the missing edges. + if (!super) { + super = superDesc->getDescribedType(); + } else if (!sub) { + sub = subDesc->getDescribedType(); + } else if (!subDesc) { + subDesc = sub->getDescriptorType(); + } else if (!superDesc) { + // This is the only type that is allowed to be missing. + return; + } + // Add all the edges. Don't worry about duplicating existing edges because + // checking whether they're necessary now would be about as expensive as + // discarding them later. + // TODO: We will be able to assume this once we update the descriptor + // validation rules. + if (HeapType::isSubType(*sub, *super)) { + noteSubtype(*sub, *super); + } + noteSubtype(*subDesc, *superDesc); + noteDescriptor(*super, *superDesc); + noteDescriptor(*sub, *subDesc); + } + void rewriteTypes(Module& wasm) { struct Rewriter : GlobalTypeRewriter { Unsubtyping& parent; @@ -626,9 +902,97 @@ struct Unsubtyping : Pass { } return std::nullopt; } + void modifyTypeBuilderEntry(TypeBuilder& typeBuilder, + Index i, + HeapType oldType) override { + if (!parent.types.getDescribed(oldType)) { + typeBuilder[i].describes(std::nullopt); + } + if (!parent.types.getDescriptor(oldType)) { + typeBuilder[i].descriptor(std::nullopt); + } + } }; Rewriter(*this, wasm).update(); } + + void fixupAllocations(Module& wasm) { + if (!wasm.features.hasCustomDescriptors()) { + return; + } + // TODO: Consider running the fixup only if we are actually removing any + // descriptors. This would require a better way of detecting this than + // collecing and iterating over all the types, though. + struct Rewriter : WalkerPass> { + const TypeTree& types; + + // Allocations that might trap that have been removed from module-level + // initializers. These need to be placed in new globals to preserve any + // instantiation-time traps. + std::vector removedTrappingInits; + + Rewriter(const TypeTree& types) : types(types) {} + + bool isFunctionParallel() override { return true; } + // Only introduces locals that are set immediately before they are used. + bool requiresNonNullableLocalFixups() override { return false; } + std::unique_ptr create() override { + return std::make_unique(types); + } + + void visitStructNew(StructNew* curr) { + if (curr->type == Type::unreachable) { + return; + } + if (!curr->desc) { + return; + } + if (types.getDescriptor(curr->type.getHeapType())) { + return; + } + // We need to drop the descriptor argument. In a function context, use + // ChildLocalizer. Outside a function context just drop the operand + // because there can be no side effects anyway. + if (auto* func = getFunction()) { + // Preserve a trap from a null descriptor if necessary. + if (!getPassOptions().trapsNeverHappen && + curr->desc->type.isNullable()) { + curr->desc = + Builder(*getModule()).makeRefAs(RefAsNonNull, curr->desc); + } + auto* block = + ChildLocalizer(curr, func, *getModule(), getPassOptions()) + .getChildrenReplacement(); + block->list.push_back(curr); + block->type = curr->type; + replaceCurrent(block); + } else { + // We are dropping this descriptor, but it might have a potential trap + // nested inside it. In that case we need to preserve the trap by + // moving this descriptor to a new global. + if (curr->desc->is() && + EffectAnalyzer(getPassOptions(), *getModule(), curr->desc).trap) { + removedTrappingInits.push_back(curr->desc); + } + } + curr->desc = nullptr; + } + }; + + Rewriter rewriter(types); + rewriter.run(getPassRunner(), &wasm); + rewriter.runOnModuleCode(getPassRunner(), &wasm); + + // Insert globals necessary to preserve instantiation-time trapping of + // removed allocations. + for (Index i = 0; i < rewriter.removedTrappingInits.size(); ++i) { + auto* curr = rewriter.removedTrappingInits[i]; + auto name = Names::getValidGlobalName( + wasm, std::string("unsubtyping-removed-") + std::to_string(i)); + wasm.addGlobal( + Builder::makeGlobal(name, curr->type, curr, Builder::Immutable)); + } + } }; } // anonymous namespace diff --git a/src/support/insert_ordered.h b/src/support/insert_ordered.h index b1adb6fad97..374278918e6 100644 --- a/src/support/insert_ordered.h +++ b/src/support/insert_ordered.h @@ -129,6 +129,14 @@ template struct InsertOrderedMap { return it->second; } + const_iterator find(const Key& k) const { + auto it = Map.find(k); + if (it == Map.end()) { + return end(); + } + return it->second; + } + void erase(const Key& k) { auto it = Map.find(k); if (it != Map.end()) { diff --git a/test/lit/passes/unsubtyping-desc-tnh.wast b/test/lit/passes/unsubtyping-desc-tnh.wast new file mode 100644 index 00000000000..7b5b3770991 --- /dev/null +++ b/test/lit/passes/unsubtyping-desc-tnh.wast @@ -0,0 +1,81 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. +;; RUN: foreach %s %t wasm-opt -all --closed-world --preserve-type-order \ +;; RUN: --unsubtyping --remove-unused-types -tnh -all -S -o - | filecheck %s + +;; Because we assume traps never happen, we don't need to keep the descriptor to +;; preserve the trap in the global due to a null descriptor. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (sub (struct))) + (type $A (sub (descriptor $A.desc (struct)))) + ;; CHECK: (type $A.desc (sub (struct))) + (type $A.desc (sub (describes $A (struct)))) + ) + + ;; CHECK: (global $A.desc (ref null (exact $A.desc)) (struct.new_default $A.desc)) + (global $A.desc (ref null (exact $A.desc)) (struct.new $A.desc)) + ;; CHECK: (global $A (ref null $A) (struct.new_default $A)) + (global $A (ref null $A) (struct.new $A (global.get $A.desc))) +) + +;; Because we assume traps never happen, we do not need a ref.as_non_null to +;; preserve the trap when the descriptor is null. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (sub (struct))) + (type $A (sub (descriptor $A.desc (struct)))) + ;; CHECK: (type $A.desc (sub (struct))) + (type $A.desc (sub (describes $A (struct)))) + ) + + ;; CHECK: (type $2 (func (param (ref null (exact $A.desc))))) + + ;; CHECK: (func $nullable-descs (type $2) (param $A.desc (ref null (exact $A.desc))) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result (ref (exact $A))) + ;; CHECK-NEXT: (struct.new_default $A) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $nullable-descs (param $A.desc (ref null (exact $A.desc))) + (drop + (struct.new $A + (local.get $A.desc) + ) + ) + ) +) + +;; Nested allocations do not need to be moved to new globals when traps never +;; happen. +(module + (rec + ;; CHECK: (type $struct (sub (struct))) + (type $struct (sub (descriptor $desc (struct)))) + (type $desc (sub (describes $struct (descriptor $meta (struct))))) + (type $meta (sub (describes $desc (struct)))) + ) + + ;; CHECK: (global $g (ref $struct) (struct.new_default $struct)) + (global $g (ref $struct) (struct.new $struct (struct.new $desc (ref.null none)))) +) + +;; Same, but now the nesting is under a non-descriptor field. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (sub (struct (field (ref $struct))))) + (type $A (sub (struct (field (ref $struct))))) + ;; CHECK: (type $struct (sub (struct))) + (type $struct (sub (descriptor $desc (struct)))) + (type $desc (sub (describes $struct (descriptor $meta (struct))))) + (type $meta (sub (describes $desc (struct)))) + ) + + ;; CHECK: (global $g (ref $A) (struct.new $A + ;; CHECK-NEXT: (struct.new_default $struct) + ;; CHECK-NEXT: )) + (global $g (ref $A) (struct.new $A (struct.new $struct (struct.new $desc (ref.null none))))) +) diff --git a/test/lit/passes/unsubtyping-desc.wast b/test/lit/passes/unsubtyping-desc.wast index 50a24a67d41..5a76a1a4f65 100644 --- a/test/lit/passes/unsubtyping-desc.wast +++ b/test/lit/passes/unsubtyping-desc.wast @@ -2,6 +2,114 @@ ;; RUN: foreach %s %t wasm-opt -all --closed-world --preserve-type-order \ ;; RUN: --unsubtyping --remove-unused-types -all -S -o - | filecheck %s +;; There is nothing requiring the subtype relationship or descriptors, so we +;; should optimize both. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (sub (struct))) + (type $A (sub (descriptor $A.desc (struct)))) + ;; CHECK: (type $A.desc (sub (struct))) + (type $A.desc (sub (describes $A (struct)))) + ;; CHECK: (type $B (sub (struct))) + (type $B (sub $A (descriptor $B.desc (struct)))) + ;; CHECK: (type $B.desc (sub (struct))) + (type $B.desc (sub $A.desc (describes $B (struct)))) + ) + + ;; CHECK: (global $A (ref null $A) (struct.new_default $A)) + (global $A (ref null $A) (struct.new $A (struct.new $A.desc))) + ;; CHECK: (global $A.desc (ref null $A.desc) (struct.new_default $A.desc)) + (global $A.desc (ref null $A.desc) (struct.new $A.desc)) + ;; CHECK: (global $B (ref null $B) (struct.new_default $B)) + (global $B (ref null $B) (struct.new $B (struct.new $B.desc))) + ;; CHECK: (global $B.desc (ref null $B.desc) (struct.new_default $B.desc)) + (global $B.desc (ref null $B.desc) (struct.new $B.desc)) +) + +;; Now we require the descriptor to preserve the traps in the globals. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (sub (descriptor $A.desc (struct)))) + (type $A (sub (descriptor $A.desc (struct)))) + ;; CHECK: (type $A.desc (sub (describes $A (struct)))) + (type $A.desc (sub (describes $A (struct)))) + ) + + ;; CHECK: (global $A.desc (ref null (exact $A.desc)) (struct.new_default $A.desc)) + (global $A.desc (ref null (exact $A.desc)) (struct.new $A.desc)) + ;; CHECK: (global $A (ref null $A) (struct.new_default $A + ;; CHECK-NEXT: (global.get $A.desc) + ;; CHECK-NEXT: )) + (global $A (ref null $A) (struct.new $A (global.get $A.desc))) +) + +;; But traps on null descriptors inside a function can be fixed up, so they +;; don't require keeping the descriptors. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (sub (struct))) + (type $A (sub (descriptor $A.desc (struct)))) + ;; CHECK: (type $A.desc (sub (struct))) + (type $A.desc (sub (describes $A (struct)))) + ) + + ;; CHECK: (type $2 (func (param (ref null (exact $A.desc))))) + + ;; CHECK: (func $nullable-desc (type $2) (param $A.desc (ref null (exact $A.desc))) + ;; CHECK-NEXT: (local $1 (ref (exact $A.desc))) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result (ref (exact $A))) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $A.desc) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.new_default $A) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $nullable-desc (param $A.desc (ref null (exact $A.desc))) + (drop + (struct.new $A + (local.get $A.desc) + ) + ) + ) +) + +;; No fixup is necessary if the descriptor cannot be null in the first place. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (sub (struct))) + (type $A (sub (descriptor $A.desc (struct)))) + ;; CHECK: (type $A.desc (sub (struct))) + (type $A.desc (sub (describes $A (struct)))) + ) + ;; CHECK: (type $2 (func (param (ref (exact $A.desc))))) + + ;; CHECK: (func $nonnullable-desc (type $2) (param $A.desc (ref (exact $A.desc))) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result (ref (exact $A))) + ;; CHECK-NEXT: (struct.new_default $A) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $nonnullable-desc (param $A.desc (ref (exact $A.desc))) + (drop + ;; Now the descriptor is non-null. + (struct.new $A + (local.get $A.desc) + ) + ) + ) +) + +;; Now we require the descriptors for both types explicitly in a function. We +;; should still be able to optimize the subtype relationship. (module (rec ;; CHECK: (rec @@ -15,13 +123,71 @@ (type $B.desc (sub $A.desc (describes $B (struct)))) ) - ;; There is nothing requiring the subtype relationship, so we should optimize. + ;; CHECK: (type $4 (func (param (ref $A) (ref $B)))) + ;; CHECK: (global $A (ref null $A) (ref.null none)) (global $A (ref null $A) (ref.null none)) + ;; CHECK: (global $A.desc (ref null $A.desc) (ref.null none)) + (global $A.desc (ref null $A.desc) (ref.null none)) ;; CHECK: (global $B (ref null $B) (ref.null none)) (global $B (ref null $B) (ref.null none)) + ;; CHECK: (global $B.desc (ref null $B.desc) (ref.null none)) + (global $B.desc (ref null $B.desc) (ref.null none)) + + ;; CHECK: (func $require-descs (type $4) (param $A (ref $A)) (param $B (ref $B)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.get_desc $A + ;; CHECK-NEXT: (local.get $A) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.get_desc $B + ;; CHECK-NEXT: (local.get $B) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $require-descs (param $A (ref $A)) (param $B (ref $B)) + (drop + (ref.get_desc $A + (local.get $A) + ) + ) + (drop + (ref.get_desc $B + (local.get $B) + ) + ) + ) ) +;; Now we require B <: A, but not either descriptor, so no subtyping is required +;; between the descriptors. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (sub (struct))) + (type $A (sub (descriptor $A.desc (struct)))) + ;; CHECK: (type $A.desc (sub (struct))) + (type $A.desc (sub (describes $A (struct)))) + ;; CHECK: (type $B (sub $A (struct))) + (type $B (sub $A (descriptor $B.desc (struct)))) + ;; CHECK: (type $B.desc (sub (struct))) + (type $B.desc (sub $A.desc (describes $B (struct)))) + ) + + ;; CHECK: (global $B (ref null $B) (struct.new_default $B)) + (global $B (ref null $B) (struct.new $B (struct.new $B.desc))) + ;; CHECK: (global $A (ref null $A) (global.get $B)) + (global $A (ref null $A) (global.get $B)) + ;; CHECK: (global $A.desc (ref null $A.desc) (ref.null none)) + (global $A.desc (ref null $A.desc) (ref.null none)) + ;; CHECK: (global $B.desc (ref null $B.desc) (ref.null none)) + (global $B.desc (ref null $B.desc) (ref.null none)) +) + +;; Now we require B <: A, and that A.desc remain A's descriptor. This requires +;; that B.desc remain B's descriptor and that B.desc <: A.desc, so we cannot +;; optimize. (module (rec ;; CHECK: (rec @@ -35,13 +201,151 @@ (type $B.desc (sub $A.desc (describes $B (struct)))) ) - ;; Now we require B <: A, which implies B.desc <: A.desc. - ;; CHECK: (global $B (ref null $B) (ref.null none)) - (global $B (ref null $B) (ref.null none)) + ;; CHECK: (type $4 (func (param (ref $A)))) + + ;; CHECK: (global $B (ref null $B) (struct.new_default $B + ;; CHECK-NEXT: (struct.new_default $B.desc) + ;; CHECK-NEXT: )) + (global $B (ref null $B) (struct.new $B (struct.new $B.desc))) ;; CHECK: (global $A (ref null $A) (global.get $B)) (global $A (ref null $A) (global.get $B)) + ;; CHECK: (global $A.desc (ref null $A.desc) (ref.null none)) + (global $A.desc (ref null $A.desc) (ref.null none)) + ;; CHECK: (global $B.desc (ref null $B.desc) (ref.null none)) + (global $B.desc (ref null $B.desc) (ref.null none)) + + ;; CHECK: (func $require-desc (type $4) (param $A (ref $A)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.get_desc $A + ;; CHECK-NEXT: (local.get $A) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $require-desc (param $A (ref $A)) + (drop + (ref.get_desc $A + (local.get $A) + ) + ) + ) ) +;; Now we require B <: A, and that B.desc remain B's descriptor (this was A and +;; A.desc before). This imposes no further requirements, so we can optimize away +;; A's descriptor and B.desc's supertype. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (sub (struct))) + (type $A (sub (descriptor $A.desc (struct)))) + ;; CHECK: (type $A.desc (sub (struct))) + (type $A.desc (sub (describes $A (struct)))) + ;; CHECK: (type $B (sub $A (descriptor $B.desc (struct)))) + (type $B (sub $A (descriptor $B.desc (struct)))) + ;; CHECK: (type $B.desc (sub (describes $B (struct)))) + (type $B.desc (sub $A.desc (describes $B (struct)))) + ) + + ;; CHECK: (type $4 (func (param (ref $B)))) + + ;; CHECK: (global $B (ref null $B) (struct.new_default $B + ;; CHECK-NEXT: (struct.new_default $B.desc) + ;; CHECK-NEXT: )) + (global $B (ref null $B) (struct.new $B (struct.new $B.desc))) + ;; CHECK: (global $A (ref null $A) (global.get $B)) + (global $A (ref null $A) (global.get $B)) + ;; CHECK: (global $A.desc (ref null $A.desc) (ref.null none)) + (global $A.desc (ref null $A.desc) (ref.null none)) + ;; CHECK: (global $B.desc (ref null $B.desc) (ref.null none)) + (global $B.desc (ref null $B.desc) (ref.null none)) + + ;; CHECK: (func $require-desc (type $4) (param $B (ref $B)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.get_desc $B + ;; CHECK-NEXT: (local.get $B) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $require-desc (param $B (ref $B)) + (drop + ;; This changed. + (ref.get_desc $B + (local.get $B) + ) + ) + ) +) + +;; Now we require B.desc <: A.desc, but we don't require them to remain +;; descriptors, so we can still optimize out B's superytpe. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (sub (struct))) + (type $A (sub (descriptor $A.desc (struct)))) + ;; CHECK: (type $A.desc (sub (struct))) + (type $A.desc (sub (describes $A (struct)))) + ;; CHECK: (type $B (sub (struct))) + (type $B (sub $A (descriptor $B.desc (struct)))) + ;; CHECK: (type $B.desc (sub $A.desc (struct))) + (type $B.desc (sub $A.desc (describes $B (struct)))) + ) + + ;; CHECK: (global $B.desc (ref null $B.desc) (ref.null none)) + (global $B.desc (ref null $B.desc) (ref.null none)) + ;; CHECK: (global $A.desc (ref null $A.desc) (global.get $B.desc)) + (global $A.desc (ref null $A.desc) (global.get $B.desc)) + ;; CHECK: (global $A (ref null $A) (ref.null none)) + (global $A (ref null $A) (ref.null none)) + ;; CHECK: (global $B (ref null $B) (ref.null none)) + (global $B (ref null $B) (ref.null none)) +) + +;; Now we still require B.desc <: A.desc, but now we require A.desc to remain a +;; descriptor. This requires A <: B and for B.desc to remain a descriptor as +;; well, so we cannot optimize +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (sub (descriptor $A.desc (struct)))) + (type $A (sub (descriptor $A.desc (struct)))) + ;; CHECK: (type $A.desc (sub (describes $A (struct)))) + (type $A.desc (sub (describes $A (struct)))) + ;; CHECK: (type $B (sub $A (descriptor $B.desc (struct)))) + (type $B (sub $A (descriptor $B.desc (struct)))) + ;; CHECK: (type $B.desc (sub $A.desc (describes $B (struct)))) + (type $B.desc (sub $A.desc (describes $B (struct)))) + ) + ;; CHECK: (type $4 (func (param (ref $A)))) + + ;; CHECK: (global $B.desc (ref null $B.desc) (ref.null none)) + (global $B.desc (ref null $B.desc) (ref.null none)) + ;; CHECK: (global $A.desc (ref null $A.desc) (global.get $B.desc)) + (global $A.desc (ref null $A.desc) (global.get $B.desc)) + ;; CHECK: (global $A (ref null $A) (ref.null none)) + (global $A (ref null $A) (ref.null none)) + ;; CHECK: (global $B (ref null $B) (ref.null none)) + (global $B (ref null $B) (ref.null none)) + + ;; CHECK: (func $require-desc (type $4) (param $A (ref $A)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.get_desc $A + ;; CHECK-NEXT: (local.get $A) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $require-desc (param $A (ref $A)) + (drop + (ref.get_desc $A + (local.get $A) + ) + ) + ) +) + +;; Now we still require B.desc <: A.desc, but now it is B.desc we require to +;; remain a descriptor. This still requires A <: B and for A.desc to remain a +;; descriptor as well, so we cannot optimize. (module (rec ;; CHECK: (rec @@ -54,14 +358,304 @@ ;; CHECK: (type $B.desc (sub $A.desc (describes $B (struct)))) (type $B.desc (sub $A.desc (describes $B (struct)))) ) + ;; CHECK: (type $4 (func (param (ref $B)))) - ;; Now we require B.desc <: A.desc, which similarly implies B <: A. ;; CHECK: (global $B.desc (ref null $B.desc) (ref.null none)) (global $B.desc (ref null $B.desc) (ref.null none)) ;; CHECK: (global $A.desc (ref null $A.desc) (global.get $B.desc)) (global $A.desc (ref null $A.desc) (global.get $B.desc)) + ;; CHECK: (global $A (ref null $A) (ref.null none)) + (global $A (ref null $A) (ref.null none)) + ;; CHECK: (global $B (ref null $B) (ref.null none)) + (global $B (ref null $B) (ref.null none)) + + ;; CHECK: (func $require-desc (type $4) (param $B (ref $B)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.get_desc $B + ;; CHECK-NEXT: (local.get $B) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $require-desc (param $B (ref $B)) + (drop + (ref.get_desc $B + (local.get $B) + ) + ) + ) +) + +;; ref.cast_desc requires a descriptor and also still affects subtyping like a +;; normal cast. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $top (sub (descriptor $top.desc (struct)))) + (type $top (sub (descriptor $top.desc (struct)))) + ;; CHECK: (type $bot (sub $top (descriptor $bot.desc (struct)))) + (type $bot (sub $top (descriptor $bot.desc (struct)))) + ;; CHECK: (type $top.desc (sub (describes $top (struct)))) + (type $top.desc (sub (describes $top (struct)))) + ;; CHECK: (type $bot.desc (sub $top.desc (describes $bot (struct)))) + (type $bot.desc (sub $top.desc (describes $bot (struct)))) + ) + + ;; CHECK: (type $4 (func (param anyref (ref $top.desc)))) + + ;; CHECK: (global $bot-sub-any anyref (struct.new_default $bot + ;; CHECK-NEXT: (struct.new_default $bot.desc) + ;; CHECK-NEXT: )) + (global $bot-sub-any anyref (struct.new $bot (struct.new $bot.desc))) + + ;; CHECK: (func $ref.cast_desc (type $4) (param $any anyref) (param $top.desc (ref $top.desc)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.cast_desc (ref null $top) + ;; CHECK-NEXT: (local.get $any) + ;; CHECK-NEXT: (local.get $top.desc) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $ref.cast_desc (param $any anyref) (param $top.desc (ref $top.desc)) + (drop + (ref.cast_desc (ref null $top) + (local.get $any) + (local.get $top.desc) + ) + ) + ) +) + +;; If the ref.cast_desc is exact, then it doesn't need to transitively require +;; any subtypings except that the cast destination is a subtype of the cast +;; source. TODO. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $top (sub (descriptor $top.desc (struct)))) + (type $top (sub (descriptor $top.desc (struct)))) + ;; CHECK: (type $bot (sub $top (descriptor $bot.desc (struct)))) + (type $bot (sub $top (descriptor $bot.desc (struct)))) + ;; CHECK: (type $top.desc (sub (describes $top (struct)))) + (type $top.desc (sub (describes $top (struct)))) + ;; CHECK: (type $bot.desc (sub $top.desc (describes $bot (struct)))) + (type $bot.desc (sub $top.desc (describes $bot (struct)))) + ) + + ;; CHECK: (type $4 (func (param anyref (ref (exact $top.desc))))) + + ;; CHECK: (global $bot-sub-any anyref (struct.new_default $bot + ;; CHECK-NEXT: (struct.new_default $bot.desc) + ;; CHECK-NEXT: )) + (global $bot-sub-any anyref (struct.new $bot (struct.new $bot.desc))) + + ;; CHECK: (func $ref.cast_desc (type $4) (param $any anyref) (param $top.desc (ref (exact $top.desc))) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.cast_desc (ref null (exact $top)) + ;; CHECK-NEXT: (local.get $any) + ;; CHECK-NEXT: (local.get $top.desc) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $ref.cast_desc (param $any anyref) (param $top.desc (ref (exact $top.desc))) + (drop + ;; This is now exact. + (ref.cast_desc (ref null (exact $top)) + (local.get $any) + (local.get $top.desc) + ) + ) + ) +) + +;; br_on_cast_desc requires a descriptor and also still affects subtyping like a +;; normal cast. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $top (sub (descriptor $top.desc (struct)))) + (type $top (sub (descriptor $top.desc (struct)))) + ;; CHECK: (type $bot (sub $top (descriptor $bot.desc (struct)))) + (type $bot (sub $top (descriptor $bot.desc (struct)))) + ;; CHECK: (type $top.desc (sub (describes $top (struct)))) + (type $top.desc (sub (describes $top (struct)))) + ;; CHECK: (type $bot.desc (sub $top.desc (describes $bot (struct)))) + (type $bot.desc (sub $top.desc (describes $bot (struct)))) + ) + + ;; CHECK: (type $4 (func (param anyref (ref $top.desc)))) + + ;; CHECK: (global $bot-sub-any anyref (struct.new_default $bot + ;; CHECK-NEXT: (struct.new_default $bot.desc) + ;; CHECK-NEXT: )) + (global $bot-sub-any anyref (struct.new $bot (struct.new $bot.desc))) + + ;; CHECK: (func $br_on_cast_desc (type $4) (param $any anyref) (param $top.desc (ref $top.desc)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $l (result anyref) + ;; CHECK-NEXT: (br_on_cast_desc $l anyref (ref null $top) + ;; CHECK-NEXT: (local.get $any) + ;; CHECK-NEXT: (local.get $top.desc) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br_on_cast_desc (param $any anyref) (param $top.desc (ref $top.desc)) + (drop + (block $l (result anyref) + (br_on_cast_desc $l anyref (ref null $top) + (local.get $any) + (local.get $top.desc) + ) + ) + ) + ) +) + +;; If the br_on_cast_desc is exact, then it doesn't need to transitively require +;; any subtypings except that the cast destination is a subtype of the cast +;; source. TODO. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $top (sub (descriptor $top.desc (struct)))) + (type $top (sub (descriptor $top.desc (struct)))) + ;; CHECK: (type $bot (sub $top (descriptor $bot.desc (struct)))) + (type $bot (sub $top (descriptor $bot.desc (struct)))) + ;; CHECK: (type $top.desc (sub (describes $top (struct)))) + (type $top.desc (sub (describes $top (struct)))) + ;; CHECK: (type $bot.desc (sub $top.desc (describes $bot (struct)))) + (type $bot.desc (sub $top.desc (describes $bot (struct)))) + ) + + ;; CHECK: (type $4 (func (param anyref (ref (exact $top.desc))))) + + ;; CHECK: (global $bot-sub-any anyref (struct.new_default $bot + ;; CHECK-NEXT: (struct.new_default $bot.desc) + ;; CHECK-NEXT: )) + (global $bot-sub-any anyref (struct.new $bot (struct.new $bot.desc))) + + ;; CHECK: (func $br_on_cast_desc (type $4) (param $any anyref) (param $top.desc (ref (exact $top.desc))) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $l (result anyref) + ;; CHECK-NEXT: (br_on_cast_desc $l anyref (ref null (exact $top)) + ;; CHECK-NEXT: (local.get $any) + ;; CHECK-NEXT: (local.get $top.desc) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br_on_cast_desc (param $any anyref) (param $top.desc (ref (exact $top.desc))) + (drop + (block $l (result anyref) + ;; This is now exact. + (br_on_cast_desc $l anyref (ref null (exact $top)) + (local.get $any) + (local.get $top.desc) + ) + ) + ) + ) +) + +;; br_on_cast_desc_fail requires a descriptor and also still affects subtyping +;; like a normal cast. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $top (sub (descriptor $top.desc (struct)))) + (type $top (sub (descriptor $top.desc (struct)))) + ;; CHECK: (type $bot (sub $top (descriptor $bot.desc (struct)))) + (type $bot (sub $top (descriptor $bot.desc (struct)))) + ;; CHECK: (type $top.desc (sub (describes $top (struct)))) + (type $top.desc (sub (describes $top (struct)))) + ;; CHECK: (type $bot.desc (sub $top.desc (describes $bot (struct)))) + (type $bot.desc (sub $top.desc (describes $bot (struct)))) + ) + + ;; CHECK: (type $4 (func (param anyref (ref $top.desc)))) + + ;; CHECK: (global $bot-sub-any anyref (struct.new_default $bot + ;; CHECK-NEXT: (struct.new_default $bot.desc) + ;; CHECK-NEXT: )) + (global $bot-sub-any anyref (struct.new $bot (struct.new $bot.desc))) + + ;; CHECK: (func $br_on_cast_desc_fail (type $4) (param $any anyref) (param $top.desc (ref $top.desc)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $l (result anyref) + ;; CHECK-NEXT: (br_on_cast_desc_fail $l anyref (ref null $top) + ;; CHECK-NEXT: (local.get $any) + ;; CHECK-NEXT: (local.get $top.desc) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br_on_cast_desc_fail (param $any anyref) (param $top.desc (ref $top.desc)) + (drop + (block $l (result anyref) + (br_on_cast_desc_fail $l anyref (ref null $top) + (local.get $any) + (local.get $top.desc) + ) + ) + ) + ) ) +;; If the br_on_cast_desc_fail is exact, then it doesn't need to transitively +;; require any subtypings except that the cast destination is a subtype of the +;; cast source. TODO. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $top (sub (descriptor $top.desc (struct)))) + (type $top (sub (descriptor $top.desc (struct)))) + ;; CHECK: (type $bot (sub $top (descriptor $bot.desc (struct)))) + (type $bot (sub $top (descriptor $bot.desc (struct)))) + ;; CHECK: (type $top.desc (sub (describes $top (struct)))) + (type $top.desc (sub (describes $top (struct)))) + ;; CHECK: (type $bot.desc (sub $top.desc (describes $bot (struct)))) + (type $bot.desc (sub $top.desc (describes $bot (struct)))) + ) + + ;; CHECK: (type $4 (func (param anyref (ref (exact $top.desc))))) + + ;; CHECK: (global $bot-sub-any anyref (struct.new_default $bot + ;; CHECK-NEXT: (struct.new_default $bot.desc) + ;; CHECK-NEXT: )) + (global $bot-sub-any anyref (struct.new $bot (struct.new $bot.desc))) + + ;; CHECK: (func $br_on_cast_desc_fail (type $4) (param $any anyref) (param $top.desc (ref (exact $top.desc))) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block $l (result anyref) + ;; CHECK-NEXT: (br_on_cast_desc_fail $l anyref (ref null (exact $top)) + ;; CHECK-NEXT: (local.get $any) + ;; CHECK-NEXT: (local.get $top.desc) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $br_on_cast_desc_fail (param $any anyref) (param $top.desc (ref (exact $top.desc))) + (drop + (block $l (result anyref) + ;; This is now exact. + (br_on_cast_desc_fail $l anyref (ref null (exact $top)) + (local.get $any) + (local.get $top.desc) + ) + ) + ) + ) +) + +;; top ->(0) top.desc +;; ^ +;; |(2) mid mid.desc +;; | ^(1) +;; bot bot.desc +;; +;; bot <: top implies bot.desc <: top.desc, but we already have +;; bot.desc <: mid.desc, so that gives us bot.desc <: mid.desc <: top.desc. +;; This is only valid if we also have bot <: mid <: top. (module (rec ;; CHECK: (rec @@ -78,16 +672,7 @@ ;; CHECK: (type $bot.desc (sub $mid.desc (describes $bot (struct)))) (type $bot.desc (sub $mid.desc (describes $bot (struct)))) ) - - ;; top -> top.desc - ;; ^ - ;; |(2) mid -> mid.desc - ;; | ^ (1) - ;; bot -> bot.desc - ;; - ;; bot <: top implies bot.desc <: top.desc, but we already have - ;; bot.desc <: mid.desc, so that gives us bot.desc <: mid.desc <: top.desc. - ;; This is only valid if we also have bot <: mid <: top. + ;; CHECK: (type $6 (func (param (ref $top)))) ;; CHECK: (global $bot-mid-desc (ref null $mid.desc) (struct.new_default $bot.desc)) (global $bot-mid-desc (ref null $mid.desc) (struct.new $bot.desc)) @@ -95,8 +680,35 @@ ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: )) (global $bot-top (ref null $top) (struct.new $bot (ref.null none))) + + ;; CHECK: (func $require-desc (type $6) (param $top (ref $top)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.get_desc $top + ;; CHECK-NEXT: (local.get $top) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $require-desc (param $top (ref $top)) + (drop + ;; This is enough to require all the descriptors to remain descriptors. + (ref.get_desc $top + (local.get $top) + ) + ) + ) ) +;; Same as above, but the order of the initial subtypings is reversed. +;; +;; top ->(0) top.desc +;; ^ +;; |(1) mid mid.desc +;; | ^(2) +;; bot bot.desc +;; +;; bot <: top implies bot.desc <: top.desc. When we add bot.desc <: mid.desc, +;; that gives us bot.desc <: mid.desc <: top.desc. This is only valid if we +;; also have bot <: mid <: top. (module (rec ;; CHECK: (rec @@ -113,18 +725,59 @@ ;; CHECK: (type $bot.desc (sub $mid.desc (describes $bot (struct)))) (type $bot.desc (sub $mid.desc (describes $bot (struct)))) ) + ;; CHECK: (type $6 (func (param (ref $top)))) - ;; Same as above, but the order of the initial subtypings is reversed. - ;; - ;; top -> top.desc - ;; ^ - ;; |(1) mid -> mid.desc - ;; | ^ (2) - ;; bot -> bot.desc - ;; - ;; bot <: top implies bot.desc <: top.desc. When we add bot.desc <: mid.desc, - ;; that gives us bot.desc <: mid.desc <: top.desc. This is only valid if we - ;; also have bot <: mid <: top. + ;; CHECK: (global $bot-top (ref null $top) (struct.new_default $bot + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: )) + (global $bot-top (ref null $top) (struct.new $bot (ref.null none))) + ;; CHECK: (global $bot-mid-desc (ref null $mid.desc) (struct.new_default $bot.desc)) + (global $bot-mid-desc (ref null $mid.desc) (struct.new $bot.desc)) + + ;; CHECK: (func $require-desc (type $6) (param $top (ref $top)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.get_desc $top + ;; CHECK-NEXT: (local.get $top) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $require-desc (param $top (ref $top)) + (drop + ;; This is enough to require all the descriptors to remain descriptors. + (ref.get_desc $top + (local.get $top) + ) + ) + ) +) + +;; Same as above, but now we initially require bot.desc to remain a descriptor +;; rather than top.desc. This means we can now optimize out top's descriptor and +;; mid.desc's supertype. +;; +;; top top.desc +;; ^ +;; |(1) mid mid.desc +;; | ^ (2) +;; bot ->(0) bot.desc +;; +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $top (sub (struct))) + (type $top (sub (descriptor $top.desc (struct)))) + ;; CHECK: (type $mid (sub $top (descriptor $mid.desc (struct)))) + (type $mid (sub $top (descriptor $mid.desc (struct)))) + ;; CHECK: (type $bot (sub $mid (descriptor $bot.desc (struct)))) + (type $bot (sub $mid (descriptor $bot.desc (struct)))) + ;; CHECK: (type $top.desc (sub (struct))) + (type $top.desc (sub (describes $top (struct)))) + ;; CHECK: (type $mid.desc (sub (describes $mid (struct)))) + (type $mid.desc (sub $top.desc (describes $mid (struct)))) + ;; CHECK: (type $bot.desc (sub $mid.desc (describes $bot (struct)))) + (type $bot.desc (sub $mid.desc (describes $bot (struct)))) + ) + ;; CHECK: (type $6 (func (param (ref $bot)))) ;; CHECK: (global $bot-top (ref null $top) (struct.new_default $bot ;; CHECK-NEXT: (ref.null none) @@ -132,8 +785,37 @@ (global $bot-top (ref null $top) (struct.new $bot (ref.null none))) ;; CHECK: (global $bot-mid-desc (ref null $mid.desc) (struct.new_default $bot.desc)) (global $bot-mid-desc (ref null $mid.desc) (struct.new $bot.desc)) + + ;; CHECK: (global $top.desc (ref null $top.desc) (ref.null none)) + (global $top.desc (ref null $top.desc) (ref.null none)) + + ;; CHECK: (func $require-desc (type $6) (param $bot (ref $bot)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.get_desc $bot + ;; CHECK-NEXT: (local.get $bot) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $require-desc (param $bot (ref $bot)) + (drop + (ref.get_desc $bot + (local.get $bot) + ) + ) + ) ) +;; Now go the other direction: +;; +;; top ->(0) top.desc +;; ^ +;; mid mid.desc |(2) +;; ^ (1) | +;; bot bot.desc +;; +;; bot.desc <: top.desc implies bot <: top, but we already have bot <: mid, so +;; that gives us bot <: mid <: top. This is only valid if we also have +;; bot.desc <: mid.desc <: top.desc. (module (rec ;; CHECK: (rec @@ -151,17 +833,7 @@ (type $bot.desc (sub $mid.desc (describes $bot (struct)))) ) - ;; Now go the other direction: - ;; - ;; top ---> top.desc - ;; ^ - ;; mid -> mid.desc |(2) - ;; ^ (1) | - ;; bot ---> bot.desc - ;; - ;; bot.desc <: top.desc implies bot <: top, but we already have bot <: mid, so - ;; that gives us bot <: mid <: top. This is only valid if we also have - ;; bot.desc <: mid.desc <: top.desc. + ;; CHECK: (type $6 (func (param (ref $top)))) ;; CHECK: (global $bot-mid (ref null $mid) (struct.new_default $bot ;; CHECK-NEXT: (ref.null none) @@ -169,8 +841,35 @@ (global $bot-mid (ref null $mid) (struct.new $bot (ref.null none))) ;; CHECK: (global $bot-top-desc (ref null $top.desc) (struct.new_default $bot.desc)) (global $bot-top-desc (ref null $top.desc) (struct.new $bot.desc)) + + ;; CHECK: (func $require-desc (type $6) (param $top (ref $top)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.get_desc $top + ;; CHECK-NEXT: (local.get $top) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $require-desc (param $top (ref $top)) + (drop + ;; This is enough to require all the descriptors to remain descriptors. + (ref.get_desc $top + (local.get $top) + ) + ) + ) ) +;; Same as above, but the order of the initial subtypings is reversed. +;; +;; top ->(0) top.desc +;; ^ +;; mid mid.desc |(1) +;; ^ (2) | +;; bot bot.desc +;; +;; bot.desc <: top.desc implies bot <: top. When we add bot <: mid, that gives +;; us bot <: mid <: top. This is only valid if we also have +;; bot.desc <: mid.desc <: top.desc. (module (rec ;; CHECK: (rec @@ -188,28 +887,339 @@ (type $bot.desc (sub $mid.desc (describes $bot (struct)))) ) - ;; Same as above, but the order of the initial subtypings is reversed. - ;; - ;; top ---> top.desc - ;; ^ - ;; mid -> mid.desc |(1) - ;; ^ (2) | - ;; bot ---> bot.desc - ;; - ;; bot.desc <: top.desc implies bot <: top. When we add bot <: mid, that gives - ;; us bot <: mid <: top. This is only valid if we also have - ;; bot.desc <: mid.desc <: top.desc. + ;; CHECK: (type $6 (func (param (ref $top)))) + ;; CHECK: (global $bot-top-desc (ref null $top.desc) (struct.new_default $bot.desc)) (global $bot-top-desc (ref null $top.desc) (struct.new $bot.desc)) ;; CHECK: (global $bot-mid (ref null $mid) (struct.new_default $bot ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: )) (global $bot-mid (ref null $mid) (struct.new $bot (ref.null none))) + + ;; CHECK: (func $require-desc (type $6) (param $top (ref $top)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.get_desc $top + ;; CHECK-NEXT: (local.get $top) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $require-desc (param $top (ref $top)) + (drop + ;; This is enough to require all the descriptors to remain descriptors. + (ref.get_desc $top + (local.get $top) + ) + ) + ) ) +;; Same as above, but now we initially require bot.desc to remain a descriptor +;; rather than top.desc. We still cannot optimize anything. +;; +;; top top.desc +;; ^ +;; mid mid.desc |(1) +;; ^ (2) | +;; bot ->(0) bot.desc +;; +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $top (sub (descriptor $top.desc (struct)))) + (type $top (sub (descriptor $top.desc (struct)))) + ;; CHECK: (type $mid (sub $top (descriptor $mid.desc (struct)))) + (type $mid (sub $top (descriptor $mid.desc (struct)))) + ;; CHECK: (type $bot (sub $mid (descriptor $bot.desc (struct)))) + (type $bot (sub $mid (descriptor $bot.desc (struct)))) + ;; CHECK: (type $top.desc (sub (describes $top (struct)))) + (type $top.desc (sub (describes $top (struct)))) + ;; CHECK: (type $mid.desc (sub $top.desc (describes $mid (struct)))) + (type $mid.desc (sub $top.desc (describes $mid (struct)))) + ;; CHECK: (type $bot.desc (sub $mid.desc (describes $bot (struct)))) + (type $bot.desc (sub $mid.desc (describes $bot (struct)))) + ) + + ;; CHECK: (type $6 (func (param (ref $bot)))) + + ;; CHECK: (global $bot-top-desc (ref null $top.desc) (struct.new_default $bot.desc)) + (global $bot-top-desc (ref null $top.desc) (struct.new $bot.desc)) + ;; CHECK: (global $bot-mid (ref null $mid) (struct.new_default $bot + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: )) + (global $bot-mid (ref null $mid) (struct.new $bot (ref.null none))) + + ;; CHECK: (func $require-desc (type $6) (param $bot (ref $bot)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.get_desc $bot + ;; CHECK-NEXT: (local.get $bot) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $require-desc (param $bot (ref $bot)) + (drop + ;; This is enough to require all the descriptors to remain descriptors. + (ref.get_desc $bot + (local.get $bot) + ) + ) + ) +) + +;; Test the case where a newly discovered descriptor has a supertype that now +;; needs to be a descriptor as well. Set this up: +;; +;; top top.desc +;; ^ (0) +;; mid mid.desc +;; ^ (1) +;; bot ->(0) bot.desc +;; +;; The discovery of bot.desc < mid.desc requires mid -> mid.desc because we +;; already have bot -> bot.desc at that point. The discovery of mid -> mid.desc +;; then requires mid <: top and top -> top.desc because we already have +;; mid.desc <: top.desc. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $top (sub (descriptor $top.desc (struct)))) + (type $top (sub (descriptor $top.desc (struct)))) + ;; CHECK: (type $mid (sub $top (descriptor $mid.desc (struct)))) + (type $mid (sub $top (descriptor $mid.desc (struct)))) + ;; CHECK: (type $bot (sub $mid (descriptor $bot.desc (struct)))) + (type $bot (sub $mid (descriptor $bot.desc (struct)))) + ;; CHECK: (type $top.desc (sub (describes $top (struct)))) + (type $top.desc (sub (describes $top (struct)))) + ;; CHECK: (type $mid.desc (sub $top.desc (describes $mid (struct)))) + (type $mid.desc (sub $top.desc (describes $mid (struct)))) + ;; CHECK: (type $bot.desc (sub $mid.desc (describes $bot (struct)))) + (type $bot.desc (sub $mid.desc (describes $bot (struct)))) + ;; CHECK: (type $X (sub (struct (field (ref null $mid.desc))))) + (type $X (sub (struct (field (ref null $mid.desc))))) + ;; CHECK: (type $Y (sub $X (struct (field (ref null $bot.desc))))) + (type $Y (sub $X (struct (field (ref null $bot.desc))))) + ) + + ;; X <: Y implies bot.desc <: mid.desc (but the indirection delays the + ;; processing of the latter). + ;; CHECK: (type $8 (func)) + + ;; CHECK: (global $Y-sub-X (ref $X) (struct.new $Y + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: )) + (global $Y-sub-X (ref $X) (struct.new $Y (ref.null none))) + + ;; mid.desc <: top.desc. + ;; CHECK: (global $mid.desc-sub-top.desc (ref $top.desc) (struct.new_default $mid.desc)) + (global $mid.desc-sub-top.desc (ref $top.desc) (struct.new $mid.desc)) + + ;; CHECK: (func $require-desc (type $8) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.get_desc $bot + ;; CHECK-NEXT: (struct.new_default $bot + ;; CHECK-NEXT: (struct.new_default $bot.desc) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $require-desc + ;; Require bot described-by bot.desc. + (drop + (ref.get_desc $bot + (struct.new $bot + (struct.new $bot.desc) + ) + ) + ) + ) +) + +;; When we optimize out descriptors, we may need to update allocations. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $struct (struct)) + (type $struct (descriptor $desc (struct))) + ;; CHECK: (type $desc (struct)) + (type $desc (describes $struct (struct))) + ) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (import "" "" (func $effect (type $2))) + (import "" "" (func $effect)) + + ;; CHECK: (global $global (ref null $struct) (struct.new_default $struct)) + (global $global (ref null $struct) (struct.new $struct (struct.new $desc))) + + ;; CHECK: (func $func (type $2) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result (ref (exact $struct))) + ;; CHECK-NEXT: (struct.new_default $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $func + (drop + (struct.new $struct + (struct.new $desc) + ) + ) + ) + + ;; CHECK: (func $func-effect (type $2) + ;; CHECK-NEXT: (local $0 (ref (exact $desc))) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result (ref (exact $struct))) + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (block (result (ref (exact $desc))) + ;; CHECK-NEXT: (call $effect) + ;; CHECK-NEXT: (struct.new_default $desc) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.new_default $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $func-effect + (drop + (struct.new $struct + (block (result (ref (exact $desc))) + (call $effect) + (struct.new $desc) + ) + ) + ) + ) + + ;; CHECK: (func $func-null (type $2) + ;; CHECK-NEXT: (local $0 (ref none)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result (ref (exact $struct))) + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.new_default $struct) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $func-null + (drop + (struct.new $struct + (ref.null none) + ) + ) + ) + + ;; CHECK: (func $func-unreachable (type $2) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block ;; (replaces unreachable StructNew we can't emit) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $func-unreachable + (drop + (struct.new $struct + (unreachable) + ) + ) + ) +) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $struct (descriptor $desc (struct))) + (type $struct (descriptor $desc (struct))) + ;; CHECK: (type $desc (describes $struct (struct))) + (type $desc (describes $struct (struct))) + ) + ;; CHECK: (type $2 (func)) + + ;; CHECK: (func $struct (type $2) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result (ref (exact $desc))) + ;; CHECK-NEXT: (struct.new_default $desc) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.get_desc $struct + ;; CHECK-NEXT: (struct.new_default $struct + ;; CHECK-NEXT: (struct.new_default $desc) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $struct + (drop + ;; Processing this subtyping when the subtype ($desc) is a descriptor + ;; and the supertype does not have a descriptor because it is abstract + ;; should not cause problems. + (block (result (ref eq)) + (struct.new_default $desc) + ) + ) + (drop + (ref.get_desc $struct + (struct.new_default $struct + (struct.new_default $desc) + ) + ) + ) + ) +) + +;; When the possibly-trapping global allocations are nested inside other +;; allocations that will be removed, they need to be moved to new globals. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $struct (sub (struct))) + (type $struct (sub (descriptor $desc (struct)))) + ;; CHECK: (type $desc (sub (descriptor $meta (struct)))) + (type $desc (sub (describes $struct (descriptor $meta (struct))))) + ;; CHECK: (type $meta (sub (describes $desc (struct)))) + (type $meta (sub (describes $desc (struct)))) + ) + + ;; CHECK: (global $g (ref $struct) (struct.new_default $struct)) + (global $g (ref $struct) (struct.new $struct (struct.new $desc (ref.null none)))) +) + +;; CHECK: (global $unsubtyping-removed-0 (ref (exact $desc)) (struct.new_default $desc +;; CHECK-NEXT: (ref.null none) +;; CHECK-NEXT: )) +(module + ;; Same, but now the nesting is under a non-descriptor field. + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (sub (struct (field (ref $struct))))) + (type $A (sub (struct (field (ref $struct))))) + ;; CHECK: (type $struct (sub (struct))) + (type $struct (sub (descriptor $desc (struct)))) + ;; CHECK: (type $desc (sub (descriptor $meta (struct)))) + (type $desc (sub (describes $struct (descriptor $meta (struct))))) + ;; CHECK: (type $meta (sub (describes $desc (struct)))) + (type $meta (sub (describes $desc (struct)))) + ) + + ;; CHECK: (global $g (ref $A) (struct.new $A + ;; CHECK-NEXT: (struct.new_default $struct) + ;; CHECK-NEXT: )) + (global $g (ref $A) (struct.new $A (struct.new $struct (struct.new $desc (ref.null none))))) +) + +;; CHECK: (global $unsubtyping-removed-0 (ref (exact $desc)) (struct.new_default $desc +;; CHECK-NEXT: (ref.null none) +;; CHECK-NEXT: )) +(module ;; This will be invalid soon, but in the meantime we should not be confused when ;; the types described by two related descriptors are unrelated. -(module (rec ;; CHECK: (rec ;; CHECK-NEXT: (type $A (descriptor $super (struct))) diff --git a/test/lit/passes/unsubtyping.wast b/test/lit/passes/unsubtyping.wast index 8bcfa250d45..cf80ac48d85 100644 --- a/test/lit/passes/unsubtyping.wast +++ b/test/lit/passes/unsubtyping.wast @@ -1892,3 +1892,31 @@ ) ) ) + +;; Even though our analysis has its own visitor for StructNew, it should still +;; be able to collect subtype constraints from StructNews. +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (sub (struct (field (ref null $A))))) + (type $A (sub (struct (field (ref null $A))))) + ;; CHECK: (type $B (sub $A (struct (field (ref null $A))))) + (type $B (sub $A (struct (field (ref null $A))))) + + ;; CHECK: (type $2 (func (param (ref null $B)))) + + ;; CHECK: (func $test (type $2) (param $B (ref null $B)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.new $A + ;; CHECK-NEXT: (local.get $B) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (param $B (ref null $B)) + (drop + ;; This requires B <: A. + (struct.new $A + (local.get $B) + ) + ) + ) +)