summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xscripts/fuzz_opt.py6
-rw-r--r--src/cfg/domtree.h7
-rw-r--r--src/passes/CMakeLists.txt1
-rw-r--r--src/passes/OnceReduction.cpp444
-rw-r--r--src/passes/pass.cpp6
-rw-r--r--src/passes/passes.h1
-rw-r--r--test/lit/help/optimization-opts.test3
-rw-r--r--test/lit/passes/once-reduction.wast1429
8 files changed, 1894 insertions, 3 deletions
diff --git a/scripts/fuzz_opt.py b/scripts/fuzz_opt.py
index 3e2d83e34..9ccdeb1e4 100755
--- a/scripts/fuzz_opt.py
+++ b/scripts/fuzz_opt.py
@@ -165,8 +165,13 @@ def randomize_fuzz_settings():
IMPORTANT_INITIAL_CONTENTS = [
+ # Perenially-important passes
os.path.join('lit', 'passes', 'optimize-instructions.wast'),
os.path.join('passes', 'optimize-instructions_fuzz-exec.wast'),
+
+ # Recently-added or modified passes. These can be added to and pruned
+ # frequently.
+ os.path.join('lit', 'passes', 'once-reduction.wast'),
]
IMPORTANT_INITIAL_CONTENTS = [os.path.join(shared.get_test_dir('.'), t) for t in IMPORTANT_INITIAL_CONTENTS]
@@ -1058,6 +1063,7 @@ opt_choices = [
["--memory-packing"],
["--merge-blocks"],
['--merge-locals'],
+ ['--once-reduction'],
["--optimize-instructions"],
["--optimize-stack-ir"],
["--generate-stack-ir", "--optimize-stack-ir"],
diff --git a/src/cfg/domtree.h b/src/cfg/domtree.h
index 5e54f7c32..5753a3114 100644
--- a/src/cfg/domtree.h
+++ b/src/cfg/domtree.h
@@ -47,6 +47,10 @@ namespace wasm {
template<typename BasicBlock> struct DomTree {
std::vector<Index> iDoms;
+ // Use a nonsense value to indicate what has yet to be initialized or what is
+ // irrelevant.
+ enum { nonsense = Index(-1) };
+
DomTree(std::vector<std::unique_ptr<BasicBlock>>& blocks);
};
@@ -84,9 +88,6 @@ DomTree<BasicBlock>::DomTree(std::vector<std::unique_ptr<BasicBlock>>& blocks) {
blockIndices[blocks[i].get()] = i;
}
- // Use a nonsense value to indicate what has yet to be initialized.
- const Index nonsense = -1;
-
// Initialize the iDoms array. The entry starts with its own index, which is
// used as a guard value in effect (we will never process it, and we will fix
// up this value at the very end). All other nodes start with a nonsense value
diff --git a/src/passes/CMakeLists.txt b/src/passes/CMakeLists.txt
index ad5600f1b..b61b895ef 100644
--- a/src/passes/CMakeLists.txt
+++ b/src/passes/CMakeLists.txt
@@ -50,6 +50,7 @@ set(passes_SOURCES
NameList.cpp
NameTypes.cpp
NoExitRuntime.cpp
+ OnceReduction.cpp
OptimizeAddedConstants.cpp
OptimizeInstructions.cpp
OptimizeForJS.cpp
diff --git a/src/passes/OnceReduction.cpp b/src/passes/OnceReduction.cpp
new file mode 100644
index 000000000..73e221c69
--- /dev/null
+++ b/src/passes/OnceReduction.cpp
@@ -0,0 +1,444 @@
+/*
+ * Copyright 2021 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.
+ */
+
+//
+// Reduces the amount of calls to functions that only run once. A "run-once"
+// or "once" function is a function guarded by a global to make sure it runs a
+// single time:
+//
+// global foo$once = 0;
+//
+// function foo() {
+// if (foo$once) return;
+// foo$once = 1;
+// ..do some work..
+// }
+//
+// If we verify that there are no other kind of sets to that global - that is,
+// it is only used to guard this code - then we can remove subsequent calls to
+// the function,
+//
+// foo();
+// ..stuff..
+// foo(); // this call can be removed
+//
+// The latter call can be removed since it has definitely run by then.
+//
+// TODO: "Once" globals are effectively boolean in that all non-zero values are
+// indistinguishable, and so we could rewrite them all to be 1.
+//
+
+#include <atomic>
+
+#include "cfg/domtree.h"
+#include "ir/utils.h"
+#include "pass.h"
+#include "support/unique_deferring_queue.h"
+#include "wasm-builder.h"
+#include "wasm.h"
+
+namespace wasm {
+
+namespace {
+
+struct OptInfo {
+ // Maps global names to whether they are possible indicators of "once"
+ // functions. A "once" global has these properties:
+ //
+ // * They are only ever written to with non-zero values.
+ // * They are never read from except in the beginning of a "once" function
+ // (otherwise, execution might be affected by the specific values of the
+ // global, instead of just using it to guard the "once" function).
+ //
+ // Those properties ensure that the global is monotonic in the sense that it
+ // begins at zero and, if they are written to, will only receive a non-zero
+ // value - they never return to 0.
+ std::unordered_map<Name, std::atomic<bool>> onceGlobals;
+
+ // Maps functions to whether they are "once", by indicating the global that
+ // they use for that purpose. An empty name means they are not "once".
+ std::unordered_map<Name, Name> onceFuncs;
+
+ // For each function, the "once" globals that are definitely set after calling
+ // it. If the function is "once" itself, that is included, but it also
+ // includes any other "once" functions we definitely call, and so forth.
+ // The "new" version is written to in each iteration, and then swapped with
+ // the main one (to avoid reading and writing in parallel).
+ std::unordered_map<Name, std::unordered_set<Name>> onceGlobalsSetInFuncs,
+ newOnceGlobalsSetInFuncs;
+};
+
+struct Scanner : public WalkerPass<PostWalker<Scanner>> {
+ bool isFunctionParallel() override { return true; }
+
+ Scanner(OptInfo& optInfo) : optInfo(optInfo) {}
+
+ Scanner* create() override { return new Scanner(optInfo); }
+
+ // All the globals we read from. Any read of a global prevents us from
+ // optimizing, unless it is the single read at the top of an "only" function
+ // (as other reads might be used to check for the value of the global in
+ // complex ways that we do not want to try to reason about).
+ std::unordered_map<Name, Index> readGlobals;
+
+ void visitGlobalGet(GlobalGet* curr) { readGlobals[curr->name]++; }
+
+ void visitGlobalSet(GlobalSet* curr) {
+ if (!curr->value->type.isInteger()) {
+ // This is either a type we don't care about, or an unreachable set which
+ // we also don't care about.
+ return;
+ }
+
+ if (auto* c = curr->value->dynCast<Const>()) {
+ if (c->value.getInteger() > 0) {
+ // This writes a non-zero constant, which is what we hoped for.
+ return;
+ }
+ }
+
+ // This is not a constant, or it is zero - failure.
+ optInfo.onceGlobals.at(curr->name) = false;
+ }
+
+ void visitFunction(Function* curr) {
+ // TODO: support params and results?
+ if (curr->getParams() == Type::none && curr->getResults() == Type::none) {
+ auto global = getOnceGlobal(curr->body);
+ if (global.is()) {
+ // This is a "once" function, as best we can tell for now. Further
+ // information may cause a problem, say, if the global is used in a bad
+ // way in another function, so we may undo this.
+ optInfo.onceFuncs.at(curr->name) = global;
+
+ // We can ignore the get in the "once" pattern at the top of the
+ // function.
+ readGlobals[global]--;
+ }
+ }
+
+ for (auto& kv : readGlobals) {
+ auto global = kv.first;
+ auto count = kv.second;
+ if (count > 0) {
+ // This global has reads we cannot reason about, so do not optimize it.
+ optInfo.onceGlobals.at(global) = false;
+ }
+ }
+ }
+
+ // Check if a function body is in the "once" pattern. Return the name of the
+ // global if so, or an empty name otherwise.
+ //
+ // TODO: This pattern can show up in random places and not just at the top of
+ // the "once" function - say, if that function was inlined somewhere -
+ // so it might be good to look for the more general pattern everywhere.
+ // TODO: Handle related patterns like if (!once) { .. }, but other opts will
+ // tend to normalize to this form anyhow.
+ Name getOnceGlobal(Expression* body) {
+ // Look the pattern mentioned above:
+ //
+ // function foo() {
+ // if (foo$once) return;
+ // foo$once = 1;
+ // ...
+ //
+ auto* block = body->dynCast<Block>();
+ if (!block) {
+ return Name();
+ }
+ auto& list = block->list;
+ if (list.size() < 2) {
+ return Name();
+ }
+ auto* iff = list[0]->dynCast<If>();
+ if (!iff) {
+ return Name();
+ }
+ auto* get = iff->condition->dynCast<GlobalGet>();
+ if (!get) {
+ return Name();
+ }
+ if (!iff->ifTrue->is<Return>() || iff->ifFalse) {
+ return Name();
+ }
+ auto* set = list[1]->dynCast<GlobalSet>();
+
+ // Note that we have already checked the set's value earlier - that if it is
+ // reached then it writes a non-zero constant. Those are properties that we
+ // need from all sets. For this specific set, we also need it to actually
+ // perform the write, that is, to not be unreachable (otherwise, the global
+ // is not written here, and the function can execute more than once).
+ if (!set || set->name != get->name || set->type == Type::unreachable) {
+ return Name();
+ }
+ return get->name;
+ }
+
+private:
+ OptInfo& optInfo;
+};
+
+// Information in a basic block. We track relevant expressions, which are calls
+// calls to "once" functions, and writes to "once" globals.
+struct BlockInfo {
+ std::vector<Expression*> exprs;
+};
+
+// Performs optimization in all functions. This reads onceGlobalsSetInFuncs in
+// order to know what "once" globals are written by each function (so that when
+// we see a call, we can infer things), and when it finishes with a function it
+// has learned which "once" globals it must set, and it then writes out
+// newOnceGlobalsSetInFuncs with that result. Later iterations will then use
+// those values in place of onceGlobalsSetInFuncs, which propagate things to
+// callers. This in effect mixes local optimization with the global propagation
+// - as we need to run the full local optimization in order to infer the new
+// values for onceGlobalsSetInFuncs, that is unavoidable (in principle, we could
+// also do a full propagation to a fixed point in between running local
+// optimization, but that would require more code - it might be more efficient,
+// though).
+struct Optimizer
+ : public WalkerPass<CFGWalker<Optimizer, Visitor<Optimizer>, BlockInfo>> {
+ bool isFunctionParallel() override { return true; }
+
+ Optimizer(OptInfo& optInfo) : optInfo(optInfo) {}
+
+ Optimizer* create() override { return new Optimizer(optInfo); }
+
+ void visitGlobalSet(GlobalSet* curr) {
+ if (currBasicBlock) {
+ currBasicBlock->contents.exprs.push_back(curr);
+ }
+ }
+
+ void visitCall(Call* curr) {
+ if (currBasicBlock) {
+ currBasicBlock->contents.exprs.push_back(curr);
+ }
+ }
+
+ void doWalkFunction(Function* func) {
+ using Parent =
+ WalkerPass<CFGWalker<Optimizer, Visitor<Optimizer>, BlockInfo>>;
+
+ // Walk the function to builds the CFG.
+ Parent::doWalkFunction(func);
+ if (basicBlocks.empty()) {
+ return;
+ }
+
+ // Build a dominator tree, which then tells us what to remove: if a call
+ // appears in block A, then we do not need to make any calls in any blocks
+ // dominated by A.
+ DomTree<Parent::BasicBlock> domTree(basicBlocks);
+
+ // Perform the work by going through the blocks in reverse postorder and
+ // filling out which "once" globals have been written to.
+
+ // Each index in this vector is the set of "once" globals written to in the
+ // basic block with the same index.
+ std::vector<std::unordered_set<Name>> onceGlobalsWrittenVec;
+ auto numBlocks = basicBlocks.size();
+ onceGlobalsWrittenVec.resize(numBlocks);
+
+ for (Index i = 0; i < numBlocks; i++) {
+ auto* block = basicBlocks[i].get();
+
+ // Note that we take a reference here, which is how the data we accumulate
+ // ends up stored. The blocks we dominate will see it later.
+ auto& onceGlobalsWritten = onceGlobalsWrittenVec[i];
+
+ // Note information from our immediate dominator.
+ // TODO: we could also intersect information from all of our preds.
+ auto parent = domTree.iDoms[i];
+ if (parent == domTree.nonsense) {
+ // This is either the entry node (which we need to process), or an
+ // unreachable block (which we do not need to process - we leave that to
+ // DCE).
+ if (i > 0) {
+ continue;
+ }
+ } else {
+ // This block has an immediate dominator, so we know that everything
+ // written to there can be assumed written.
+ onceGlobalsWritten = onceGlobalsWrittenVec[parent];
+ }
+
+ // Process the block's expressions.
+ auto& exprs = block->contents.exprs;
+ for (auto* expr : exprs) {
+ // Given the name of a "once" global that is written by this
+ // instruction, optimize.
+ auto optimizeOnce = [&](Name globalName) {
+ assert(optInfo.onceGlobals.at(globalName));
+ if (onceGlobalsWritten.count(globalName)) {
+ // This global has already been written, so this expr is not needed,
+ // regardless of whether it is a global.set or a call.
+ //
+ // Note that assertions below verify that there are no children that
+ // we need to keep around, and so we can just nop the entire node.
+ ExpressionManipulator::nop(expr);
+ } else {
+ // From here on, this global is set, hopefully allowing us to
+ // optimize away others.
+ onceGlobalsWritten.insert(globalName);
+ }
+ };
+
+ if (auto* set = expr->dynCast<GlobalSet>()) {
+ if (optInfo.onceGlobals.at(set->name)) {
+ // This global is written.
+ assert(set->value->is<Const>());
+ optimizeOnce(set->name);
+ }
+ } else if (auto* call = expr->dynCast<Call>()) {
+ if (optInfo.onceFuncs.at(call->target).is()) {
+ // The global used by the "once" func is written.
+ assert(call->operands.empty());
+ optimizeOnce(optInfo.onceFuncs.at(call->target));
+ continue;
+ }
+
+ // This is not a call to a "once" func. However, we may have inferred
+ // that it definitely sets some "once" globals before it returns, and
+ // we can use that information.
+ for (auto globalName :
+ optInfo.onceGlobalsSetInFuncs.at(call->target)) {
+ onceGlobalsWritten.insert(globalName);
+ }
+ } else {
+ WASM_UNREACHABLE("invalid expr");
+ }
+ }
+ }
+
+ // As a result of the above optimization, we know which "once" globals are
+ // definitely written in this function. Regardless of whether this is a
+ // "once" function itself, that set of globals can be used in further
+ // optimizations, as any call to this function must set those.
+ // TODO: Aside from the entry block, we could intersect all the exit blocks.
+ optInfo.newOnceGlobalsSetInFuncs[func->name] =
+ std::move(onceGlobalsWrittenVec[0]);
+ }
+
+private:
+ OptInfo& optInfo;
+};
+
+} // anonymous namespace
+
+struct OnceReduction : public Pass {
+ void run(PassRunner* runner, Module* module) override {
+ OptInfo optInfo;
+
+ // Fill out the initial data.
+ for (auto& global : module->globals) {
+ // For a global to possibly be "once", it must be an integer, and to not
+ // be imported (as a mutable import may be read and written to from the
+ // outside). As we scan code we will turn this into false if we see
+ // anything that proves the global is not "once".
+ // TODO: This limitation could perhaps only be on mutable ones, but
+ // immutable globals will not be considered "once" anyhow as they do
+ // not fit the pattern of being written after the first call.
+ // TODO: non-integer types?
+ optInfo.onceGlobals[global->name] =
+ global->type.isInteger() && !global->imported();
+ }
+ for (auto& func : module->functions) {
+ // Fill in the map so that it can be operated on in parallel.
+ optInfo.onceFuncs[func->name] = Name();
+ }
+ for (auto& ex : module->exports) {
+ if (ex->kind == ExternalKind::Global) {
+ // An exported global cannot be "once" since the outside may read and
+ // write to it in ways we are unaware.
+ // TODO: See comment above on mutability.
+ optInfo.onceGlobals[ex->value] = false;
+ }
+ }
+
+ // Scan the module to find out which globals and functions are "once".
+ Scanner(optInfo).run(runner, module);
+
+ // Combine the information. We found which globals appear to be "once", but
+ // other information may have proven they are not so, in fact. Specifically,
+ // for a function to be "once" we need its global to also be such.
+ for (auto& kv : optInfo.onceFuncs) {
+ Name& onceGlobal = kv.second;
+ if (onceGlobal.is() && !optInfo.onceGlobals[onceGlobal]) {
+ onceGlobal = Name();
+ }
+ }
+
+ // Optimize using what we found. Keep iterating while we find things to
+ // optimize, which we estimate using a counter of the total number of once
+ // globals set by functions: as that increases, it means we are propagating
+ // useful information.
+ // TODO: limit # of iterations?
+ Index lastOnceGlobalsSet = 0;
+
+ // First, initialize onceGlobalsSetInFuncs for the first iteration, by
+ // ensuring each item is present, and adding the "once" global for "once"
+ // funcs.
+ bool foundOnce = false;
+ for (auto& func : module->functions) {
+ // Either way, at least fill the data structure for parallel operation.
+ auto& set = optInfo.onceGlobalsSetInFuncs[func->name];
+
+ auto global = optInfo.onceFuncs[func->name];
+ if (global.is()) {
+ set.insert(global);
+ foundOnce = true;
+ }
+ }
+
+ if (!foundOnce) {
+ // Nothing to optimize.
+ return;
+ }
+
+ while (1) {
+ // Initialize all the items in the new data structure that will be
+ // populated.
+ for (auto& func : module->functions) {
+ optInfo.newOnceGlobalsSetInFuncs[func->name];
+ }
+
+ Optimizer(optInfo).run(runner, module);
+
+ optInfo.onceGlobalsSetInFuncs =
+ std::move(optInfo.newOnceGlobalsSetInFuncs);
+
+ // Count how many once globals are set, and see if we have any more work
+ // to do.
+ Index currOnceGlobalsSet = 0;
+ for (auto& kv : optInfo.onceGlobalsSetInFuncs) {
+ auto& globals = kv.second;
+ currOnceGlobalsSet += globals.size();
+ }
+ assert(currOnceGlobalsSet >= lastOnceGlobalsSet);
+ if (currOnceGlobalsSet > lastOnceGlobalsSet) {
+ lastOnceGlobalsSet = currOnceGlobalsSet;
+ continue;
+ }
+ return;
+ }
+ }
+};
+
+Pass* createOnceReductionPass() { return new OnceReduction(); }
+
+} // namespace wasm
diff --git a/src/passes/pass.cpp b/src/passes/pass.cpp
index 3713ed8b7..878ecb3cb 100644
--- a/src/passes/pass.cpp
+++ b/src/passes/pass.cpp
@@ -233,6 +233,9 @@ void PassRegistry::registerPasses() {
"removes calls to atexit(), which is valid if the C runtime "
"will never be exited",
createNoExitRuntimePass);
+ registerPass("once-reduction",
+ "reduces calls to code that only runs once",
+ createOnceReductionPass);
registerPass("optimize-added-constants",
"optimizes added constants into load/store offsets",
createOptimizeAddedConstantsPass);
@@ -511,6 +514,9 @@ void PassRunner::addDefaultFunctionOptimizationPasses() {
void PassRunner::addDefaultGlobalOptimizationPrePasses() {
addIfNoDWARFIssues("duplicate-function-elimination");
addIfNoDWARFIssues("memory-packing");
+ if (options.optimizeLevel >= 2) {
+ addIfNoDWARFIssues("once-reduction");
+ }
if (wasm->features.hasGC() && getTypeSystem() == TypeSystem::Nominal &&
options.optimizeLevel >= 2) {
addIfNoDWARFIssues("cfp");
diff --git a/src/passes/passes.h b/src/passes/passes.h
index 9a9d6378d..715e5c443 100644
--- a/src/passes/passes.h
+++ b/src/passes/passes.h
@@ -77,6 +77,7 @@ Pass* createMetricsPass();
Pass* createNameListPass();
Pass* createNameTypesPass();
Pass* createNoExitRuntimePass();
+Pass* createOnceReductionPass();
Pass* createOptimizeAddedConstantsPass();
Pass* createOptimizeAddedConstantsPropagatePass();
Pass* createOptimizeInstructionsPass();
diff --git a/test/lit/help/optimization-opts.test b/test/lit/help/optimization-opts.test
index 52695077a..18f8970c2 100644
--- a/test/lit/help/optimization-opts.test
+++ b/test/lit/help/optimization-opts.test
@@ -348,6 +348,9 @@
;; CHECK-NEXT: is valid if the C runtime will
;; CHECK-NEXT: never be exited
;; CHECK-NEXT:
+;; CHECK-NEXT: --once-reduction reduces calls to code that only
+;; CHECK-NEXT: runs once
+;; CHECK-NEXT:
;; CHECK-NEXT: --optimize-added-constants optimizes added constants into
;; CHECK-NEXT: load/store offsets
;; CHECK-NEXT:
diff --git a/test/lit/passes/once-reduction.wast b/test/lit/passes/once-reduction.wast
new file mode 100644
index 000000000..6cd0c9d64
--- /dev/null
+++ b/test/lit/passes/once-reduction.wast
@@ -0,0 +1,1429 @@
+;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.
+;; RUN: foreach %s %t wasm-opt --once-reduction -all -S -o - | filecheck %s
+
+(module
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (global $once (mut i32) (i32.const 0))
+ (global $once (mut i32) (i32.const 0))
+
+ ;; CHECK: (func $once
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $once
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $once
+ ;; A minimal "once" function.
+ (if
+ (global.get $once)
+ (return)
+ )
+ (global.set $once (i32.const 1))
+ )
+
+ ;; CHECK: (func $caller
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ (func $caller
+ ;; Call a once function more than once, in a way that we can optimize: the
+ ;; first dominates the second. The second call will become a nop.
+ (call $once)
+ (call $once)
+ )
+)
+
+(module
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (global $once (mut i32) (i32.const 0))
+ (global $once (mut i32) (i32.const 0))
+
+ ;; CHECK: (func $once
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $once
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (i32.const 100)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $once
+ (if
+ (global.get $once)
+ (return)
+ )
+ (global.set $once (i32.const 1))
+ ;; Add some more content in the function.
+ (drop (i32.const 100))
+ )
+
+ ;; CHECK: (func $caller-if-1
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: (block $block
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ (func $caller-if-1
+ ;; Add more calls, and ones that are conditional.
+ (if
+ (i32.const 1)
+ (block
+ (call $once)
+ (call $once)
+ (call $once)
+ (call $once)
+ )
+ )
+ (call $once)
+ (call $once)
+ )
+
+ ;; CHECK: (func $caller-if-2
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (block $block
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ (func $caller-if-2
+ ;; Call in both arms. As we only handle dominance, and not merges, the first
+ ;; call after the if is *not* optimized.
+ (if
+ (i32.const 1)
+ (call $once)
+ (block
+ (call $once)
+ (call $once)
+ )
+ )
+ (call $once)
+ (call $once)
+ )
+
+ ;; CHECK: (func $caller-loop-1
+ ;; CHECK-NEXT: (loop $loop
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (br_if $loop
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ (func $caller-loop-1
+ ;; Add calls in a loop.
+ (loop $loop
+ (if
+ (i32.const 1)
+ (call $once)
+ )
+ (call $once)
+ (call $once)
+ (br_if $loop (i32.const 1))
+ )
+ (call $once)
+ (call $once)
+ )
+
+ ;; CHECK: (func $caller-loop-2
+ ;; CHECK-NEXT: (loop $loop
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (br_if $loop
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ (func $caller-loop-2
+ ;; Add a single conditional call in a loop.
+ (loop $loop
+ (if
+ (i32.const 1)
+ (call $once)
+ )
+ (br_if $loop (i32.const 1))
+ )
+ (call $once)
+ (call $once)
+ )
+
+ ;; CHECK: (func $caller-single
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: )
+ (func $caller-single
+ ;; A short function with a single call.
+ (call $once)
+ )
+
+ ;; CHECK: (func $caller-empty
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ (func $caller-empty
+ ;; A tiny function with nothing at all, just to verify we do not crash on
+ ;; such things.
+ )
+)
+
+;; Corner case: Initial value is not zero. We can still optimize this here,
+;; though in fact the function will never execute the payload call of foo(),
+;; which in theory we could further optimize.
+(module
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (import "env" "foo" (func $foo))
+ (import "env" "foo" (func $foo))
+
+ ;; CHECK: (global $once (mut i32) (i32.const 42))
+ (global $once (mut i32) (i32.const 42))
+
+ ;; CHECK: (func $once
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $once
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (call $foo)
+ ;; CHECK-NEXT: )
+ (func $once
+ (if
+ (global.get $once)
+ (return)
+ )
+ (global.set $once (i32.const 1))
+ (call $foo)
+ )
+
+ ;; CHECK: (func $caller
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ (func $caller
+ (call $once)
+ (call $once)
+ )
+)
+
+;; Corner case: function is not quite once, there is code before the if, so no
+;; optimization will happen.
+(module
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (import "env" "foo" (func $foo))
+ (import "env" "foo" (func $foo))
+
+ ;; CHECK: (global $once (mut i32) (i32.const 42))
+ (global $once (mut i32) (i32.const 42))
+
+ ;; CHECK: (func $once
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $once
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (call $foo)
+ ;; CHECK-NEXT: )
+ (func $once
+ (nop)
+ (if
+ (global.get $once)
+ (return)
+ )
+ (global.set $once (i32.const 1))
+ (call $foo)
+ )
+
+ ;; CHECK: (func $caller
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: )
+ (func $caller
+ (call $once)
+ (call $once)
+ )
+)
+
+;; Corner case: a nop after the if.
+(module
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (import "env" "foo" (func $foo))
+ (import "env" "foo" (func $foo))
+
+ ;; CHECK: (global $once (mut i32) (i32.const 42))
+ (global $once (mut i32) (i32.const 42))
+
+ ;; CHECK: (func $once
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (global.set $once
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (call $foo)
+ ;; CHECK-NEXT: )
+ (func $once
+ (if
+ (global.get $once)
+ (return)
+ )
+ (nop)
+ (global.set $once (i32.const 1))
+ (call $foo)
+ )
+
+ ;; CHECK: (func $caller
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: )
+ (func $caller
+ (call $once)
+ (call $once)
+ )
+)
+
+;; Corner case: The if has an else.
+(module
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (import "env" "foo" (func $foo))
+ (import "env" "foo" (func $foo))
+
+ ;; CHECK: (global $once (mut i32) (i32.const 42))
+ (global $once (mut i32) (i32.const 42))
+
+ ;; CHECK: (func $once
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: (call $foo)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $once
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (call $foo)
+ ;; CHECK-NEXT: )
+ (func $once
+ (if
+ (global.get $once)
+ (return)
+ (call $foo)
+ )
+ (global.set $once (i32.const 1))
+ (call $foo)
+ )
+
+ ;; CHECK: (func $caller
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: )
+ (func $caller
+ (call $once)
+ (call $once)
+ )
+)
+
+;; Corner case: different global names in the get and set
+(module
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (global $once1 (mut i32) (i32.const 0))
+ (global $once1 (mut i32) (i32.const 0))
+ ;; CHECK: (global $once2 (mut i32) (i32.const 0))
+ (global $once2 (mut i32) (i32.const 0))
+
+ ;; CHECK: (func $once
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once1)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $once2
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $once
+ (if
+ (global.get $once1)
+ (return)
+ )
+ (global.set $once2 (i32.const 1))
+ )
+
+ ;; CHECK: (func $caller
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: )
+ (func $caller
+ (call $once)
+ (call $once)
+ )
+)
+
+;; Corner case: The global is written a zero.
+(module
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (global $once (mut i32) (i32.const 0))
+ (global $once (mut i32) (i32.const 0))
+
+ ;; CHECK: (func $once
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $once
+ ;; CHECK-NEXT: (i32.const 0)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $once
+ (if
+ (global.get $once)
+ (return)
+ )
+ (global.set $once (i32.const 0))
+ )
+
+ ;; CHECK: (func $caller
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: )
+ (func $caller
+ (call $once)
+ (call $once)
+ )
+)
+
+;; Corner case: The global is written a zero elsewhere.
+(module
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (global $once (mut i32) (i32.const 0))
+ (global $once (mut i32) (i32.const 0))
+
+ ;; CHECK: (func $once
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $once
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $once
+ (if
+ (global.get $once)
+ (return)
+ )
+ (global.set $once (i32.const 1))
+ )
+
+ ;; CHECK: (func $caller
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (global.set $once
+ ;; CHECK-NEXT: (i32.const 0)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $caller
+ (call $once)
+ (call $once)
+ (global.set $once (i32.const 0))
+ )
+)
+
+;; Corner case: The global is written a non-zero value elsewhere. This is ok to
+;; optimize, and in fact we can write a value different than 1 both there and
+;; in the "once" function, and we can still optimize.
+(module
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (global $once (mut i32) (i32.const 0))
+ (global $once (mut i32) (i32.const 0))
+
+ ;; CHECK: (func $once
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $once
+ ;; CHECK-NEXT: (i32.const 42)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $once
+ (if
+ (global.get $once)
+ (return)
+ )
+ (global.set $once (i32.const 42))
+ )
+
+ ;; CHECK: (func $caller
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ (func $caller
+ (call $once)
+ (call $once)
+ (global.set $once (i32.const 1337))
+ )
+
+ ;; CHECK: (func $caller-2
+ ;; CHECK-NEXT: (global.set $once
+ ;; CHECK-NEXT: (i32.const 1337)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ (func $caller-2
+ ;; Reverse order of the above.
+ (global.set $once (i32.const 1337))
+ (call $once)
+ (call $once)
+ )
+)
+
+;; It is ok to call the "once" function inside itself - as that call appears
+;; behind a set of the global, the call is redundant and we optimize it away.
+(module
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (global $once (mut i32) (i32.const 0))
+ (global $once (mut i32) (i32.const 0))
+
+ ;; CHECK: (func $once
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $once
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ (func $once
+ (if
+ (global.get $once)
+ (return)
+ )
+ (global.set $once (i32.const 1))
+ (call $once)
+ )
+)
+
+;; Corner case: Non-integer global, which we ignore.
+(module
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (global $once (mut f64) (f64.const 0))
+ (global $once (mut f64) (f64.const 0))
+
+ ;; CHECK: (func $once
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (i32.trunc_f64_s
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $once
+ ;; CHECK-NEXT: (f64.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $once
+ (if
+ ;; We must cast this to an integer for the wasm to validate.
+ (i32.trunc_f64_s
+ (global.get $once)
+ )
+ (return)
+ )
+ (global.set $once (f64.const 1))
+ )
+
+ ;; CHECK: (func $caller
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: )
+ (func $caller
+ (call $once)
+ (call $once)
+ )
+)
+
+;; Corner case: Non-constant initial value. This is fine, as the initial value
+;; does not matter (if it is zero, then this is a "classic" "once" global; if
+;; not then it will never be written to, and the "once" function will never run
+;; at all, which is fine too)
+(module
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (import "env" "glob" (global $import i32))
+ (import "env" "glob" (global $import i32))
+
+ ;; CHECK: (global $once (mut i32) (global.get $import))
+ (global $once (mut i32) (global.get $import))
+
+ ;; CHECK: (func $once
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $once
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $once
+ (if
+ (global.get $once)
+ (return)
+ )
+ (global.set $once (i32.const 1))
+ )
+
+ ;; CHECK: (func $caller
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ (func $caller
+ (call $once)
+ (call $once)
+ )
+)
+
+;; Corner case: Non-constant later value.
+(module
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (global $once (mut i32) (i32.const 0))
+ (global $once (mut i32) (i32.const 0))
+
+ ;; CHECK: (func $once
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $once
+ ;; CHECK-NEXT: (i32.eqz
+ ;; CHECK-NEXT: (i32.eqz
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $once
+ (if
+ (global.get $once)
+ (return)
+ )
+ (global.set $once (i32.eqz (i32.eqz (i32.const 1))))
+ )
+
+ ;; CHECK: (func $caller
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: )
+ (func $caller
+ (call $once)
+ (call $once)
+ )
+)
+
+;; Corner case: "Once" function has a param.
+(module
+ ;; CHECK: (type $i32_=>_none (func (param i32)))
+
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (global $once (mut i32) (i32.const 0))
+ (global $once (mut i32) (i32.const 0))
+
+ ;; CHECK: (func $once (param $x i32)
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $once
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $once (param $x i32)
+ (if
+ (global.get $once)
+ (return)
+ )
+ (global.set $once (i32.const 1))
+ )
+
+ ;; CHECK: (func $caller
+ ;; CHECK-NEXT: (call $once
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (call $once
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $caller
+ (call $once (i32.const 1))
+ (call $once (i32.const 1))
+ )
+)
+
+;; Corner case: "Once" function has a result.
+(module
+ ;; CHECK: (type $none_=>_i32 (func (result i32)))
+
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (global $once (mut i32) (i32.const 0))
+ (global $once (mut i32) (i32.const 0))
+
+ ;; CHECK: (func $once (result i32)
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: (return
+ ;; CHECK-NEXT: (i32.const 2)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $once
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (i32.const 3)
+ ;; CHECK-NEXT: )
+ (func $once (result i32)
+ (if
+ (global.get $once)
+ (return (i32.const 2))
+ )
+ (global.set $once (i32.const 1))
+ (i32.const 3)
+ )
+
+ ;; CHECK: (func $caller
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $caller
+ (drop (call $once))
+ (drop (call $once))
+ )
+)
+
+;; Corner case: "Once" function body is not a block.
+(module
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (global $once (mut i32) (i32.const 0))
+ (global $once (mut i32) (i32.const 0))
+
+ ;; CHECK: (func $once
+ ;; CHECK-NEXT: (loop $loop
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $once
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $once
+ (loop $loop
+ (if
+ (global.get $once)
+ (return)
+ )
+ (global.set $once (i32.const 1))
+ )
+ )
+
+ ;; CHECK: (func $caller
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: )
+ (func $caller
+ (call $once)
+ (call $once)
+ )
+)
+
+;; Corner case: Once body is too short.
+(module
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (global $once (mut i32) (i32.const 0))
+ (global $once (mut i32) (i32.const 0))
+
+ ;; CHECK: (func $once
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $once
+ (if
+ (global.get $once)
+ (return)
+ )
+ )
+
+ ;; CHECK: (func $caller
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: )
+ (func $caller
+ (call $once)
+ (call $once)
+ )
+)
+
+;; Corner case: Additional reads of the global.
+(module
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (global $once (mut i32) (i32.const 0))
+ (global $once (mut i32) (i32.const 0))
+
+ ;; CHECK: (func $once
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $once
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $once
+ (if
+ (global.get $once)
+ (return)
+ )
+ (global.set $once (i32.const 1))
+ )
+
+ ;; CHECK: (func $caller
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $caller
+ (call $once)
+ (call $once)
+ (drop (global.get $once))
+ )
+)
+
+;; Corner case: Additional reads of the global in the "once" func.
+(module
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (global $once (mut i32) (i32.const 0))
+ (global $once (mut i32) (i32.const 0))
+
+ ;; CHECK: (func $once
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $once
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $once
+ (if
+ (global.get $once)
+ (return)
+ )
+ (global.set $once (i32.const 1))
+ (drop (global.get $once))
+ )
+
+ ;; CHECK: (func $caller
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: )
+ (func $caller
+ (call $once)
+ (call $once)
+ )
+)
+
+;; Corner case: Optimization opportunties in unreachable code (which we can
+;; ignore, but should not error on).
+(module
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (global $once (mut i32) (i32.const 0))
+ (global $once (mut i32) (i32.const 0))
+
+ ;; CHECK: (func $once
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $once
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $once
+ (if
+ (global.get $once)
+ (return)
+ )
+ (global.set $once (i32.const 1))
+ )
+
+ ;; CHECK: (func $caller
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (unreachable)
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: )
+ (func $caller
+ (call $once)
+ (call $once)
+ (unreachable)
+ (call $once)
+ (call $once)
+ )
+)
+
+;; Add a very long chain of control flow.
+(module
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (global $once (mut i32) (i32.const 0))
+ (global $once (mut i32) (i32.const 0))
+
+ ;; CHECK: (func $once
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $once
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $once
+ (if
+ (global.get $once)
+ (return)
+ )
+ (global.set $once (i32.const 1))
+ )
+
+ ;; CHECK: (func $caller
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ (func $caller
+ (if
+ (i32.const 1)
+ (call $once)
+ )
+ (if
+ (i32.const 1)
+ (call $once)
+ )
+ (if
+ (i32.const 1)
+ (call $once)
+ )
+ (call $once)
+ (if
+ (i32.const 1)
+ (call $once)
+ )
+ (call $once)
+ (if
+ (i32.const 1)
+ (nop)
+ (nop)
+ )
+ (call $once)
+ (if
+ (i32.const 1)
+ (nop)
+ (call $once)
+ )
+ (call $once)
+ (if
+ (i32.const 1)
+ (call $once)
+ )
+ (call $once)
+ (if
+ (i32.const 1)
+ (nop)
+ (call $once)
+ )
+ (call $once)
+ (if
+ (i32.const 1)
+ (call $once)
+ )
+ (call $once)
+ (if
+ (i32.const 1)
+ (call $once)
+ )
+ (call $once)
+ (call $once)
+ )
+)
+
+;; A test with a try-catch. This verifies that we emit their contents properly
+;; in reverse postorder and do not hit any assertions.
+(module
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (type $i32_=>_none (func (param i32)))
+
+ ;; CHECK: (global $once (mut i32) (i32.const 0))
+
+ ;; CHECK: (tag $tag (param i32))
+ (tag $tag (param i32))
+
+ (global $once (mut i32) (i32.const 0))
+
+ ;; CHECK: (func $once
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $once
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $once
+ (if
+ (global.get $once)
+ (return)
+ )
+ (global.set $once (i32.const 1))
+ )
+
+ ;; CHECK: (func $try-catch
+ ;; CHECK-NEXT: (try $label$5
+ ;; CHECK-NEXT: (do
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (catch $tag
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (pop i32)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $try-catch
+ (try $label$5
+ (do
+ (if
+ (i32.const 1)
+ (call $once)
+ )
+ )
+ (catch $tag
+ (drop
+ (pop i32)
+ )
+ )
+ )
+ )
+)
+
+(module
+ ;; Test a module with more than one global that we can optimize, and more than
+ ;; one that we cannot.
+
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (global $once1 (mut i32) (i32.const 0))
+ (global $once1 (mut i32) (i32.const 0))
+ ;; CHECK: (global $many1 (mut i32) (i32.const 0))
+ (global $many1 (mut i32) (i32.const 0))
+ ;; CHECK: (global $once2 (mut i32) (i32.const 0))
+ (global $once2 (mut i32) (i32.const 0))
+ ;; CHECK: (global $many2 (mut i32) (i32.const 0))
+ (global $many2 (mut i32) (i32.const 0))
+
+ ;; CHECK: (func $once1
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once1)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $once1
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (call $many1)
+ ;; CHECK-NEXT: (call $once2)
+ ;; CHECK-NEXT: (call $many2)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (call $many1)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (call $many2)
+ ;; CHECK-NEXT: )
+ (func $once1
+ (if
+ (global.get $once1)
+ (return)
+ )
+ (global.set $once1 (i32.const 1))
+ (call $once1)
+ (call $many1)
+ (call $once2)
+ (call $many2)
+ (call $once1)
+ (call $many1)
+ (call $once2)
+ (call $many2)
+ )
+
+ ;; CHECK: (func $many1
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $many1)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $many1
+ ;; CHECK-NEXT: (i32.const 0)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (call $many2)
+ ;; CHECK-NEXT: (call $once1)
+ ;; CHECK-NEXT: (call $many1)
+ ;; CHECK-NEXT: (call $once2)
+ ;; CHECK-NEXT: (call $many2)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (call $many1)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ (func $many1
+ (if
+ (global.get $many1)
+ (return)
+ )
+ (global.set $many1 (i32.const 0)) ;; prevent this global being "once"
+ (call $many2)
+ (call $once1)
+ (call $many1)
+ (call $once2)
+ (call $many2)
+ (call $once1)
+ (call $many1)
+ (call $once2)
+ )
+
+ ;; CHECK: (func $once2
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once2)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $once2
+ ;; CHECK-NEXT: (i32.const 2)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (call $many2)
+ ;; CHECK-NEXT: (call $once1)
+ ;; CHECK-NEXT: (call $many1)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (call $many2)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (call $many1)
+ ;; CHECK-NEXT: )
+ (func $once2
+ (if
+ (global.get $once2)
+ (return)
+ )
+ (global.set $once2 (i32.const 2))
+ (call $once2)
+ (call $many2)
+ (call $once1)
+ (call $many1)
+ (call $once2)
+ (call $many2)
+ (call $once1)
+ (call $many1)
+ )
+
+ ;; CHECK: (func $many2
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $many2)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $many1
+ ;; CHECK-NEXT: (i32.const 0)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (call $many1)
+ ;; CHECK-NEXT: (call $once2)
+ ;; CHECK-NEXT: (call $many2)
+ ;; CHECK-NEXT: (call $once1)
+ ;; CHECK-NEXT: (call $many1)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: (call $many2)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ (func $many2
+ (if
+ (global.get $many2)
+ (return)
+ )
+ (global.set $many1 (i32.const 0))
+ (call $many1)
+ (call $once2)
+ (call $many2)
+ (call $once1)
+ (call $many1)
+ (call $once2)
+ (call $many2)
+ (call $once1)
+ )
+)
+
+;; Test for propagation of information about called functions: if A->B->C->D
+;; and D calls some "once" functions, then A can infer that it's call to B does
+;; so.
+(module
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (global $once (mut i32) (i32.const 0))
+ (global $once (mut i32) (i32.const 0))
+
+ ;; CHECK: (func $once
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $once
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $once
+ (if
+ (global.get $once)
+ (return)
+ )
+ (global.set $once (i32.const 1))
+ )
+
+ ;; CHECK: (func $A
+ ;; CHECK-NEXT: (call $B)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ (func $A
+ ;; We can infer that calling B calls C and then D, and D calls this "once"
+ ;; function, so we can remove the call after it
+ (call $B)
+ (call $once)
+ )
+
+ ;; CHECK: (func $B
+ ;; CHECK-NEXT: (call $C)
+ ;; CHECK-NEXT: )
+ (func $B
+ (call $C)
+ )
+
+ ;; CHECK: (func $C
+ ;; CHECK-NEXT: (call $D)
+ ;; CHECK-NEXT: )
+ (func $C
+ (call $D)
+ )
+
+ ;; CHECK: (func $D
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ (func $D
+ (call $once)
+ (call $once)
+ )
+
+ ;; CHECK: (func $bad-A
+ ;; CHECK-NEXT: (call $bad-B)
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: )
+ (func $bad-A
+ ;; Call a function that does *not* do anything useful. We should not remove
+ ;; the second call here.
+ (call $bad-B)
+ (call $once)
+ )
+
+ ;; CHECK: (func $bad-B
+ ;; CHECK-NEXT: (nop)
+ ;; CHECK-NEXT: )
+ (func $bad-B
+ )
+)
+
+;; Corner case: Imported mutable global. We cannot optimize it, since the
+;; outside may read and write it.
+(module
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (import "env" "glob" (global $once (mut i32)))
+ (import "env" "glob" (global $once (mut i32)))
+
+ ;; CHECK: (func $once
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $once
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $once
+ (if
+ (global.get $once)
+ (return)
+ )
+ (global.set $once (i32.const 1))
+ )
+
+ ;; CHECK: (func $caller
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: )
+ (func $caller
+ (call $once)
+ (call $once)
+ )
+)
+
+;; Corner case: Exported mutable global. We cannot optimize it, since the
+;; outside may read and write it.
+(module
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (global $once (mut i32) (i32.const 0))
+ (global $once (mut i32) (i32.const 0))
+
+ ;; CHECK: (export "once-global" (global $once))
+ (export "once-global" (global $once))
+
+ ;; CHECK: (func $once
+ ;; CHECK-NEXT: (if
+ ;; CHECK-NEXT: (global.get $once)
+ ;; CHECK-NEXT: (return)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (global.set $once
+ ;; CHECK-NEXT: (i32.const 1)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $once
+ (if
+ (global.get $once)
+ (return)
+ )
+ (global.set $once (i32.const 1))
+ )
+
+ ;; CHECK: (func $caller
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: (call $once)
+ ;; CHECK-NEXT: )
+ (func $caller
+ (call $once)
+ (call $once)
+ )
+)