diff --git a/src/ir/localize.h b/src/ir/localize.h index 85e4415f5ef..b36fe639612 100644 --- a/src/ir/localize.h +++ b/src/ir/localize.h @@ -130,6 +130,8 @@ struct ChildLocalizer { // effects we can't remove, or if it interacts with other children. bool needLocal = effects[i].hasUnremovableSideEffects(); if (!needLocal) { + // TODO: Avoid quadratic time here by accumulating effects and checking + // vs the accumulation. for (Index j = 0; j < num; j++) { if (j != i && effects[i].invalidates(effects[j])) { needLocal = true; diff --git a/src/passes/GlobalTypeOptimization.cpp b/src/passes/GlobalTypeOptimization.cpp index 5362135616e..eec8e206d7d 100644 --- a/src/passes/GlobalTypeOptimization.cpp +++ b/src/passes/GlobalTypeOptimization.cpp @@ -29,7 +29,9 @@ #include "ir/type-updating.h" #include "ir/utils.h" #include "pass.h" +#include "support/permutations.h" #include "wasm-builder.h" +#include "wasm-type-ordering.h" #include "wasm-type.h" #include "wasm.h" @@ -160,19 +162,23 @@ struct GlobalTypeOptimization : public Pass { // 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); - auto subSupers = combinedSetGetInfos; - propagator.propagateToSuperAndSubTypes(subSupers); - auto subs = std::move(combinedSetGetInfos); - propagator.propagateToSubTypes(subs); - - // Process the propagated info. - for (auto type : propagator.subTypes.types) { + auto dataFromSubsAndSupersMap = combinedSetGetInfos; + propagator.propagateToSuperAndSubTypes(dataFromSubsAndSupersMap); + auto dataFromSupersMap = std::move(combinedSetGetInfos); + propagator.propagateToSubTypes(dataFromSupersMap); + + // Process the propagated info. We look at supertypes first, as the order of + // fields in a supertype is a constraint on what subtypes can do. That is, + // we decide for each supertype what the optimal order is, and consider that + // fixed, and then subtypes can decide how to sort fields that they append. + HeapTypeOrdering::SupertypesFirst sorted; + for (auto type : sorted.sort(propagator.subTypes.types)) { if (!type.isStruct()) { continue; } auto& fields = type.getStruct().fields; - auto& subSuper = subSupers[type]; - auto& sub = subs[type]; + auto& dataFromSubsAndSupers = dataFromSubsAndSupersMap[type]; + auto& dataFromSupers = dataFromSupersMap[type]; // Process immutability. for (Index i = 0; i < fields.size(); i++) { @@ -181,7 +187,7 @@ struct GlobalTypeOptimization : public Pass { continue; } - if (subSuper[i].hasWrite) { + if (dataFromSubsAndSupers[i].hasWrite) { // A set exists. continue; } @@ -192,48 +198,132 @@ struct GlobalTypeOptimization : public Pass { vec[i] = true; } - // Process removability. We check separately for the ability to - // remove in a general way based on sub+super-propagated info (that is, - // fields that are not used in sub- or super-types, and so we can - // definitely remove them from all the relevant types) and also in the - // specific way that only works for removing at the end, which as - // mentioned above only looks at super-types. + // Process removability. std::set removableIndexes; for (Index i = 0; i < fields.size(); i++) { - if (!subSuper[i].hasRead) { - removableIndexes.insert(i); - } - } - for (int i = int(fields.size()) - 1; i >= 0; i--) { - // Unlike above, a write would stop us here: above we propagated to both - // sub- and super-types, which means if we see no reads then there is no - // possible read of the data at all. But here we just propagated to - // subtypes, and so we need to care about the case where the parent - // writes to a field but does not read from it - we still need those - // writes to happen as children may read them. (Note that if no child - // reads this field, and since we check for reads in parents here, that - // means the field is not read anywhere at all, and we would have - // handled that case in the previous loop anyhow.) - if (!sub[i].hasRead && !sub[i].hasWrite) { + // If there is no read whatsoever, in either subs or supers, then we can + // remove the field. That is so even if there are writes (it would be a + // pointless "write-only field"). + auto hasNoReadsAnywhere = !dataFromSubsAndSupers[i].hasRead; + + // Check for reads or writes in ourselves and our supers. If there are + // none, then operations only happen in our strict subtypes, and those + // subtypes can define the field there, and we don't need it here. + auto hasNoReadsOrWritesInSupers = + !dataFromSupers[i].hasRead && !dataFromSupers[i].hasWrite; + + if (hasNoReadsAnywhere || hasNoReadsOrWritesInSupers) { removableIndexes.insert(i); - } else { - // Once we see something we can't remove, we must stop, as we can only - // remove from the end in this case. - break; } } - if (!removableIndexes.empty()) { - auto& indexesAfterRemoval = indexesAfterRemovals[type]; - indexesAfterRemoval.resize(fields.size()); - Index skip = 0; - for (Index i = 0; i < fields.size(); i++) { - if (!removableIndexes.count(i)) { - indexesAfterRemoval[i] = i - skip; + + // We need to compute the new set of indexes if we are removing fields, or + // if our parent removed fields. In the latter case, our parent may have + // reordered fields even if we ourselves are not removing anything, and we + // must update to match the parent's order. + auto super = type.getDeclaredSuperType(); + auto superHasUpdates = super && indexesAfterRemovals.count(*super); + if (!removableIndexes.empty() || superHasUpdates) { + // We are removing fields. Reorder them to allow that, as in the general + // case we can only remove fields from the end, so that if our subtypes + // still need the fields they can append them. For example: + // + // type A = { x: i32, y: f64 }; + // type B : A = { x: 132, y: f64, z: v128 }; + // + // If field x is used in B but never in A then we want to remove it, but + // we cannot end up with this: + // + // type A = { y: f64 }; + // type B : A = { x: 132, y: f64, z: v128 }; + // + // Here B no longer extends A's fields. Instead, we reorder A, which + // then imposes the same order on B's fields: + // + // type A = { y: f64, x: i32 }; + // type B : A = { y: f64, x: i32, z: v128 }; + // + // And after that, it is safe to remove x in A: B will then append it, + // just like it appends z, leading to this: + // + // type A = { y: f64 }; + // type B : A = { y: f64, x: i32, z: v128 }; + // + std::vector indexesAfterRemoval(fields.size()); + + // The next new index to use. + Index next = 0; + + // If we have a super, then we extend it, and must match its fields. + // That is, we can only append fields: we cannot reorder or remove any + // field that is in the super. + Index numSuperFields = 0; + if (super) { + // We have visited the super before. Get the information about its + // fields. + std::vector superIndexes; + auto iter = indexesAfterRemovals.find(*super); + if (iter != indexesAfterRemovals.end()) { + superIndexes = iter->second; } else { + // We did not store any information about the parent, because we + // found nothing to optimize there. That means it is not removing or + // reordering anything, so its new indexes are trivial. + superIndexes = makeIdentity(super->getStruct().fields.size()); + } + + numSuperFields = superIndexes.size(); + + // Fields we keep but the super removed will be handled at the end. + std::vector keptFieldsNotInSuper; + + // Go over the super fields and handle them. + for (Index i = 0; i < superIndexes.size(); ++i) { + auto superIndex = superIndexes[i]; + if (superIndex == RemovedField) { + if (removableIndexes.count(i)) { + // This was removed in the super, and in us as well. + indexesAfterRemoval[i] = RemovedField; + } else { + // This was removed in the super, but we actually need it. It + // must appear after all other super fields, when we get to the + // proper index for that, later. That is, we are reordering. + keptFieldsNotInSuper.push_back(i); + } + } else { + // The super kept this field, so we must keep it as well. + assert(!removableIndexes.count(i)); + // We need to keep it at the same index so we remain compatible. + indexesAfterRemoval[i] = superIndex; + // Update |next| to refer to the next available index. Due to + // possible reordering in the parent, we may not see indexes in + // order here, so just take the max at each point in time. + next = std::max(next, superIndex + 1); + } + } + + // Handle fields we keep but the super removed. + for (auto i : keptFieldsNotInSuper) { + indexesAfterRemoval[i] = next++; + } + } + + // Go over the fields only defined in us, and not in any super. + for (Index i = numSuperFields; i < fields.size(); ++i) { + if (removableIndexes.count(i)) { indexesAfterRemoval[i] = RemovedField; - skip++; + } else { + indexesAfterRemoval[i] = next++; } } + + // Only store the new indexes we computed if we found something + // interesting. We might not, if e.g. our parent removes fields and we + // add them back in the exact order we started with. In such cases, + // avoid wasting memory and also time later. + if (indexesAfterRemoval != makeIdentity(indexesAfterRemoval.size())) { + indexesAfterRemovals[type] = indexesAfterRemoval; + } } } @@ -273,15 +363,16 @@ struct GlobalTypeOptimization : public Pass { } } - // Remove fields where we can. + // Remove/reorder fields where we can. auto remIter = parent.indexesAfterRemovals.find(oldStructType); if (remIter != parent.indexesAfterRemovals.end()) { auto& indexesAfterRemoval = remIter->second; Index removed = 0; + auto copy = newFields; for (Index i = 0; i < newFields.size(); i++) { auto newIndex = indexesAfterRemoval[i]; if (newIndex != RemovedField) { - newFields[newIndex] = newFields[i]; + newFields[newIndex] = copy[i]; } else { removed++; } @@ -347,26 +438,32 @@ struct GlobalTypeOptimization : public Pass { auto& operands = curr->operands; assert(indexesAfterRemoval.size() == operands.size()); - // Localize things so that we can simply remove the operands we no - // longer need. + // Ensure any children with non-trivial effects are replaced with + // local.gets, so that we can remove/reorder to our hearts' content. ChildLocalizer localizer( curr, getFunction(), *getModule(), getPassOptions()); replaceCurrent(localizer.getReplacement()); - // Remove the unneeded operands. + // 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] = operands[i]; + operands[newIndex] = old[i]; } else { removed++; } } - operands.resize(operands.size() - removed); - // We should only get here if we did actual work. - assert(removed > 0); + 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())); + } } void visitStructSet(StructSet* curr) { diff --git a/src/wasm-type-ordering.h b/src/wasm-type-ordering.h index 72b1818f72d..981ac004d23 100644 --- a/src/wasm-type-ordering.h +++ b/src/wasm-type-ordering.h @@ -68,9 +68,6 @@ struct SupertypesFirstBase }; struct SupertypesFirst : SupertypesFirstBase { - template - SupertypesFirst(const T& types) : SupertypesFirstBase(types) {} - std::optional getDeclaredSuperType(HeapType type) { return type.getDeclaredSuperType(); } diff --git a/test/lit/passes/gto-removals.wast b/test/lit/passes/gto-removals.wast index c5e148afaff..c2e82aee9c8 100644 --- a/test/lit/passes/gto-removals.wast +++ b/test/lit/passes/gto-removals.wast @@ -637,26 +637,27 @@ ) ) -;; We can remove fields from the end if they are only used in subtypes, because -;; the subtypes can always add fields at the end (and only at the end). +;; We can remove fields if they are only used in subtypes, because we can +;; reorder the fields in the super and re-add them in the sub, appending on top +;; of the now-shorter super. (module ;; CHECK: (rec - ;; CHECK-NEXT: (type $parent (sub (struct (field i32) (field i64)))) + ;; CHECK-NEXT: (type $parent (sub (struct (field i64)))) (type $parent (sub (struct (field i32) (field i64) (field f32) (field f64)))) - ;; CHECK: (type $child (sub $parent (struct (field i32) (field i64) (field f32) (field f64) (field anyref)))) + ;; CHECK: (type $child (sub $parent (struct (field i64) (field i32) (field f32) (field f64) (field anyref)))) (type $child (sub $parent (struct (field i32) (field i64) (field f32) (field f64) (field anyref)))) ;; CHECK: (type $2 (func (param (ref $parent) (ref $child)))) ;; CHECK: (func $func (type $2) (param $x (ref $parent)) (param $y (ref $child)) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (struct.get $parent 1 + ;; CHECK-NEXT: (struct.get $parent 0 ;; CHECK-NEXT: (local.get $x) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (struct.get $child 0 + ;; CHECK-NEXT: (struct.get $child 1 ;; CHECK-NEXT: (local.get $y) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -678,9 +679,10 @@ ;; CHECK-NEXT: ) (func $func (param $x (ref $parent)) (param $y (ref $child)) ;; The parent has fields 0, 1, 2, 3 and the child adds 4. - ;; Use fields only 1 in the parent, and all the rest in the child. We can - ;; only remove from the end in the child, which means we can remove 2 and 3 - ;; in the parent, but not 0. + ;; Use only field 1 in the parent, and all the rest in the child. We can + ;; reorder field 1 to the start of the parent (flipping its position with + ;; field 0) and then remove all the fields but the now-first. The child + ;; keeps all fields, but is reordered. (drop (struct.get $parent 1 (local.get $x))) (drop (struct.get $child 0 (local.get $y))) (drop (struct.get $child 2 (local.get $y))) @@ -691,31 +693,31 @@ (module ;; CHECK: (rec - ;; CHECK-NEXT: (type $parent (sub (struct (field i32) (field i64) (field (mut f32))))) + ;; CHECK-NEXT: (type $parent (sub (struct (field i64) (field (mut f32))))) (type $parent (sub (struct (field (mut i32)) (field (mut i64)) (field (mut f32)) (field (mut f64))))) - ;; CHECK: (type $child (sub $parent (struct (field i32) (field i64) (field (mut f32)) (field f64) (field anyref)))) + ;; CHECK: (type $child (sub $parent (struct (field i64) (field (mut f32)) (field i32) (field f64) (field anyref)))) (type $child (sub $parent (struct (field (mut i32)) (field (mut i64)) (field (mut f32)) (field (mut f64)) (field (mut anyref))))) ;; CHECK: (type $2 (func (param (ref $parent) (ref $child)))) ;; CHECK: (func $func (type $2) (param $x (ref $parent)) (param $y (ref $child)) - ;; CHECK-NEXT: (struct.set $parent 2 + ;; CHECK-NEXT: (struct.set $parent 1 ;; CHECK-NEXT: (local.get $x) ;; CHECK-NEXT: (f32.const 0) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (struct.get $parent 1 + ;; CHECK-NEXT: (struct.get $parent 0 ;; CHECK-NEXT: (local.get $x) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (struct.get $child 0 + ;; CHECK-NEXT: (struct.get $child 2 ;; CHECK-NEXT: (local.get $y) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (struct.get $child 2 + ;; CHECK-NEXT: (struct.get $child 1 ;; CHECK-NEXT: (local.get $y) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -743,6 +745,54 @@ ) ) +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $parent (sub (struct (field i64) (field (mut f32))))) + (type $parent (sub (struct (field (mut i32)) (field (mut i64)) (field (mut f32)) (field (mut f64))))) + + ;; CHECK: (type $child (sub $parent (struct (field i64) (field (mut f32)) (field i32) (field anyref)))) + (type $child (sub $parent (struct (field (mut i32)) (field (mut i64)) (field (mut f32)) (field (mut f64)) (field (mut anyref))))) + + ;; CHECK: (type $2 (func (param (ref $parent) (ref $child)))) + + ;; CHECK: (func $func (type $2) (param $x (ref $parent)) (param $y (ref $child)) + ;; CHECK-NEXT: (struct.set $parent 1 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (f32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $parent 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $child 2 + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $child 1 + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $child 3 + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $func (param $x (ref $parent)) (param $y (ref $child)) + ;; As above, but now we remove fields in the child as well: 3 is not used. + (struct.set $parent 2 (local.get $x) (f32.const 0)) + + (drop (struct.get $parent 1 (local.get $x))) + (drop (struct.get $child 0 (local.get $y))) + (drop (struct.get $child 2 (local.get $y))) + ;; the read of 3 was removed here. + (drop (struct.get $child 4 (local.get $y))) + ) +) + ;; A parent with two children, and there are only reads of the parent. Those ;; reads might be of data of either child, of course (as a refernce to the ;; parent might point to them), so we cannot optimize here. @@ -977,3 +1027,381 @@ ) ) ) + +;; A parent with two children, with fields used in various combinations. +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (sub (struct (field i64) (field v128) (field nullref)))) + (type $A (sub (struct (field i32) (field i64) (field f32) (field f64) (field anyref) (field v128) (field nullref)))) + + ;; CHECK: (type $C (sub $A (struct (field i64) (field v128) (field nullref) (field f64) (field anyref)))) + (type $C (sub $A (struct (field i32) (field i64) (field f32) (field f64) (field anyref) (field v128) (field nullref)))) + + ;; CHECK: (type $B (sub $A (struct (field i64) (field v128) (field nullref) (field f32) (field anyref)))) + (type $B (sub $A (struct (field i32) (field i64) (field f32) (field f64) (field anyref) (field v128) (field nullref)))) + ) + + ;; CHECK: (type $3 (func (param anyref))) + + ;; CHECK: (func $func (type $3) (param $x anyref) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $A 0 + ;; CHECK-NEXT: (ref.cast (ref $A) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $B 3 + ;; CHECK-NEXT: (ref.cast (ref $B) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $C 3 + ;; CHECK-NEXT: (ref.cast (ref $C) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $B 4 + ;; CHECK-NEXT: (ref.cast (ref $B) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $C 4 + ;; CHECK-NEXT: (ref.cast (ref $C) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $A 1 + ;; CHECK-NEXT: (ref.cast (ref $A) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $C 1 + ;; CHECK-NEXT: (ref.cast (ref $C) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $A 2 + ;; CHECK-NEXT: (ref.cast (ref $A) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $B 2 + ;; CHECK-NEXT: (ref.cast (ref $B) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $func (param $x anyref) + ;; Field 0 (i32) is used nowhere. + ;; Field 1 (i64) is used only in $A. + ;; Field 2 (f32) is used only in $B. + ;; Field 3 (f64) is used only in $C. + ;; Field 4 (anyref) is used only in $B and $C. + ;; Field 5 (v128) is used only in $A and $C. + ;; Field 6 (nullref) is used only in $A and $B. + ;; As a result: + ;; * A can keep only fields 1, 5, 6 (i64, v128, nullref). + ;; * B keeps A's fields, and appends 2, 4 (f32, anyref). + ;; * C keeps A's fields, and appends 3, 4 (f64, anyref). + + (drop (struct.get $A 1 (ref.cast (ref $A) (local.get $x)))) + + (drop (struct.get $B 2 (ref.cast (ref $B) (local.get $x)))) + + (drop (struct.get $C 3 (ref.cast (ref $C) (local.get $x)))) + + (drop (struct.get $B 4 (ref.cast (ref $B) (local.get $x)))) + (drop (struct.get $C 4 (ref.cast (ref $C) (local.get $x)))) + + (drop (struct.get $A 5 (ref.cast (ref $A) (local.get $x)))) + (drop (struct.get $C 5 (ref.cast (ref $C) (local.get $x)))) + + (drop (struct.get $A 6 (ref.cast (ref $A) (local.get $x)))) + (drop (struct.get $B 6 (ref.cast (ref $B) (local.get $x)))) + ) +) + +;; As above, but instead of $A having children $B, $C, now they are a chain, +;; $A :> $B :> $C +;; $C must now also include $B's fields (specifically field 2, the f32). +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (sub (struct (field i64) (field v128) (field nullref)))) + (type $A (sub (struct (field i32) (field i64) (field f32) (field f64) (field anyref) (field v128) (field nullref)))) + + ;; CHECK: (type $B (sub $A (struct (field i64) (field v128) (field nullref) (field f32) (field anyref)))) + (type $B (sub $A (struct (field i32) (field i64) (field f32) (field f64) (field anyref) (field v128) (field nullref)))) + + ;; CHECK: (type $C (sub $B (struct (field i64) (field v128) (field nullref) (field f32) (field anyref) (field f64)))) + (type $C (sub $B (struct (field i32) (field i64) (field f32) (field f64) (field anyref) (field v128) (field nullref)))) + ) + + ;; CHECK: (type $3 (func (param anyref))) + + ;; CHECK: (func $func (type $3) (param $x anyref) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $A 0 + ;; CHECK-NEXT: (ref.cast (ref $A) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $B 3 + ;; CHECK-NEXT: (ref.cast (ref $B) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $C 5 + ;; CHECK-NEXT: (ref.cast (ref $C) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $B 4 + ;; CHECK-NEXT: (ref.cast (ref $B) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $C 4 + ;; CHECK-NEXT: (ref.cast (ref $C) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $A 1 + ;; CHECK-NEXT: (ref.cast (ref $A) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $C 1 + ;; CHECK-NEXT: (ref.cast (ref $C) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $A 2 + ;; CHECK-NEXT: (ref.cast (ref $A) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $B 2 + ;; CHECK-NEXT: (ref.cast (ref $B) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $func (param $x anyref) + ;; Same uses as before. + + (drop (struct.get $A 1 (ref.cast (ref $A) (local.get $x)))) + + (drop (struct.get $B 2 (ref.cast (ref $B) (local.get $x)))) + + (drop (struct.get $C 3 (ref.cast (ref $C) (local.get $x)))) + + (drop (struct.get $B 4 (ref.cast (ref $B) (local.get $x)))) + (drop (struct.get $C 4 (ref.cast (ref $C) (local.get $x)))) + + (drop (struct.get $A 5 (ref.cast (ref $A) (local.get $x)))) + (drop (struct.get $C 5 (ref.cast (ref $C) (local.get $x)))) + + (drop (struct.get $A 6 (ref.cast (ref $A) (local.get $x)))) + (drop (struct.get $B 6 (ref.cast (ref $B) (local.get $x)))) + ) +) + +;; The parent $A is an empty struct, with nothing to remove. See we do not error +;; here. +(module + ;; CHECK: (type $A (sub (struct))) + (type $A (sub (struct))) + + ;; CHECK: (type $B (sub $A (struct))) + (type $B (sub $A (struct))) + + ;; CHECK: (type $2 (func (param (ref $B)))) + + ;; CHECK: (func $func (type $2) (param $x (ref $B)) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $func (param $x (ref $B)) + ;; Use $B in a param to keep it alive, and lead us to process it and $A. + ) +) + +;; As above, but now $B has fields to remove. +(module + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (sub (struct))) + (type $A (sub (struct))) + + ;; CHECK: (type $B (sub $A (struct))) + (type $B (sub $A (struct (field i32) (field i64)))) + + ;; CHECK: (type $2 (func (param (ref $B)))) + + ;; CHECK: (func $func (type $2) (param $x (ref $B)) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $func (param $x (ref $B)) + ) +) + +;; As above, but now $B's fields are used. +(module + ;; CHECK: (type $A (sub (struct))) + (type $A (sub (struct))) + + ;; CHECK: (type $B (sub $A (struct (field i32) (field i64)))) + (type $B (sub $A (struct (field i32) (field i64)))) + + ;; CHECK: (type $2 (func (param (ref $B)))) + + ;; CHECK: (func $func (type $2) (param $x (ref $B)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $B 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $B 1 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $func (param $x (ref $B)) + (drop (struct.get $B 0 (local.get $x))) + (drop (struct.get $B 1 (local.get $x))) + ) +) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (sub (struct (field i32)))) + (type $A (sub (struct (field i32)))) + ;; CHECK: (type $B (sub $A (struct (field i32)))) + (type $B (sub $A (struct (field i32) (field f64)))) + ) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (func $test (type $2) + ;; CHECK-NEXT: (local $x (ref null $A)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $A 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $B 0 + ;; CHECK-NEXT: (ref.cast (ref $B) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test + (local $x (ref null $A)) + ;; We cannot remove anything from $A, but we can from $B. That $A is + ;; unchanged should not confuse us. + (drop + (struct.get $A 0 + (local.get $x) + ) + ) + ;; $B reads field 0, but not its new field 1. + (drop + (struct.get $B 0 + (ref.cast (ref $B) + (local.get $x) + ) + ) + ) + ) +) + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $A (sub (struct))) + (type $A (sub (struct (field i32)))) + ;; CHECK: (type $B (sub $A (struct (field i32) (field f64)))) + (type $B (sub $A (struct (field i32) (field f64)))) + ) + + ;; CHECK: (type $2 (func)) + + ;; CHECK: (func $test (type $2) + ;; CHECK-NEXT: (local $x (ref null $B)) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (struct.new $B + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: (f64.const 3.14159) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $B 0 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.get $B 1 + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test + (local $x (ref null $B)) + ;; We can remove everything from $A, but nothing from $B. That $A changes + ;; entirely, and $B changes not at all, should not cause any errors. + (local.set $x + (struct.new $B + (i32.const 42) + (f64.const 3.14159) + ) + ) + (drop + (struct.get $B 0 + (local.get $x) + ) + ) + (drop + (struct.get $B 1 + (local.get $x) + ) + ) + ) +)