summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBruce He <44327446+zm2he@users.noreply.github.com>2023-06-06 16:11:38 -0400
committerGitHub <noreply@github.com>2023-06-06 20:11:38 +0000
commit7b093e8d0cfe09471aed4eb40c093f0603f11efb (patch)
tree298dba196da4c5be133dae14f5c13f6ac32b1b87
parent90f471ded4f9cdacb630197d192da93f56d01ee9 (diff)
downloadbinaryen-7b093e8d0cfe09471aed4eb40c093f0603f11efb.tar.gz
binaryen-7b093e8d0cfe09471aed4eb40c093f0603f11efb.tar.bz2
binaryen-7b093e8d0cfe09471aed4eb40c093f0603f11efb.zip
Move casts which are immediate children of local.gets to earlier local.gets (#5744)
In the OptimizeCasts pass, it is useful to move more refined casts as early as possible without causing side-effects. This will allow such casts to potentially trap earlier, and will allow the OptimizeCasts pass to use more refined casts earlier. This change allows a more refined cast to be duplicated at an earlier local.get expression. The later instance of the cast will then be eliminated in a later optimization pass. For example, if we have the following instructions: (drop (local.get $x) ) (drop (ref.cast $A (local.get $x) ) (drop (ref.cast $B (local.get $x) ) ) Where $B is a sublcass of $A, we can convert this to: (drop (ref.cast $B (local.get $x) ) ) (drop (ref.cast $A (local.get $x) ) (drop (ref.cast $B (local.get $x) ) ) Concretely we will save the first cast to a local and use it in the other local.gets.
-rw-r--r--src/passes/OptimizeCasts.cpp340
-rw-r--r--test/lit/passes/optimize-casts.wast849
2 files changed, 1166 insertions, 23 deletions
diff --git a/src/passes/OptimizeCasts.cpp b/src/passes/OptimizeCasts.cpp
index 8f13981bf..e4d7b1647 100644
--- a/src/passes/OptimizeCasts.cpp
+++ b/src/passes/OptimizeCasts.cpp
@@ -72,14 +72,32 @@
// sense in new pass anyhow, and things should be simpler overall to keep such
// casts all in one pass, here.
//
-// TODO: 1. Move casts earlier in a basic block as well, at least in
-// traps-never-happen mode where we can assume they never fail, and
-// perhaps in other situations too.
-// TODO: 2. Look past individual basic blocks? This may be worth considering
+// Also, we can move more refined casts earlier in a basic block before applying
+// the above optimization. This may allow more refined casts to be used by the
+// optimization earlier and allow trapping casts to trap earlier. For instance,
+// the below example:
+//
+// (some.operation
+// (local.get $ref)
+// (ref.cast .. (local.get $ref))
+// )
+//
+// could be converted to:
+//
+// (some.operation
+// (ref.cast (local.get $ref))
+// (ref.cast .. (local.get $ref))
+// )
+//
+// Note that right now, we only consider RefAs with op RefAsNonNull as a cast.
+// RefAs with ExternInternalize and ExternExternalize are not considered casts
+// when obtaining fallthroughs, and so are ignored.
+//
+// TODO: 1. Look past individual basic blocks? This may be worth considering
// given the pattern of a cast appearing in an if condition that is
// then used in an if arm, for example, where simple dominance shows
// the cast can be reused.
-// TODO: 3. Look at LocalSet as well and not just Get. That would add some
+// TODO: 2. Look at LocalSet as well and not just Get. That would add some
// overlap with the other passes mentioned above (SimplifyLocals and
// RedundantSetElimination also track sets and can switch a get to use
// a better set's index when that refines the type). But once we do the
@@ -91,6 +109,7 @@
// make them operate "backwards" and/or past basic blocks).
//
+#include "ir/effects.h"
#include "ir/linear-execution.h"
#include "ir/properties.h"
#include "ir/utils.h"
@@ -102,6 +121,284 @@ namespace wasm {
namespace {
+// Contains information about a RefCast we want to move to a target LocalGet.
+struct RefCastInfo {
+ LocalGet* target = nullptr;
+ RefCast* bestCast = nullptr;
+};
+
+// Contains information about a RefAs we want to move to a target LocalGet.
+// Currently only RefAsNonNull will be moved.
+struct RefAsInfo {
+ LocalGet* target = nullptr;
+ RefAs* bestCast = nullptr;
+};
+
+// Find a cast to move earlier to another local.get. More refined subtypes are
+// chosen over less refined ones.
+struct EarlyCastFinder
+ : public LinearExecutionWalker<EarlyCastFinder,
+ UnifiedExpressionVisitor<EarlyCastFinder>> {
+ PassOptions options;
+ size_t numLocals;
+
+ // For each local index, tracks the current earliest local.get that we can
+ // move a cast to without side-effects, and the most refined cast that we can
+ // move to it (could be already at the earliest local.get).
+ //
+ // Note that we track a cast already on the get since we only want to move
+ // better casts to it: if the best cast is already on the get, there is no
+ // work to do.
+ std::vector<RefCastInfo> currRefCastMove;
+ std::vector<RefAsInfo> currRefAsMove;
+
+ // Used to analyze expressions to see if casts can be moved past them.
+ EffectAnalyzer testRefCast;
+ EffectAnalyzer testRefAs;
+
+ // Maps LocalGets to the most refined RefCast to move to it, to be used by the
+ // EarlyCastApplier. If the most refined RefCast is already at the desired
+ // LocalGet, it does not appear here. In the normal case, only one RefCast
+ // needs to be moved to a LocalGet; if a LocalGet is cast to multiple types
+ // which are not subtypes of each other then a trap is inevitable, and we
+ // assume this would already be optimized away beforehand, so we don't care
+ // about this special case.
+ std::unordered_map<LocalGet*, RefCast*> refCastToApply;
+
+ // Maps LocalGets to a RefAs to move to it, to be used by the
+ // EarlyCastApplier. As of right now RefAsNonNull is the only non-extern cast,
+ // so we only have one type of RefAs cast to move.
+ std::unordered_map<LocalGet*, RefAs*> refAsToApply;
+
+ EarlyCastFinder(PassOptions options, Module* module, Function* func)
+ : options(options), numLocals(func->getNumLocals()),
+ currRefCastMove(func->getNumLocals()),
+ currRefAsMove(func->getNumLocals()), testRefCast(options, *module),
+ testRefAs(options, *module) {
+
+ // TODO: generalize this when we handle more than RefAsNonNull.
+ RefCast dummyRefCast(module->allocator);
+ RefAs dummyRefAs(module->allocator);
+ dummyRefAs.op = RefAsNonNull;
+
+ testRefCast.visit(&dummyRefCast);
+ testRefAs.visit(&dummyRefAs);
+ }
+
+ // We track information as we go, looking for the best cast to move backwards,
+ // and when we hit a barrier - a position we can't optimize past - then we
+ // flush/finalize the work we've done so far, since nothing better can appear
+ // later. We ignore the best cast if it is already at the desired location.
+ void flushRefCastResult(size_t index, Module& module) {
+ auto& target = currRefCastMove[index].target;
+ if (target) {
+ auto& bestCast = currRefCastMove[index].bestCast;
+ if (bestCast) {
+ // If the fallthrough is equal to the target, this means the cast is
+ // already at the target local.get and doesn't need to be duplicated
+ // again.
+ auto* fallthrough =
+ Properties::getFallthrough(bestCast, options, module);
+ if (fallthrough != target) {
+ refCastToApply[target] = bestCast;
+ }
+ bestCast = nullptr;
+ }
+ target = nullptr;
+ }
+ }
+
+ // Does the same as above function, but for RefAs instead of RefCast.
+ void flushRefAsResult(size_t index, Module& module) {
+ auto& target = currRefAsMove[index].target;
+ if (target) {
+ auto& bestCast = currRefAsMove[index].bestCast;
+ if (bestCast) {
+ // As in flushRefCastResult, we need to check if the cast is already at
+ // the target and thus does not need to be moved.
+ auto* fallthrough =
+ Properties::getFallthrough(bestCast, options, module);
+ if (fallthrough != target) {
+ refAsToApply[target] = bestCast;
+ }
+ bestCast = nullptr;
+ }
+ target = nullptr;
+ }
+ }
+
+ inline void flushAll() {
+ for (size_t i = 0; i < numLocals; i++) {
+ flushRefCastResult(i, *getModule());
+ flushRefAsResult(i, *getModule());
+ }
+ }
+
+ static void doNoteNonLinear(EarlyCastFinder* self, Expression** currp) {
+ self->flushAll();
+ }
+
+ void visitFunction(Function* curr) { flushAll(); }
+
+ void visitExpression(Expression* curr) {
+ // A new one is instantiated for each expression to determine
+ // if a cast can be moved past it.
+ ShallowEffectAnalyzer currAnalyzer(options, *getModule(), curr);
+
+ if (testRefCast.invalidates(currAnalyzer)) {
+ for (size_t i = 0; i < numLocals; i++) {
+ flushRefCastResult(i, *getModule());
+ }
+ }
+
+ if (testRefAs.invalidates(currAnalyzer)) {
+ for (size_t i = 0; i < numLocals; i++) {
+ flushRefAsResult(i, *getModule());
+ }
+ }
+ }
+
+ void visitLocalSet(LocalSet* curr) {
+ visitExpression(curr);
+
+ flushRefCastResult(curr->index, *getModule());
+ flushRefAsResult(curr->index, *getModule());
+ }
+
+ void visitLocalGet(LocalGet* curr) {
+ visitExpression(curr);
+
+ if (!currRefCastMove[curr->index].target) {
+ currRefCastMove[curr->index].target = curr;
+ }
+
+ // As we only move RefAsNonNull RefAs casts right now, we should
+ // ignore a LocalGet if the type is already non-nullable, as
+ // adding an extra ref.as_non_null has no effect.
+ if (!currRefAsMove[curr->index].target && curr->type.isNullable()) {
+ currRefAsMove[curr->index].target = curr;
+ }
+ }
+
+ void visitRefAs(RefAs* curr) {
+ visitExpression(curr);
+
+ // TODO: support more than RefAsNonNull.
+ if (curr->op != RefAsNonNull) {
+ return;
+ }
+
+ auto* fallthrough = Properties::getFallthrough(curr, options, *getModule());
+ if (auto* get = fallthrough->dynCast<LocalGet>()) {
+ auto& bestMove = currRefAsMove[get->index];
+ if (bestMove.target && !bestMove.bestCast) {
+ bestMove.bestCast = curr;
+ }
+ }
+ }
+
+ void visitRefCast(RefCast* curr) {
+ visitExpression(curr);
+
+ // Using fallthroughs here is fine due to the following cases.
+ // Suppose we have types $A->$B->$C (where $C is the most refined)
+ // and $D, which is an unrelated type.
+ // Case 1:
+ // (ref.cast $A (ref.cast $C (local.get $x)))
+ //
+ // ref.cast $C is initially chosen for $x. Then we consider ref.cast $A.
+ // Since $A is less refined than $C, we ignore it.
+ //
+ // Case 2:
+ // (ref.cast $C (ref.cast $A (local.get $x)))
+ //
+ // ref.cast $A is initially chosen for $x. Then we consider ref.cast $C,
+ // which is more refined than ref.cast $A, so we replace it with ref.cast
+ // $C.
+ //
+ // Case 3:
+ // (ref.cast $B (ref.cast $B (local.get $x)))
+ //
+ // We initially choose to move the inner ref.cast $B. When we consider the
+ // outer ref.cast $B, we can see that it has the same type as tge existing
+ // ref.cast $B, so we ignore it.
+ //
+ // Case 4:
+ // (ref.cast $D (ref.cast $C (local.get $x)))
+ //
+ // This would produce a trap and should already be optimized away
+ // beforehand.
+ //
+ // If the best cast is already at the target location, we will still track
+ // it in currRefCastMove to see if we can obtain a better cast. However, we
+ // won't flush it.
+
+ auto* fallthrough = Properties::getFallthrough(curr, options, *getModule());
+ if (auto* get = fallthrough->dynCast<LocalGet>()) {
+ auto& bestMove = currRefCastMove[get->index];
+ // Do not move a cast if its type is not related to the target
+ // local.get's type (i.e. not in a subtyping relationship). Otherwise
+ // a type error will occur. Also, if the target local.get's type is
+ // already more refined than this current cast, there is no point in
+ // moving it.
+ if (bestMove.target && bestMove.target->type != curr->type &&
+ Type::isSubType(curr->type, bestMove.target->type)) {
+ if (!bestMove.bestCast) {
+ // If there isn't any other cast to move, the current cast is the
+ // best.
+ bestMove.bestCast = curr;
+ } else if (bestMove.bestCast->type != curr->type &&
+ Type::isSubType(curr->type, bestMove.bestCast->type)) {
+ // If the current cast is more refined than the best cast to move,
+ // change the best cast to move.
+ bestMove.bestCast = curr;
+ }
+ // We don't care about the safety of the cast at present. If there are
+ // two casts with the same type one being safe and one being unsafe, the
+ // first cast that we visit will be chosen to be moved. Perhaps in the
+ // future we can consider prioritizing unsafe casts over safe ones since
+ // users may be more interested in that.
+ }
+ }
+ }
+
+ bool hasCastsToMove() {
+ return refCastToApply.size() > 0 || refAsToApply.size() > 0;
+ }
+};
+
+// Given a set of RefAs and RefCast casts to move to specified
+// earlier expressions, duplicate the cast at the specified
+// earlier expression. The original cast that we are moving will
+// be optimized out by a later pass once we have applied the same
+// cast earlier.
+struct EarlyCastApplier : public PostWalker<EarlyCastApplier> {
+ EarlyCastFinder& finder;
+
+ EarlyCastApplier(EarlyCastFinder& finder) : finder(finder) {}
+
+ // RefCast casts are applied before RefAs casts. If there are multiple
+ // casts to apply to a location, they are nested within one another. Only
+ // at most one RefCast and at most one RefAs can be applied.
+ void visitLocalGet(LocalGet* curr) {
+ Expression* currPtr = curr;
+
+ auto refCastIter = finder.refCastToApply.find(curr);
+ if (refCastIter != finder.refCastToApply.end()) {
+ currPtr = replaceCurrent(Builder(*getModule())
+ .makeRefCast(currPtr,
+ refCastIter->second->type,
+ refCastIter->second->safety));
+ }
+
+ auto refAsIter = finder.refAsToApply.find(curr);
+ if (refAsIter != finder.refAsToApply.end()) {
+ replaceCurrent(
+ Builder(*getModule()).makeRefAs(refAsIter->second->op, currPtr));
+ }
+ }
+};
+
// Find the best casted verisons of local.gets: other local.gets with the same
// value, but cast to a more refined type.
struct BestCastFinder : public LinearExecutionWalker<BestCastFinder> {
@@ -215,22 +512,33 @@ struct OptimizeCasts : public WalkerPass<PostWalker<OptimizeCasts>> {
return;
}
- // First, find the best casts that we want to use.
+ // Look for casts which can be moved earlier.
+ EarlyCastFinder earlyCastFinder(getPassOptions(), getModule(), func);
+ earlyCastFinder.walkFunctionInModule(func, getModule());
+
+ if (earlyCastFinder.hasCastsToMove()) {
+ // Duplicate casts to earlier locations if possible.
+ EarlyCastApplier earlyCastApplier(earlyCastFinder);
+ earlyCastApplier.walkFunctionInModule(func, getModule());
+
+ // Adding more casts causes types to be refined, that should be
+ // propagated.
+ ReFinalize().walkFunctionInModule(func, getModule());
+ }
+
+ // Find the best casts that we want to use.
BestCastFinder finder;
finder.options = getPassOptions();
finder.walkFunctionInModule(func, getModule());
- if (finder.lessCastedGets.empty()) {
- // Nothing to do.
- return;
- }
+ if (!finder.lessCastedGets.empty()) {
+ // Apply the requests: use the best casts.
+ FindingApplier applier(finder);
+ applier.walkFunctionInModule(func, getModule());
- // Apply the requests: use the best casts.
- FindingApplier applier(finder);
- applier.walkFunctionInModule(func, getModule());
-
- // LocalGet type changes must be propagated.
- ReFinalize().walkFunctionInModule(func, getModule());
+ // LocalGet type changes must be propagated.
+ ReFinalize().walkFunctionInModule(func, getModule());
+ }
}
};
diff --git a/test/lit/passes/optimize-casts.wast b/test/lit/passes/optimize-casts.wast
index 27e38f7f5..aefd4a6ea 100644
--- a/test/lit/passes/optimize-casts.wast
+++ b/test/lit/passes/optimize-casts.wast
@@ -8,12 +8,15 @@
;; CHECK: (type $B (struct_subtype $A))
(type $B (struct_subtype $A))
+ ;; CHECK: (type $D (array (mut i32)))
+ (type $D (array (mut i32)))
+
+ ;; CHECK: (global $a (mut i32) (i32.const 0))
+ (global $a (mut i32) (i32.const 0))
+
;; CHECK: (func $ref.as (type $ref?|$A|_=>_none) (param $x (ref null $A))
;; CHECK-NEXT: (local $1 (ref $A))
;; CHECK-NEXT: (drop
- ;; CHECK-NEXT: (local.get $x)
- ;; CHECK-NEXT: )
- ;; CHECK-NEXT: (drop
;; CHECK-NEXT: (local.tee $1
;; CHECK-NEXT: (ref.as_non_null
;; CHECK-NEXT: (local.get $x)
@@ -21,6 +24,11 @@
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.as_non_null
+ ;; CHECK-NEXT: (local.get $1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
;; CHECK-NEXT: (local.get $1)
;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
@@ -30,12 +38,14 @@
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $ref.as (param $x (ref null $A))
- ;; After the first ref.as, we can use the cast value in later gets, which is
- ;; more refined.
+ ;; We duplicate the ref.as to the first local.get, since it is more refined
+ ;; than the local.get alone. We then use the refined index throughout.
(drop
(local.get $x)
)
(drop
+ ;; In this case we don't need this ref.as here after the pass as it is
+ ;; duplicated above, but we leave that to later opts.
(ref.as_non_null
(local.get $x)
)
@@ -43,8 +53,8 @@
(drop
(local.get $x)
)
- ;; In this case we don't really need the last ref.as here, but we leave that
- ;; for later opts.
+ ;; In this case we don't really need the last ref.as here, because of earlier
+ ;; ref.as expressions, but we leave that for later opts.
(drop
(ref.as_non_null
(local.get $x)
@@ -73,6 +83,9 @@
(func $ref.as-no (param $x (ref $A))
;; As above, but the param is now non-nullable anyhow, so we should do
;; nothing.
+
+ ;; Because of this, a ref.as_non_null cast is not moved up to the first
+ ;; local.get $x even though it could because it would make no difference.
(drop
(local.get $x)
)
@@ -170,9 +183,15 @@
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $a
+ ;; CHECK-NEXT: (i32.const 10)
+ ;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (local.get $1)
;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $a
+ ;; CHECK-NEXT: (i32.const 20)
+ ;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (local.tee $2
;; CHECK-NEXT: (ref.cast $B
@@ -180,6 +199,9 @@
;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $a
+ ;; CHECK-NEXT: (i32.const 30)
+ ;; CHECK-NEXT: )
;; CHECK-NEXT: (drop
;; CHECK-NEXT: (local.get $2)
;; CHECK-NEXT: )
@@ -190,15 +212,26 @@
(local.get $x)
)
)
+ ;; global.sets prevent casts from being moved before them
+ ;; but uses can be added after them.
+ (global.set $a
+ (i32.const 10)
+ )
;; Here we should use $A.
(drop
(local.get $x)
)
+ (global.set $a
+ (i32.const 20)
+ )
(drop
(ref.cast $B
(local.get $x)
)
)
+ (global.set $a
+ (i32.const 30)
+ )
;; Here we should use $B, which is even better.
(drop
(local.get $x)
@@ -387,6 +420,808 @@
)
)
+ ;; CHECK: (func $move-cast-1 (type $ref|struct|_=>_none) (param $x (ref struct))
+ ;; CHECK-NEXT: (local $1 (ref $B))
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.cast $B
+ ;; CHECK-NEXT: (local.tee $1
+ ;; CHECK-NEXT: (ref.cast $B
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.get $1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.cast $B
+ ;; CHECK-NEXT: (local.get $1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $move-cast-1 (param $x (ref struct))
+ (drop
+ ;; The later cast to $B will be moved between ref.cast $A
+ ;; and local.get $x. This will cause this ref.cast $A to be
+ ;; converted to a second ref.cast $B due to ReFinalize().
+ (ref.cast $A
+ (local.get $x)
+ )
+ )
+ (drop
+ (local.get $x)
+ )
+ (drop
+ ;; The most refined cast of $x is to $B, which we can move up to
+ ;; the top and reuse from there.
+ (ref.cast $B
+ (local.get $x)
+ )
+ )
+ )
+
+ ;; CHECK: (func $move-cast-2 (type $ref|struct|_=>_none) (param $x (ref struct))
+ ;; CHECK-NEXT: (local $1 (ref $B))
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.cast $B
+ ;; CHECK-NEXT: (local.tee $1
+ ;; CHECK-NEXT: (ref.cast $B
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.cast $B
+ ;; CHECK-NEXT: (local.get $1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.get $1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $move-cast-2 (param $x (ref struct))
+ (drop
+ ;; As in $move-cast-1, the later cast to $B will be moved
+ ;; between ref.cast $A and local.get $x, causing ref.cast $A
+ ;; to be converted into a second ref.cast $B by ReFinalize();
+ (ref.cast $A
+ (local.get $x)
+ )
+ )
+ (drop
+ ;; This will be moved up to the first local.get $x.
+ (ref.cast $B
+ (local.get $x)
+ )
+ )
+ (drop
+ (local.get $x)
+ )
+ )
+
+ ;; CHECK: (func $move-cast-3 (type $ref|struct|_=>_none) (param $x (ref struct))
+ ;; CHECK-NEXT: (local $1 (ref $B))
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.tee $1
+ ;; CHECK-NEXT: (ref.cast $B
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.cast $B
+ ;; CHECK-NEXT: (local.get $1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.cast $B
+ ;; CHECK-NEXT: (local.get $1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $move-cast-3 (param $x (ref struct))
+ (drop
+ (local.get $x)
+ )
+ (drop
+ ;; Converted to $B by ReFinalize().
+ (ref.cast $A
+ (local.get $x)
+ )
+ )
+ (drop
+ ;; This will be moved up to the first local.get $x.
+ (ref.cast $B
+ (local.get $x)
+ )
+ )
+ )
+
+ ;; CHECK: (func $move-cast-4 (type $ref|struct|_=>_none) (param $x (ref struct))
+ ;; CHECK-NEXT: (local $1 (ref $B))
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.tee $1
+ ;; CHECK-NEXT: (ref.cast $B
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.cast $B
+ ;; CHECK-NEXT: (local.get $1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.cast $B
+ ;; CHECK-NEXT: (local.get $1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $move-cast-4 (param $x (ref struct))
+ (drop
+ (local.get $x)
+ )
+ (drop
+ ;; This will be moved up to the first local.get $x.
+ (ref.cast $B
+ (local.get $x)
+ )
+ )
+ (drop
+ ;; Converted to $B by ReFinalize().
+ (ref.cast $A
+ (local.get $x)
+ )
+ )
+ )
+
+ ;; CHECK: (func $move-cast-5 (type $ref|struct|_=>_none) (param $x (ref struct))
+ ;; CHECK-NEXT: (local $1 (ref $B))
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.tee $1
+ ;; CHECK-NEXT: (ref.cast $B
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.cast $B
+ ;; CHECK-NEXT: (local.get $1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.get $1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $move-cast-5 (param $x (ref struct))
+ (drop
+ ;; The first location is already the most refined cast, so nothing will be moved up.
+ ;; (But we will save the cast to a local and re-use it below.)
+ (ref.cast $B
+ (local.get $x)
+ )
+ )
+ (drop
+ ;; Converted to $B by ReFinalize().
+ (ref.cast $A
+ (local.get $x)
+ )
+ )
+ (drop
+ (local.get $x)
+ )
+ )
+
+ ;; CHECK: (func $move-cast-6 (type $ref|struct|_=>_none) (param $x (ref struct))
+ ;; CHECK-NEXT: (local $1 (ref $B))
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.tee $1
+ ;; CHECK-NEXT: (ref.cast $B
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.get $1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.cast $B
+ ;; CHECK-NEXT: (local.get $1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $move-cast-6 (param $x (ref struct))
+ (drop
+ ;; This is already the most refined cast, so nothing will be moved.
+ (ref.cast $B
+ (local.get $x)
+ )
+ )
+ (drop
+ (local.get $x)
+ )
+ (drop
+ ;; Converted to $B by ReFinalize().
+ (ref.cast $A
+ (local.get $x)
+ )
+ )
+ )
+
+ ;; CHECK: (func $no-move-already-refined-local (type $ref|$B|_=>_none) (param $x (ref $B))
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.cast $B
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $no-move-already-refined-local (param $x (ref $B))
+ (drop
+ (local.get $x)
+ )
+ (drop
+ ;; Since we know $x is of type $B, this cast to a less refined type $A
+ ;; will not be moved higher.
+ (ref.cast $A
+ (local.get $x)
+ )
+ )
+ )
+
+ ;; CHECK: (func $no-move-ref.as-to-non-nullable-local (type $ref|struct|_=>_none) (param $x (ref struct))
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.as_non_null
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $no-move-ref.as-to-non-nullable-local (param $x (ref struct))
+ (drop
+ (local.get $x)
+ )
+ (drop
+ ;; Since $x is non-nullable, this cast is useless. Hence, this
+ ;; will not be duplicated to the first local.get, since doing
+ ;; so would also be useless.
+ (ref.as_non_null
+ (local.get $x)
+ )
+ )
+ )
+
+ ;; CHECK: (func $avoid-erroneous-cast-move (type $ref|$A|_=>_none) (param $x (ref $A))
+ ;; CHECK-NEXT: (local $a (ref $A))
+ ;; CHECK-NEXT: (local.set $a
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.cast $D
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $avoid-erroneous-cast-move (param $x (ref $A))
+ ;; This test shows that we avoid moving a cast earlier if doing so would
+ ;; violate typing rules.
+ (local $a (ref $A))
+ (local.set $a
+ ;; We could move the ref.cast $D here. However, as $a is already known
+ ;; to have type ref null $A, not type $D, it would fail, since those
+ ;; types are incompatible. Moving the cast will also cause the
+ ;; local.set $b to fail, since $b is of type ref null $A, not $D.
+ (local.get $x)
+ )
+ (drop
+ (ref.cast $D
+ (local.get $x)
+ )
+ )
+ )
+
+ ;; CHECK: (func $move-as-1 (type $structref_=>_none) (param $x structref)
+ ;; CHECK-NEXT: (local $1 (ref struct))
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.tee $1
+ ;; CHECK-NEXT: (ref.as_non_null
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.as_non_null
+ ;; CHECK-NEXT: (local.get $1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $move-as-1 (param $x (ref null struct))
+ (drop
+ (local.get $x)
+ )
+ (drop
+ ;; The most refined cast of $x is this ref.as_non_null, so we will move it
+ ;; and reuse from there.
+ (ref.as_non_null
+ (local.get $x)
+ )
+ )
+ )
+
+ ;; CHECK: (func $move-as-2 (type $structref_=>_none) (param $x structref)
+ ;; CHECK-NEXT: (local $1 (ref struct))
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.tee $1
+ ;; CHECK-NEXT: (ref.as_non_null
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.get $1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $move-as-2 (param $x (ref null struct))
+ (drop
+ ;; This is already the most refined cast, so the cast is not copied
+ ;; (but we do save it to a local and use it below).
+ (ref.as_non_null
+ (local.get $x)
+ )
+ )
+ (drop
+ (local.get $x)
+ )
+ )
+
+ ;; CHECK: (func $move-cast-side-effects (type $ref|struct|_ref|struct|_=>_none) (param $x (ref struct)) (param $y (ref struct))
+ ;; CHECK-NEXT: (local $2 (ref $A))
+ ;; CHECK-NEXT: (local $3 (ref $B))
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $a
+ ;; CHECK-NEXT: (i32.const 10)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.tee $2
+ ;; CHECK-NEXT: (ref.cast $A
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.tee $3
+ ;; CHECK-NEXT: (ref.cast $B
+ ;; CHECK-NEXT: (local.get $y)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.cast $A
+ ;; CHECK-NEXT: (local.get $2)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (local.set $x
+ ;; CHECK-NEXT: (local.get $3)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.cast $B
+ ;; CHECK-NEXT: (local.get $3)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.cast $B
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $move-cast-side-effects (param $x (ref struct)) (param $y (ref struct))
+ ;; This verifies that casts cannot be moved past side-effect producing
+ ;; operations like global.set, and that casts cannot be moved past a local.set
+ ;; to its own local index.
+ (drop
+ (local.get $x)
+ )
+ ;; Cannot move past global set due to trap possibility, so the cast to $A will
+ ;; move up to here but not further up.
+ (global.set $a
+ (i32.const 10)
+ )
+ (drop
+ (local.get $x)
+ )
+ (drop
+ (local.get $y)
+ )
+ (drop
+ (ref.cast $A
+ (local.get $x)
+ )
+ )
+ ;; Casts to $x cannot be moved past local.set $x, but the cast of $y can and will be.
+ (local.set $x
+ (local.get $y)
+ )
+ (drop
+ ;; This can be moved past local.set $x.
+ (ref.cast $B
+ (local.get $y)
+ )
+ )
+ (drop
+ ;; This cannot be moved past local.set $x.
+ (ref.cast $B
+ (local.get $x)
+ )
+ )
+ )
+
+ ;; CHECK: (func $move-ref.as-for-separate-index (type $structref_structref_=>_none) (param $x structref) (param $y structref)
+ ;; CHECK-NEXT: (local $2 (ref struct))
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.tee $2
+ ;; CHECK-NEXT: (ref.as_non_null
+ ;; CHECK-NEXT: (local.get $y)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (local.set $x
+ ;; CHECK-NEXT: (local.get $2)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.as_non_null
+ ;; CHECK-NEXT: (local.get $2)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.as_non_null
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $move-ref.as-for-separate-index (param $x (ref null struct)) (param $y (ref null struct))
+ ;; This test shows that local index $x and local index $y are tracked separately.
+ (drop
+ ;; The later local.set $x will prevent casts from being moved here.
+ (local.get $x)
+ )
+ (drop
+ ;; A ref.as_non_null will be moved here, because the local.set
+ ;; will only prevent casts involving local.get $x from being moved.
+ (local.get $y)
+ )
+ (local.set $x
+ (local.get $y)
+ )
+ (drop
+ (ref.as_non_null
+ (local.get $y)
+ )
+ )
+ (drop
+ (ref.as_non_null
+ (local.get $x)
+ )
+ )
+ )
+
+ ;; CHECK: (func $move-ref.as-and-ref.cast (type $structref_=>_none) (param $x structref)
+ ;; CHECK-NEXT: (local $1 (ref $A))
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.tee $1
+ ;; CHECK-NEXT: (ref.as_non_null
+ ;; CHECK-NEXT: (ref.cast null $A
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.as_non_null
+ ;; CHECK-NEXT: (ref.cast $A
+ ;; CHECK-NEXT: (local.get $1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $move-ref.as-and-ref.cast (param $x (ref null struct))
+ ;; This test shows how a nested ref.as_non_null and ref.cast can be
+ ;; moved to the same local.get.
+ (drop
+ (local.get $x)
+ )
+ (drop
+ ;; Here these two nested casts will be moved up to the earlier local.get.
+ (ref.as_non_null
+ ;; This will be converted to a non-nullable cast because the local we
+ ;; save to in the optimization ($1) is now non-nullable.
+ (ref.cast null $A
+ (local.get $x)
+ )
+ )
+ )
+ )
+
+ ;; CHECK: (func $move-ref.as-and-ref.cast-2 (type $structref_=>_none) (param $x structref)
+ ;; CHECK-NEXT: (local $1 (ref $A))
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.tee $1
+ ;; CHECK-NEXT: (ref.as_non_null
+ ;; CHECK-NEXT: (ref.cast null $A
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.cast $A
+ ;; CHECK-NEXT: (local.get $1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.as_non_null
+ ;; CHECK-NEXT: (local.get $1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $move-ref.as-and-ref.cast-2 (param $x (ref null struct))
+ ;; This test shows how a ref.cast followed by a ref.as_non_null
+ ;; can both be moved to an earlier local.get.
+ (drop
+ ;; The separate ref.as_non_null and the ref.cast below will both be moved here.
+ (local.get $x)
+ )
+ (drop
+ ;; This is converted to ref.cast $A, because we will save $x to
+ ;; a non-nullable $A local as part of the optimization.
+ (ref.cast null $A
+ (local.get $x)
+ )
+ )
+ (drop
+ (ref.as_non_null
+ (local.get $x)
+ )
+ )
+ )
+
+ ;; CHECK: (func $move-ref.as-and-ref.cast-3 (type $structref_=>_none) (param $x structref)
+ ;; CHECK-NEXT: (local $1 (ref $A))
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.tee $1
+ ;; CHECK-NEXT: (ref.as_non_null
+ ;; CHECK-NEXT: (ref.cast null $A
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.as_non_null
+ ;; CHECK-NEXT: (local.get $1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.cast $A
+ ;; CHECK-NEXT: (local.get $1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $move-ref.as-and-ref.cast-3 (param $x (ref null struct))
+ ;; This test shows how a ref.as_non_null followed by a ref.cast can be
+ ;; both moved to an earlier local.get.
+ (drop
+ ;; Even though the ref.as_non_null appears first, it will still
+ ;; be the outer cast when both casts are moved here.
+ (local.get $x)
+ )
+ (drop
+ (ref.as_non_null
+ (local.get $x)
+ )
+ )
+ (drop
+ ;; This is converted to ref.cast $A, because we will save $x to
+ ;; a non-nullable $A local as part of the optimization.
+ (ref.cast null $A
+ (local.get $x)
+ )
+ )
+ )
+
+ ;; CHECK: (func $unoptimizable-nested-casts (type $structref_=>_none) (param $x structref)
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.cast $B
+ ;; CHECK-NEXT: (ref.as_non_null
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $unoptimizable-nested-casts (param $x (ref null struct))
+ ;; No optimizations should be made here for this nested cast.
+ ;; This test is here to ensure this.
+ (drop
+ (ref.cast $B
+ (ref.as_non_null
+ (local.get $x)
+ )
+ )
+ )
+ )
+
+ ;; CHECK: (func $no-move-over-self-tee (type $structref_structref_=>_none) (param $x structref) (param $y structref)
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.cast $A
+ ;; CHECK-NEXT: (local.tee $x
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $no-move-over-self-tee (param $x (ref null struct)) (param $y (ref null struct))
+ (drop
+ (local.get $x)
+ )
+ (drop
+ ;; We do not move this ref.cast of $x because $x is set by the local.tee,
+ ;; and we do not move casts past a set of a local index. This is treated
+ ;; like a local.set and we do not have a special case for this.
+ (ref.cast $A
+ (local.tee $x
+ (local.get $x)
+ )
+ )
+ )
+ )
+
+ ;; CHECK: (func $move-over-tee (type $structref_structref_=>_none) (param $x structref) (param $y structref)
+ ;; CHECK-NEXT: (local $a structref)
+ ;; CHECK-NEXT: (local $3 (ref $A))
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.tee $3
+ ;; CHECK-NEXT: (ref.cast $A
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.cast $A
+ ;; CHECK-NEXT: (local.tee $a
+ ;; CHECK-NEXT: (local.get $3)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $move-over-tee (param $x (ref null struct)) (param $y (ref null struct))
+ (local $a (ref null struct))
+ (drop
+ (local.get $x)
+ )
+ (drop
+ ;; We can move this ref.cast because the local.tee sets another local index.
+ (ref.cast $A
+ (local.tee $a
+ (local.get $x)
+ )
+ )
+ )
+ )
+
+ ;; CHECK: (func $move-identical-repeated-casts (type $ref|struct|_=>_none) (param $x (ref struct))
+ ;; CHECK-NEXT: (local $1 (ref $A))
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.tee $1
+ ;; CHECK-NEXT: (ref.cast $A
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.cast $A
+ ;; CHECK-NEXT: (local.get $1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.cast $A
+ ;; CHECK-NEXT: (local.get $1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $move-identical-repeated-casts (param $x (ref struct))
+ ;; This tests the case where there are two casts with equal type which can
+ ;; be moved to an earlier local.get. Only one of the casts will be duplicated
+ ;; to the earliest local.get (which one is not visible to the test).
+ (drop
+ (local.get $x)
+ )
+ (drop
+ (ref.cast $A
+ (local.get $x)
+ )
+ )
+ (drop
+ (ref.cast $A
+ (local.get $x)
+ )
+ )
+ )
+
+ ;; CHECK: (func $no-move-past-non-linear (type $structref_=>_none) (param $x structref)
+ ;; CHECK-NEXT: (local $1 (ref struct))
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (i32.const 0)
+ ;; CHECK-NEXT: (block
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (local.tee $1
+ ;; CHECK-NEXT: (ref.as_non_null
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.as_non_null
+ ;; CHECK-NEXT: (local.get $1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (ref.cast $A
+ ;; CHECK-NEXT: (local.get $x)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $no-move-past-non-linear (param $x (ref null struct))
+ (drop
+ ;; No cast can be moved up here, since this is immediately
+ ;; followed by the if statement, which resets the state of
+ ;; the optimization pass and blocks subsequent casts from
+ ;; being moved past it.
+ (local.get $x)
+ )
+ (if
+ (i32.const 0)
+ (block
+ (drop
+ ;; The ref.as_non_null can be moved here because
+ ;; it is in the same block in the same arm of the
+ ;; if statement.
+ (local.get $x)
+ )
+ (drop
+ (ref.as_non_null
+ (local.get $x)
+ )
+ )
+ )
+ )
+ (drop
+ ;; This cannot be moved earlier because it is blocked by
+ ;; the if statement. All state information is cleared when
+ ;; entering and leaving the if statement.
+ (ref.cast $A
+ (local.get $x)
+ )
+ )
+ )
+
;; CHECK: (func $get (type $none_=>_ref|struct|) (result (ref struct))
;; CHECK-NEXT: (unreachable)
;; CHECK-NEXT: )