From fb093b21a7e4054c50eb9b5fb2d0c92fa35fdf9a Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 25 Sep 2025 17:50:25 -0700 Subject: [PATCH 1/2] [NFC] Add debug tracing to Unsubtyping --- src/passes/Unsubtyping.cpp | 40 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/passes/Unsubtyping.cpp b/src/passes/Unsubtyping.cpp index 13dc6432be4..98a88e1fa51 100644 --- a/src/passes/Unsubtyping.cpp +++ b/src/passes/Unsubtyping.cpp @@ -37,6 +37,12 @@ #include "support/insert_ordered.h" #endif +#if UNSUBTYPING_DEBUG +#define DBG(x) x +#else +#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 @@ -122,6 +128,16 @@ template using Set = InsertOrderedSet; template using Map = std::unordered_map; template using Set = std::unordered_set; #endif + +#if UNSUBTYPING_DEBUG +Name getTypeName(Module& wasm, HeapType type) { + if (auto it = wasm.typeNames.find(type); it != wasm.typeNames.end()) { + return it->second.name; + } + return Name("(unnamed)"); +} +#endif + // A tree (or rather a forest) of types with the ability to query and set // supertypes in constant time and efficiently iterate over supertypes and // subtypes. @@ -272,6 +288,22 @@ struct TypeTree { Subtypes subtypes(HeapType type) { return {this, getIndex(type)}; } +#if UNSUBTYPING_DEBUG + void dump(Module& wasm) { + for (auto& node : nodes) { + std::cerr << getTypeName(wasm, node.type); + if (auto super = getSupertype(node.type)) { + std::cerr << " <: " << getTypeName(wasm, *super); + } + std::cerr << ", children:"; + for (auto child : node.children) { + std::cerr << " " << getTypeName(wasm, nodes[child].type); + } + std::cerr << '\n'; + } + } +#endif + private: Index getIndex(HeapType type) { auto [it, inserted] = indices.insert({type, nodes.size()}); @@ -293,7 +325,10 @@ struct Unsubtyping : Pass { // Map from cast source types to their destinations. Map> casts; + DBG(Module* wasm = nullptr); + void run(Module* wasm) override { + DBG(this->wasm = wasm); if (!wasm->features.hasGC()) { return; } @@ -310,6 +345,7 @@ struct Unsubtyping : Pass { process(sub, super); } + DBG(types.dump(*wasm)); rewriteTypes(*wasm); // Cast types may be refinable if their source and target types are no @@ -324,6 +360,8 @@ struct Unsubtyping : Pass { if (sub == super || sub.isBottom()) { return; } + DBG(std::cerr << "noting " << getTypeName(*wasm, sub) + << " <: " << getTypeName(*wasm, super) << '\n'); work.push_back({sub, super}); } @@ -482,6 +520,8 @@ struct Unsubtyping : Pass { } void process(HeapType sub, HeapType super) { + DBG(std::cerr << "processing " << getTypeName(*wasm, sub) + << " <: " << getTypeName(*wasm, super) << '\n'); assert(HeapType::isSubType(sub, super)); auto oldSuper = types.getSupertype(sub); if (oldSuper) { From 1931b55893bc5e8cf14744a04f8539d499dbf39c Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Thu, 25 Sep 2025 18:09:40 -0700 Subject: [PATCH 2/2] [Custom Descriptors] Propagate subtyping to describees in Unsubtyping An upcoming change to the custom descriptor validation rules will require describees to be in a subtype relationship whenever their descriptors are. This is similar to the requirement we already have in the opposite direction. Update Unsubtyping to respect this upcoming rule. --- src/passes/Unsubtyping.cpp | 43 ++------------- test/lit/passes/unsubtyping-desc.wast | 78 +++++++++++++++++++++++++-- 2 files changed, 80 insertions(+), 41 deletions(-) diff --git a/src/passes/Unsubtyping.cpp b/src/passes/Unsubtyping.cpp index 98a88e1fa51..2d318dac3b6 100644 --- a/src/passes/Unsubtyping.cpp +++ b/src/passes/Unsubtyping.cpp @@ -534,7 +534,6 @@ struct Unsubtyping : Pass { } if (HeapType::isSubType(*oldSuper, super)) { // sub <: oldSuper <: super - processDescribed(sub, *oldSuper, super); noteSubtype(*oldSuper, super); // We already handled sub <: oldSuper, so we're done. return; @@ -544,7 +543,6 @@ 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. - processDescribed(sub, super, *oldSuper); process(super, *oldSuper); } @@ -556,42 +554,6 @@ struct Unsubtyping : Pass { processCasts(sub, super, oldSuper); } - void processDescribed(HeapType sub, HeapType mid, HeapType super) { - // We are establishing sub <: mid <: super. If super describes the immediate - // supertype of the type sub describes, then once we insert mid between them - // we would have this: - // - // A -> super - // ^ ^ - // | mid - // | ^ - // C -> sub - // - // This violates the requirement that the descriptor of C's immediate - // supertype must be the immediate supertype of C's descriptor. To fix it, - // we have to find the type B that mid describes and insert it between A and - // C: - // - // A -> super - // ^ ^ - // B -> mid - // ^ ^ - // C -> sub - // - // We do this eagerly before we establish sub <: mid <: super so that if - // establishing that subtyping requires recursively establishing other - // subtypings, we can depend on the invariant that the described types are - // always set up correctly beforehand. - auto subDescribed = sub.getDescribedType(); - auto superDescribed = super.getDescribedType(); - if (subDescribed && superDescribed && - types.getSupertype(*subDescribed) == superDescribed) { - auto midDescribed = mid.getDescribedType(); - assert(midDescribed); - process(*subDescribed, *midDescribed); - } - } - void processDefinitions(HeapType sub, HeapType super) { if (super.isBasic()) { return; @@ -627,6 +589,11 @@ struct Unsubtyping : Pass { noteSubtype(*desc, *superDesc); } } + if (auto desc = sub.getDescribedType()) { + if (auto superDesc = super.getDescribedType()) { + noteSubtype(*desc, *superDesc); + } + } } void diff --git a/test/lit/passes/unsubtyping-desc.wast b/test/lit/passes/unsubtyping-desc.wast index 69c8d3f4d56..adfdd1a0c85 100644 --- a/test/lit/passes/unsubtyping-desc.wast +++ b/test/lit/passes/unsubtyping-desc.wast @@ -49,14 +49,13 @@ (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 (descriptor $B.desc (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)))) ) - ;; Now we directly require B.desc <: A.desc. This does *not* imply B <: A, so - ;; we can optimize $B (but not $B.desc). + ;; 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)) @@ -134,3 +133,76 @@ ;; 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)) ) + +(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)))) + ) + + ;; 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: (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: (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)) +) + +(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)))) + ) + + ;; 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: (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))) +)