From deca26fef9c0811e50cab27270cdf5ca26051f39 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 29 Sep 2025 10:08:37 -0700 Subject: [PATCH 01/10] [Custom Descriptors] Optimize descriptors in Unsubtyping With the upcoming stricter validation rules for descriptor types, we can no longer use the placeholder describee trick in GlobalTypeOptimization, so its ability to optimize out unneeded descriptors will be significantly restricted. Because of the increasing coupling between the validation for subtyping and descriptor relationships, it makes most sense to optimize both at the same time in Unsubtyping. Update Unsubtyping to start the analysis assuming no descriptors are necessary, then add them in as it discovers that they are indeed necessary either to preserve behavior or validitiy. Discovering new required descriptors can imply that new subtypings are necessary and vice versa. --- src/ir/subtype-exprs.h | 12 +- src/passes/Unsubtyping.cpp | 372 +++++++++- src/support/insert_ordered.h | 8 + test/lit/passes/unsubtyping-desc.wast | 976 ++++++++++++++++++++++++-- 4 files changed, 1282 insertions(+), 86 deletions(-) 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 248ed995fe4..c36d0a60f29 100644 --- a/src/passes/Unsubtyping.cpp +++ b/src/passes/Unsubtyping.cpp @@ -17,12 +17,15 @@ #define UNSUBTYPING_DEBUG 0 #include +#include +#include #if !UNSUBTYPING_DEBUG #include #include #endif +#include "ir/localize.h" #include "ir/module-utils.h" #include "ir/subtype-exprs.h" #include "ir/type-updating.h" @@ -43,18 +46,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 +115,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 +165,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 +199,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 +255,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 +288,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 +391,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 +414,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 +451,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 +484,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 +501,22 @@ struct Unsubtyping : Pass { noteSubtype(sub.getHeapType(), super.getHeapType()); } + void noteDescriptor(HeapType described, HeapType descriptor) { + DBG(std::cerr << "noting " << ModuleHeapType(*wasm, described) + << " described by " << 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,10 +527,15 @@ 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) {} void noteSubtype(Type sub, Type super) { @@ -473,6 +619,38 @@ 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) { + 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()); + } }; // Collect subtyping constraints and casts from functions in parallel. @@ -488,6 +666,8 @@ 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. @@ -508,9 +688,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 +717,85 @@ 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. + // If the supertype has a descriptor type, then the subtype must be + // described by a corresponding subtype of the supertype's descriptor. (On + // the other hand, no further requirements are placed on the superytpe if + // the subtype has a descriptor.) + if (auto desc = types.getDescriptor(super)) { + auto subDesc = sub.getDescriptorType(); + assert(subDesc); + noteDescriptor(sub, *subDesc); + noteSubtype(*subDesc, *desc); + } + // If the supertype describes a type, then the subtype must describe a + // corresponding subtype of the supertype's described type. + if (auto desc = types.getDescribed(super)) { + auto subDesc = sub.getDescribedType(); + assert(subDesc); + noteDescriptor(*subDesc, sub); + noteSubtype(*subDesc, *desc); + } + // If the subtype describes a type, then the supertype must describe the + // supertype of the subtype's described type. + if (!super.isBasic()) { + if (auto desc = types.getDescribed(sub)) { + auto superDesc = super.getDescribedType(); + assert(superDesc); + noteDescriptor(*superDesc, super); + noteSubtype(*desc, *superDesc); + } + } + + // Find the implied subtypings from the type definitions and casts. processDefinitions(sub, super); processCasts(sub, super, oldSuper); } + void processDescriptor(HeapType described, HeapType descriptor) { + 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); + + // Every immediate subtype of the described type must also be described by a + // corresponding immediate subtype of the descriptor. (On the other hand, + // no further requirements are placed on the supertype of the described + // type.) + for (auto sub : types.immediateSubtypes(described)) { + auto subDesc = sub.getDescriptorType(); + assert(subDesc); + noteDescriptor(sub, *subDesc); + noteSubtype(*subDesc, descriptor); + } + // Every immediate subtype of the descriptor type must also describe a + // corresponding immediate subtype of the described typed. + for (auto sub : types.immediateSubtypes(descriptor)) { + auto subDesc = sub.getDescribedType(); + assert(subDesc); + noteDescriptor(*subDesc, sub); + noteSubtype(*subDesc, described); + } + // The immediate superytpe of the descriptor, if it exists, must describe + // the immediate supertype of the described type. + if (auto superDescriptor = types.getSupertype(descriptor); + superDescriptor && !superDescriptor->isBasic()) { + auto superDescribed = superDescriptor->getDescribedType(); + assert(superDescribed); + noteDescriptor(*superDescribed, *superDescriptor); + noteSubtype(described, *superDescribed); + } + } + void processDefinitions(HeapType sub, HeapType super) { if (super.isBasic()) { return; @@ -575,16 +826,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()) { - noteSubtype(*desc, *superDesc); - } - } } void @@ -623,9 +864,68 @@ 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; + + Rewriter(const TypeTree& types) : types(types) {} + + bool isFunctionParallel() override { return true; } + 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()) { + auto* block = + ChildLocalizer(curr, func, *getModule(), getPassOptions()) + .getChildrenReplacement(); + block->list.push_back(curr); + block->type = curr->type; + replaceCurrent(block); + } + curr->desc = nullptr; + } + }; + + PassRunner runner(getPassRunner()); + runner.add(std::make_unique(types)); + runner.run(); + Rewriter(types).runOnModuleCode(getPassRunner(), &wasm); + } }; } // 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.wast b/test/lit/passes/unsubtyping-desc.wast index adfdd1a0c85..8607e775a36 100644 --- a/test/lit/passes/unsubtyping-desc.wast +++ b/test/lit/passes/unsubtyping-desc.wast @@ -2,6 +2,33 @@ ;; 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) (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) (struct.new_default $B)) + (global $B (ref null $B) (struct.new $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)) +) + +;; Now we require the descriptors for both types. We should still be able to +;; optimize the subtype relationship. (module (rec ;; CHECK: (rec @@ -15,13 +42,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 +120,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 +277,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 -> 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 +591,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 +599,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 -> 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 +644,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)))) + + ;; 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)) - ;; 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: (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 -> 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 +704,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 ---> 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 +752,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 +760,87 @@ (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 ---> 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 + ;; 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 $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 ---> bot.desc +;; (module (rec ;; CHECK: (rec @@ -188,21 +858,231 @@ (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 $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: (drop + ;; CHECK-NEXT: (block (result (ref (exact $struct))) + ;; 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) + ) + ) + ) + ) ) From 58dbd93eee2fd1035e05e1b4eee19a8bd6854ba2 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 29 Sep 2025 16:27:52 -0700 Subject: [PATCH 02/10] superytpe --- src/passes/Unsubtyping.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/passes/Unsubtyping.cpp b/src/passes/Unsubtyping.cpp index c36d0a60f29..ba7c86daef5 100644 --- a/src/passes/Unsubtyping.cpp +++ b/src/passes/Unsubtyping.cpp @@ -724,7 +724,7 @@ struct Unsubtyping : Pass { // If the supertype has a descriptor type, then the subtype must be // described by a corresponding subtype of the supertype's descriptor. (On - // the other hand, no further requirements are placed on the superytpe if + // the other hand, no further requirements are placed on the supertype if // the subtype has a descriptor.) if (auto desc = types.getDescriptor(super)) { auto subDesc = sub.getDescriptorType(); From 06f8b60faf062da68fe7d31ae6ee5721ad584d46 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 29 Sep 2025 16:29:06 -0700 Subject: [PATCH 03/10] requiresNonNullableLocalFixups comment --- src/passes/Unsubtyping.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/passes/Unsubtyping.cpp b/src/passes/Unsubtyping.cpp index ba7c86daef5..19a7087c903 100644 --- a/src/passes/Unsubtyping.cpp +++ b/src/passes/Unsubtyping.cpp @@ -891,6 +891,7 @@ struct Unsubtyping : Pass { 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); From 752061c547ceec2a20a86c908764fbf360599a33 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 29 Sep 2025 18:29:00 -0700 Subject: [PATCH 04/10] generalize --- src/passes/Unsubtyping.cpp | 113 +++++++++++++++++++------------------ 1 file changed, 58 insertions(+), 55 deletions(-) diff --git a/src/passes/Unsubtyping.cpp b/src/passes/Unsubtyping.cpp index 19a7087c903..2be4fea9809 100644 --- a/src/passes/Unsubtyping.cpp +++ b/src/passes/Unsubtyping.cpp @@ -502,9 +502,8 @@ struct Unsubtyping : Pass { } void noteDescriptor(HeapType described, HeapType descriptor) { - DBG(std::cerr << "noting " << ModuleHeapType(*wasm, described) - << " described by " << ModuleHeapType(*wasm, descriptor) - << '\n'); + DBG(std::cerr << "noting " << ModuleHeapType(*wasm, described) << " -> " + << ModuleHeapType(*wasm, descriptor) << '\n'); work.push_back({Kind::Descriptor, described, descriptor}); } @@ -722,33 +721,16 @@ struct Unsubtyping : Pass { types.setSupertype(sub, super); - // If the supertype has a descriptor type, then the subtype must be - // described by a corresponding subtype of the supertype's descriptor. (On - // the other hand, no further requirements are placed on the supertype if - // the subtype has a descriptor.) - if (auto desc = types.getDescriptor(super)) { - auto subDesc = sub.getDescriptorType(); - assert(subDesc); - noteDescriptor(sub, *subDesc); - noteSubtype(*subDesc, *desc); - } - // If the supertype describes a type, then the subtype must describe a - // corresponding subtype of the supertype's described type. - if (auto desc = types.getDescribed(super)) { - auto subDesc = sub.getDescribedType(); - assert(subDesc); - noteDescriptor(*subDesc, sub); - noteSubtype(*subDesc, *desc); - } - // If the subtype describes a type, then the supertype must describe the - // supertype of the subtype's described type. - if (!super.isBasic()) { - if (auto desc = types.getDescribed(sub)) { - auto superDesc = super.getDescribedType(); - assert(superDesc); - noteDescriptor(*superDesc, super); - noteSubtype(*desc, *superDesc); - } + // 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. @@ -757,6 +739,8 @@ struct Unsubtyping : Pass { } 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)) { @@ -767,32 +751,16 @@ struct Unsubtyping : Pass { types.setDescriptor(described, descriptor); - // Every immediate subtype of the described type must also be described by a - // corresponding immediate subtype of the descriptor. (On the other hand, - // no further requirements are placed on the supertype of the described - // type.) + // 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)) { - auto subDesc = sub.getDescriptorType(); - assert(subDesc); - noteDescriptor(sub, *subDesc); - noteSubtype(*subDesc, descriptor); - } - // Every immediate subtype of the descriptor type must also describe a - // corresponding immediate subtype of the described typed. - for (auto sub : types.immediateSubtypes(descriptor)) { - auto subDesc = sub.getDescribedType(); - assert(subDesc); - noteDescriptor(*subDesc, sub); - noteSubtype(*subDesc, described); - } - // The immediate superytpe of the descriptor, if it exists, must describe - // the immediate supertype of the described type. - if (auto superDescriptor = types.getSupertype(descriptor); - superDescriptor && !superDescriptor->isBasic()) { - auto superDescribed = superDescriptor->getDescribedType(); - assert(superDescribed); - noteDescriptor(*superDescribed, *superDescriptor); - noteSubtype(described, *superDescribed); + completeDescriptorSquare( + described, descriptor, sub, types.getDescriptor(sub)); + } + for (auto subDesc : types.immediateSubtypes(descriptor)) { + completeDescriptorSquare( + described, descriptor, types.getDescribed(subDesc), subDesc); } } @@ -852,6 +820,41 @@ 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. + noteSubtype(*sub, *super); + noteSubtype(*subDesc, *superDesc); + noteDescriptor(*super, *superDesc); + noteDescriptor(*sub, *subDesc); + } + void rewriteTypes(Module& wasm) { struct Rewriter : GlobalTypeRewriter { Unsubtyping& parent; From 159f6803beb71b46d1895bc07b708d7c0d8b58d0 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 29 Sep 2025 19:58:14 -0700 Subject: [PATCH 05/10] handle soon-to-be-invalid types --- src/passes/Unsubtyping.cpp | 6 +++++- test/lit/passes/unsubtyping-desc.wast | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/passes/Unsubtyping.cpp b/src/passes/Unsubtyping.cpp index 2be4fea9809..441ace45ee0 100644 --- a/src/passes/Unsubtyping.cpp +++ b/src/passes/Unsubtyping.cpp @@ -849,7 +849,11 @@ struct Unsubtyping : Pass { // 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. - noteSubtype(*sub, *super); + // 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); diff --git a/test/lit/passes/unsubtyping-desc.wast b/test/lit/passes/unsubtyping-desc.wast index 8607e775a36..77109d06df5 100644 --- a/test/lit/passes/unsubtyping-desc.wast +++ b/test/lit/passes/unsubtyping-desc.wast @@ -1086,3 +1086,22 @@ ) ) ) + +;; 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))) + (type $A (descriptor $super (struct))) + ;; CHECK: (type $B (descriptor $sub (struct))) + (type $B (descriptor $sub (struct))) + ;; CHECK: (type $super (sub (describes $A (struct)))) + (type $super (sub (describes $A (struct)))) + ;; CHECK: (type $sub (sub $super (describes $B (struct)))) + (type $sub (sub $super (describes $B (struct)))) + ) + ;; CHECK: (global $public (ref null $B) (ref.null none)) + (global $public (export "public") (ref null $B) (ref.null none)) +) +;; CHECK: (export "public" (global $public)) From 2dde94d3ecd4bf7b2c008218bb945642d1f2f201 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 29 Sep 2025 20:31:45 -0700 Subject: [PATCH 06/10] preserve traps --- src/passes/Unsubtyping.cpp | 34 ++++++- test/lit/passes/unsubtyping-desc-tnh.wast | 49 ++++++++++ test/lit/passes/unsubtyping-desc.wast | 106 ++++++++++++++++++++-- 3 files changed, 177 insertions(+), 12 deletions(-) create mode 100644 test/lit/passes/unsubtyping-desc-tnh.wast diff --git a/src/passes/Unsubtyping.cpp b/src/passes/Unsubtyping.cpp index 441ace45ee0..8bad39bec00 100644 --- a/src/passes/Unsubtyping.cpp +++ b/src/passes/Unsubtyping.cpp @@ -535,8 +535,13 @@ struct Unsubtyping : Pass { : 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()); @@ -650,13 +655,30 @@ struct Unsubtyping : Pass { } noteDescriptor(curr->desc->type.getHeapType()); } + void visitStructNew(StructNew* 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.isNullable()) { + 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); } }); @@ -670,7 +692,7 @@ struct Unsubtyping : Pass { } // 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) { @@ -918,6 +940,12 @@ struct Unsubtyping : Pass { // 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(); diff --git a/test/lit/passes/unsubtyping-desc-tnh.wast b/test/lit/passes/unsubtyping-desc-tnh.wast new file mode 100644 index 00000000000..7462096a264 --- /dev/null +++ b/test/lit/passes/unsubtyping-desc-tnh.wast @@ -0,0 +1,49 @@ +;; 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) + ) + ) + ) +) diff --git a/test/lit/passes/unsubtyping-desc.wast b/test/lit/passes/unsubtyping-desc.wast index 77109d06df5..1acb0da2e42 100644 --- a/test/lit/passes/unsubtyping-desc.wast +++ b/test/lit/passes/unsubtyping-desc.wast @@ -17,18 +17,100 @@ (type $B.desc (sub $A.desc (describes $B (struct)))) ) - ;; 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 $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 (ref.null none))) - ;; CHECK: (global $B.desc (ref null $B.desc) (ref.null none)) - (global $B.desc (ref null $B.desc) (ref.null none)) + (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-descs (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-descs (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 $nullable-descs (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 $nullable-descs (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. We should still be able to -;; optimize the subtype relationship. +;; 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 @@ -1011,8 +1093,14 @@ ) ;; 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: ) From 7e6d0937ac1e807b4e18127f70d6b0cdbd995c33 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 29 Sep 2025 21:08:46 -0700 Subject: [PATCH 07/10] missing super call --- src/passes/Unsubtyping.cpp | 2 ++ test/lit/passes/unsubtyping.wast | 28 ++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/passes/Unsubtyping.cpp b/src/passes/Unsubtyping.cpp index 8bad39bec00..d33f4d85f8d 100644 --- a/src/passes/Unsubtyping.cpp +++ b/src/passes/Unsubtyping.cpp @@ -636,6 +636,7 @@ struct Unsubtyping : Pass { info.descriptors.insert({*desc, type}); } void visitRefGetDesc(RefGetDesc* curr) { + Super::visitRefGetDesc(curr); if (!curr->ref->type.isStruct()) { return; } @@ -656,6 +657,7 @@ struct Unsubtyping : Pass { noteDescriptor(curr->desc->type.getHeapType()); } void visitStructNew(StructNew* curr) { + Super::visitStructNew(curr); if (curr->type == Type::unreachable || !curr->desc) { return; } 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) + ) + ) + ) +) From 2ec9a0eacf4b780073bb6231be09cef6b73776c0 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Tue, 30 Sep 2025 16:57:41 -0700 Subject: [PATCH 08/10] nested allocations to new globals --- src/passes/Unsubtyping.cpp | 43 ++++++++++++++++++---- test/lit/passes/unsubtyping-desc-tnh.wast | 32 ++++++++++++++++ test/lit/passes/unsubtyping-desc.wast | 45 ++++++++++++++++++++++- 3 files changed, 112 insertions(+), 8 deletions(-) diff --git a/src/passes/Unsubtyping.cpp b/src/passes/Unsubtyping.cpp index d33f4d85f8d..2e45cdba316 100644 --- a/src/passes/Unsubtyping.cpp +++ b/src/passes/Unsubtyping.cpp @@ -25,8 +25,10 @@ #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" @@ -667,10 +669,15 @@ struct Unsubtyping : Pass { // 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.isNullable()) { - noteDescribed(curr->type.getHeapType()); + 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()); } }; @@ -919,6 +926,11 @@ struct Unsubtyping : Pass { 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; } @@ -954,15 +966,32 @@ struct Unsubtyping : Pass { 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; } }; - PassRunner runner(getPassRunner()); - runner.add(std::make_unique(types)); - runner.run(); - Rewriter(types).runOnModuleCode(getPassRunner(), &wasm); + 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)); + } } }; diff --git a/test/lit/passes/unsubtyping-desc-tnh.wast b/test/lit/passes/unsubtyping-desc-tnh.wast index 7462096a264..7b5b3770991 100644 --- a/test/lit/passes/unsubtyping-desc-tnh.wast +++ b/test/lit/passes/unsubtyping-desc-tnh.wast @@ -47,3 +47,35 @@ ) ) ) + +;; 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 1acb0da2e42..dfd2ecfd7cb 100644 --- a/test/lit/passes/unsubtyping-desc.wast +++ b/test/lit/passes/unsubtyping-desc.wast @@ -1175,9 +1175,52 @@ ) ) +;; 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))) From 2b0df59175dfa952786520aeb9eaa5efcf9912b1 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Tue, 30 Sep 2025 17:14:53 -0700 Subject: [PATCH 09/10] missing period and better names --- test/lit/passes/unsubtyping-desc.wast | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/lit/passes/unsubtyping-desc.wast b/test/lit/passes/unsubtyping-desc.wast index dfd2ecfd7cb..e9ae8e1b3d7 100644 --- a/test/lit/passes/unsubtyping-desc.wast +++ b/test/lit/passes/unsubtyping-desc.wast @@ -71,7 +71,7 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - (func $nullable-descs (param $A.desc (ref null (exact $A.desc))) + (func $nullable-desc (param $A.desc (ref null (exact $A.desc))) (drop (struct.new $A (local.get $A.desc) @@ -89,7 +89,6 @@ ;; CHECK: (type $A.desc (sub (struct))) (type $A.desc (sub (describes $A (struct)))) ) - ;; CHECK: (type $2 (func (param (ref (exact $A.desc))))) ;; CHECK: (func $nullable-descs (type $2) (param $A.desc (ref (exact $A.desc))) @@ -99,7 +98,7 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - (func $nullable-descs (param $A.desc (ref (exact $A.desc))) + (func $nonnullable-desc (param $A.desc (ref (exact $A.desc))) (drop ;; Now the descriptor is non-null. (struct.new $A @@ -346,7 +345,7 @@ ;; 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 +;; descriptor as well, so we cannot optimize. (module (rec ;; CHECK: (rec From 7f02e04e6f25d606eb23e7b5b83f9f287c2df234 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Wed, 1 Oct 2025 16:57:47 -0700 Subject: [PATCH 10/10] diagram consistency --- test/lit/passes/unsubtyping-desc.wast | 42 +++++++++++++-------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/test/lit/passes/unsubtyping-desc.wast b/test/lit/passes/unsubtyping-desc.wast index 173a974dadd..5a76a1a4f65 100644 --- a/test/lit/passes/unsubtyping-desc.wast +++ b/test/lit/passes/unsubtyping-desc.wast @@ -647,11 +647,11 @@ ) ) -;; top -> top.desc +;; top ->(0) top.desc ;; ^ -;; |(2) mid -> mid.desc -;; | ^ (1) -;; bot -> bot.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. @@ -700,11 +700,11 @@ ;; Same as above, but the order of the initial subtypings is reversed. ;; -;; top -> top.desc +;; top ->(0) top.desc ;; ^ -;; |(1) mid -> mid.desc -;; | ^ (2) -;; bot -> bot.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 @@ -755,11 +755,11 @@ ;; rather than top.desc. This means we can now optimize out top's descriptor and ;; mid.desc's supertype. ;; -;; top -> top.desc +;; top top.desc ;; ^ -;; |(1) mid -> mid.desc -;; | ^ (2) -;; bot -> bot.desc +;; |(1) mid mid.desc +;; | ^ (2) +;; bot ->(0) bot.desc ;; (module (rec @@ -807,11 +807,11 @@ ;; Now go the other direction: ;; -;; top ---> top.desc +;; top ->(0) top.desc ;; ^ -;; mid -> mid.desc |(2) +;; mid mid.desc |(2) ;; ^ (1) | -;; bot ---> bot.desc +;; 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 @@ -861,11 +861,11 @@ ;; Same as above, but the order of the initial subtypings is reversed. ;; -;; top ---> top.desc +;; top ->(0) top.desc ;; ^ -;; mid -> mid.desc |(1) +;; mid mid.desc |(1) ;; ^ (2) | -;; bot ---> bot.desc +;; 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 @@ -916,11 +916,11 @@ ;; 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 +;; top top.desc ;; ^ -;; mid -> mid.desc |(1) +;; mid mid.desc |(1) ;; ^ (2) | -;; bot ---> bot.desc +;; bot ->(0) bot.desc ;; (module (rec