summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlon Zakai <azakai@google.com>2022-11-17 15:00:28 -0800
committerGitHub <noreply@github.com>2022-11-17 23:00:28 +0000
commit977d653f9801b3eedc7dd667e4068573e73d2bb5 (patch)
tree8876e96e5f31d32b6a656a627b02e635db1314af
parent5f5c70255cfa917efee9855ce1f8340b017e0adb (diff)
downloadbinaryen-977d653f9801b3eedc7dd667e4068573e73d2bb5.tar.gz
binaryen-977d653f9801b3eedc7dd667e4068573e73d2bb5.tar.bz2
binaryen-977d653f9801b3eedc7dd667e4068573e73d2bb5.zip
[Wasm GC] Start an OptimizeCasts pass and reuse cast values there (#5263)
(some.operation (ref.cast .. (local.get $ref)) (local.get $ref) ) => (some.operation (local.tee $temp (ref.cast .. (local.get $ref)) ) (local.get $temp) ) This can help cases where we cast for some reason but happen to not use the cast value in all places. This occurs in j2wasm in itable calls sometimes: The this pointer is is refined, but the itable may be done with an unrefined pointer, which is less optimizable. So far this is just inside basic blocks, but that is enough for the cast of itable calls and other common patterns I see.
-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)
+ )
+)