summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xscripts/fuzz_opt.py1
-rw-r--r--src/passes/CMakeLists.txt1
-rw-r--r--src/passes/Monomorphize.cpp3
-rw-r--r--src/passes/OptimizeCasts.cpp229
-rw-r--r--src/passes/pass.cpp3
-rw-r--r--src/passes/passes.h1
-rw-r--r--test/lit/help/wasm-opt.test2
-rw-r--r--test/lit/help/wasm2js.test2
-rw-r--r--test/lit/passes/optimize-casts.wast398
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)
+ )
+)