diff options
author | Bruce He <44327446+zm2he@users.noreply.github.com> | 2023-06-06 16:11:38 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-06-06 20:11:38 +0000 |
commit | 7b093e8d0cfe09471aed4eb40c093f0603f11efb (patch) | |
tree | 298dba196da4c5be133dae14f5c13f6ac32b1b87 | |
parent | 90f471ded4f9cdacb630197d192da93f56d01ee9 (diff) | |
download | binaryen-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.cpp | 340 | ||||
-rw-r--r-- | test/lit/passes/optimize-casts.wast | 849 |
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: ) |