summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/ir/localize.h2
-rw-r--r--src/passes/GlobalTypeOptimization.cpp201
-rw-r--r--src/wasm-type-ordering.h3
-rw-r--r--test/lit/passes/gto-removals.wast458
4 files changed, 594 insertions, 70 deletions
diff --git a/src/ir/localize.h b/src/ir/localize.h
index 85e4415f5..b36fe6396 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 536213561..eec8e206d 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<FieldInfo> 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<Index> 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<Index> 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<Index> 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<Index> 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<Expression*> 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 72b1818f7..981ac004d 100644
--- a/src/wasm-type-ordering.h
+++ b/src/wasm-type-ordering.h
@@ -68,9 +68,6 @@ struct SupertypesFirstBase
};
struct SupertypesFirst : SupertypesFirstBase<SupertypesFirst> {
- template<typename T>
- SupertypesFirst(const T& types) : SupertypesFirstBase(types) {}
-
std::optional<HeapType> getDeclaredSuperType(HeapType type) {
return type.getDeclaredSuperType();
}
diff --git a/test/lit/passes/gto-removals.wast b/test/lit/passes/gto-removals.wast
index c5e148afa..c2e82aee9 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)
+ )
+ )
+ )
+)