diff --git a/src/ir/struct-utils.h b/src/ir/struct-utils.h index e1726c05671..6db4e7f9e70 100644 --- a/src/ir/struct-utils.h +++ b/src/ir/struct-utils.h @@ -44,6 +44,16 @@ template struct StructValues : public std::vector { assert(index < this->size()); return std::vector::operator[](index); } + + // Store the descriptor as another field. (This could be a std::optional to + // indicate that the descriptor's existence depends on the type, but that + // would add overhead & code clutter (type checks). If there is no descriptor, + // this will just hang around with the default values, not harming anything + // except perhaps for looking a little odd during debugging. And whenever we + // combine() a non-existent descriptor, we are doing unneeded work, but the + // data here is typically just a few bools, so it is simpler and likely + // faster to just copy those rather than check if the type has a descriptor.) + T desc; }; // Maps heap types to a StructValues for that heap type. @@ -69,6 +79,7 @@ struct StructValuesMap : public std::unordered_map> { for (Index i = 0; i < info.size(); i++) { combinedInfos[type][i].combine(info[i]); } + combinedInfos[type].desc.combine(info.desc); } } @@ -80,6 +91,8 @@ struct StructValuesMap : public std::unordered_map> { x.dump(o); o << " "; }; + o << " desc: "; + vec.desc.dump(o); o << '\n'; } } @@ -129,7 +142,7 @@ struct FunctionStructValuesMap // // void noteCopy(HeapType type, Index index, T& info); // -// * Note a read +// * Note a read. // // void noteRead(HeapType type, Index index, T& info); // @@ -137,6 +150,9 @@ struct FunctionStructValuesMap // because in struct.new we know more about the type - we know the actual exact // type being written to, and not just that it is of a subtype of the // instruction's type, which helps later. +// +// Descriptors are treated as fields in that we call the above functions on +// them. We pass DescriptorIndex for their index as a fake value. template struct StructScanner : public WalkerPass>> { @@ -146,6 +162,8 @@ struct StructScanner SubType& self() { return *static_cast(this); } + static const Index DescriptorIndex = -1; + StructScanner(FunctionStructValuesMap& functionNewInfos, FunctionStructValuesMap& functionSetGetInfos) : functionNewInfos(functionNewInfos), @@ -168,6 +186,10 @@ struct StructScanner noteExpressionOrCopy(curr->operands[i], heapType, i, infos[i]); } } + + if (curr->desc) { + self().noteExpression(curr->desc, heapType, DescriptorIndex, infos.desc); + } } void visitStructSet(StructSet* curr) { @@ -236,6 +258,42 @@ struct StructScanner noteExpressionOrCopy(curr->replacement, heapType, index, info); } + void visitRefCast(RefCast* curr) { + if (curr->desc) { + // We may try to read a descriptor from anything arriving in |curr->ref|, + // but the only things that matter are the things we cast to: other types + // can lack a descriptor, and are skipped anyhow. So the only effective + // read is of the cast type. + handleDescRead(curr->getCastType()); + } + } + + void visitBrOn(BrOn* curr) { + if (curr->desc && + (curr->op == BrOnCastDesc || curr->op == BrOnCastDescFail)) { + handleDescRead(curr->getCastType()); + } + } + + void visitRefGetDesc(RefGetDesc* curr) { + // Unlike a cast, anything in |curr->ref| may be read from. + handleDescRead(curr->ref->type); + } + + void handleDescRead(Type type) { + if (type == Type::unreachable) { + return; + } + auto heapType = type.getHeapType(); + if (heapType.isStruct()) { + // Any subtype of the reference here may be read from. + self().noteRead(heapType, + DescriptorIndex, + functionSetGetInfos[this->getFunction()][heapType].desc); + return; + } + } + void noteExpressionOrCopy(Expression* expr, HeapType type, Index index, T& info) { // Look at the value falling through, if it has the exact same type @@ -268,8 +326,8 @@ struct StructScanner FunctionStructValuesMap& functionSetGetInfos; }; -// Helper class to propagate information about fields to sub- and/or super- -// classes in the type hierarchy. While propagating it calls a method +// Helper class to propagate information to sub- and/or super- classes in the +// type hierarchy. While propagating it calls a method // // to.combine(from) // @@ -286,18 +344,32 @@ template class TypeHierarchyPropagator { SubTypes subTypes; + // Propagate given a StructValuesMap, which means we need to take into + // account fields. void propagateToSuperTypes(StructValuesMap& infos) { propagate(infos, false, true); } - void propagateToSubTypes(StructValuesMap& infos) { propagate(infos, true, false); } - void propagateToSuperAndSubTypes(StructValuesMap& infos) { propagate(infos, true, true); } + // Propagate on a simpler map of structs and infos (that is, not using + // separate values for the fields, as StructValuesMap does). This is useful + // when not tracking individual fields, but something more general about + // types. + using StructMap = std::unordered_map; + + void propagateToSuperTypes(StructMap& infos) { + propagate(infos, false, true); + } + void propagateToSubTypes(StructMap& infos) { propagate(infos, true, false); } + void propagateToSuperAndSubTypes(StructMap& infos) { + propagate(infos, true, true); + } + private: void propagate(StructValuesMap& combinedInfos, bool toSubTypes, @@ -320,6 +392,11 @@ template class TypeHierarchyPropagator { work.push(*superType); } } + // Propagate the descriptor to the super, if the super has one. + if (superType->getDescriptorType() && + superInfos.desc.combine(infos.desc)) { + work.push(*superType); + } } } @@ -333,6 +410,41 @@ template class TypeHierarchyPropagator { work.push(subType); } } + // Propagate the descriptor. + if (subInfos.desc.combine(infos.desc)) { + work.push(subType); + } + } + } + } + } + + void propagate(StructMap& combinedInfos, bool toSubTypes, bool toSuperTypes) { + UniqueDeferredQueue work; + for (auto& [type, _] : combinedInfos) { + work.push(type); + } + while (!work.empty()) { + auto type = work.pop(); + auto& info = combinedInfos[type]; + + if (toSuperTypes) { + // Propagate to the supertype. + if (auto superType = type.getDeclaredSuperType()) { + auto& superInfo = combinedInfos[*superType]; + if (superInfo.combine(info)) { + work.push(*superType); + } + } + } + + if (toSubTypes) { + // Propagate shared fields to the subtypes. + for (auto subType : subTypes.getImmediateSubTypes(type)) { + auto& subInfo = combinedInfos[subType]; + if (subInfo.combine(info)) { + work.push(subType); + } } } } diff --git a/src/passes/ConstantFieldPropagation.cpp b/src/passes/ConstantFieldPropagation.cpp index 48e5a9c7770..75f74559f3e 100644 --- a/src/passes/ConstantFieldPropagation.cpp +++ b/src/passes/ConstantFieldPropagation.cpp @@ -443,6 +443,11 @@ struct PCVScanner } void noteCopy(HeapType type, Index index, PossibleConstantValues& info) { + if (index == DescriptorIndex) { + // We cannot continue on below, where we index into the vector of values. + return; + } + // Note copies, as they must be considered later. See the comment on the // propagation of values below. functionCopyInfos[getFunction()][type][index] = true; diff --git a/src/passes/GlobalTypeOptimization.cpp b/src/passes/GlobalTypeOptimization.cpp index 6c1872d2bb4..6b6b90b245c 100644 --- a/src/passes/GlobalTypeOptimization.cpp +++ b/src/passes/GlobalTypeOptimization.cpp @@ -44,6 +44,10 @@ 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. bool hasWrite = false; bool hasRead = false; @@ -62,6 +66,10 @@ struct FieldInfo { } return changed; } + + void dump(std::ostream& o) { + o << "[write: " << hasWrite << " hasRead: " << hasRead << ']'; + } }; struct FieldInfoScanner @@ -81,6 +89,11 @@ struct FieldInfoScanner HeapType type, Index index, FieldInfo& info) { + if (index == DescriptorIndex && expr->type.isNonNullable()) { + // A non-dangerous write to a descriptor, which as mentioned above, we do + // not track. + return; + } info.noteWrite(); } @@ -122,6 +135,9 @@ 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; + void run(Module* module) override { if (!module->features.hasGC()) { return; @@ -131,6 +147,8 @@ 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); @@ -140,8 +158,23 @@ struct GlobalTypeOptimization : public Pass { // Combine the data from the functions. functionSetGetInfos.combineInto(combinedSetGetInfos); - // TODO: combine newInfos as well, once we have a need for that (we will - // when we do things like subtyping). + + // 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: // @@ -169,7 +202,8 @@ struct GlobalTypeOptimization : public Pass { // subtypes (as wasm only allows the type to differ if the fields are // immutable). Note that by making more things immutable we therefore // make it possible to apply more specific subtypes in subtype fields. - StructUtils::TypeHierarchyPropagator propagator(*module); + SubTypes subTypes(*module); + StructUtils::TypeHierarchyPropagator propagator(subTypes); auto dataFromSubsAndSupersMap = combinedSetGetInfos; propagator.propagateToSuperAndSubTypes(dataFromSubsAndSupersMap); auto dataFromSupersMap = std::move(combinedSetGetInfos); @@ -365,18 +399,97 @@ struct GlobalTypeOptimization : public Pass { indexesAfterRemovals[type] = indexesAfterRemoval; } } + + // Process the descriptor. + if (auto desc = type.getDescriptorType()) { + // To remove a descriptor, it must not be used in subtypes. 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. + if (!dataFromSupers.desc.hasRead && + (!dataFromSupers.desc.hasWrite || trapsNeverHappen)) { + haveUnneededDescriptors.insert(type); + } + } + } + + // Handle descriptor subtyping: + // + // A -> A.desc + // ^ + // B -> B.desc + // + // Here the descriptors subtype, but *not* the describees. We cannot + // remove A's descriptor without also removing $B's, so we need to propagate + // that "must remain a descriptor" property among descriptors. + if (!haveUnneededDescriptors.empty()) { + + struct DescriptorInfo { + // Whether this descriptor is needed - it must keep describing. + bool needed = false; + + bool combine(const DescriptorInfo& other) { + if (!needed && other.needed) { + needed = true; + return true; + } + return false; + } + }; + + StructUtils::TypeHierarchyPropagator descPropagator( + subTypes); + + // Populate the initial data: Any descriptor we did not see was unneeded, + // is needed. + StructUtils::TypeHierarchyPropagator::StructMap map; + for (auto type : subTypes.types) { + if (auto desc = type.getDescriptorType()) { + if (!haveUnneededDescriptors.count(type)) { + // This descriptor type is needed. + map[*desc].needed = true; + } + } + } + + // Propagate. + descPropagator.propagateToSuperAndSubTypes(map); + + // Remove optimization opportunities that the propagation ruled out. + // TODO: We could do better here, + // + // A -> A.desc A A.desc <- A2 + // ^ => ^ + // B -> B.desc B -> B.desc + // + // Starting from the left, we can remove A's descriptor *but keep A.desc + // as being a descriptor*, by making it describe a new type A2. That would + // keep subtyping working for the descriptors, and later passes could + // remove the unused A2. + for (auto& [type, info] : map) { + if (info.needed) { + auto described = type.getDescribedType(); + assert(described); + haveUnneededDescriptors.erase(*described); + } + } } - // If we found fields that can be removed, remove them from instructions. + // 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()) { - removeFieldsInInstructions(*module); + if (!indexesAfterRemovals.empty() || !haveUnneededDescriptors.empty()) { + updateInstructions(*module); } // Update the types in the entire module. - if (!indexesAfterRemovals.empty() || !canBecomeImmutable.empty()) { + if (!indexesAfterRemovals.empty() || !canBecomeImmutable.empty() || + !haveUnneededDescriptors.empty()) { updateTypes(*module); } } @@ -441,6 +554,26 @@ 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.setDescriptor(i, std::nullopt); + } + + // Remove an unneeded describes. + if (auto described = oldType.getDescribedType()) { + if (parent.haveUnneededDescriptors.count(*described)) { + typeBuilder.setDescribed(i, std::nullopt); + } + } + } }; TypeRewriter(wasm, *this).update(); @@ -448,7 +581,7 @@ struct GlobalTypeOptimization : public Pass { // After updating the types to remove certain fields, we must also remove // them from struct instructions. - void removeFieldsInInstructions(Module& wasm) { + void updateInstructions(Module& wasm) { struct FieldRemover : public WalkerPass> { bool isFunctionParallel() override { return true; } @@ -471,19 +604,20 @@ struct GlobalTypeOptimization : public Pass { if (curr->type == Type::unreachable) { return; } - if (curr->isWithDefault()) { - // Nothing to do, a default was written and will no longer be. - return; - } - auto iter = parent.indexesAfterRemovals.find(curr->type.getHeapType()); - if (iter == parent.indexesAfterRemovals.end()) { - 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& indexesAfterRemoval = iter->second; - - auto& operands = curr->operands; - assert(indexesAfterRemoval.size() == operands.size()); // Ensure any children with non-trivial effects are replaced with // local.gets, so that we can remove/reorder to our hearts' content. @@ -498,29 +632,45 @@ struct GlobalTypeOptimization : public Pass { needEHFixups = true; } - // Remove and reorder operands. - 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 (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); + } else { + // If we didn't remove anything then we must have reordered (or else + // we have done pointless work). + assert(*indexesAfterRemoval != + makeIdentity(indexesAfterRemoval->size())); + } } - 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())); + + 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; } } diff --git a/src/passes/TypeRefining.cpp b/src/passes/TypeRefining.cpp index da113808ec9..3ec7d2923a8 100644 --- a/src/passes/TypeRefining.cpp +++ b/src/passes/TypeRefining.cpp @@ -61,6 +61,11 @@ struct FieldInfoScanner HeapType type, Index index, FieldInfo& info) { + if (index == DescriptorIndex) { + // We cannot continue on below, where we index into the vector of values. + return; + } + auto noted = expr->type; // Do not introduce new exact fields that might requires invalid // casts. Keep any existing exact fields, though. diff --git a/test/lit/passes/gto-desc-tnh.wast b/test/lit/passes/gto-desc-tnh.wast new file mode 100644 index 00000000000..dbf4f6ed938 --- /dev/null +++ b/test/lit/passes/gto-desc-tnh.wast @@ -0,0 +1,258 @@ +;; 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 also prevents optimizations on $A: we cannot remove that +;; descriptor either, or its subtype would break. +;; +;; This tests subtyping of descriptors *without* subtyping of describees. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (sub (descriptor $A.desc (struct)))) + ;; T_N_H: (rec + ;; T_N_H-NEXT: (type $A (sub (struct))) + (type $A (sub (descriptor $A.desc (struct)))) + ;; CHECK: (type $A.desc (sub (describes $A (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 $4 (func)) + + ;; CHECK: (func $test (type $4) + ;; 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. +;; +;; $B's descriptor seems removable, but the subtyping of the descriptors +;; prevents this. +(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 (descriptor $B.desc (struct)))) + ;; T_N_H: (type $B (sub (descriptor $B.desc (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 (describes $B (struct)))) + (type $B.desc (sub $A.desc (describes $B (struct)))) + ) + + ;; CHECK: (type $4 (func)) + + ;; CHECK: (func $test (type $4) + ;; 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 $4 (func)) + + ;; T_N_H: (func $test (type $4) + ;; 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 d7762626e35..465bfaf392b 100644 --- a/test/lit/passes/gto-desc.wast +++ b/test/lit/passes/gto-desc.wast @@ -1,6 +1,6 @@ ;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. -;; RUN: wasm-opt %s -all --closed-world --gto --preserve-type-order -S -o - | filecheck %s +;; RUN: foreach %s %t wasm-opt -all --closed-world --gto --preserve-type-order -S -o - | filecheck %s (module (rec @@ -17,6 +17,8 @@ ;; 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)) @@ -127,4 +129,1013 @@ ) ) ) + + ;; 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. +(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_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 subtypes, 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. This also stops everything. +(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 $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. +(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 $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) + ) + ) + ) +) + +;; Sibling types $A and $B. The supertype that connects them should not stop us +;; from optimizing $B here, even though $A cannot be optimized. +(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) + ) + ) +) +