From f466bd391b1697f89e21f0b50c6edcf07bae3361 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 29 Sep 2025 16:22:15 -0700 Subject: [PATCH] [Custom Descriptors] Stop optimizing descriptors in GTO Due to the interactions between validation for descriptor and described types and validation for subtypes, it is simpler and more effective to optimize out unused descriptors in Unsubtyping, so we no longer need to do so in GTO. --- src/passes/GlobalTypeOptimization.cpp | 275 +----- test/lit/passes/gto-desc-tnh.wast | 265 ----- test/lit/passes/gto-desc.wast | 1278 +------------------------ test/lit/passes/unsubtyping-desc.wast | 35 + 4 files changed, 78 insertions(+), 1775 deletions(-) delete mode 100644 test/lit/passes/gto-desc-tnh.wast diff --git a/src/passes/GlobalTypeOptimization.cpp b/src/passes/GlobalTypeOptimization.cpp index 0124f4715d2..6316dc7e57e 100644 --- a/src/passes/GlobalTypeOptimization.cpp +++ b/src/passes/GlobalTypeOptimization.cpp @@ -24,7 +24,6 @@ #include "ir/eh-utils.h" #include "ir/localize.h" -#include "ir/module-utils.h" #include "ir/names.h" #include "ir/ordering.h" #include "ir/struct-utils.h" @@ -42,10 +41,8 @@ namespace { // Information about usage of a field. struct FieldInfo { - // This represents a normal write for normal fields. For a descriptor, we only - // note "dangerous" writes, specifically ones which might trap (when the - // descriptor in a struct.new is nullable), which is a special situation we - // must avoid. + // This represents a normal write for normal fields. (Unused descriptors are + // optimized in Unsubtyping instead.) bool hasWrite = false; bool hasRead = false; @@ -87,9 +84,8 @@ struct FieldInfoScanner HeapType type, Index index, FieldInfo& info) { - if (index == StructUtils::DescriptorIndex && expr->type.isNonNullable()) { - // A non-dangerous write to a descriptor, which as mentioned above, we do - // not track. + if (index == StructUtils::DescriptorIndex) { + // We do not optimize descriptors. Ignore them. return; } info.noteWrite(); @@ -105,6 +101,9 @@ struct FieldInfoScanner } void noteRead(HeapType type, Index index, FieldInfo& info) { + if (index == StructUtils::DescriptorIndex) { + return; + } info.noteRead(); } @@ -133,15 +132,6 @@ struct GlobalTypeOptimization : public Pass { static const Index RemovedField = Index(-1); std::unordered_map> indexesAfterRemovals; - // The types that no longer need a descriptor. - std::unordered_set haveUnneededDescriptors; - - // Descriptor types that are not needed by their described types but that - // still need to be descriptors for their own subtypes and supertypes to be - // valid. We will keep them descriptors by having them describe trivial new - // placeholder types. - std::unordered_set descriptorsOfPlaceholders; - void run(Module* module) override { if (!module->features.hasGC()) { return; @@ -151,8 +141,6 @@ struct GlobalTypeOptimization : public Pass { Fatal() << "GTO requires --closed-world"; } - auto trapsNeverHappen = getPassOptions().trapsNeverHappen; - // Find and analyze struct operations inside each function. StructUtils::FunctionStructValuesMap functionNewInfos(*module), functionSetGetInfos(*module); @@ -163,23 +151,6 @@ struct GlobalTypeOptimization : public Pass { // Combine the data from the functions. functionSetGetInfos.combineInto(combinedSetGetInfos); - // Custom descriptor handling: We need to look at struct.news, which - // normally we ignore (nothing in a struct.new can cause fields to remain - // mutable, or force the field to stay around. We cannot ignore them with CD - // because struct.news can now trap, and removing the descriptor could - // change things, so we must be careful. (Without traps, though, this is - // unnecessary.) - if (module->features.hasCustomDescriptors() && !trapsNeverHappen) { - for (auto& [func, infos] : functionNewInfos) { - for (auto& [type, info] : infos) { - if (info.desc.hasWrite) { - // Copy the descriptor write to the info we will propagate below. - combinedSetGetInfos[type].desc.noteWrite(); - } - } - } - } - // Propagate information to super and subtypes on set/get infos: // // * For removing unread fields, we can only remove a field if it is never @@ -408,85 +379,18 @@ struct GlobalTypeOptimization : public Pass { indexesAfterRemovals[type] = indexesAfterRemoval; } } - - // Process the descriptor. - if (auto desc = type.getDescriptorType()) { - // To remove a descriptor, it must not be read via supertypes and it - // must not have to remain in supertypes because e.g. they are public. - // It must also have no write (see above, we note only dangerous writes - // which might trap), as if it could trap, we'd have no easy way to - // remove it in a global scope. - // TODO: We could check and handle the global scope specifically, but - // the trapsNeverHappen flag avoids this problem entirely anyhow. - // - // This does not handle descriptor subtyping, see below. - auto super = type.getDeclaredSuperType(); - bool superNeedsDescriptor = super && super->getDescriptorType() && - !haveUnneededDescriptors.count(*super); - bool descriptorIsUsed = - dataFromSupers.desc.hasRead || - (dataFromSupers.desc.hasWrite && !trapsNeverHappen); - if (!superNeedsDescriptor && !descriptorIsUsed) { - haveUnneededDescriptors.insert(type); - } - } - } - - // Handle descriptor subtyping: - // - // A -> A.desc - // ^ - // B -> B.desc - // - // Say we want to optimize A to no longer have a descriptor. Then A.desc - // will no longer describe A. But A.desc still needs to be a descriptor for - // it to remain a valid supertype of B.desc. To allow the optimization of A - // to proceed, we will introduce a placeholder type for A.desc to describe, - // keeping it a descriptor type. - if (!haveUnneededDescriptors.empty()) { - StructUtils::TypeHierarchyPropagator - descPropagator(subTypes); - - // Populate the initial data: Any descriptor we did not see was unneeded, - // is needed. - StructUtils::TypeHierarchyPropagator< - StructUtils::CombinableBool>::StructMap remainingDesciptors; - for (auto type : subTypes.types) { - if (auto desc = type.getDescriptorType()) { - if (!haveUnneededDescriptors.count(type)) { - // This descriptor type is needed. - remainingDesciptors[*desc].value = true; - } - } - } - - // Propagate. - descPropagator.propagateToSuperAndSubTypes(remainingDesciptors); - - // Determine the set of descriptor types that will need placeholder - // describees. - for (auto [type, kept] : remainingDesciptors) { - if (kept.value) { - auto desc = type.getDescribedType(); - assert(desc); - if (haveUnneededDescriptors.count(*desc)) { - descriptorsOfPlaceholders.insert(type); - } - } - } } // If we found things that can be removed, remove them from instructions. // (Note that we must do this first, while we still have the old heap types // that we can identify, and only after this should we update all the types // throughout the module.) - if (!indexesAfterRemovals.empty() || !haveUnneededDescriptors.empty()) { + if (!indexesAfterRemovals.empty()) { updateInstructions(*module); } // Update the types in the entire module. - if (!indexesAfterRemovals.empty() || !canBecomeImmutable.empty() || - !haveUnneededDescriptors.empty()) { + if (!indexesAfterRemovals.empty() || !canBecomeImmutable.empty()) { updateTypes(*module); } } @@ -494,41 +398,10 @@ struct GlobalTypeOptimization : public Pass { void updateTypes(Module& wasm) { class TypeRewriter : public GlobalTypeRewriter { GlobalTypeOptimization& parent; - // Differentiate the first occurrence of a descriptor of a placeholder - // type, which is actually saving space for the placeholder itself, and - // the next occurrence, which is real. - bool sawPlaceholder = false; public: TypeRewriter(Module& wasm, GlobalTypeOptimization& parent) : GlobalTypeRewriter(wasm), parent(parent) {} - - std::vector getSortedTypes(PredecessorGraph preds) override { - auto types = GlobalTypeRewriter::getSortedTypes(std::move(preds)); - // Intersperse placeholder types throughout the sorted types. Each - // descriptor type that needs a placeholder describee will be duplicated - // in the list of sorted types. The first appearance will be modified to - // become the placeholder describee and the subsequent appearance will - // be modified to describe the preceding placeholder. We represent the - // placeholder slots here using their intended descriptor types for two - // reasons: first, it lets us easily recognize the placeholder slots - // in modifyTypeBuilderEntry. Second, it ensures that the type-to-index - // mapping created in GlobalTypeRewriter::rebuiltTypes does not end up - // mapping the placeholder types to any index, since their mappings are - // immediately overwritten by the following "real" occurrences of the - // descriptor types. - std::vector typesWithPlaceholders; - for (auto type : types) { - if (parent.descriptorsOfPlaceholders.count(type)) { - // Insert the type an extra time to make space for the placeholder. - typesWithPlaceholders.push_back(type); - } - typesWithPlaceholders.push_back(type); - } - - return typesWithPlaceholders; - } - void modifyStruct(HeapType oldStructType, Struct& struct_) override { auto& newFields = struct_.fields; @@ -581,45 +454,6 @@ struct GlobalTypeOptimization : public Pass { } } } - - void modifyTypeBuilderEntry(TypeBuilder& typeBuilder, - Index i, - HeapType oldType) override { - if (!oldType.isStruct()) { - return; - } - - // Remove an unneeded descriptor. - if (parent.haveUnneededDescriptors.count(oldType)) { - typeBuilder[i].descriptor(std::nullopt); - } - - // Remove an unneeded describee or describe a placeholder type. - if (auto described = oldType.getDescribedType()) { - if (parent.haveUnneededDescriptors.count(*described)) { - if (parent.descriptorsOfPlaceholders.count(oldType)) { - if (!sawPlaceholder) { - // This is the placeholder describee. Set its descriptor to be - // the succeeding real descriptor type. - typeBuilder[i] = Struct{}; - typeBuilder[i].setShared(described->getShared()); - typeBuilder[i].descriptor(typeBuilder[i + 1]); - typeBuilder[i].describes(std::nullopt); - typeBuilder[i].subTypeOf(std::nullopt); - sawPlaceholder = true; - } else { - // This is the real placedholder-describing descriptor type. - // Have it describe the preceding placeholder describee. - typeBuilder[i].describes(typeBuilder[i - 1]); - sawPlaceholder = false; - } - } else { - // This type no longer needs its describee. - typeBuilder[i].describes(std::nullopt); - } - } - } - } }; TypeRewriter(wasm, *this).update(); @@ -650,20 +484,18 @@ struct GlobalTypeOptimization : public Pass { if (curr->type == Type::unreachable) { return; } + if (curr->isWithDefault()) { + // No indices to remove. + return; + } auto type = curr->type.getHeapType(); - auto removeDesc = parent.haveUnneededDescriptors.count(type); - - // There may be no indexes to remove, if we are only removing the - // descriptor. - std::vector* indexesAfterRemoval = nullptr; - // There are also no indexes to remove if we only write default values. - if (!curr->isWithDefault()) { - auto iter = parent.indexesAfterRemovals.find(type); - if (iter != parent.indexesAfterRemovals.end()) { - indexesAfterRemoval = &iter->second; - } + + auto iter = parent.indexesAfterRemovals.find(type); + if (iter == parent.indexesAfterRemovals.end()) { + return; } + std::vector& indexesAfterRemoval = iter->second; // Ensure any children with non-trivial effects are replaced with // local.gets, so that we can remove/reorder to our hearts' content. @@ -678,45 +510,32 @@ struct GlobalTypeOptimization : public Pass { needEHFixups = true; } - if (indexesAfterRemoval) { - // Remove and reorder operands. - auto& operands = curr->operands; - assert(indexesAfterRemoval->size() == operands.size()); - - Index removed = 0; - std::vector old(operands.begin(), operands.end()); - for (Index i = 0; i < operands.size(); ++i) { - auto newIndex = (*indexesAfterRemoval)[i]; - if (newIndex != RemovedField) { - assert(newIndex < operands.size()); - operands[newIndex] = old[i]; - } else { - ++removed; - if (!func && - EffectAnalyzer(getPassOptions(), *getModule(), old[i]).trap) { - removedTrappingInits.push_back(old[i]); - } - } - } - if (removed) { - operands.resize(operands.size() - removed); + // Remove and reorder operands. + auto& operands = curr->operands; + assert(indexesAfterRemoval.size() == operands.size()); + + Index removed = 0; + std::vector old(operands.begin(), operands.end()); + for (Index i = 0; i < operands.size(); ++i) { + auto newIndex = indexesAfterRemoval[i]; + if (newIndex != RemovedField) { + assert(newIndex < operands.size()); + operands[newIndex] = old[i]; } else { - // If we didn't remove anything then we must have reordered (or else - // we have done pointless work). - assert(*indexesAfterRemoval != - makeIdentity(indexesAfterRemoval->size())); + ++removed; + if (!func && + EffectAnalyzer(getPassOptions(), *getModule(), old[i]).trap) { + removedTrappingInits.push_back(old[i]); + } } } - - if (removeDesc) { - // We already handled the case of a possible trap here, so we can - // remove the descriptor, but must be careful of nested effects (our - // descriptor may be ok to remove, but a nested struct.new may not). - if (!func && - EffectAnalyzer(getPassOptions(), *getModule(), curr->desc).trap) { - removedTrappingInits.push_back(curr->desc); - } - curr->desc = nullptr; + if (removed) { + operands.resize(operands.size() - removed); + } else { + // If we didn't remove anything then we must have reordered (or else + // we have done pointless work). + assert(indexesAfterRemoval != + makeIdentity(indexesAfterRemoval.size())); } } @@ -761,18 +580,6 @@ struct GlobalTypeOptimization : public Pass { curr->index = newIndex; } - void visitRefCast(RefCast* curr) { - // Unreachable ref.cast_desc instructions would not have been counted as - // reading the descriptor field, so their descriptor operands may no - // longer be descriptors. This is invalid, so replace such casts - // entirely. - if (curr->type == Type::unreachable && curr->desc && - curr->desc->type != Type::unreachable) { - assert(curr->ref->type == Type::unreachable); - replaceCurrent(curr->ref); - } - } - void visitFunction(Function* curr) { if (needEHFixups) { EHUtils::handleBlockNestedPops(curr, *getModule()); diff --git a/test/lit/passes/gto-desc-tnh.wast b/test/lit/passes/gto-desc-tnh.wast deleted file mode 100644 index 2a6c10be046..00000000000 --- a/test/lit/passes/gto-desc-tnh.wast +++ /dev/null @@ -1,265 +0,0 @@ -;; 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 --gto --preserve-type-order -S -o - | filecheck %s -;; RUN: foreach %s %t wasm-opt -tnh -all --closed-world --gto --preserve-type-order -S -o - | filecheck %s --check-prefix=T_N_H - -;; The descriptor passed in is nullable, so it might trap. We only remove it -;; when traps never happen. -(module - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $A (descriptor $B (struct))) - ;; T_N_H: (rec - ;; T_N_H-NEXT: (type $A (struct)) - (type $A (descriptor $B (struct))) - ;; CHECK: (type $B (describes $A (struct))) - ;; T_N_H: (type $B (struct)) - (type $B (describes $A (struct))) - ) - - ;; CHECK: (type $2 (func (param (ref null (exact $B))))) - - ;; CHECK: (func $test (type $2) (param $nullable (ref null (exact $B))) - ;; CHECK-NEXT: (local $A (ref $A)) - ;; CHECK-NEXT: (local $B (ref $B)) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (struct.new_default $A - ;; CHECK-NEXT: (local.get $nullable) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; T_N_H: (type $2 (func (param (ref null (exact $B))))) - - ;; T_N_H: (func $test (type $2) (param $nullable (ref null (exact $B))) - ;; T_N_H-NEXT: (local $A (ref $A)) - ;; T_N_H-NEXT: (local $B (ref $B)) - ;; T_N_H-NEXT: (drop - ;; T_N_H-NEXT: (struct.new_default $A) - ;; T_N_H-NEXT: ) - ;; T_N_H-NEXT: ) - (func $test (param $nullable (ref null (exact $B))) - (local $A (ref $A)) - (local $B (ref $B)) - (drop - (struct.new $A - (local.get $nullable) - ) - ) - ) -) - -;; As above, with the struct.new in the global scope. -(module - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $A (descriptor $B (struct))) - ;; T_N_H: (rec - ;; T_N_H-NEXT: (type $A (struct)) - (type $A (descriptor $B (struct))) - ;; CHECK: (type $B (describes $A (struct))) - ;; T_N_H: (type $B (struct)) - (type $B (describes $A (struct))) - ) - - - ;; CHECK: (type $2 (func)) - - ;; CHECK: (global $g anyref (struct.new_default $A - ;; CHECK-NEXT: (ref.null none) - ;; CHECK-NEXT: )) - ;; T_N_H: (type $2 (func)) - - ;; T_N_H: (global $g anyref (struct.new_default $A)) - (global $g anyref (struct.new $A - (ref.null $B) - )) - - ;; CHECK: (func $test (type $2) - ;; CHECK-NEXT: (local $A (ref $A)) - ;; CHECK-NEXT: (local $B (ref $B)) - ;; CHECK-NEXT: ) - ;; T_N_H: (func $test (type $2) - ;; T_N_H-NEXT: (local $A (ref $A)) - ;; T_N_H-NEXT: (local $B (ref $B)) - ;; T_N_H-NEXT: ) - (func $test - (local $A (ref $A)) - (local $B (ref $B)) - ) -) - -;; $A and $B have descriptors, and the descriptors also subtype: -;; -;; A -> A.desc -;; ^ -;; B -> B.desc -;; -;; $B is written a null descriptor, so we cannot optimize it when traps are -;; possible. This means $A.desc must remain a descriptor even as we optimize $A, -;; so we give $A.desc a placeholder describee. With TNH, we can optimize without -;; the placeholder. -;; -;; This tests subtyping of descriptors *without* subtyping of describees. -(module - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $A (sub (struct))) - ;; T_N_H: (rec - ;; T_N_H-NEXT: (type $A (sub (struct))) - (type $A (sub (descriptor $A.desc (struct)))) - ;; CHECK: (type $1 (sub (descriptor $A.desc (struct)))) - - ;; CHECK: (type $A.desc (sub (describes $1 (struct)))) - ;; T_N_H: (type $A.desc (sub (struct))) - (type $A.desc (sub (describes $A (struct )))) - - ;; CHECK: (type $B (sub (descriptor $B.desc (struct)))) - ;; T_N_H: (type $B (sub (struct))) - (type $B (sub (descriptor $B.desc (struct)))) - ;; CHECK: (type $B.desc (sub $A.desc (describes $B (struct)))) - ;; T_N_H: (type $B.desc (sub $A.desc (struct))) - (type $B.desc (sub $A.desc (describes $B (struct)))) - ) - - ;; CHECK: (type $5 (func)) - - ;; CHECK: (func $test (type $5) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (struct.new_default $B - ;; CHECK-NEXT: (ref.null none) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; T_N_H: (type $4 (func)) - - ;; T_N_H: (func $test (type $4) - ;; T_N_H-NEXT: (drop - ;; T_N_H-NEXT: (struct.new_default $B) - ;; T_N_H-NEXT: ) - ;; T_N_H-NEXT: ) - (func $test - (drop - (struct.new_default $B - (ref.null none) - ) - ) - ) -) - -;; A similar situation, but now the thing that stops optimizing $B is not a -;; null descriptor but a use. We cannot optimize even without traps. -;; Subtyping of descriptors *without* subtyping of describees. -;; -;; $A's descriptor can be removed, but $A.desc needs to be given a placeholder -;; describee. -(module - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $A (sub (descriptor $A.desc (struct)))) - ;; T_N_H: (rec - ;; T_N_H-NEXT: (type $A (sub (descriptor $A.desc (struct)))) - (type $A (sub (descriptor $A.desc (struct)))) - ;; CHECK: (type $A.desc (sub (describes $A (struct)))) - ;; T_N_H: (type $A.desc (sub (describes $A (struct)))) - (type $A.desc (sub (describes $A (struct )))) - - ;; CHECK: (type $B (sub (struct))) - ;; T_N_H: (type $B (sub (struct))) - (type $B (sub (descriptor $B.desc (struct)))) - ;; CHECK: (type $3 (sub (descriptor $B.desc (struct)))) - - ;; CHECK: (type $B.desc (sub $A.desc (describes $3 (struct)))) - ;; T_N_H: (type $3 (sub (descriptor $B.desc (struct)))) - - ;; T_N_H: (type $B.desc (sub $A.desc (describes $3 (struct)))) - (type $B.desc (sub $A.desc (describes $B (struct)))) - ) - - ;; CHECK: (type $5 (func)) - - ;; CHECK: (func $test (type $5) - ;; CHECK-NEXT: (local $B (ref $B)) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (ref.get_desc $A - ;; CHECK-NEXT: (struct.new_default $A - ;; CHECK-NEXT: (struct.new_default $A.desc) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; T_N_H: (type $5 (func)) - - ;; T_N_H: (func $test (type $5) - ;; T_N_H-NEXT: (local $B (ref $B)) - ;; T_N_H-NEXT: (drop - ;; T_N_H-NEXT: (ref.get_desc $A - ;; T_N_H-NEXT: (struct.new_default $A - ;; T_N_H-NEXT: (struct.new_default $A.desc) - ;; T_N_H-NEXT: ) - ;; T_N_H-NEXT: ) - ;; T_N_H-NEXT: ) - ;; T_N_H-NEXT: ) - (func $test - (local $B (ref $B)) ;; keep $B alive - (drop - (ref.get_desc $A - (struct.new $A - (struct.new $A.desc) - ) - ) - ) - ) -) - -;; A chain of descriptors, where we can remove the outer one, but must be -;; careful not to remove nested children. -(module - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $A (struct)) - ;; T_N_H: (rec - ;; T_N_H-NEXT: (type $A (struct)) - (type $A (descriptor $B (struct))) - ;; CHECK: (type $B (descriptor $C (struct))) - ;; T_N_H: (type $B (struct)) - (type $B (describes $A (descriptor $C (struct)))) - ;; CHECK: (type $C (describes $B (struct))) - ;; T_N_H: (type $C (struct)) - (type $C (describes $B (struct))) - ) - - ;; CHECK: (type $3 (func)) - - ;; CHECK: (global $g anyref (struct.new_default $A)) - ;; T_N_H: (type $3 (func)) - - ;; T_N_H: (global $g anyref (struct.new_default $A)) - (global $g anyref - (struct.new $A ;; The outer struct.new $A is ok to remove, - (struct.new $B ;; but the inner struct.new $B is not, due - (ref.null $C) ;; to its null descriptor. We keep the inner - ) ;; one around as a new global later (but not - ) ;; if traps cannot happen). - ) - - ;; CHECK: (global $gto-removed-0 (ref (exact $B)) (struct.new_default $B - ;; CHECK-NEXT: (ref.null none) - ;; CHECK-NEXT: )) - - ;; CHECK: (func $test (type $3) - ;; CHECK-NEXT: (local $A (ref $A)) - ;; CHECK-NEXT: (local $B (ref $B)) - ;; CHECK-NEXT: (local $C (ref $C)) - ;; CHECK-NEXT: ) - ;; T_N_H: (func $test (type $3) - ;; T_N_H-NEXT: (local $A (ref $A)) - ;; T_N_H-NEXT: (local $B (ref $B)) - ;; T_N_H-NEXT: (local $C (ref $C)) - ;; T_N_H-NEXT: ) - (func $test - (local $A (ref $A)) - (local $B (ref $B)) - (local $C (ref $C)) - ) -) - diff --git a/test/lit/passes/gto-desc.wast b/test/lit/passes/gto-desc.wast index 77b051db95b..1f6fe34b08a 100644 --- a/test/lit/passes/gto-desc.wast +++ b/test/lit/passes/gto-desc.wast @@ -17,8 +17,6 @@ ;; CHECK: (type $4 (func (param (ref $struct) (ref $used-pair)))) - ;; CHECK: (type $5 (func (param (ref $struct)))) - ;; CHECK: (global $nullable-desc (ref null (exact $desc)) (struct.new_default $desc)) (global $nullable-desc (ref null (exact $desc)) (struct.new $desc)) @@ -129,280 +127,10 @@ ) ) ) - - ;; CHECK: (func $use-desc (type $5) (param $struct (ref $struct)) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (ref.get_desc $struct - ;; CHECK-NEXT: (local.get $struct) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $use-desc (param $struct (ref $struct)) - ;; Use the descriptor, so this test is not trivial. - (drop - (ref.get_desc $struct - (local.get $struct) - ) - ) - ) -) - -;; The descriptor here is not needed. -(module - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $A (struct)) - (type $A (descriptor $B (struct))) - ;; CHECK: (type $B (struct)) - (type $B (describes $A (struct))) - ) - - ;; CHECK: (type $2 (func)) - - ;; CHECK: (func $test (type $2) - ;; CHECK-NEXT: (local $A (ref $A)) - ;; CHECK-NEXT: (local $B (ref $B)) - ;; CHECK-NEXT: ) - (func $test - (local $A (ref $A)) - (local $B (ref $B)) - ) -) - -;; As above, but even creating the type does not force us to keep the -;; descriptor, if it is never used. -(module - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $A (struct)) - (type $A (descriptor $B (struct))) - ;; CHECK: (type $B (struct)) - (type $B (describes $A (struct))) - ) - - ;; CHECK: (type $2 (func)) - - ;; CHECK: (func $test (type $2) - ;; CHECK-NEXT: (local $A (ref $A)) - ;; CHECK-NEXT: (local $B (ref $B)) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (struct.new_default $A) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $test - (local $A (ref $A)) - (local $B (ref $B)) - (drop - (struct.new $A - (struct.new $B) - ) - ) - ) -) - -;; As above, with the creation in the global scope. -(module - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $A (struct)) - (type $A (descriptor $B (struct))) - ;; CHECK: (type $B (struct)) - (type $B (describes $A (struct))) - ) - - - ;; CHECK: (type $2 (func)) - - ;; CHECK: (global $g anyref (struct.new_default $A)) - (global $g anyref (struct.new $A - (struct.new $B) - )) - - ;; CHECK: (func $test (type $2) - ;; CHECK-NEXT: (local $A (ref $A)) - ;; CHECK-NEXT: (local $B (ref $B)) - ;; CHECK-NEXT: ) - (func $test - (local $A (ref $A)) - (local $B (ref $B)) - ) -) - -;; Both descriptors in this chain are unneeded. -(module - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $A (struct)) - (type $A (descriptor $B (struct))) - ;; CHECK: (type $B (struct)) - (type $B (describes $A (descriptor $C (struct)))) - ;; CHECK: (type $C (struct)) - (type $C (describes $B (struct))) - ) - - ;; CHECK: (type $3 (func)) - - ;; CHECK: (func $test (type $3) - ;; CHECK-NEXT: (local $A (ref $A)) - ;; CHECK-NEXT: (local $B (ref $B)) - ;; CHECK-NEXT: (local $C (ref $C)) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (struct.new_default $A) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $test - (local $A (ref $A)) - (local $B (ref $B)) - (local $C (ref $C)) - (drop - (struct.new $A - (struct.new $B - (struct.new $C) - ) - ) - ) - ) -) - -;; ref.get_desc keeps the descriptor. -(module - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $A (descriptor $B (struct))) - (type $A (descriptor $B (struct))) - ;; CHECK: (type $B (describes $A (struct))) - (type $B (describes $A (struct))) - ) - - ;; CHECK: (type $2 (func)) - - ;; CHECK: (func $test (type $2) - ;; CHECK-NEXT: (local $A (ref $A)) - ;; CHECK-NEXT: (local $B (ref $B)) - ;; CHECK-NEXT: (local.set $A - ;; CHECK-NEXT: (struct.new_default $A - ;; CHECK-NEXT: (struct.new_default $B) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (ref.get_desc $A - ;; CHECK-NEXT: (local.get $A) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $test - (local $A (ref $A)) - (local $B (ref $B)) - (local.set $A - (struct.new $A - (struct.new $B) - ) - ) - (drop - (ref.get_desc $A - (local.get $A) - ) - ) - ) -) - -;; ref.cast_desc keeps the descriptor. -(module - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $A (descriptor $B (struct))) - (type $A (descriptor $B (struct))) - ;; CHECK: (type $B (describes $A (struct))) - (type $B (describes $A (struct))) - ) - - ;; CHECK: (type $2 (func)) - - ;; CHECK: (func $test (type $2) - ;; CHECK-NEXT: (local $A (ref $A)) - ;; CHECK-NEXT: (local $B (ref $B)) - ;; CHECK-NEXT: (local.set $A - ;; CHECK-NEXT: (struct.new_default $A - ;; CHECK-NEXT: (struct.new_default $B) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (ref.cast_desc (ref (exact $A)) - ;; CHECK-NEXT: (local.get $A) - ;; CHECK-NEXT: (struct.new_default $B) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $test - (local $A (ref $A)) - (local $B (ref $B)) - (local.set $A - (struct.new $A - (struct.new $B) - ) - ) - (drop - (ref.cast_desc (ref null (exact $A)) - (local.get $A) - (struct.new $B) - ) - ) - ) -) - -;; br_on_cast_desc keeps the descriptor. -(module - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $A (descriptor $B (struct))) - (type $A (descriptor $B (struct))) - ;; CHECK: (type $B (describes $A (struct))) - (type $B (describes $A (struct))) - ) - - ;; CHECK: (type $2 (func)) - - ;; CHECK: (func $test (type $2) - ;; CHECK-NEXT: (local $A (ref $A)) - ;; CHECK-NEXT: (local $B (ref $B)) - ;; CHECK-NEXT: (local.set $A - ;; CHECK-NEXT: (struct.new_default $A - ;; CHECK-NEXT: (struct.new_default $B) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (block $l (result (ref null $A)) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (br_on_cast_desc $l (ref $A) (ref (exact $A)) - ;; CHECK-NEXT: (local.get $A) - ;; CHECK-NEXT: (struct.new_default $B) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (unreachable) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $test - (local $A (ref $A)) - (local $B (ref $B)) - (local.set $A - (struct.new $A - (struct.new $B) - ) - ) - (drop - (block $l (result (ref null $A)) - (br_on_cast_desc $l anyref (ref null $A) - (local.get $A) - (struct.new $B) - ) - (unreachable) - ) - ) - ) ) -;; br_on_cast_desc_fail keeps the descriptor. +;; The descriptor here is not needed, but we now optimize descriptors in +;; Unsubtyping, so we do nothing here. (module (rec ;; CHECK: (rec @@ -417,1013 +145,11 @@ ;; CHECK: (func $test (type $2) ;; CHECK-NEXT: (local $A (ref $A)) ;; CHECK-NEXT: (local $B (ref $B)) - ;; CHECK-NEXT: (local.set $A - ;; CHECK-NEXT: (struct.new_default $A - ;; CHECK-NEXT: (struct.new_default $B) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (block $l (result (ref null $A)) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (br_on_cast_desc_fail $l (ref $A) (ref (exact $A)) - ;; CHECK-NEXT: (local.get $A) - ;; CHECK-NEXT: (struct.new_default $B) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (unreachable) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $test (local $A (ref $A)) (local $B (ref $B)) - (local.set $A - (struct.new $A - (struct.new $B) - ) - ) - (drop - (block $l (result (ref null $A)) - (br_on_cast_desc_fail $l anyref (ref null $A) - (local.get $A) - (struct.new $B) - ) - (unreachable) - ) - ) - ) -) - -;; The descriptor can be removed, but its effects must be preserved. -(module - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $A (struct)) - (type $A (descriptor $B (struct))) - ;; CHECK: (type $B (struct)) - (type $B (describes $A (struct))) - ) - - ;; CHECK: (type $2 (func)) - - ;; CHECK: (func $test (type $2) - ;; CHECK-NEXT: (local $A (ref $A)) - ;; CHECK-NEXT: (local $B (ref (exact $B))) - ;; CHECK-NEXT: (local $2 (ref (exact $B))) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (block (result (ref (exact $A))) - ;; CHECK-NEXT: (local.set $2 - ;; CHECK-NEXT: (local.tee $B - ;; CHECK-NEXT: (struct.new_default $B) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (struct.new_default $A) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $test - (local $A (ref $A)) - (local $B (ref (exact $B))) - (drop - (struct.new $A - (local.tee $B - (struct.new $B) - ) - ) - ) - ) -) - -;; Test removing indexes /immutability as well as removing a descriptor. -(module - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $A (struct (field $c i32))) - (type $A (descriptor $B (struct (field $a (mut i32)) (field $b (mut i32)) (field $c (mut i32))))) - ;; CHECK: (type $B (struct)) - (type $B (describes $A (struct (field (mut i32))))) - ) - - ;; CHECK: (type $2 (func)) - - ;; CHECK: (func $test (type $2) - ;; CHECK-NEXT: (local $A (ref $A)) - ;; CHECK-NEXT: (local $B (ref (exact $B))) - ;; CHECK-NEXT: (local $2 (ref (exact $B))) - ;; CHECK-NEXT: (local.set $A - ;; CHECK-NEXT: (block (result (ref (exact $A))) - ;; CHECK-NEXT: (local.set $2 - ;; CHECK-NEXT: (local.tee $B - ;; CHECK-NEXT: (struct.new_default $B) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (struct.new $A - ;; CHECK-NEXT: (i32.const 30) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (ref.as_non_null - ;; CHECK-NEXT: (block (result (ref $A)) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.const 42) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $A) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (struct.get $A $c - ;; CHECK-NEXT: (local.get $A) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (ref.as_non_null - ;; CHECK-NEXT: (block (result (ref (exact $B))) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.const 9999) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $B) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $test - (local $A (ref $A)) - (local $B (ref (exact $B))) - (local.set $A - (struct.new $A - (i32.const 10) - (i32.const 20) - (i32.const 30) - (local.tee $B - (struct.new $B - (i32.const 1337) - ) - ) - ) - ) - ;; Write only the middle field (we can remove it as write-only). - (struct.set $A $b - (local.get $A) - (i32.const 42) - ) - ;; Read only the last field. We can make it immutable. - (drop - (struct.get $A $c - (local.get $A) - ) - ) - ;; Also mutate the field in the descriptor, which should not bother us. - (struct.set $B 0 - (local.get $B) - (i32.const 9999) - ) ) ) -;; As above, but with struct.new_default. -(module - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $A (struct (field i32))) - (type $A (descriptor $B (struct (field (mut i32)) (field (mut i32)) (field (mut i32))))) - ;; CHECK: (type $B (struct)) - (type $B (describes $A (struct (field (mut i32))))) - ) - - ;; CHECK: (type $2 (func)) - - ;; CHECK: (func $test (type $2) - ;; CHECK-NEXT: (local $A (ref $A)) - ;; CHECK-NEXT: (local $B (ref (exact $B))) - ;; CHECK-NEXT: (local $2 (ref (exact $B))) - ;; CHECK-NEXT: (local.set $A - ;; CHECK-NEXT: (block (result (ref (exact $A))) - ;; CHECK-NEXT: (local.set $2 - ;; CHECK-NEXT: (local.tee $B - ;; CHECK-NEXT: (struct.new_default $B) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (struct.new_default $A) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (ref.as_non_null - ;; CHECK-NEXT: (block (result (ref $A)) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.const 42) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $A) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (struct.get $A 0 - ;; CHECK-NEXT: (local.get $A) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (ref.as_non_null - ;; CHECK-NEXT: (block (result (ref (exact $B))) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.const 9999) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $B) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $test - (local $A (ref $A)) - (local $B (ref (exact $B))) - (local.set $A - (struct.new_default $A - (local.tee $B - (struct.new $B - (i32.const 1337) - ) - ) - ) - ) - (struct.set $A 1 - (local.get $A) - (i32.const 42) - ) - (drop - (struct.get $A 2 - (local.get $A) - ) - ) - (struct.set $B 0 - (local.get $B) - (i32.const 9999) - ) - ) -) - -;; Multiple types with descriptors, only one of whom can be removed. -(module - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $A (struct)) - (type $A (descriptor $B (struct))) - ;; CHECK: (type $B (struct)) - (type $B (describes $A (struct))) - - ;; CHECK: (type $C (descriptor $D (struct))) - (type $C (descriptor $D (struct))) - ;; CHECK: (type $D (describes $C (struct))) - (type $D (describes $C (struct))) - ) - - ;; CHECK: (type $4 (func)) - - ;; CHECK: (func $test (type $4) - ;; CHECK-NEXT: (local $A (ref $A)) - ;; CHECK-NEXT: (local $C (ref $C)) - ;; CHECK-NEXT: (local.set $A - ;; CHECK-NEXT: (struct.new_default $A) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $C - ;; CHECK-NEXT: (struct.new_default $C - ;; CHECK-NEXT: (struct.new_default $D) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (ref.get_desc $C - ;; CHECK-NEXT: (local.get $C) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $test - (local $A (ref $A)) - (local $C (ref $C)) - (local.set $A - (struct.new $A - (struct.new $B) - ) - ) - (local.set $C - (struct.new $C - (struct.new $D) - ) - ) - ;; Use the descriptor in $C but not $A. - (drop - (ref.get_desc $C - (local.get $C) - ) - ) - ) -) - -;; Subtyping. All these descriptors can be optimized away. -(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 $A.desc (struct))) - (type $B.desc (sub $A.desc (describes $B (struct)))) - - ;; CHECK: (type $C (sub $B (struct))) - (type $C (sub $B (descriptor $C.desc (struct)))) - ;; CHECK: (type $C.desc (sub $B.desc (struct))) - (type $C.desc (sub $B.desc (describes $C (struct)))) - ) - - ;; CHECK: (type $6 (func)) - - ;; CHECK: (func $test (type $6) - ;; CHECK-NEXT: (local $A (ref $A)) - ;; CHECK-NEXT: (local $A.desc (ref $A.desc)) - ;; CHECK-NEXT: (local $B (ref $B)) - ;; CHECK-NEXT: (local $B.desc (ref $B.desc)) - ;; CHECK-NEXT: (local $C (ref $C)) - ;; CHECK-NEXT: (local $C.desc (ref $C.desc)) - ;; CHECK-NEXT: (local.set $A - ;; CHECK-NEXT: (struct.new_default $A) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $B - ;; CHECK-NEXT: (struct.new_default $B) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $C - ;; CHECK-NEXT: (struct.new_default $C) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $test - (local $A (ref $A)) - (local $A.desc (ref $A.desc)) - (local $B (ref $B)) - (local $B.desc (ref $B.desc)) - (local $C (ref $C)) - (local $C.desc (ref $C.desc)) - (local.set $A - (struct.new $A - (struct.new $A.desc) - ) - ) - (local.set $B - (struct.new $B - (struct.new $B.desc) - ) - ) - (local.set $C - (struct.new $C - (struct.new $C.desc) - ) - ) - ) -) - -;; As above, but add a use of $A's descriptor. We cannot remove a descriptor -;; without removing it from supertypes, so we cannot optimize anything. -(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 $C (sub $B (descriptor $C.desc (struct)))) - (type $C (sub $B (descriptor $C.desc (struct)))) - ;; CHECK: (type $C.desc (sub $B.desc (describes $C (struct)))) - (type $C.desc (sub $B.desc (describes $C (struct)))) - ) - - ;; CHECK: (type $6 (func)) - - ;; CHECK: (func $test (type $6) - ;; CHECK-NEXT: (local $A (ref $A)) - ;; CHECK-NEXT: (local $A.desc (ref $A.desc)) - ;; CHECK-NEXT: (local $B (ref $B)) - ;; CHECK-NEXT: (local $B.desc (ref $B.desc)) - ;; CHECK-NEXT: (local $C (ref $C)) - ;; CHECK-NEXT: (local $C.desc (ref $C.desc)) - ;; CHECK-NEXT: (local.set $A - ;; CHECK-NEXT: (struct.new_default $A - ;; CHECK-NEXT: (struct.new_default $A.desc) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $B - ;; CHECK-NEXT: (struct.new_default $B - ;; CHECK-NEXT: (struct.new_default $B.desc) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $C - ;; CHECK-NEXT: (struct.new_default $C - ;; CHECK-NEXT: (struct.new_default $C.desc) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (ref.get_desc $A - ;; CHECK-NEXT: (local.get $A) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $test - (local $A (ref $A)) - (local $A.desc (ref $A.desc)) - (local $B (ref $B)) - (local $B.desc (ref $B.desc)) - (local $C (ref $C)) - (local $C.desc (ref $C.desc)) - (local.set $A - (struct.new $A - (struct.new $A.desc) - ) - ) - (local.set $B - (struct.new $B - (struct.new $B.desc) - ) - ) - (local.set $C - (struct.new $C - (struct.new $C.desc) - ) - ) - ;; This is new. - (drop - (ref.get_desc $A - (local.get $A) - ) - ) - ) -) - -;; As above, but use $B's. Now we can optimize $A's descriptor, but we need to -;; give it a placeholder type to describe. -(module - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $A (sub (struct))) - (type $A (sub (descriptor $A.desc (struct)))) - ;; CHECK: (type $1 (sub (descriptor $A.desc (struct)))) - - ;; CHECK: (type $A.desc (sub (describes $1 (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 $C (sub $B (descriptor $C.desc (struct)))) - (type $C (sub $B (descriptor $C.desc (struct)))) - ;; CHECK: (type $C.desc (sub $B.desc (describes $C (struct)))) - (type $C.desc (sub $B.desc (describes $C (struct)))) - ) - - ;; CHECK: (type $7 (func)) - - ;; CHECK: (func $test (type $7) - ;; CHECK-NEXT: (local $A (ref $A)) - ;; CHECK-NEXT: (local $A.desc (ref $A.desc)) - ;; CHECK-NEXT: (local $B (ref $B)) - ;; CHECK-NEXT: (local $B.desc (ref $B.desc)) - ;; CHECK-NEXT: (local $C (ref $C)) - ;; CHECK-NEXT: (local $C.desc (ref $C.desc)) - ;; CHECK-NEXT: (local.set $A - ;; CHECK-NEXT: (struct.new_default $A) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $B - ;; CHECK-NEXT: (struct.new_default $B - ;; CHECK-NEXT: (struct.new_default $B.desc) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $C - ;; CHECK-NEXT: (struct.new_default $C - ;; CHECK-NEXT: (struct.new_default $C.desc) - ;; 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 $test - (local $A (ref $A)) - (local $A.desc (ref $A.desc)) - (local $B (ref $B)) - (local $B.desc (ref $B.desc)) - (local $C (ref $C)) - (local $C.desc (ref $C.desc)) - (local.set $A - (struct.new $A - (struct.new $A.desc) - ) - ) - (local.set $B - (struct.new $B - (struct.new $B.desc) - ) - ) - (local.set $C - (struct.new $C - (struct.new $C.desc) - ) - ) - ;; This changed. - (drop - (ref.get_desc $B - (local.get $B) - ) - ) - ) -) - -;; As above, with $C. Now we optimize $A and $B with placeholders. -(module - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $A (sub (struct))) - (type $A (sub (descriptor $A.desc (struct)))) - ;; CHECK: (type $1 (sub (descriptor $A.desc (struct)))) - - ;; CHECK: (type $A.desc (sub (describes $1 (struct)))) - (type $A.desc (sub (describes $A (struct)))) - - ;; CHECK: (type $B (sub $A (struct))) - (type $B (sub $A (descriptor $B.desc (struct)))) - ;; CHECK: (type $4 (sub (descriptor $B.desc (struct)))) - - ;; CHECK: (type $B.desc (sub $A.desc (describes $4 (struct)))) - (type $B.desc (sub $A.desc (describes $B (struct)))) - - ;; CHECK: (type $C (sub $B (descriptor $C.desc (struct)))) - (type $C (sub $B (descriptor $C.desc (struct)))) - ;; CHECK: (type $C.desc (sub $B.desc (describes $C (struct)))) - (type $C.desc (sub $B.desc (describes $C (struct)))) - ) - - ;; CHECK: (type $8 (func)) - - ;; CHECK: (func $test (type $8) - ;; CHECK-NEXT: (local $A (ref $A)) - ;; CHECK-NEXT: (local $A.desc (ref $A.desc)) - ;; CHECK-NEXT: (local $B (ref $B)) - ;; CHECK-NEXT: (local $B.desc (ref $B.desc)) - ;; CHECK-NEXT: (local $C (ref $C)) - ;; CHECK-NEXT: (local $C.desc (ref $C.desc)) - ;; CHECK-NEXT: (local.set $A - ;; CHECK-NEXT: (struct.new_default $A) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $B - ;; CHECK-NEXT: (struct.new_default $B) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $C - ;; CHECK-NEXT: (struct.new_default $C - ;; CHECK-NEXT: (struct.new_default $C.desc) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (ref.get_desc $C - ;; CHECK-NEXT: (local.get $C) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $test - (local $A (ref $A)) - (local $A.desc (ref $A.desc)) - (local $B (ref $B)) - (local $B.desc (ref $B.desc)) - (local $C (ref $C)) - (local $C.desc (ref $C.desc)) - (local.set $A - (struct.new $A - (struct.new $A.desc) - ) - ) - (local.set $B - (struct.new $B - (struct.new $B.desc) - ) - ) - (local.set $C - (struct.new $C - (struct.new $C.desc) - ) - ) - ;; This changed. - (drop - (ref.get_desc $C - (local.get $C) - ) - ) - ) -) - -;; Here we cannot optimize with placeholders because the public supertype needs -;; to keep its descriptor and its subtypes therefore need to keep their related -;; descriptors. -(module - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $public (sub (descriptor $public.desc (struct)))) - (type $public (sub (descriptor $public.desc (struct)))) - ;; CHECK: (type $public.desc (sub (describes $public (struct)))) - (type $public.desc (sub (describes $public (struct)))) - ) - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $unused (sub $public (descriptor $unused.desc (struct)))) - (type $unused (sub $public (descriptor $unused.desc (struct)))) - ;; CHECK: (type $unused.desc (sub $public.desc (describes $unused (struct)))) - (type $unused.desc (sub $public.desc (describes $unused (struct)))) - - ;; CHECK: (type $used (sub $unused (descriptor $used.desc (struct)))) - (type $used (sub $unused (descriptor $used.desc (struct)))) - ;; CHECK: (type $used.desc (sub $unused.desc (describes $used (struct)))) - (type $used.desc (sub $unused.desc (describes $used (struct)))) - ) - - ;; CHECK: (type $6 (func (param (ref $used)) (result (ref $used.desc)))) - - ;; CHECK: (global $public (ref null $public) (ref.null none)) - (global $public (export "public") (ref null $public) (ref.null none)) - - ;; CHECK: (export "public" (global $public)) - - ;; CHECK: (func $use (type $6) (param $used (ref $used)) (result (ref $used.desc)) - ;; CHECK-NEXT: (ref.get_desc $used - ;; CHECK-NEXT: (local.get $used) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $use (param $used (ref $used)) (result (ref $used.desc)) - (ref.get_desc $used - (local.get $used) - ) - ) -) - -;; Now the public supertype does not have a descriptor, so we can still -;; optimize. -(module - (rec - ;; CHECK: (type $public (sub (struct))) - (type $public (sub (struct))) - ) - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $unused (sub $public (struct))) - (type $unused (sub $public (descriptor $unused.desc (struct)))) - ;; CHECK: (type $2 (sub (descriptor $unused.desc (struct)))) - - ;; CHECK: (type $unused.desc (sub (describes $2 (struct)))) - (type $unused.desc (sub (describes $unused (struct)))) - - ;; CHECK: (type $used (sub $unused (descriptor $used.desc (struct)))) - (type $used (sub $unused (descriptor $used.desc (struct)))) - ;; CHECK: (type $used.desc (sub $unused.desc (describes $used (struct)))) - (type $used.desc (sub $unused.desc (describes $used (struct)))) - ) - - ;; CHECK: (type $6 (func (param (ref $used)) (result (ref $used.desc)))) - - ;; CHECK: (global $public (ref null $public) (ref.null none)) - (global $public (export "public") (ref null $public) (ref.null none)) - - ;; CHECK: (export "public" (global $public)) - - ;; CHECK: (func $use (type $6) (param $used (ref $used)) (result (ref $used.desc)) - ;; CHECK-NEXT: (ref.get_desc $used - ;; CHECK-NEXT: (local.get $used) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $use (param $used (ref $used)) (result (ref $used.desc)) - (ref.get_desc $used - (local.get $used) - ) - ) -) - -;; Now we can optimize $A with a placeholder, but the placeholder must be -;; shared. -(module - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $A (sub (shared (struct)))) - (type $A (sub (shared (descriptor $A.desc (struct))))) - ;; CHECK: (type $1 (sub (shared (descriptor $A.desc (struct))))) - - ;; CHECK: (type $A.desc (sub (shared (describes $1 (struct))))) - (type $A.desc (sub (shared (describes $A (struct))))) - - ;; CHECK: (type $B (sub $A (shared (descriptor $B.desc (struct))))) - (type $B (sub $A (shared (descriptor $B.desc (struct))))) - ;; CHECK: (type $B.desc (sub $A.desc (shared (describes $B (struct))))) - (type $B.desc (sub $A.desc (shared (describes $B (struct))))) - ) - - ;; CHECK: (type $5 (func)) - - ;; CHECK: (func $test (type $5) - ;; CHECK-NEXT: (local $A (ref $A)) - ;; CHECK-NEXT: (local $A.desc (ref $A.desc)) - ;; CHECK-NEXT: (local $B (ref $B)) - ;; CHECK-NEXT: (local $B.desc (ref $B.desc)) - ;; CHECK-NEXT: (local.set $A - ;; CHECK-NEXT: (struct.new_default $A) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $B - ;; CHECK-NEXT: (struct.new_default $B - ;; CHECK-NEXT: (struct.new_default $B.desc) - ;; 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 $test - (local $A (ref $A)) - (local $A.desc (ref $A.desc)) - (local $B (ref $B)) - (local $B.desc (ref $B.desc)) - (local.set $A - (struct.new $A - (struct.new $A.desc) - ) - ) - (local.set $B - (struct.new $B - (struct.new $B.desc) - ) - ) - (drop - (ref.get_desc $B - (local.get $B) - ) - ) - ) -) - -;; Here we would optimize $unused with a placeholder, except that we don't -;; include it in the output at all because it is unused. We should not get -;; confused and try to emit a placeholder for it anyway. -(module - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $public (sub (descriptor $public.desc (struct)))) - (type $public (sub (descriptor $public.desc (struct)))) - ;; CHECK: (type $public.desc (sub (describes $public (struct)))) - (type $public.desc (sub (describes $public (struct)))) - ) - (rec - (type $unused (descriptor $unused.desc (struct))) - (type $unused.desc (sub $public.desc (describes $unused (struct)))) - ;; CHECK: (type $private (struct)) - (type $private (struct)) - ) - - ;; CHECK: (global $public (ref null $public) (ref.null none)) - (global $public (export "public") (ref null $public) (ref.null none)) - ;; CHECK: (global $private (ref null $private) (ref.null none)) - (global $private (ref null $private) (ref.null none)) -) - -;; Regression test for a bug where we accidentally replaced empty struct types -;; with placeholders. -;; CHECK: (export "public" (global $public)) -(module - ;; CHECK: (type $public (struct)) - (type $public (struct)) - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $super (sub (struct))) - (type $super (sub (descriptor $super.desc (struct)))) - ;; CHECK: (type $2 (sub (descriptor $super.desc (struct)))) - - ;; CHECK: (type $super.desc (sub (describes $2 (struct)))) - (type $super.desc (sub (describes $super (struct)))) - ;; CHECK: (type $sub (sub $super (descriptor $sub.desc (struct)))) - (type $sub (sub $super (descriptor $sub.desc (struct)))) - ;; CHECK: (type $sub.desc (sub $super.desc (describes $sub (struct)))) - (type $sub.desc (sub $super.desc (describes $sub (struct)))) - ) - - ;; CHECK: (type $6 (func (param (ref $sub)))) - - ;; CHECK: (global $public (ref null $public) (ref.null none)) - (global $public (export "public") (ref null $public) (ref.null none)) - - ;; CHECK: (export "public" (global $public)) - - ;; CHECK: (func $test (type $6) (param $sub (ref $sub)) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (ref.get_desc $sub - ;; CHECK-NEXT: (local.get $sub) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (struct.new_default $public) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $test (param $sub (ref $sub)) - ;; Use $sub's descriptor to keep it a descriptor. This forces $super.desc to - ;; describe a placeholder. - (drop - (ref.get_desc $sub - (local.get $sub) - ) - ) - ;; We should not accidentally replace this trivial struct with the - ;; placeholder, which would cause a validation failure because we do not - ;; supply a descriptor in this allocation. - (drop - (struct.new_default $public) - ) - ) -) - -;; Regression test for a bug where a placeholder was not set up to be described -;; by its intended descriptor if that intended descriptor also happened to have -;; its own unneeded descriptor. -(module - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $public (descriptor $public.desc (struct))) - (type $public (descriptor $public.desc (struct))) - ;; CHECK: (type $public.desc (sub (describes $public (struct)))) - (type $public.desc (sub (describes $public (struct)))) - ) - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $private (struct)) - (type $private (descriptor $private.desc (struct))) - ;; CHECK: (type $3 (sub (descriptor $private.desc (struct)))) - - ;; CHECK: (type $private.desc (sub $public.desc (describes $3 (struct)))) - (type $private.desc (sub $public.desc (describes $private (descriptor $private.meta (struct))))) - ;; CHECK: (type $private.meta (struct)) - (type $private.meta (describes $private.desc (struct))) - ) - - ;; Force $private.desc to remain a descriptor. - ;; CHECK: (global $public (ref null $public) (ref.null none)) - (global $public (export "public") (ref null $public) (ref.null none)) - - ;; CHECK: (global $private (ref null $private) (ref.null none)) - (global $private (ref null $private) (ref.null none)) -) - -;; Sibling types $A and $B. The supertype that connects them should not stop us -;; from optimizing $B here, even though $A cannot be optimized. -;; CHECK: (export "public" (global $public)) -(module - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $super (sub (struct))) - (type $super (sub (struct))) - - ;; CHECK: (type $A (sub $super (descriptor $A.desc (struct)))) - (type $A (sub $super (descriptor $A.desc (struct)))) - ;; CHECK: (type $A.desc (describes $A (struct))) - (type $A.desc (describes $A (struct))) - - ;; CHECK: (type $B (sub $super (struct))) - (type $B (sub $super (descriptor $B.desc (struct)))) - ;; CHECK: (type $B.desc (struct)) - (type $B.desc (describes $B (struct))) - ) - - ;; CHECK: (type $5 (func (param (ref $A)))) - - ;; CHECK: (func $test (type $5) (param $A (ref $A)) - ;; CHECK-NEXT: (local $B (ref $B)) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (ref.get_desc $A - ;; CHECK-NEXT: (local.get $A) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $test (param $A (ref $A)) - (local $B (ref $B)) - (drop - (ref.get_desc $A - (local.get $A) - ) - ) - ) -) - -;; As above, but now with a cast of the supertype. We can still optimize $B but -;; not $A. -(module - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $super (sub (struct))) - (type $super (sub (struct))) - - ;; CHECK: (type $A (sub $super (descriptor $A.desc (struct)))) - (type $A (sub $super (descriptor $A.desc (struct)))) - ;; CHECK: (type $A.desc (describes $A (struct))) - (type $A.desc (describes $A (struct))) - - ;; CHECK: (type $B (sub $super (struct))) - (type $B (sub $super (descriptor $B.desc (struct)))) - ;; CHECK: (type $B.desc (struct)) - (type $B.desc (describes $B (struct))) - ) - - ;; CHECK: (type $5 (func (param (ref $super) (ref (exact $A.desc))) (result anyref))) - - ;; CHECK: (func $test (type $5) (param $ref (ref $super)) (param $desc (ref (exact $A.desc))) (result anyref) - ;; CHECK-NEXT: (local $B (ref $B)) - ;; CHECK-NEXT: (ref.cast_desc (ref (exact $A)) - ;; CHECK-NEXT: (local.get $ref) - ;; CHECK-NEXT: (local.get $desc) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $test (param $ref (ref $super)) (param $desc (ref (exact $A.desc))) (result anyref) - (local $B (ref $B)) - (ref.cast_desc (ref (exact $A)) - (local.get $ref) - (local.get $desc) - ) - ) -) - -;; As above, but now with a cast of a basic type. We can still optimize $B but -;; not $A. -(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 (struct))) - (type $B (sub (descriptor $B.desc (struct)))) - ;; CHECK: (type $B.desc (sub (struct))) - (type $B.desc (sub (describes $B (struct)))) - ) - - ;; CHECK: (type $4 (func (param anyref (ref (exact $A.desc))) (result (ref (exact $A))))) - - ;; CHECK: (func $cast_anyref (type $4) (param $ref anyref) (param $desc (ref (exact $A.desc))) (result (ref (exact $A))) - ;; CHECK-NEXT: (local $B (ref $B)) - ;; CHECK-NEXT: (ref.cast_desc (ref (exact $A)) - ;; CHECK-NEXT: (local.get $ref) - ;; CHECK-NEXT: (local.get $desc) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $cast_anyref (param $ref anyref) (param $desc (ref (exact $A.desc))) (result (ref (exact $A))) - (local $B (ref $B)) - ;; The cast input is anyref. That means any struct could be sent in, but we - ;; only need to prevent optimization of $A, who is the cast output. $B can - ;; still be optimized. - (ref.cast_desc (ref (exact $A)) - (local.get $ref) - (local.get $desc) - ) - ) -) - -;; We can optimize away the descriptor since its only use is unreachable, but -;; we must update that use to remain valid despite being unreachable. -(module - (rec - ;; CHECK: (rec - ;; CHECK-NEXT: (type $struct (sub (struct))) - (type $struct (sub (descriptor $desc (struct)))) - ;; CHECK: (type $desc (sub (struct))) - (type $desc (sub (describes $struct (struct)))) - ) - ;; CHECK: (type $2 (func)) - - ;; CHECK: (func $test (type $2) - ;; CHECK-NEXT: (local $struct (ref $struct)) - ;; CHECK-NEXT: (local $desc (ref $desc)) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (unreachable) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - (func $test - (local $struct (ref $struct)) - (local $desc (ref $desc)) - (drop - (ref.cast_desc (ref $struct) - (unreachable) - (struct.new $desc) - ) - ) - ) -) diff --git a/test/lit/passes/unsubtyping-desc.wast b/test/lit/passes/unsubtyping-desc.wast index bf0af50c0f2..6d43fad0a06 100644 --- a/test/lit/passes/unsubtyping-desc.wast +++ b/test/lit/passes/unsubtyping-desc.wast @@ -1208,6 +1208,41 @@ ;; CHECK: (global $unsubtyping-removed-0 (ref (exact $desc)) (struct.new_default $desc ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: )) +(module + ;; We could optimize away the descriptor since its only use is unreachable, + ;; but instead we do nothing and depend on other passes clean up the + ;; unreachable code first. + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $struct (sub (descriptor $desc (struct)))) + (type $struct (sub (descriptor $desc (struct)))) + ;; CHECK: (type $desc (sub (describes $struct (struct)))) + (type $desc (sub (describes $struct (struct)))) + ) + ;; CHECK: (type $2 (func)) + + ;; CHECK: (func $test (type $2) + ;; CHECK-NEXT: (local $struct (ref $struct)) + ;; CHECK-NEXT: (local $desc (ref $desc)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.cast_desc (ref (exact $struct)) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: (struct.new_default $desc) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test + (local $struct (ref $struct)) + (local $desc (ref $desc)) + (drop + (ref.cast_desc (ref $struct) + (unreachable) + (struct.new $desc) + ) + ) + ) +) + (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.