diff options
-rwxr-xr-x | scripts/fuzz_opt.py | 1 | ||||
-rw-r--r-- | src/passes/CMakeLists.txt | 1 | ||||
-rw-r--r-- | src/passes/Monomorphize.cpp | 3 | ||||
-rw-r--r-- | src/passes/OptimizeCasts.cpp | 229 | ||||
-rw-r--r-- | src/passes/pass.cpp | 3 | ||||
-rw-r--r-- | src/passes/passes.h | 1 | ||||
-rw-r--r-- | test/lit/help/wasm-opt.test | 2 | ||||
-rw-r--r-- | test/lit/help/wasm2js.test | 2 | ||||
-rw-r--r-- | test/lit/passes/optimize-casts.wast | 398 |
9 files changed, 640 insertions, 0 deletions
diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py index 49f98fc1a..2f4fad5c2 100755 --- a/scripts/fuzz_opt.py +++ b/scripts/fuzz_opt.py @@ -1297,6 +1297,7 @@ opt_choices = [ ['--monomorphize'], ['--monomorphize-always'], ['--once-reduction'], + ["--optimize-casts"], ["--optimize-instructions"], ["--optimize-stack-ir"], ["--generate-stack-ir", "--optimize-stack-ir"], diff --git a/src/passes/CMakeLists.txt b/src/passes/CMakeLists.txt index 8204f1070..c19f19473 100644 --- a/src/passes/CMakeLists.txt +++ b/src/passes/CMakeLists.txt @@ -68,6 +68,7 @@ set(passes_SOURCES NameTypes.cpp OnceReduction.cpp OptimizeAddedConstants.cpp + OptimizeCasts.cpp OptimizeInstructions.cpp OptimizeForJS.cpp PickLoadSigns.cpp diff --git a/src/passes/Monomorphize.cpp b/src/passes/Monomorphize.cpp index 80e908a83..f8ee4a6e6 100644 --- a/src/passes/Monomorphize.cpp +++ b/src/passes/Monomorphize.cpp @@ -214,6 +214,9 @@ struct Monomorphize : public Pass { // expect to help. That would be faster, but we'd always run the risk of // missing things, especially as new passes are added later and we don't // think to add them here. + // Alternatively, perhaps we should have a mode that does use -O1 or + // even -O2 or above, as in theory any optimization could end up + // mattering a lot here. void doMinimalOpts(Function* func) { PassRunner runner(getPassRunner()); runner.options.optimizeLevel = 1; diff --git a/src/passes/OptimizeCasts.cpp b/src/passes/OptimizeCasts.cpp new file mode 100644 index 000000000..854110157 --- /dev/null +++ b/src/passes/OptimizeCasts.cpp @@ -0,0 +1,229 @@ +/* + * Copyright 2022 WebAssembly Community Group participants + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// +// Refine uses of locals where possible. For example, consider this: +// +// (some.operation +// (ref.cast .. (local.get $ref)) +// (local.get $ref) +// ) +// +// The second use might as well use the refined/cast value as well: +// +// (some.operation +// (local.tee $temp +// (ref.cast .. (local.get $ref)) +// ) +// (local.get $temp) +// ) +// +// This change adds a local but it switches some local.gets to use a local of a +// more refined type. That can help other optimizations later. +// +// An example of an important pattern this handles are itable calls: +// +// (call_ref +// (ref.cast $actual.type +// (local.get $object) +// ) +// (struct.get $vtable .. +// (ref.cast $vtable +// (struct.get $itable .. +// (local.get $object) +// ) +// ) +// ) +// ) +// +// We cast to the actual type for the |this| parameter, but we technically do +// not need to do so for reading its itable - since the itable may be of a +// generic type, and we cast the vtable afterwards anyhow. But since we cast +// |this|, we can use the cast value for the itable get, which may then lead to +// removing the vtable cast after we refine the itable type. And that can lead +// to devirtualization later. +// +// Closely related things appear in other passes: +// +// * SimplifyLocals will find locals already containing a more refined type and +// switch to them. RedundantSetElimination does the same across basic blocks. +// In theory one of them could be extended to also add new locals, and then +// they would be doing something similar to this pass. +// * LocalCSE finds repeated expressions and stores them in locals for use +// later. In theory that pass could be extended to look not for exact copies +// but for equivalent things through a cast, and then it would be doing +// something similar to this pass. +// +// However, while those other passes could be extended to cover what this pass +// does, we will have further cast-specific optimizations to add, which make +// sense in new pass anyhow, and things should be simpler overall to keep such +// casts all in one pass, here. +// +// TODO: Move casts earlier in a basic block as well, at least in traps-never- +// happen mode where we can assume they never fail. +// TODO: Look past individual basic blocks? +// TODO: Look at LocalSet as well and not just Get. That would add some overlap +// with the other passes mentioned above, but once we do things like +// moving casts earlier as in the other TODO, we'd be doing uniquely +// useful things with LocalSet here. +// + +#include "ir/linear-execution.h" +#include "ir/properties.h" +#include "ir/utils.h" +#include "pass.h" +#include "wasm-builder.h" +#include "wasm.h" + +namespace wasm { + +namespace { + +// 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> { + + PassOptions options; + + // Map local indices to the most refined downcastings of local.gets from those + // indices. + // + // This is tracked in each basic block, and cleared between them. + std::unordered_map<Index, Expression*> mostCastedGets; + + // For each most-downcasted local.get, a vector of other local.gets that could + // be replaced with gets of the downcasted value. + // + // This is tracked until the end of the entire function, and contains the + // information we need to optimize later. That is, entries here are things we + // want to apply. + std::unordered_map<Expression*, std::vector<LocalGet*>> lessCastedGets; + + static void doNoteNonLinear(BestCastFinder* self, Expression** currp) { + self->mostCastedGets.clear(); + } + + void visitLocalSet(LocalSet* curr) { + // Clear any information about this local; it has a new value here. + mostCastedGets.erase(curr->index); + } + + void visitLocalGet(LocalGet* curr) { + auto iter = mostCastedGets.find(curr->index); + if (iter != mostCastedGets.end()) { + auto* bestCast = iter->second; + if (curr->type != bestCast->type && + Type::isSubType(bestCast->type, curr->type)) { + // The best cast has a more refined type, note that we want to use it. + lessCastedGets[bestCast].push_back(curr); + } + } + } + + void visitRefAs(RefAs* curr) { handleRefinement(curr); } + + void visitRefCast(RefCast* curr) { handleRefinement(curr); } + + void handleRefinement(Expression* curr) { + auto* fallthrough = Properties::getFallthrough(curr, options, *getModule()); + if (auto* get = fallthrough->dynCast<LocalGet>()) { + auto*& bestCast = mostCastedGets[get->index]; + if (!bestCast) { + // This is the first. + bestCast = curr; + return; + } + + // See if we are better than the current best. + if (curr->type != bestCast->type && + Type::isSubType(curr->type, bestCast->type)) { + bestCast = curr; + } + } + } +}; + +// Given a set of best casts, apply them: save each best cast in a local and use +// it in the places that want to. +// +// It is simpler to do this in another pass after BestCastFinder so that we do +// not need to worry about corner cases with invalidation of pointers in things +// we've already walked past. +struct FindingApplier : public PostWalker<FindingApplier> { + BestCastFinder& finder; + + FindingApplier(BestCastFinder& finder) : finder(finder) {} + + void visitRefAs(RefAs* curr) { handleRefinement(curr); } + + void visitRefCast(RefCast* curr) { handleRefinement(curr); } + + void handleRefinement(Expression* curr) { + auto iter = finder.lessCastedGets.find(curr); + if (iter == finder.lessCastedGets.end()) { + return; + } + + // This expression was the best cast for some gets. Add a new local to + // store this value, then use it for the gets. + auto var = Builder::addVar(getFunction(), curr->type); + auto& gets = iter->second; + for (auto* get : gets) { + get->index = var; + get->type = curr->type; + } + + // Replace ourselves with a tee. + replaceCurrent(Builder(*getModule()).makeLocalTee(var, curr, curr->type)); + } +}; + +} // anonymous namespace + +struct OptimizeCasts : public WalkerPass<PostWalker<OptimizeCasts>> { + bool isFunctionParallel() override { return true; } + + std::unique_ptr<Pass> create() override { + return std::make_unique<OptimizeCasts>(); + } + + void doWalkFunction(Function* func) { + if (!getModule()->features.hasGC()) { + return; + } + + // First, 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; + } + + // Apply the requests: use the best casts. + FindingApplier applier(finder); + applier.walkFunctionInModule(func, getModule()); + + // LocalGet type changes must be propagated. + ReFinalize().walkFunctionInModule(func, getModule()); + } +}; + +Pass* createOptimizeCastsPass() { return new OptimizeCasts(); } + +} // namespace wasm diff --git a/src/passes/pass.cpp b/src/passes/pass.cpp index 7f79313e2..68561f7ee 100644 --- a/src/passes/pass.cpp +++ b/src/passes/pass.cpp @@ -293,6 +293,8 @@ void PassRegistry::registerPasses() { "optimizes added constants into load/store offsets, propagating " "them across locals too", createOptimizeAddedConstantsPropagatePass); + registerPass( + "optimize-casts", "eliminate and reuse casts", createOptimizeCastsPass); registerPass("optimize-instructions", "optimizes instruction combinations", createOptimizeInstructionsPass); @@ -546,6 +548,7 @@ void PassRunner::addDefaultFunctionOptimizationPasses() { addIfNoDWARFIssues("merge-locals"); // very slow on e.g. sqlite } if (options.optimizeLevel > 1 && wasm->features.hasGC()) { + addIfNoDWARFIssues("optimize-casts"); // Coalescing may prevent subtyping (as a coalesced local must have the // supertype of all those combined into it), so subtype first. // TODO: when optimizing for size, maybe the order should reverse? diff --git a/src/passes/passes.h b/src/passes/passes.h index 65923cd25..ae0cc62e2 100644 --- a/src/passes/passes.h +++ b/src/passes/passes.h @@ -93,6 +93,7 @@ Pass* createOnceReductionPass(); Pass* createOptimizeAddedConstantsPass(); Pass* createOptimizeAddedConstantsPropagatePass(); Pass* createOptimizeInstructionsPass(); +Pass* createOptimizeCastsPass(); Pass* createOptimizeForJSPass(); Pass* createOptimizeStackIRPass(); Pass* createPickLoadSignsPass(); diff --git a/test/lit/help/wasm-opt.test b/test/lit/help/wasm-opt.test index 2af38f982..bc3d710ac 100644 --- a/test/lit/help/wasm-opt.test +++ b/test/lit/help/wasm-opt.test @@ -296,6 +296,8 @@ ;; CHECK-NEXT: load/store offsets, propagating ;; CHECK-NEXT: them across locals too ;; CHECK-NEXT: +;; CHECK-NEXT: --optimize-casts eliminate and reuse casts +;; CHECK-NEXT: ;; CHECK-NEXT: --optimize-for-js early optimize of the ;; CHECK-NEXT: instruction combinations for js ;; CHECK-NEXT: diff --git a/test/lit/help/wasm2js.test b/test/lit/help/wasm2js.test index e5669563d..ce2748343 100644 --- a/test/lit/help/wasm2js.test +++ b/test/lit/help/wasm2js.test @@ -255,6 +255,8 @@ ;; CHECK-NEXT: load/store offsets, propagating ;; CHECK-NEXT: them across locals too ;; CHECK-NEXT: +;; CHECK-NEXT: --optimize-casts eliminate and reuse casts +;; CHECK-NEXT: ;; CHECK-NEXT: --optimize-for-js early optimize of the ;; CHECK-NEXT: instruction combinations for js ;; CHECK-NEXT: diff --git a/test/lit/passes/optimize-casts.wast b/test/lit/passes/optimize-casts.wast new file mode 100644 index 000000000..449416e42 --- /dev/null +++ b/test/lit/passes/optimize-casts.wast @@ -0,0 +1,398 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. +;; RUN: wasm-opt %s --optimize-casts -all --nominal -S -o - \ +;; RUN: | filecheck %s + +(module + ;; CHECK: (type $A (struct_subtype data)) + (type $A (struct_subtype data)) + + ;; CHECK: (type $B (struct_subtype $A)) + (type $B (struct_subtype $A)) + + ;; 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) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; 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. + (drop + (local.get $x) + ) + (drop + (ref.as_non_null + (local.get $x) + ) + ) + (drop + (local.get $x) + ) + ;; In this case we don't really need the last ref.as here, but we leave that + ;; for later opts. + (drop + (ref.as_non_null + (local.get $x) + ) + ) + ) + + ;; CHECK: (func $ref.as-no (type $ref|$A|_=>_none) (param $x (ref $A)) + ;; 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: (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 $ref.as-no (param $x (ref $A)) + ;; As above, but the param is now non-nullable anyhow, so we should do + ;; nothing. + (drop + (local.get $x) + ) + (drop + (ref.as_non_null + (local.get $x) + ) + ) + (drop + (local.get $x) + ) + (drop + (ref.as_non_null + (local.get $x) + ) + ) + ) + + ;; CHECK: (func $ref.cast (type $ref|data|_=>_none) (param $x (ref data)) + ;; CHECK-NEXT: (local $1 (ref $A)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.tee $1 + ;; CHECK-NEXT: (ref.cast_static $A + ;; 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: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $ref.cast (param $x (ref data)) + ;; As $ref.as but with ref.casts: we should use the cast value after it has + ;; been computed, in both gets. + (drop + (ref.cast_static $A + (local.get $x) + ) + ) + (drop + (local.get $x) + ) + (drop + (local.get $x) + ) + ) + + ;; CHECK: (func $not-past-set (type $ref|data|_=>_none) (param $x (ref data)) + ;; CHECK-NEXT: (local $1 (ref $A)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.tee $1 + ;; CHECK-NEXT: (ref.cast_static $A + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (call $get) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $not-past-set (param $x (ref data)) + (drop + (ref.cast_static $A + (local.get $x) + ) + ) + (drop + (local.get $x) + ) + ;; The local.set in the middle stops us from helping the last get. + (local.set $x + (call $get) + ) + (drop + (local.get $x) + ) + ) + + ;; CHECK: (func $best (type $ref|data|_=>_none) (param $x (ref data)) + ;; CHECK-NEXT: (local $1 (ref $A)) + ;; CHECK-NEXT: (local $2 (ref $B)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.tee $1 + ;; CHECK-NEXT: (ref.cast_static $A + ;; 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: (local.tee $2 + ;; CHECK-NEXT: (ref.cast_static $B + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $best (param $x (ref data)) + (drop + (ref.cast_static $A + (local.get $x) + ) + ) + ;; Here we should use $A. + (drop + (local.get $x) + ) + (drop + (ref.cast_static $B + (local.get $x) + ) + ) + ;; Here we should use $B, which is even better. + (drop + (local.get $x) + ) + ) + + ;; CHECK: (func $best-2 (type $ref|data|_=>_none) (param $x (ref data)) + ;; CHECK-NEXT: (local $1 (ref $B)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.tee $1 + ;; CHECK-NEXT: (ref.cast_static $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_static $A + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $best-2 (param $x (ref data)) + ;; As above, but with the casts reversed. Now we should use $B in both + ;; gets. + (drop + (ref.cast_static $B + (local.get $x) + ) + ) + (drop + (local.get $x) + ) + (drop + (ref.cast_static $A + (local.get $x) + ) + ) + (drop + (local.get $x) + ) + ) + + ;; CHECK: (func $fallthrough (type $ref|data|_=>_none) (param $x (ref data)) + ;; CHECK-NEXT: (local $1 (ref $A)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.tee $1 + ;; CHECK-NEXT: (ref.cast_static $A + ;; CHECK-NEXT: (block (result (ref data)) + ;; 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: ) + (func $fallthrough (param $x (ref data)) + (drop + (ref.cast_static $A + ;; We look through the block, and optimize. + (block (result (ref data)) + (local.get $x) + ) + ) + ) + (drop + (local.get $x) + ) + ) + + ;; CHECK: (func $past-basic-block (type $ref|data|_=>_none) (param $x (ref data)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.cast_static $A + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (return) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $past-basic-block (param $x (ref data)) + (drop + (ref.cast_static $A + (local.get $x) + ) + ) + ;; The if means the later get is in another basic block. We do not handle + ;; this atm. + (if + (i32.const 0) + (return) + ) + (drop + (local.get $x) + ) + ) + + ;; CHECK: (func $multiple (type $ref|data|_ref|data|_=>_none) (param $x (ref data)) (param $y (ref data)) + ;; CHECK-NEXT: (local $a (ref data)) + ;; CHECK-NEXT: (local $b (ref data)) + ;; CHECK-NEXT: (local $4 (ref $A)) + ;; CHECK-NEXT: (local $5 (ref $A)) + ;; CHECK-NEXT: (local.set $a + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $b + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.tee $4 + ;; CHECK-NEXT: (ref.cast_static $A + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.tee $5 + ;; CHECK-NEXT: (ref.cast_static $A + ;; CHECK-NEXT: (local.get $b) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $5) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $b + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $b) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $multiple (param $x (ref data)) (param $y (ref data)) + (local $a (ref data)) + (local $b (ref data)) + ;; Two different locals, with overlapping lives. + (local.set $a + (local.get $x) + ) + (local.set $b + (local.get $y) + ) + (drop + (ref.cast_static $A + (local.get $a) + ) + ) + (drop + (ref.cast_static $A + (local.get $b) + ) + ) + ;; These two can be optimized. + (drop + (local.get $a) + ) + (drop + (local.get $b) + ) + (local.set $b + (local.get $x) + ) + ;; Now only the first can be, since $b changed. + (drop + (local.get $a) + ) + (drop + (local.get $b) + ) + ) + + ;; CHECK: (func $get (type $none_=>_ref|data|) (result (ref data)) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $get (result (ref data)) + ;; Helper for the above. + (unreachable) + ) +) |