diff --git a/src/ir/constraint.cpp b/src/ir/constraint.cpp index 57420a1733f..84d3d257512 100644 --- a/src/ir/constraint.cpp +++ b/src/ir/constraint.cpp @@ -126,12 +126,9 @@ void AndedConstraintSet::approximateAnd(const Constraint& c) { void AndedConstraintSet::approximateOr(const AndedConstraintSet& other) { // If one is empty (no constraints, everything is true, and we can prove - // nothing useful) then it does not add anything to the other. - if (empty()) { - *this = other; - return; - } - if (other.empty()) { + // nothing useful) then we can prove nothing after the OR. + if (empty() || other.empty()) { + clear(); return; } @@ -153,4 +150,111 @@ void AndedConstraintSet::approximateOr(const AndedConstraintSet& other) { clear(); } +std::optional LocalConstraint::parse(Expression* curr) { + auto parseEqZArgument = + [&](Expression* value) -> std::optional { + if (auto* get = value->dynCast()) { + // Canonicalize EqZ to Eq of 0. + auto value = Literal::makeZero(get->type); + return LocalConstraint{get->index, Constraint{Abstract::Eq, {value}}}; + } + // TODO: Recursively parse and reverse a constraint + return {}; + }; + + if (auto* unary = curr->dynCast()) { + if (Abstract::getUnary(unary->type, Abstract::EqZ) == unary->op) { + return parseEqZArgument(unary->value); + } + return {}; + } + + if (auto* refIsNull = curr->dynCast()) { + return parseEqZArgument(refIsNull->value); + } + + // Parse a get or a constant. + auto parseTerm = [&](Expression* expr) -> std::optional { + if (auto* get = expr->dynCast()) { + return Term{get->index}; + } + if (Properties::isSingleConstantExpression(expr)) { + return Term{Properties::getLiteral(expr)}; + } + return {}; + }; + + auto parseBinaryArguments = + [&](Abstract::Op op, + Expression* left, + Expression* right) -> std::optional { + // The left must be a get. + if (auto* get = left->dynCast()) { + // The right can be any term. + if (auto value = parseTerm(right)) { + return LocalConstraint{get->index, Constraint{op, *value}}; + } + } + return {}; + }; + + if (auto* binary = curr->dynCast()) { + // The operation must be one we recognize. + for (auto op : {Abstract::Eq, Abstract::Ne}) { + if (Abstract::getBinary(binary->type, op) == binary->op) { + return parseBinaryArguments(op, binary->left, binary->right); + } + } + return {}; + } + + if (auto* refEq = curr->dynCast()) { + return parseBinaryArguments(Abstract::Eq, refEq->left, refEq->right); + } + + return {}; +} + +void LocalConstraintMap::approximateOr(const LocalConstraintMap& other) { + // Find things in both, and OR them. + for (auto& [local, constraints] : other) { + if (auto iter = find(local); iter != end()) { + iter->second.approximateOr(constraints); + } + } + + // Remove things only in us. + std::erase_if(*this, [&](const auto& item) { + const auto& [local, constraints] = item; + return !other.contains(local); + }); +} + +std::ostream& operator<<(std::ostream& o, const Constraint& constraint) { + o << "Constraint{" << /*constraint.op <<*/ ", "; + if (auto* c = std::get_if(&constraint.term)) { + o << *c; + } else if (auto* i = std::get_if(&constraint.term)) { + o << "Index(" << *i << ')'; + } + o << '}'; + return o; +} + +std::ostream& operator<<(std::ostream& o, + const AndedConstraintSet& constraints) { + o << "AndedConstraintSet{"; + bool first = true; + for (auto& constraint : constraints) { + if (first) { + first = false; + } else { + o << ", "; + } + o << constraint; + } + o << '}'; + return o; +} + } // namespace wasm::constraint diff --git a/src/ir/constraint.h b/src/ir/constraint.h index 4a689ceec3a..a9bcded5cff 100644 --- a/src/ir/constraint.h +++ b/src/ir/constraint.h @@ -121,6 +121,35 @@ struct AndedConstraintSet : inplace_vector { void approximateOr(const AndedConstraintSet& other); }; +// A local plus a constraint on it. +struct LocalConstraint { + Index local; + Constraint constraint; + + // Try to parse BinaryenIR into a local to which a constraint is applied. For + // example + // + // (i32.eq (local.get $r) (i32.const 10)) + // + // parses into + // + // LocalConstraint($r, { x == 10 }) + // + static std::optional parse(Expression* curr); +}; + +// A map of locals and their constraints. +struct LocalConstraintMap + : public std::unordered_map { + // Perform an OR as above on each local that appears in both maps. If a local + // appears only in one, we can infer nothing, and drop it. + void approximateOr(const LocalConstraintMap& other); +}; + +std::ostream& operator<<(std::ostream& o, const Constraint& constraint); +std::ostream& operator<<(std::ostream& o, + const AndedConstraintSet& constraints); + } // namespace wasm::constraint #endif // wasm_ir_constraint_h diff --git a/src/passes/CMakeLists.txt b/src/passes/CMakeLists.txt index 2dfda96d0e5..684077171c8 100644 --- a/src/passes/CMakeLists.txt +++ b/src/passes/CMakeLists.txt @@ -26,6 +26,7 @@ set(passes_SOURCES CodeFolding.cpp ConstantFieldPropagation.cpp ConstHoisting.cpp + ConstraintAnalysis.cpp DataFlowOpts.cpp DeadArgumentElimination.cpp DeadArgumentElimination2.cpp diff --git a/src/passes/ConstraintAnalysis.cpp b/src/passes/ConstraintAnalysis.cpp new file mode 100644 index 00000000000..74f1f3e87ff --- /dev/null +++ b/src/passes/ConstraintAnalysis.cpp @@ -0,0 +1,217 @@ +/* + * Copyright 2026 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. + */ + +// +// Use mathematical constraint solving to optimize. For example: +// +// if (x == 10) { +// assert(x != 0); // redundant and can be removed. +// } +// + +#include "cfg/cfg-traversal.h" +#include "ir/constraint.h" +#include "ir/drop.h" +#include "ir/literal-utils.h" +#include "ir/local-graph.h" +#include "ir/properties.h" +#include "pass.h" +#include "support/unique_deferring_queue.h" +#include "support/utilities.h" +#include "wasm-builder.h" +#include "wasm.h" + +namespace wasm { + +using namespace wasm::constraint; + +namespace { + +// In each basic block we will store the relevant operations, which are all +// local gets and sets, branches, and uses of them. +struct Info { + std::vector actions; + + // For each local index, we track the constraints we know about it. We only do + // so at the start of each block, which is enough for the analysis below. + // + // We use an optional here to represent the "null" state before any + // information arrives. (From the perspective of set theory, nullopt can be + // taken to mean the empty set is the set of values possible for each local.) + std::optional startConstraints; +}; + +struct ConstraintAnalysis + : public WalkerPass< + CFGWalker, Info>> { + bool isFunctionParallel() override { return true; } + + // Locals are not modified here. + bool requiresNonNullableLocalFixups() override { return false; } + + std::unique_ptr create() override { + return std::make_unique(); + } + + // Branches outside of the function can be ignored, as we only look at local + // state in the function. + bool ignoreBranchesOutsideOfFunc = true; + + // Store the actions we care about. + void addAction() { + if (currBasicBlock) { + currBasicBlock->contents.actions.push_back(getCurrentPointer()); + } + } + + void visitLocalSet(LocalSet* curr) { addAction(); } + void visitUnary(Unary* curr) { addAction(); } + void visitBinary(Binary* curr) { addAction(); } + void visitRefEq(RefEq* curr) { addAction(); } + void visitRefIsNull(RefIsNull* curr) { addAction(); } + + void visitFunction(Function* curr) { + if (!entry) { + // Body is unreachable, no entry block. + return; + } + // TODO: optimize for speed, find relevant locals etc. + flow(); + optimize(); + } + + // Flow infos around until we have inferred all we can about the constraints + // in each location. + void flow() { + // Start from the entry. That block has incoming values - defaults - for + // each var. + auto& entryConstraints = entry->contents.startConstraints; + entryConstraints.emplace(); + auto* func = getFunction(); + auto numLocals = func->getNumLocals(); + for (Index i = 0; i < numLocals; i++) { + if (!func->isVar(i)) { + continue; + } + auto type = func->getLocalType(i); + // TODO: support tuples + if (type.size() == 1 && LiteralUtils::canMakeZero(type)) { + auto value = Literal::makeZero(type); + (*entryConstraints)[i].approximateAnd( + Constraint{Abstract::Eq, {value}}); + } + } + + // Starting from the entry, keep going while we find something new. + UniqueDeferredQueue work; + work.push(entry); + while (!work.empty()) { + auto* block = work.pop(); + + // Start at the top of the block, then go through, applying things. + LocalConstraintMap constraints = *block->contents.startConstraints; + for (auto** currp : block->contents.actions) { + applyToConstraints(*currp, constraints); + } + + // We now know the values at the end of the block. Flow it onward, and + // where it causes changes, queue more work. + for (auto* out : block->out) { + auto& outConstraints = out->contents.startConstraints; + if (!outConstraints) { + // This is the first data arriving. + outConstraints.emplace(constraints); + work.push(out); + continue; + } + + // This is later data, which may or may not cause changes. + auto old = outConstraints; + outConstraints->approximateOr(constraints); + if (*outConstraints != old) { + work.push(out); + } + } + } + } + + // After inferring all we can, apply it to optimize the code. + void optimize() { + for (auto& block : basicBlocks) { + // Follow the general shape of flow(): we need to see what the state is + // at each intermediate point inside the block. (Flowing between blocks is + // of course not needed at this stage.) + auto& constraints = block->contents.startConstraints; + if (!constraints) { + // Unreachable. + continue; + } + + for (auto** currp : block->contents.actions) { + applyToConstraints(*currp, *constraints); + optimizeExpression(currp, *constraints); + } + } + } + + // Given an expression and the constraints on it, optimize it. + void optimizeExpression(Expression** currp, + const LocalConstraintMap& constraints) { + auto* curr = *currp; + auto parsed = LocalConstraint::parse(curr); + if (!parsed) { + return; + } + + auto iter = constraints.find(parsed->local); + if (iter == constraints.end()) { + return; + } + auto& localConstraints = iter->second; + Result result = localConstraints.proves(parsed->constraint); + if (result == Unknown) { + // If we parsed something using two locals, like x != y, we can also look + // for the flipped condition among y's constraints TODO + return; + } + + // We know the result! + auto& wasm = *getModule(); + auto value = + LiteralUtils::makeFromInt32(result == True ? 1 : 0, curr->type, wasm); + *currp = getDroppedChildrenAndAppend( + curr, wasm, getPassOptions(), value, DropMode::IgnoreParentEffects); + } + + // Given an expression, apply it to the constraints. For example, a local.set + // sets the value for that local. + void applyToConstraints(Expression* curr, LocalConstraintMap& constraints) { + if (auto* set = curr->dynCast()) { + auto& localConstraints = constraints[set->index]; + localConstraints.clear(); + if (Properties::isSingleConstantExpression(set->value)) { + auto value = Properties::getLiteral(set->value); + localConstraints.approximateAnd(Constraint{Abstract::Eq, {value}}); + } + } + } +}; + +} // anonymous namespace + +Pass* createConstraintAnalysisPass() { return new ConstraintAnalysis(); } + +} // namespace wasm diff --git a/src/passes/pass.cpp b/src/passes/pass.cpp index 672936f3444..06339ca5a6b 100644 --- a/src/passes/pass.cpp +++ b/src/passes/pass.cpp @@ -129,6 +129,9 @@ void PassRegistry::registerPasses() { registerPass("cfp-reftest", "propagate constant struct field values, using ref.test", createConstantFieldPropagationRefTestPass); + registerPass("constraint-analysis", + "finds and uses mathematical constraints on locals", + createConstraintAnalysisPass); registerPass( "dce", "removes unreachable code", createDeadCodeEliminationPass); registerPass("dealign", diff --git a/src/passes/passes.h b/src/passes/passes.h index caeaf207a42..3e289460eec 100644 --- a/src/passes/passes.h +++ b/src/passes/passes.h @@ -33,6 +33,7 @@ Pass* createCodePushingPass(); Pass* createConstHoistingPass(); Pass* createConstantFieldPropagationPass(); Pass* createConstantFieldPropagationRefTestPass(); +Pass* createConstraintAnalysisPass(); Pass* createDAEPass(); Pass* createDAEOptimizingPass(); Pass* createDAE2Pass(); diff --git a/test/gtest/constraint.cpp b/test/gtest/constraint.cpp index a8bb66f75d6..71ade020c02 100644 --- a/test/gtest/constraint.cpp +++ b/test/gtest/constraint.cpp @@ -139,15 +139,16 @@ TEST(ConstraintTest, TestOrTrivial) { // { } AndedConstraintSet empty; - // Anything ORed with the empty set is unchanged. + // Anything ORed with the empty set becomes the empty set: if one side can + // prove nothing, neither can the result. auto t = s; t.approximateOr(empty); - EXPECT_EQ(t, s); + EXPECT_EQ(t, empty); // Flipped. t = empty; t.approximateOr(s); - EXPECT_EQ(t, s); + EXPECT_EQ(t, empty); // ORing with oneself changes nothing t = s; diff --git a/test/lit/help/wasm-metadce.test b/test/lit/help/wasm-metadce.test index aadf987cf33..c658c715677 100644 --- a/test/lit/help/wasm-metadce.test +++ b/test/lit/help/wasm-metadce.test @@ -109,6 +109,9 @@ ;; CHECK-NEXT: --const-hoisting hoist repeated constants to a ;; CHECK-NEXT: local ;; CHECK-NEXT: +;; CHECK-NEXT: --constraint-analysis finds and uses mathematical +;; CHECK-NEXT: constraints on locals +;; CHECK-NEXT: ;; CHECK-NEXT: --dae removes arguments to calls in an ;; CHECK-NEXT: lto-like manner ;; CHECK-NEXT: diff --git a/test/lit/help/wasm-opt.test b/test/lit/help/wasm-opt.test index 4dec96a4104..d7000a2cbb1 100644 --- a/test/lit/help/wasm-opt.test +++ b/test/lit/help/wasm-opt.test @@ -145,6 +145,9 @@ ;; CHECK-NEXT: --const-hoisting hoist repeated constants to a ;; CHECK-NEXT: local ;; CHECK-NEXT: +;; CHECK-NEXT: --constraint-analysis finds and uses mathematical +;; CHECK-NEXT: constraints on locals +;; CHECK-NEXT: ;; CHECK-NEXT: --dae removes arguments to calls in an ;; CHECK-NEXT: lto-like manner ;; CHECK-NEXT: diff --git a/test/lit/help/wasm2js.test b/test/lit/help/wasm2js.test index 550677a376f..4ef39e73fae 100644 --- a/test/lit/help/wasm2js.test +++ b/test/lit/help/wasm2js.test @@ -73,6 +73,9 @@ ;; CHECK-NEXT: --const-hoisting hoist repeated constants to a ;; CHECK-NEXT: local ;; CHECK-NEXT: +;; CHECK-NEXT: --constraint-analysis finds and uses mathematical +;; CHECK-NEXT: constraints on locals +;; CHECK-NEXT: ;; CHECK-NEXT: --dae removes arguments to calls in an ;; CHECK-NEXT: lto-like manner ;; CHECK-NEXT: diff --git a/test/lit/passes/constraint-analysis.wast b/test/lit/passes/constraint-analysis.wast new file mode 100644 index 00000000000..cac599c30be --- /dev/null +++ b/test/lit/passes/constraint-analysis.wast @@ -0,0 +1,798 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. + +;; RUN: wasm-opt %s --constraint-analysis -all -S -o - | filecheck %s + +;; Also run after optimize-instructions, which canonicalizes the order of +;; things like binary children. We want to see that this pass optimizes the +;; IR that optimize-instructions emits. + +;; RUN: wasm-opt %s --optimize-instructions --constraint-analysis -all -S -o - | filecheck %s --check-prefix=OPTIN + +(module + ;; CHECK: (func $simple (type $1) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPTIN: (func $simple (type $1) + ;; OPTIN-NEXT: (local $x i32) + ;; OPTIN-NEXT: (local.set $x + ;; OPTIN-NEXT: (i32.const 10) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.const 1) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.const 0) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.const 0) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.const 1) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + (func $simple + (local $x i32) + ;; Set x to 10, and then compare it to 10 and 20 using == and !=, all in a + ;; single basic block. + (local.set $x + (i32.const 10) + ) + (drop + (i32.eq + (local.get $x) + (i32.const 10) + ) + ) + (drop + (i32.eq + (local.get $x) + (i32.const 20) + ) + ) + (drop + (i32.ne + (local.get $x) + (i32.const 10) + ) + ) + (drop + (i32.ne + (local.get $x) + (i32.const 20) + ) + ) + ) + + ;; CHECK: (func $unknown (type $1) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: (i32.const 20) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 30) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.div_s + ;; CHECK-NEXT: (i32.const 1337) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 31) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPTIN: (func $unknown (type $1) + ;; OPTIN-NEXT: (local $x i32) + ;; OPTIN-NEXT: (local.set $x + ;; OPTIN-NEXT: (i32.const 30) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.const 1) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (local.set $x + ;; OPTIN-NEXT: (i32.div_u + ;; OPTIN-NEXT: (i32.const 1337) + ;; OPTIN-NEXT: (i32.const 42) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.eq + ;; OPTIN-NEXT: (local.get $x) + ;; OPTIN-NEXT: (i32.const 31) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + (func $unknown + (local $x i32) + ;; Set x to an add. We can only optimize this if optimize-instructions first + ;; simplifies it to a constant. + (local.set $x + (i32.add + (i32.const 10) + (i32.const 20) + ) + ) + (drop + (i32.eq + (local.get $x) + (i32.const 30) + ) + ) + ;; When optimize-instructions does not help, we infer nothing. + (local.set $x + (i32.div_s + (i32.const 1337) + (i32.const 42) + ) + ) + (drop + (i32.eq + (local.get $x) + (i32.const 31) + ) + ) + ) + + ;; CHECK: (func $multi-local (type $1) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local $y i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $y + ;; CHECK-NEXT: (i32.const 20) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 15) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPTIN: (func $multi-local (type $1) + ;; OPTIN-NEXT: (local $x i32) + ;; OPTIN-NEXT: (local $y i32) + ;; OPTIN-NEXT: (local.set $x + ;; OPTIN-NEXT: (i32.const 10) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (local.set $y + ;; OPTIN-NEXT: (i32.const 20) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.const 1) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.const 1) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (local.set $x + ;; OPTIN-NEXT: (i32.const 15) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.const 0) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.const 1) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.const 1) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + (func $multi-local + (local $x i32) + (local $y i32) + ;; x is 10, y is 20 + (local.set $x + (i32.const 10) + ) + (local.set $y + (i32.const 20) + ) + ;; Verify those values. + (drop + (i32.eq + (local.get $x) + (i32.const 10) + ) + ) + (drop + (i32.eq + (local.get $y) + (i32.const 20) + ) + ) + ;; Overwrite x to 15. + (local.set $x + (i32.const 15) + ) + (drop + (i32.eq + (local.get $x) + (i32.const 10) + ) + ) + (drop + (i32.eq + (local.get $x) + (i32.const 15) + ) + ) + ;; y is unchanged. + (drop + (i32.eq + (local.get $y) + (i32.const 20) + ) + ) + ) + + ;; CHECK: (func $multi-block (type $0) (param $param i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $param) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPTIN: (func $multi-block (type $0) (param $param i32) + ;; OPTIN-NEXT: (local $x i32) + ;; OPTIN-NEXT: (local.set $x + ;; OPTIN-NEXT: (i32.const 10) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (if (result i32) + ;; OPTIN-NEXT: (local.get $param) + ;; OPTIN-NEXT: (then + ;; OPTIN-NEXT: (i32.const 1) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (else + ;; OPTIN-NEXT: (i32.const 0) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.const 1) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + (func $multi-block (param $param i32) + (local $x i32) + (local.set $x + (i32.const 10) + ) + ;; We can infer into these basic blocks. + (if + (local.get $param) + (then + (drop + (i32.eq + (local.get $x) + (i32.const 10) + ) + ) + ) + (else + (drop + (i32.ne ;; also test a not-equals here; result is 0 + (local.get $x) + (i32.const 10) + ) + ) + ) + ) + ;; We can infer after the merge. + (drop + (i32.eq + (local.get $x) + (i32.const 10) + ) + ) + ) + + ;; CHECK: (func $multi-block-split (type $0) (param $param i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $param) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 20) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 20) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPTIN: (func $multi-block-split (type $0) (param $param i32) + ;; OPTIN-NEXT: (local $x i32) + ;; OPTIN-NEXT: (local.set $x + ;; OPTIN-NEXT: (if (result i32) + ;; OPTIN-NEXT: (local.get $param) + ;; OPTIN-NEXT: (then + ;; OPTIN-NEXT: (i32.const 10) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (else + ;; OPTIN-NEXT: (i32.const 20) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.eq + ;; OPTIN-NEXT: (local.get $x) + ;; OPTIN-NEXT: (i32.const 10) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.eq + ;; OPTIN-NEXT: (local.get $x) + ;; OPTIN-NEXT: (i32.const 20) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + (func $multi-block-split (param $param i32) + (local $x i32) + (if + (local.get $param) + (then + (local.set $x + (i32.const 10) + ) + ) + (else + (local.set $x + (i32.const 20) + ) + ) + ) + ;; x is either 10 or 20, but not known to be one or the other. + (drop + (i32.eq + (local.get $x) + (i32.const 10) + ) + ) + (drop + (i32.eq + (local.get $x) + (i32.const 20) + ) + ) + ;; TODO: test we can infer x >= 10 etc. + ) + + ;; CHECK: (func $multi-block-split-2 (type $0) (param $param i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $param) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 20) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 20) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPTIN: (func $multi-block-split-2 (type $0) (param $param i32) + ;; OPTIN-NEXT: (local $x i32) + ;; OPTIN-NEXT: (local.set $x + ;; OPTIN-NEXT: (i32.const 10) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (if + ;; OPTIN-NEXT: (local.get $param) + ;; OPTIN-NEXT: (then + ;; OPTIN-NEXT: (local.set $x + ;; OPTIN-NEXT: (i32.const 20) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.eq + ;; OPTIN-NEXT: (local.get $x) + ;; OPTIN-NEXT: (i32.const 10) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.eq + ;; OPTIN-NEXT: (local.get $x) + ;; OPTIN-NEXT: (i32.const 20) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + (func $multi-block-split-2 (param $param i32) + (local $x i32) + ;; As above, but one if arm, and a set before the if. + (local.set $x + (i32.const 10) + ) + (if + (local.get $param) + (then + (local.set $x + (i32.const 20) + ) + ) + ) + ;; Again, we cannot infer. + (drop + (i32.eq + (local.get $x) + (i32.const 10) + ) + ) + (drop + (i32.eq + (local.get $x) + (i32.const 20) + ) + ) + ) + + ;; CHECK: (func $multi-block-split-yes (type $0) (param $param i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (local.get $param) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPTIN: (func $multi-block-split-yes (type $0) (param $param i32) + ;; OPTIN-NEXT: (local $x i32) + ;; OPTIN-NEXT: (local.set $x + ;; OPTIN-NEXT: (i32.const 10) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (if + ;; OPTIN-NEXT: (local.get $param) + ;; OPTIN-NEXT: (then + ;; OPTIN-NEXT: (local.set $x + ;; OPTIN-NEXT: (i32.const 10) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.const 1) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.const 0) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + (func $multi-block-split-yes (param $param i32) + (local $x i32) + ;; As above, but now we set 10 again in the if arm. + (local.set $x + (i32.const 10) + ) + (if + (local.get $param) + (then + (local.set $x + (i32.const 10) + ) + ) + ) + ;; Now we can infer 1 and 0 here. + (drop + (i32.eq + (local.get $x) + (i32.const 10) + ) + ) + (drop + (i32.eq + (local.get $x) + (i32.const 20) + ) + ) + ) + + ;; CHECK: (func $loop (type $0) (param $param i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (loop $loop + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br_if $loop + ;; CHECK-NEXT: (local.get $param) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPTIN: (func $loop (type $0) (param $param i32) + ;; OPTIN-NEXT: (local $x i32) + ;; OPTIN-NEXT: (local.set $x + ;; OPTIN-NEXT: (i32.const 10) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (loop $loop + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.const 1) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.const 1) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (br_if $loop + ;; OPTIN-NEXT: (local.get $param) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + (func $loop (param $param i32) + (local $x i32) + ;; Set $x to 10 before the loop. + (local.set $x + (i32.const 10) + ) + (loop $loop + ;; Despite the backedges, we can infer x is 10 and not 20. + (drop + (i32.eq + (local.get $x) + (i32.const 10) + ) + ) + (drop + (i32.ne + (local.get $x) + (i32.const 20) + ) + ) + (br_if $loop + (local.get $param) + ) + ) + ) + + ;; CHECK: (func $loop-no (type $0) (param $param i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (loop $loop + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.ne + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 20) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.const 20) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br_if $loop + ;; CHECK-NEXT: (local.get $param) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPTIN: (func $loop-no (type $0) (param $param i32) + ;; OPTIN-NEXT: (local $x i32) + ;; OPTIN-NEXT: (local.set $x + ;; OPTIN-NEXT: (i32.const 10) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (loop $loop + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.eq + ;; OPTIN-NEXT: (local.get $x) + ;; OPTIN-NEXT: (i32.const 10) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.ne + ;; OPTIN-NEXT: (local.get $x) + ;; OPTIN-NEXT: (i32.const 20) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (local.set $x + ;; OPTIN-NEXT: (i32.const 20) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (br_if $loop + ;; OPTIN-NEXT: (local.get $param) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + (func $loop-no (param $param i32) + (local $x i32) + ;; As above, but now with another value set in the loop. We cannot infer. + (local.set $x + (i32.const 10) + ) + (loop $loop + (drop + (i32.eq + (local.get $x) + (i32.const 10) + ) + ) + (drop + (i32.ne + (local.get $x) + (i32.const 20) + ) + ) + (local.set $x + (i32.const 20) + ) + (br_if $loop + (local.get $param) + ) + ) + ) + + ;; CHECK: (func $default-var (type $0) (param $param i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (local $eq eqref) + ;; CHECK-NEXT: (local $nn-eq (ref eq)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $param) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPTIN: (func $default-var (type $0) (param $param i32) + ;; OPTIN-NEXT: (local $x i32) + ;; OPTIN-NEXT: (local $eq eqref) + ;; OPTIN-NEXT: (local $nn-eq (ref eq)) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.const 1) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.const 1) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.eqz + ;; OPTIN-NEXT: (local.get $param) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.const 1) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.const 1) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + (func $default-var (param $param i32) + (local $x i32) + (local $eq eqref) + ;; A non-nullable local. We cannot add a get for it (it would not validate), + ;; but check we do not error. + (local $nn-eq (ref eq)) + ;; locals begin with default values, so we can infer here. + (drop + (i32.eq + (local.get $x) + (i32.const 0) + ) + ) + (drop + (i32.eqz + (local.get $x) + ) + ) + ;; We can infer nothing for a param. + (drop + (i32.eq + (local.get $param) + (i32.const 0) + ) + ) + ;; We can infer a null reference. + (drop + (ref.eq + (local.get $eq) + (ref.null eq) + ) + ) + (drop + (ref.is_null + (local.get $eq) + ) + ) + ) +)