From 4ff74fafb2f0185a57ab482c40ab7a6028ae722e Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 4 Aug 2026 15:09:53 -0700 Subject: [PATCH 1/8] go --- src/ir/constraint.cpp | 103 ++ src/ir/constraint.h | 8 +- src/ir/match.h | 19 + src/passes/ConstraintAnalysis.cpp | 91 +- test/gtest/constraint.cpp | 113 +++ .../lit/passes/constraint-analysis-loops.wast | 922 +++++++++++++++++- 6 files changed, 1242 insertions(+), 14 deletions(-) diff --git a/src/ir/constraint.cpp b/src/ir/constraint.cpp index b2b018500d3..0489d87b618 100644 --- a/src/ir/constraint.cpp +++ b/src/ir/constraint.cpp @@ -517,6 +517,109 @@ void BasicBlockConstraintMap::set(Index index, const Constraint& c) { approximateAnd(index, c); } +void BasicBlockConstraintMap::set(Index index, + const AndedConstraintSet& constraints) { + // As above, but with a loop after. + assert(!unreachable); + eraseStaleRefs(index); + map.erase(index); + + // Apply the constraints, if there are any. + if (constraints.provesNothing()) { + setProvesNothing(index); + } else { + for (auto& c : constraints) { + approximateAnd(index, c); + } + } +} + +// Set the value in an expression to a local, replacing anything before. +void BasicBlockConstraintMap::set(Index index, Expression* value) { + using namespace Match; + using namespace Abstract; + + // Apply a constraint to a value, x = C. + if (Properties::isSingleConstantExpression(value)) { + auto c = Properties::getLiteral(value); + set(index, Constraint{Abstract::Eq, {c}}); + return; + } + + // Apply a constraint to a local, x = y. + if (auto* get = value->dynCast()) { + set(index, Constraint{Abstract::Eq, {get->index}}); + return; + } + + // Apply an increment of a local, x = y + 1. + Index y; + if (matches(value, binary(Abstract::Add, local(&y), ival(1)))) { + // The local y must have old constraints that we know how to increment. + auto old = get(y); + + // Iterate over the old constraints and increment each one. + auto success = true; + for (auto& c : old) { + auto* N = std::get_if(&c.term); + if (!N) { + // A non-constant term, which we don't know how to increment. + success = false; + break; + } + + switch (c.op) { + // x == N, x++ => x == N+1. + case Eq: + // TODO: overflows here and below + c.term = Term(N->add(Literal::makeFromInt32(1, N->type))); + continue; + // x >= N, x++ => x > N + case GeS: + c.op = GtS; + continue; + case GeU: + c.op = GtU; + continue; + // x < N, x++ => x <= N + case LtS: + c.op = LeS; + continue; + case LtU: + c.op = LeU; + continue; + // x <= N, x++ => x <= N+1 if no overflow + case LeS: + if (N->isSignedMax()) { + success = false; + break; + } + *N = N->add(Literal::makeFromInt32(1, N->type)); + continue; + case LeU: + if (N->isUnsignedMax()) { + success = false; + break; + } + *N = N->add(Literal::makeFromInt32(1, N->type)); + continue; + default: + // Something we don't recognize. + success = false; + break; + } + } + + if (success) { + set(index, old); + return; + } + } + + // We know and can prove nothing. + setProvesNothing(index); +} + void BasicBlockConstraintMap::setProvesNothing(Index index) { assert(!unreachable); eraseStaleRefs(index); diff --git a/src/ir/constraint.h b/src/ir/constraint.h index 9fea231fbc7..07d4254cb7e 100644 --- a/src/ir/constraint.h +++ b/src/ir/constraint.h @@ -251,9 +251,15 @@ struct BasicBlockConstraintMap { assert(map.empty()); } - // Apply a constraint to a local. + // Apply a constraint to a local, replacing anything before. void set(Index index, const Constraint& c); + // Apply a set of constraints to a local, replacing anything before. + void set(Index index, const AndedConstraintSet& constraints); + + // Set the value in an expression to a local, replacing anything before. + void set(Index index, Expression* value); + // Mark a local as unknown and able to prove nothing. void setProvesNothing(Index index); diff --git a/src/ir/match.h b/src/ir/match.h index 383ff8d057a..a60bad43235 100644 --- a/src/ir/match.h +++ b/src/ir/match.h @@ -613,6 +613,18 @@ SelectMatcher(Select** binder, S1&& s1, S2&& s2, S3&& s3) { return Matcher(binder, {}, s1, s2, s3); } +// LocalGet +template<> struct NumComponents { + static constexpr size_t value = 1; +}; +template<> struct GetComponent { + Index operator()(LocalGet* curr) { return curr->index; } +}; +template +inline decltype(auto) LocalGetMatcher(LocalGet** binder, S&& s) { + return Matcher(binder, {}, s); +} + } // namespace Internal // Public matching API @@ -878,6 +890,13 @@ inline decltype(auto) select(Select** binder, S1&& s1, S2&& s2, S3&& s3) { return Internal::SelectMatcher(binder, s1, s2, s3); } +inline decltype(auto) local() { + return Internal::LocalGetMatcher(nullptr, Internal::Any(nullptr)); +} +inline decltype(auto) local(Index* binder) { + return Internal::LocalGetMatcher(nullptr, Internal::Any(binder)); +} + } // namespace wasm::Match #endif // wasm_ir_match_h diff --git a/src/passes/ConstraintAnalysis.cpp b/src/passes/ConstraintAnalysis.cpp index 5eb6a24fcf4..da557d0b21f 100644 --- a/src/passes/ConstraintAnalysis.cpp +++ b/src/passes/ConstraintAnalysis.cpp @@ -36,6 +36,12 @@ #include "wasm-builder.h" #include "wasm.h" +#define CONSTRAINT_DEBUG 0 + +#ifndef CONSTRAINT_DEBUG +#define CONSTRAINT_DEBUG 0 +#endif + namespace wasm { using namespace wasm::constraint; @@ -187,6 +193,7 @@ struct ConstraintAnalysis } computeRelevantLocals(); + prepareToFlow(); flow(); optimize(); } @@ -217,9 +224,42 @@ struct ConstraintAnalysis } } + // Maintain a maximum amount of operations. The one non-linear thing that can + // happen is when we increment a local in a loop: it may go from 0 to 1, then + // branch back to the top and merge, making it in the range [0, 1], then get + // incremented and loop again, leading to [0, 2] and so forth, only stopping + // when it reaches the loop bound, which may be very high. We don't want to + // spend significant time on such constant operations, as other passes will + // propagate them anyhow, so we keep our time bounded. When this reaches 0, + // we will not do loop operations that might lead to such incrementing. + Index maxWorkLeft = 0; + + void prepareToFlow() { + // Compute a bound for maxOperations. flow() will spend time on each block, + // operation in a block, and branch, so add all those up. + for (auto& block : basicBlocks) { + maxWorkLeft += 1 + block->contents.actions.size() + block->out.size(); + } + + // We also allow a multiple of all the above: loop optimization generally + // requires us to process it twice (so that we see the merge at the top). + // Use a constant of 3 to make sure to work enough. + maxWorkLeft *= 3; + } + + void decMaxWork() { + if (maxWorkLeft > 0) { + maxWorkLeft--; + } + } + // Flow infos around until we have inferred all we can about the constraints // in each location. void flow() { +#if CONSTRAINT_DEBUG + dumpCFG("flow"); +#endif + // Start from the entry as the only reachable block. That block has incoming // values - defaults - for each var. entry->contents.startConstraints.setReachable(); @@ -247,18 +287,34 @@ struct ConstraintAnalysis // Starting from the entry, keep going while we find something new. UniqueDeferredQueue work; work.push(entry); + while (!work.empty()) { auto* block = work.pop(); + decMaxWork(); + // Start at the top of the block, then go through, applying things. BasicBlockConstraintMap constraints = block->contents.startConstraints; + +#if CONSTRAINT_DEBUG + std::cout << block << " start constraints: " << constraints << '\n'; +#endif + for (auto** currp : block->contents.actions) { applyToConstraints(*currp, constraints); + + decMaxWork(); } +#if CONSTRAINT_DEBUG + std::cout << block << " end constraints: " << constraints << '\n'; +#endif + // 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) { + decMaxWork(); + auto& outStartConstraints = out->contents.startConstraints; // Find the constraints sent to this specific successor, if there is a @@ -266,15 +322,28 @@ struct ConstraintAnalysis if (auto branch = getBranchConstraints(block, out); branch && checkRelevancy(*branch)) { auto sentConstraints = constraints; - sentConstraints.approximateAnd(branch->local, branch->constraint); + applyBranchConstraints(*branch, sentConstraints); +#if CONSTRAINT_DEBUG + std::cout << block << " sending branch to " << out + << " with sent constraints: " << sentConstraints << '\n'; +#endif // If anything changed at the start of the target block, flow onwards. if (outStartConstraints.approximateOr(sentConstraints)) { +#if CONSTRAINT_DEBUG + std::cout << "out's start after " << outStartConstraints << '\n'; + std::cout << block << " branch-modified " << out + << " to start with: " << outStartConstraints << '\n'; +#endif work.push(out); } } else { // There are no specific branch constraints, so send the unmodified // |constraints|, avoiding a copy. if (outStartConstraints.approximateOr(constraints)) { +#if CONSTRAINT_DEBUG + std::cout << block << " modified " << out + << " to start with: " << outStartConstraints << '\n'; +#endif work.push(out); } } @@ -293,6 +362,9 @@ struct ConstraintAnalysis // of course not needed at this stage.) auto& constraints = block->contents.startConstraints; for (auto** currp : block->contents.actions) { +#if CONSTRAINT_DEBUG + std::cout << block << " trying to optimize " << **currp << '\n'; +#endif if (!constraints.unreachable) { applyToConstraints(*currp, constraints); optimizeExpression(currp, constraints); @@ -432,17 +504,16 @@ struct ConstraintAnalysis // No point to apply a constraint to an irrelevant local. return; } - if (Properties::isSingleConstantExpression(set->value)) { - // Apply a constraint to this value. - auto value = Properties::getLiteral(set->value); - constraints.set(set->index, Constraint{Abstract::Eq, {value}}); - } else if (auto* get = set->value->dynCast()) { - // Apply a constraint to this local. - constraints.set(set->index, Constraint{Abstract::Eq, {get->index}}); - } else { - // We know and can prove nothing. + + // The only binary operation we match is an increment (x + 1), and we do + // not always want to apply it: only in loops mode, and even then, only + // when we are allowed to keep working (see above). + if (set->value->is() && (!loops || !maxWorkLeft)) { constraints.setProvesNothing(set->index); + return; } + + constraints.set(set->index, set->value); } } diff --git a/test/gtest/constraint.cpp b/test/gtest/constraint.cpp index 0b534d12efb..260f3cd7dfd 100644 --- a/test/gtest/constraint.cpp +++ b/test/gtest/constraint.cpp @@ -503,3 +503,116 @@ TEST(ConstraintTest, TestAndLoop) { // x <= y && { x < y && x != 42 } => x < y && x != 42 checkAnd(ley, {lty[0], ne42}, {lty[0], ne42}); } + +TEST(ConstraintTest, TestBasicBlockConstraintMap) { + // Maps begin unreachable. + BasicBlockConstraintMap map; + + EXPECT_TRUE(map.unreachable); + map.setReachable(); + EXPECT_FALSE(map.unreachable); +} + +// Check that a set is equal to a constraint. +static void check(const AndedConstraintSet& s, const Constraint& c) { + EXPECT_EQ(s.size(), 1); + EXPECT_EQ(s[0], c); +} + +TEST(ConstraintTest, TestBasicBlockConstraintMap_Set) { + Constraint eq0{Eq, {Literal(int32_t(0))}}; + Constraint eq1{Eq, {Literal(int32_t(1))}}; + Constraint eq2{Eq, {Literal(int32_t(2))}}; + + BasicBlockConstraintMap map; + map.setReachable(); + + // Set local 0 to 0. It should read back the same. + map.set(0, eq0); + check(map.get(0), eq0); + + // Set another value, replacing the first. + map.set(0, eq1); + check(map.get(0), eq1); + + // Set a value using an expression. + Const c; + c.value = Literal(int32_t(2)); + c.type = Type::i32; + map.set(0, &c); + check(map.get(0), eq2); + + // Set an unfamiliar expression, leading to us knowing nothing. + Nop nop; + map.set(0, &nop); + EXPECT_TRUE(map.get(0).provesNothing()); +} + +TEST(ConstraintTest, TestIncrement) { + BasicBlockConstraintMap map; + map.setReachable(); + + // Set up an increment operation, an add which does $0 + 1 + LocalGet get; + get.index = 0; + get.type = Type::i32; + + Const c; + c.value = Literal(int32_t(1)); + c.type = Type::i32; + + Binary add; + add.op = AddInt32; + add.type = Type::i32; + add.left = &get; + add.right = &c; + + // $0 = 0, $1 = $0 + 1, so $1 = 1 (and $0 is unchanged). + map.set(0, {Eq, Literal(int32_t(0))}); + map.set(1, &add); + check(map.get(0), {Eq, Literal(int32_t(0))}); + check(map.get(1), {Eq, Literal(int32_t(1))}); + + // $0 = $0 + 1, where $0 was 0, so it is now 1. + map.set(0, &add); + check(map.get(0), {Eq, Literal(int32_t(1))}); + + // $0 >= 5, $0++ => $0 > 5 (signed) + map.set(0, {GeS, Literal(int32_t(5))}); + map.set(0, &add); + check(map.get(0), {GtS, Literal(int32_t(5))}); + + // Ditto, unsigned + map.set(0, {GeU, Literal(int32_t(5))}); + map.set(0, &add); + check(map.get(0), {GtU, Literal(int32_t(5))}); + + // $0 < 5, $0++ => $0 <= 5 (signed) + map.set(0, {LtS, Literal(int32_t(5))}); + map.set(0, &add); + check(map.get(0), {LeS, Literal(int32_t(5))}); + + // Ditto, unsigned + map.set(0, {LtU, Literal(int32_t(5))}); + map.set(0, &add); + check(map.get(0), {LeU, Literal(int32_t(5))}); + + // $0 <= 5, $0++ => $0 <= 6 (signed) + map.set(0, {LeS, Literal(int32_t(5))}); + map.set(0, &add); + check(map.get(0), {LeS, Literal(int32_t(6))}); + + // Ditto, unsigned + map.set(0, {LeU, Literal(int32_t(5))}); + map.set(0, &add); + check(map.get(0), {LeU, Literal(int32_t(6))}); + + // Multiple constraints at once: + // $0 >= 10 && $0 < 20, $0++ => $0 > 10 && $0 <= 20 + map.set(0, {GeS, Literal(int32_t(10))}); + map.approximateAnd(0, {LtS, Literal(int32_t(20))}); + map.set(0, &add); + EXPECT_EQ(map.get(0), + (AndedConstraintSet{{GtS, Literal(int32_t(10))}, + {LeS, Literal(int32_t(20))}})); +} diff --git a/test/lit/passes/constraint-analysis-loops.wast b/test/lit/passes/constraint-analysis-loops.wast index 5d90de1096f..14731b99a45 100644 --- a/test/lit/passes/constraint-analysis-loops.wast +++ b/test/lit/passes/constraint-analysis-loops.wast @@ -1,11 +1,100 @@ ;; 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 +;; Run both normally and in the "loops" mode. Many optimizations work in both +;; modes, but some require "loops" (mentioned below where that occurs). + +;; RUN: wasm-opt %s --constraint-analysis -all -S -o - | filecheck %s +;; RUN: wasm-opt %s --constraint-analysis-loops -all -S -o - | filecheck %s --check-prefix=LOOPS (module - ;; CHECK: (import "a" "b" (func $import (type $1) (result i32))) + ;; CHECK: (import "a" "b" (func $import (type $2) (result i32))) + ;; LOOPS: (import "a" "b" (func $import (type $2) (result i32))) (import "a" "b" (func $import (result i32))) + ;; CHECK: (func $infinite-loop (type $0) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (loop $loop + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $loop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; LOOPS: (func $infinite-loop (type $0) + ;; LOOPS-NEXT: (local $x i32) + ;; LOOPS-NEXT: (loop $loop + ;; LOOPS-NEXT: (local.set $x + ;; LOOPS-NEXT: (i32.add + ;; LOOPS-NEXT: (local.get $x) + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (br $loop) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + (func $infinite-loop + (local $x i32) + ;; An infinite loop. We should not hang, but nothing can be optimized. + (loop $loop + (local.set $x + (i32.add + (local.get $x) + (i32.const 1) + ) + ) + (br $loop) + ) + ) + + ;; CHECK: (func $almost-infinite-loop (type $0) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (loop $loop + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br_if $loop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; LOOPS: (func $almost-infinite-loop (type $0) + ;; LOOPS-NEXT: (local $x i32) + ;; LOOPS-NEXT: (loop $loop + ;; LOOPS-NEXT: (local.set $x + ;; LOOPS-NEXT: (i32.add + ;; LOOPS-NEXT: (local.get $x) + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (br_if $loop + ;; LOOPS-NEXT: (local.get $x) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + (func $almost-infinite-loop + (local $x i32) + ;; A loop that continues until an overflow happens. We should not hang, but + ;; nothing can be optimized. + (loop $loop + (local.set $x + (i32.add + (local.get $x) + (i32.const 1) + ) + ) + ;; Stop looping after we go all the way back to 0. + (br_if $loop + (local.get $x) + ) + ) + ) + ;; CHECK: (func $bound (type $0) ;; CHECK-NEXT: (local $x i32) ;; CHECK-NEXT: (loop $loop @@ -37,6 +126,37 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; LOOPS: (func $bound (type $0) + ;; LOOPS-NEXT: (local $x i32) + ;; LOOPS-NEXT: (loop $loop + ;; LOOPS-NEXT: (drop + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (drop + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (local.set $x + ;; LOOPS-NEXT: (call $import) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (if + ;; LOOPS-NEXT: (i32.gt_s + ;; LOOPS-NEXT: (local.get $x) + ;; LOOPS-NEXT: (i32.const 0) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (then + ;; LOOPS-NEXT: (if + ;; LOOPS-NEXT: (i32.le_s + ;; LOOPS-NEXT: (local.get $x) + ;; LOOPS-NEXT: (i32.const 100) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (then + ;; LOOPS-NEXT: (br $loop) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) (func $bound (local $x i32) (loop $loop @@ -111,6 +231,37 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; LOOPS: (func $bound-flipped-ifs (type $0) + ;; LOOPS-NEXT: (local $x i32) + ;; LOOPS-NEXT: (loop $loop + ;; LOOPS-NEXT: (drop + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (drop + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (local.set $x + ;; LOOPS-NEXT: (call $import) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (if + ;; LOOPS-NEXT: (i32.le_s + ;; LOOPS-NEXT: (local.get $x) + ;; LOOPS-NEXT: (i32.const 100) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (then + ;; LOOPS-NEXT: (if + ;; LOOPS-NEXT: (i32.gt_s + ;; LOOPS-NEXT: (local.get $x) + ;; LOOPS-NEXT: (i32.const 0) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (then + ;; LOOPS-NEXT: (br $loop) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) (func $bound-flipped-ifs (local $x i32) ;; As above, but with the ifs flipped. We optimize the same way. @@ -150,7 +301,7 @@ ) ) - ;; CHECK: (func $bound-nonconstant-no (type $2) (param $p i32) + ;; CHECK: (func $bound-nonconstant-no (type $1) (param $p i32) ;; CHECK-NEXT: (local $x i32) ;; CHECK-NEXT: (loop $loop ;; CHECK-NEXT: (drop @@ -187,6 +338,43 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; LOOPS: (func $bound-nonconstant-no (type $1) (param $p i32) + ;; LOOPS-NEXT: (local $x i32) + ;; LOOPS-NEXT: (loop $loop + ;; LOOPS-NEXT: (drop + ;; LOOPS-NEXT: (i32.ge_s + ;; LOOPS-NEXT: (local.get $x) + ;; LOOPS-NEXT: (local.get $p) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (drop + ;; LOOPS-NEXT: (i32.le_s + ;; LOOPS-NEXT: (local.get $x) + ;; LOOPS-NEXT: (local.get $p) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (local.set $x + ;; LOOPS-NEXT: (call $import) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (if + ;; LOOPS-NEXT: (i32.gt_s + ;; LOOPS-NEXT: (local.get $x) + ;; LOOPS-NEXT: (local.get $p) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (then + ;; LOOPS-NEXT: (if + ;; LOOPS-NEXT: (i32.le_s + ;; LOOPS-NEXT: (local.get $x) + ;; LOOPS-NEXT: (i32.const 100) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (then + ;; LOOPS-NEXT: (br $loop) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) (func $bound-nonconstant-no (param $p i32) (local $x i32) ;; As above, but rather than zero we have an unknown param $p. @@ -226,4 +414,732 @@ ) ) ) + + ;; CHECK: (func $bound-incremented (type $0) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (loop $loop + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.ge_s + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.lt_s + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 100) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (br $loop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; LOOPS: (func $bound-incremented (type $0) + ;; LOOPS-NEXT: (local $x i32) + ;; LOOPS-NEXT: (loop $loop + ;; LOOPS-NEXT: (drop + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (drop + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (local.set $x + ;; LOOPS-NEXT: (i32.add + ;; LOOPS-NEXT: (local.get $x) + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (if + ;; LOOPS-NEXT: (i32.lt_s + ;; LOOPS-NEXT: (local.get $x) + ;; LOOPS-NEXT: (i32.const 100) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (then + ;; LOOPS-NEXT: (br $loop) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + (func $bound-incremented + (local $x i32) + (loop $loop + ;; A realistic do-while loop, with $x++ and a bounds check. We must infer + ;; that no overflow happens in order to prove these two checks are true. + ;; + ;; The first is trivially true, as x starts at 0 - fulfilling x < 100 - + ;; and the branch back to the loop top arrives with x < 100. + (drop + (i32.lt_s + (local.get $x) + (i32.const 100) + ) + ) + ;; This is non-trivial, as we must rule out a possible overflow. The + ;; only reason that x never gets incremented so many times that it becomes + ;; negative is that the incrementation process is stopped at 100. We only + ;; manage to optimize this in "loops" mode. + (drop + (i32.ge_s + (local.get $x) + (i32.const 0) + ) + ) + ;; This changed compared to previous testcases: now we have x++. + (local.set $x + (i32.add + (local.get $x) + (i32.const 1) + ) + ) + (if + (i32.lt_s + (local.get $x) + (i32.const 100) + ) + (then + (br $loop) + ) + ) + ) + ) + + ;; CHECK: (func $bound-incremented-unsigned (type $0) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (loop $loop + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.ge_u + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.lt_u + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 100) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (br $loop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; LOOPS: (func $bound-incremented-unsigned (type $0) + ;; LOOPS-NEXT: (local $x i32) + ;; LOOPS-NEXT: (loop $loop + ;; LOOPS-NEXT: (drop + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (drop + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (local.set $x + ;; LOOPS-NEXT: (i32.add + ;; LOOPS-NEXT: (local.get $x) + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (if + ;; LOOPS-NEXT: (i32.lt_u + ;; LOOPS-NEXT: (local.get $x) + ;; LOOPS-NEXT: (i32.const 100) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (then + ;; LOOPS-NEXT: (br $loop) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + (func $bound-incremented-unsigned + ;; As above, but with unsigned operations. This is simpler, and we optimize + ;; it even without "loops" mode. + (local $x i32) + (loop $loop + (drop + (i32.lt_u + (local.get $x) + (i32.const 100) + ) + ) + (drop + (i32.ge_u + (local.get $x) + (i32.const 0) + ) + ) + (local.set $x + (i32.add + (local.get $x) + (i32.const 1) + ) + ) + (if + (i32.lt_u + (local.get $x) + (i32.const 100) + ) + (then + (br $loop) + ) + ) + ) + ) + + ;; CHECK: (func $increment-non-constant (type $1) (param $p i32) + ;; CHECK-NEXT: (local $a i32) + ;; CHECK-NEXT: (local $b i32) + ;; CHECK-NEXT: (local $scratch i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block (result i32) + ;; CHECK-NEXT: (local.set $scratch + ;; CHECK-NEXT: (i32.lt_s + ;; CHECK-NEXT: (local.get $p) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.le_s + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: (local.get $b) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.set $p + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $a + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $scratch) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; LOOPS: (func $increment-non-constant (type $1) (param $p i32) + ;; LOOPS-NEXT: (local $a i32) + ;; LOOPS-NEXT: (local $b i32) + ;; LOOPS-NEXT: (local $scratch i32) + ;; LOOPS-NEXT: (drop + ;; LOOPS-NEXT: (block (result i32) + ;; LOOPS-NEXT: (local.set $scratch + ;; LOOPS-NEXT: (i32.lt_s + ;; LOOPS-NEXT: (local.get $p) + ;; LOOPS-NEXT: (i32.const 0) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (if + ;; LOOPS-NEXT: (i32.le_s + ;; LOOPS-NEXT: (local.get $a) + ;; LOOPS-NEXT: (local.get $b) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (then + ;; LOOPS-NEXT: (local.set $p + ;; LOOPS-NEXT: (local.get $a) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (local.set $a + ;; LOOPS-NEXT: (i32.add + ;; LOOPS-NEXT: (local.get $a) + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (drop + ;; LOOPS-NEXT: (i32.eq + ;; LOOPS-NEXT: (local.get $a) + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (drop + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (local.get $scratch) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + (func $increment-non-constant (param $p i32) + (local $a i32) + (local $b i32) + (drop + (i32.lt_s + (local.get $p) + (i32.const 0) + ) + (if + (i32.le_s + (local.get $a) + (local.get $b) + ) + (then + ;; Before this set, this is what we know about $a: + ;; $a == 0 && $a <= $b + (local.set $p + (local.get $a) + ) + ;; We just set $p to $a, so now we know this about $a: + ;; $a == 0 && $a <= $b, $a == $p + ;; We then proceed to do $a++, trying to increment each of those three + ;; constraints. We should not hit an internal error on trying to increment + ;; any of the three, ending up failing on the third (though, with a higher- + ;; level view, we could use the fact that $p == 0). + (local.set $a + (i32.add + (local.get $a) + (i32.const 1) + ) + ) + ;; Since we failed to know things about $a after $a++, we cannot + ;; prove this. + (drop + (i32.eq + (local.get $a) + (i32.const 1) + ) + ) + ;; But we did not forget about $b. + (drop + (i32.eq + (local.get $b) + (i32.const 0) + ) + ) + ) + ) + ) + ) + + ;; CHECK: (func $bound-incremented-while (type $0) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (block $out + ;; CHECK-NEXT: (loop $loop + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.ge_s + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 100) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (br $out) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.ge_s + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $loop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; LOOPS: (func $bound-incremented-while (type $0) + ;; LOOPS-NEXT: (local $x i32) + ;; LOOPS-NEXT: (block $out + ;; LOOPS-NEXT: (loop $loop + ;; LOOPS-NEXT: (if + ;; LOOPS-NEXT: (i32.ge_s + ;; LOOPS-NEXT: (local.get $x) + ;; LOOPS-NEXT: (i32.const 100) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (then + ;; LOOPS-NEXT: (br $out) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (drop + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (drop + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (local.set $x + ;; LOOPS-NEXT: (i32.add + ;; LOOPS-NEXT: (local.get $x) + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (br $loop) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + (func $bound-incremented-while + ;; Similar to above, but before we had a do-while loop (loop condition at + ;; the bottom) and now it is at the top. + (local $x i32) + (block $out + (loop $loop + ;; Conditional branch at the top. + (if + (i32.ge_s + (local.get $x) + (i32.const 100) + ) + (then + (br $out) + ) + ) + ;; We can infer both of these to be true in loops mode (in normal mode, + ;; only the easy one, the first). + (drop + (i32.lt_s + (local.get $x) + (i32.const 100) + ) + ) + (drop + (i32.ge_s + (local.get $x) + (i32.const 0) + ) + ) + (local.set $x + (i32.add + (local.get $x) + (i32.const 1) + ) + ) + ;; Unconditional branch at the bottom. + (br $loop) + ) + ) + ) + + ;; CHECK: (func $bound-incremented-inc-first (type $0) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (block $out + ;; CHECK-NEXT: (loop $loop + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.ge_s + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 100) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (br $out) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.gt_s + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $loop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; LOOPS: (func $bound-incremented-inc-first (type $0) + ;; LOOPS-NEXT: (local $x i32) + ;; LOOPS-NEXT: (block $out + ;; LOOPS-NEXT: (loop $loop + ;; LOOPS-NEXT: (local.set $x + ;; LOOPS-NEXT: (i32.add + ;; LOOPS-NEXT: (local.get $x) + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (if + ;; LOOPS-NEXT: (i32.ge_s + ;; LOOPS-NEXT: (local.get $x) + ;; LOOPS-NEXT: (i32.const 100) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (then + ;; LOOPS-NEXT: (br $out) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (drop + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (drop + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (br $loop) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + (func $bound-incremented-inc-first + ;; Similar to the above "while" loop, but now with the increment before the + ;; if. + (local $x i32) + (block $out + (loop $loop + ;; Increment at the top. + (local.set $x + (i32.add + (local.get $x) + (i32.const 1) + ) + ) + ;; If after the increment. + (if + (i32.ge_s + (local.get $x) + (i32.const 100) + ) + (then + (br $out) + ) + ) + ;; x > 0 && x < 100 here (0 is impossible, compared to before). + (drop + (i32.gt_s + (local.get $x) + (i32.const 0) + ) + ) + (drop + (i32.lt_s + (local.get $x) + (i32.const 100) + ) + ) + (br $loop) + ) + ) + ) + + ;; CHECK: (func $bound-incremented-inc-first-less (type $0) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (block $out + ;; CHECK-NEXT: (loop $loop + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.gt_s + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 100) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (br $out) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.gt_s + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $loop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; LOOPS: (func $bound-incremented-inc-first-less (type $0) + ;; LOOPS-NEXT: (local $x i32) + ;; LOOPS-NEXT: (block $out + ;; LOOPS-NEXT: (loop $loop + ;; LOOPS-NEXT: (local.set $x + ;; LOOPS-NEXT: (i32.add + ;; LOOPS-NEXT: (local.get $x) + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (if + ;; LOOPS-NEXT: (i32.gt_s + ;; LOOPS-NEXT: (local.get $x) + ;; LOOPS-NEXT: (i32.const 100) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (then + ;; LOOPS-NEXT: (br $out) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (drop + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (drop + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (br $loop) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + (func $bound-incremented-inc-first-less + ;; As in the last testcase, but the if's condition changed. + (local $x i32) + (block $out + (loop $loop + (local.set $x + (i32.add + (local.get $x) + (i32.const 1) + ) + ) + ;; Before we left the loop when x >= 100. Now we leave when x > 100, + ;; so we do actually reach 100 in the code below. + (if + (i32.gt_s + (local.get $x) + (i32.const 100) + ) + (then + (br $out) + ) + ) + ;; x > 0 && x <= 100 here (but we need loops mode to get both). + (drop + (i32.gt_s + (local.get $x) + (i32.const 0) + ) + ) + (drop + (i32.le_s + (local.get $x) + (i32.const 100) + ) + ) + (br $loop) + ) + ) + ) + + ;; CHECK: (func $bound-incremented-inc-first-less-unsigned (type $0) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (block $out + ;; CHECK-NEXT: (loop $loop + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.gt_u + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 100) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (br $out) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.gt_u + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (br $loop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; LOOPS: (func $bound-incremented-inc-first-less-unsigned (type $0) + ;; LOOPS-NEXT: (local $x i32) + ;; LOOPS-NEXT: (block $out + ;; LOOPS-NEXT: (loop $loop + ;; LOOPS-NEXT: (local.set $x + ;; LOOPS-NEXT: (i32.add + ;; LOOPS-NEXT: (local.get $x) + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (if + ;; LOOPS-NEXT: (i32.gt_u + ;; LOOPS-NEXT: (local.get $x) + ;; LOOPS-NEXT: (i32.const 100) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (then + ;; LOOPS-NEXT: (br $out) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (drop + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (drop + ;; LOOPS-NEXT: (i32.const 1) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: (br $loop) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + ;; LOOPS-NEXT: ) + (func $bound-incremented-inc-first-less-unsigned + ;; As in the last testcase, but unsigned. + (local $x i32) + (block $out + (loop $loop + (local.set $x + (i32.add + (local.get $x) + (i32.const 1) + ) + ) + (if + (i32.gt_u + (local.get $x) + (i32.const 100) + ) + (then + (br $out) + ) + ) + ;; x > 0 && x <= 100 here (but we need loops mode to get both). + (drop + (i32.gt_u + (local.get $x) + (i32.const 0) + ) + ) + (drop + (i32.le_u + (local.get $x) + (i32.const 100) + ) + ) + (br $loop) + ) + ) + ) ) From 9b38ac68598678904f6b3c549d3ba681aad87ec0 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 4 Aug 2026 15:10:28 -0700 Subject: [PATCH 2/8] simpl --- .../lit/passes/constraint-analysis-loops.wast | 1078 ----------------- 1 file changed, 1078 deletions(-) diff --git a/test/lit/passes/constraint-analysis-loops.wast b/test/lit/passes/constraint-analysis-loops.wast index 14731b99a45..fcc88a3316a 100644 --- a/test/lit/passes/constraint-analysis-loops.wast +++ b/test/lit/passes/constraint-analysis-loops.wast @@ -1,10 +1,6 @@ ;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. -;; Run both normally and in the "loops" mode. Many optimizations work in both -;; modes, but some require "loops" (mentioned below where that occurs). - ;; RUN: wasm-opt %s --constraint-analysis -all -S -o - | filecheck %s -;; RUN: wasm-opt %s --constraint-analysis-loops -all -S -o - | filecheck %s --check-prefix=LOOPS (module ;; CHECK: (import "a" "b" (func $import (type $2) (result i32))) @@ -23,18 +19,6 @@ ;; CHECK-NEXT: (br $loop) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; LOOPS: (func $infinite-loop (type $0) - ;; LOOPS-NEXT: (local $x i32) - ;; LOOPS-NEXT: (loop $loop - ;; LOOPS-NEXT: (local.set $x - ;; LOOPS-NEXT: (i32.add - ;; LOOPS-NEXT: (local.get $x) - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (br $loop) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) (func $infinite-loop (local $x i32) ;; An infinite loop. We should not hang, but nothing can be optimized. @@ -63,20 +47,6 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; LOOPS: (func $almost-infinite-loop (type $0) - ;; LOOPS-NEXT: (local $x i32) - ;; LOOPS-NEXT: (loop $loop - ;; LOOPS-NEXT: (local.set $x - ;; LOOPS-NEXT: (i32.add - ;; LOOPS-NEXT: (local.get $x) - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (br_if $loop - ;; LOOPS-NEXT: (local.get $x) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) (func $almost-infinite-loop (local $x i32) ;; A loop that continues until an overflow happens. We should not hang, but @@ -94,1052 +64,4 @@ ) ) ) - - ;; CHECK: (func $bound (type $0) - ;; CHECK-NEXT: (local $x i32) - ;; 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: (local.set $x - ;; CHECK-NEXT: (call $import) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (i32.gt_s - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (i32.le_s - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (i32.const 100) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (br $loop) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; LOOPS: (func $bound (type $0) - ;; LOOPS-NEXT: (local $x i32) - ;; LOOPS-NEXT: (loop $loop - ;; LOOPS-NEXT: (drop - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (drop - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (local.set $x - ;; LOOPS-NEXT: (call $import) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (if - ;; LOOPS-NEXT: (i32.gt_s - ;; LOOPS-NEXT: (local.get $x) - ;; LOOPS-NEXT: (i32.const 0) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (then - ;; LOOPS-NEXT: (if - ;; LOOPS-NEXT: (i32.le_s - ;; LOOPS-NEXT: (local.get $x) - ;; LOOPS-NEXT: (i32.const 100) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (then - ;; LOOPS-NEXT: (br $loop) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - (func $bound - (local $x i32) - (loop $loop - ;; We arrive at the loop top with {x == 0} || {x > 0 && x <= 100}. Those - ;; OR into {x >= 0 && x <= 100} - that is, the x == 0 and x > 0 combine - ;; into x >= 0. - (drop - (i32.ge_s - (local.get $x) - (i32.const 0) - ) - ) - (drop - (i32.le_s - (local.get $x) - (i32.const 100) - ) - ) - ;; Set $x to an unknown value before applying the constraints below on the - ;; way back to the loop top. - (local.set $x - (call $import) - ) - (if - (i32.gt_s - (local.get $x) - (i32.const 0) - ) - (then - (if - (i32.le_s - (local.get $x) - (i32.const 100) - ) - (then - (br $loop) - ) - ) - ) - ) - ) - ) - - ;; CHECK: (func $bound-flipped-ifs (type $0) - ;; CHECK-NEXT: (local $x i32) - ;; 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: (local.set $x - ;; CHECK-NEXT: (call $import) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (i32.le_s - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (i32.const 100) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (i32.gt_s - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (br $loop) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; LOOPS: (func $bound-flipped-ifs (type $0) - ;; LOOPS-NEXT: (local $x i32) - ;; LOOPS-NEXT: (loop $loop - ;; LOOPS-NEXT: (drop - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (drop - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (local.set $x - ;; LOOPS-NEXT: (call $import) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (if - ;; LOOPS-NEXT: (i32.le_s - ;; LOOPS-NEXT: (local.get $x) - ;; LOOPS-NEXT: (i32.const 100) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (then - ;; LOOPS-NEXT: (if - ;; LOOPS-NEXT: (i32.gt_s - ;; LOOPS-NEXT: (local.get $x) - ;; LOOPS-NEXT: (i32.const 0) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (then - ;; LOOPS-NEXT: (br $loop) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - (func $bound-flipped-ifs - (local $x i32) - ;; As above, but with the ifs flipped. We optimize the same way. - (loop $loop - (drop - (i32.ge_s - (local.get $x) - (i32.const 0) - ) - ) - (drop - (i32.le_s - (local.get $x) - (i32.const 100) - ) - ) - (local.set $x - (call $import) - ) - (if - (i32.le_s - (local.get $x) - (i32.const 100) - ) - (then - (if - (i32.gt_s - (local.get $x) - (i32.const 0) - ) - (then - (br $loop) - ) - ) - ) - ) - ) - ) - - ;; CHECK: (func $bound-nonconstant-no (type $1) (param $p i32) - ;; CHECK-NEXT: (local $x i32) - ;; CHECK-NEXT: (loop $loop - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.ge_s - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (local.get $p) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.le_s - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (local.get $p) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $x - ;; CHECK-NEXT: (call $import) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (i32.gt_s - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (local.get $p) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (i32.le_s - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (i32.const 100) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (br $loop) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; LOOPS: (func $bound-nonconstant-no (type $1) (param $p i32) - ;; LOOPS-NEXT: (local $x i32) - ;; LOOPS-NEXT: (loop $loop - ;; LOOPS-NEXT: (drop - ;; LOOPS-NEXT: (i32.ge_s - ;; LOOPS-NEXT: (local.get $x) - ;; LOOPS-NEXT: (local.get $p) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (drop - ;; LOOPS-NEXT: (i32.le_s - ;; LOOPS-NEXT: (local.get $x) - ;; LOOPS-NEXT: (local.get $p) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (local.set $x - ;; LOOPS-NEXT: (call $import) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (if - ;; LOOPS-NEXT: (i32.gt_s - ;; LOOPS-NEXT: (local.get $x) - ;; LOOPS-NEXT: (local.get $p) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (then - ;; LOOPS-NEXT: (if - ;; LOOPS-NEXT: (i32.le_s - ;; LOOPS-NEXT: (local.get $x) - ;; LOOPS-NEXT: (i32.const 100) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (then - ;; LOOPS-NEXT: (br $loop) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - (func $bound-nonconstant-no (param $p i32) - (local $x i32) - ;; As above, but rather than zero we have an unknown param $p. - (loop $loop - ;; We can infer nothing here, as p is unknown, and x might be 0 or <= 100. - (drop - (i32.ge_s - (local.get $x) - (local.get $p) - ) - ) - (drop - (i32.le_s - (local.get $x) - (local.get $p) - ) - ) - (local.set $x - (call $import) - ) - (if - (i32.gt_s - (local.get $x) - (local.get $p) - ) - (then - (if - (i32.le_s - (local.get $x) - (i32.const 100) - ) - (then - (br $loop) - ) - ) - ) - ) - ) - ) - - ;; CHECK: (func $bound-incremented (type $0) - ;; CHECK-NEXT: (local $x i32) - ;; CHECK-NEXT: (loop $loop - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.ge_s - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $x - ;; CHECK-NEXT: (i32.add - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (i32.lt_s - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (i32.const 100) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (br $loop) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; LOOPS: (func $bound-incremented (type $0) - ;; LOOPS-NEXT: (local $x i32) - ;; LOOPS-NEXT: (loop $loop - ;; LOOPS-NEXT: (drop - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (drop - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (local.set $x - ;; LOOPS-NEXT: (i32.add - ;; LOOPS-NEXT: (local.get $x) - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (if - ;; LOOPS-NEXT: (i32.lt_s - ;; LOOPS-NEXT: (local.get $x) - ;; LOOPS-NEXT: (i32.const 100) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (then - ;; LOOPS-NEXT: (br $loop) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - (func $bound-incremented - (local $x i32) - (loop $loop - ;; A realistic do-while loop, with $x++ and a bounds check. We must infer - ;; that no overflow happens in order to prove these two checks are true. - ;; - ;; The first is trivially true, as x starts at 0 - fulfilling x < 100 - - ;; and the branch back to the loop top arrives with x < 100. - (drop - (i32.lt_s - (local.get $x) - (i32.const 100) - ) - ) - ;; This is non-trivial, as we must rule out a possible overflow. The - ;; only reason that x never gets incremented so many times that it becomes - ;; negative is that the incrementation process is stopped at 100. We only - ;; manage to optimize this in "loops" mode. - (drop - (i32.ge_s - (local.get $x) - (i32.const 0) - ) - ) - ;; This changed compared to previous testcases: now we have x++. - (local.set $x - (i32.add - (local.get $x) - (i32.const 1) - ) - ) - (if - (i32.lt_s - (local.get $x) - (i32.const 100) - ) - (then - (br $loop) - ) - ) - ) - ) - - ;; CHECK: (func $bound-incremented-unsigned (type $0) - ;; CHECK-NEXT: (local $x i32) - ;; CHECK-NEXT: (loop $loop - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.ge_u - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $x - ;; CHECK-NEXT: (i32.add - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (i32.lt_u - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (i32.const 100) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (br $loop) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; LOOPS: (func $bound-incremented-unsigned (type $0) - ;; LOOPS-NEXT: (local $x i32) - ;; LOOPS-NEXT: (loop $loop - ;; LOOPS-NEXT: (drop - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (drop - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (local.set $x - ;; LOOPS-NEXT: (i32.add - ;; LOOPS-NEXT: (local.get $x) - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (if - ;; LOOPS-NEXT: (i32.lt_u - ;; LOOPS-NEXT: (local.get $x) - ;; LOOPS-NEXT: (i32.const 100) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (then - ;; LOOPS-NEXT: (br $loop) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - (func $bound-incremented-unsigned - ;; As above, but with unsigned operations. This is simpler, and we optimize - ;; it even without "loops" mode. - (local $x i32) - (loop $loop - (drop - (i32.lt_u - (local.get $x) - (i32.const 100) - ) - ) - (drop - (i32.ge_u - (local.get $x) - (i32.const 0) - ) - ) - (local.set $x - (i32.add - (local.get $x) - (i32.const 1) - ) - ) - (if - (i32.lt_u - (local.get $x) - (i32.const 100) - ) - (then - (br $loop) - ) - ) - ) - ) - - ;; CHECK: (func $increment-non-constant (type $1) (param $p i32) - ;; CHECK-NEXT: (local $a i32) - ;; CHECK-NEXT: (local $b i32) - ;; CHECK-NEXT: (local $scratch i32) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (block (result i32) - ;; CHECK-NEXT: (local.set $scratch - ;; CHECK-NEXT: (i32.lt_s - ;; CHECK-NEXT: (local.get $p) - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (i32.le_s - ;; CHECK-NEXT: (local.get $a) - ;; CHECK-NEXT: (local.get $b) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (local.set $p - ;; CHECK-NEXT: (local.get $a) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $a - ;; CHECK-NEXT: (i32.add - ;; CHECK-NEXT: (local.get $a) - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.eq - ;; CHECK-NEXT: (local.get $a) - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.get $scratch) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; LOOPS: (func $increment-non-constant (type $1) (param $p i32) - ;; LOOPS-NEXT: (local $a i32) - ;; LOOPS-NEXT: (local $b i32) - ;; LOOPS-NEXT: (local $scratch i32) - ;; LOOPS-NEXT: (drop - ;; LOOPS-NEXT: (block (result i32) - ;; LOOPS-NEXT: (local.set $scratch - ;; LOOPS-NEXT: (i32.lt_s - ;; LOOPS-NEXT: (local.get $p) - ;; LOOPS-NEXT: (i32.const 0) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (if - ;; LOOPS-NEXT: (i32.le_s - ;; LOOPS-NEXT: (local.get $a) - ;; LOOPS-NEXT: (local.get $b) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (then - ;; LOOPS-NEXT: (local.set $p - ;; LOOPS-NEXT: (local.get $a) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (local.set $a - ;; LOOPS-NEXT: (i32.add - ;; LOOPS-NEXT: (local.get $a) - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (drop - ;; LOOPS-NEXT: (i32.eq - ;; LOOPS-NEXT: (local.get $a) - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (drop - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (local.get $scratch) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - (func $increment-non-constant (param $p i32) - (local $a i32) - (local $b i32) - (drop - (i32.lt_s - (local.get $p) - (i32.const 0) - ) - (if - (i32.le_s - (local.get $a) - (local.get $b) - ) - (then - ;; Before this set, this is what we know about $a: - ;; $a == 0 && $a <= $b - (local.set $p - (local.get $a) - ) - ;; We just set $p to $a, so now we know this about $a: - ;; $a == 0 && $a <= $b, $a == $p - ;; We then proceed to do $a++, trying to increment each of those three - ;; constraints. We should not hit an internal error on trying to increment - ;; any of the three, ending up failing on the third (though, with a higher- - ;; level view, we could use the fact that $p == 0). - (local.set $a - (i32.add - (local.get $a) - (i32.const 1) - ) - ) - ;; Since we failed to know things about $a after $a++, we cannot - ;; prove this. - (drop - (i32.eq - (local.get $a) - (i32.const 1) - ) - ) - ;; But we did not forget about $b. - (drop - (i32.eq - (local.get $b) - (i32.const 0) - ) - ) - ) - ) - ) - ) - - ;; CHECK: (func $bound-incremented-while (type $0) - ;; CHECK-NEXT: (local $x i32) - ;; CHECK-NEXT: (block $out - ;; CHECK-NEXT: (loop $loop - ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (i32.ge_s - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (i32.const 100) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (br $out) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.ge_s - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $x - ;; CHECK-NEXT: (i32.add - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (br $loop) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; LOOPS: (func $bound-incremented-while (type $0) - ;; LOOPS-NEXT: (local $x i32) - ;; LOOPS-NEXT: (block $out - ;; LOOPS-NEXT: (loop $loop - ;; LOOPS-NEXT: (if - ;; LOOPS-NEXT: (i32.ge_s - ;; LOOPS-NEXT: (local.get $x) - ;; LOOPS-NEXT: (i32.const 100) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (then - ;; LOOPS-NEXT: (br $out) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (drop - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (drop - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (local.set $x - ;; LOOPS-NEXT: (i32.add - ;; LOOPS-NEXT: (local.get $x) - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (br $loop) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - (func $bound-incremented-while - ;; Similar to above, but before we had a do-while loop (loop condition at - ;; the bottom) and now it is at the top. - (local $x i32) - (block $out - (loop $loop - ;; Conditional branch at the top. - (if - (i32.ge_s - (local.get $x) - (i32.const 100) - ) - (then - (br $out) - ) - ) - ;; We can infer both of these to be true in loops mode (in normal mode, - ;; only the easy one, the first). - (drop - (i32.lt_s - (local.get $x) - (i32.const 100) - ) - ) - (drop - (i32.ge_s - (local.get $x) - (i32.const 0) - ) - ) - (local.set $x - (i32.add - (local.get $x) - (i32.const 1) - ) - ) - ;; Unconditional branch at the bottom. - (br $loop) - ) - ) - ) - - ;; CHECK: (func $bound-incremented-inc-first (type $0) - ;; CHECK-NEXT: (local $x i32) - ;; CHECK-NEXT: (block $out - ;; CHECK-NEXT: (loop $loop - ;; CHECK-NEXT: (local.set $x - ;; CHECK-NEXT: (i32.add - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (i32.ge_s - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (i32.const 100) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (br $out) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.gt_s - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (br $loop) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; LOOPS: (func $bound-incremented-inc-first (type $0) - ;; LOOPS-NEXT: (local $x i32) - ;; LOOPS-NEXT: (block $out - ;; LOOPS-NEXT: (loop $loop - ;; LOOPS-NEXT: (local.set $x - ;; LOOPS-NEXT: (i32.add - ;; LOOPS-NEXT: (local.get $x) - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (if - ;; LOOPS-NEXT: (i32.ge_s - ;; LOOPS-NEXT: (local.get $x) - ;; LOOPS-NEXT: (i32.const 100) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (then - ;; LOOPS-NEXT: (br $out) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (drop - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (drop - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (br $loop) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - (func $bound-incremented-inc-first - ;; Similar to the above "while" loop, but now with the increment before the - ;; if. - (local $x i32) - (block $out - (loop $loop - ;; Increment at the top. - (local.set $x - (i32.add - (local.get $x) - (i32.const 1) - ) - ) - ;; If after the increment. - (if - (i32.ge_s - (local.get $x) - (i32.const 100) - ) - (then - (br $out) - ) - ) - ;; x > 0 && x < 100 here (0 is impossible, compared to before). - (drop - (i32.gt_s - (local.get $x) - (i32.const 0) - ) - ) - (drop - (i32.lt_s - (local.get $x) - (i32.const 100) - ) - ) - (br $loop) - ) - ) - ) - - ;; CHECK: (func $bound-incremented-inc-first-less (type $0) - ;; CHECK-NEXT: (local $x i32) - ;; CHECK-NEXT: (block $out - ;; CHECK-NEXT: (loop $loop - ;; CHECK-NEXT: (local.set $x - ;; CHECK-NEXT: (i32.add - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (i32.gt_s - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (i32.const 100) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (br $out) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.gt_s - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (br $loop) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; LOOPS: (func $bound-incremented-inc-first-less (type $0) - ;; LOOPS-NEXT: (local $x i32) - ;; LOOPS-NEXT: (block $out - ;; LOOPS-NEXT: (loop $loop - ;; LOOPS-NEXT: (local.set $x - ;; LOOPS-NEXT: (i32.add - ;; LOOPS-NEXT: (local.get $x) - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (if - ;; LOOPS-NEXT: (i32.gt_s - ;; LOOPS-NEXT: (local.get $x) - ;; LOOPS-NEXT: (i32.const 100) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (then - ;; LOOPS-NEXT: (br $out) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (drop - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (drop - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (br $loop) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - (func $bound-incremented-inc-first-less - ;; As in the last testcase, but the if's condition changed. - (local $x i32) - (block $out - (loop $loop - (local.set $x - (i32.add - (local.get $x) - (i32.const 1) - ) - ) - ;; Before we left the loop when x >= 100. Now we leave when x > 100, - ;; so we do actually reach 100 in the code below. - (if - (i32.gt_s - (local.get $x) - (i32.const 100) - ) - (then - (br $out) - ) - ) - ;; x > 0 && x <= 100 here (but we need loops mode to get both). - (drop - (i32.gt_s - (local.get $x) - (i32.const 0) - ) - ) - (drop - (i32.le_s - (local.get $x) - (i32.const 100) - ) - ) - (br $loop) - ) - ) - ) - - ;; CHECK: (func $bound-incremented-inc-first-less-unsigned (type $0) - ;; CHECK-NEXT: (local $x i32) - ;; CHECK-NEXT: (block $out - ;; CHECK-NEXT: (loop $loop - ;; CHECK-NEXT: (local.set $x - ;; CHECK-NEXT: (i32.add - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (if - ;; CHECK-NEXT: (i32.gt_u - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (i32.const 100) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (br $out) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.gt_u - ;; CHECK-NEXT: (local.get $x) - ;; CHECK-NEXT: (i32.const 0) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (br $loop) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; LOOPS: (func $bound-incremented-inc-first-less-unsigned (type $0) - ;; LOOPS-NEXT: (local $x i32) - ;; LOOPS-NEXT: (block $out - ;; LOOPS-NEXT: (loop $loop - ;; LOOPS-NEXT: (local.set $x - ;; LOOPS-NEXT: (i32.add - ;; LOOPS-NEXT: (local.get $x) - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (if - ;; LOOPS-NEXT: (i32.gt_u - ;; LOOPS-NEXT: (local.get $x) - ;; LOOPS-NEXT: (i32.const 100) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (then - ;; LOOPS-NEXT: (br $out) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (drop - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (drop - ;; LOOPS-NEXT: (i32.const 1) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: (br $loop) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - ;; LOOPS-NEXT: ) - (func $bound-incremented-inc-first-less-unsigned - ;; As in the last testcase, but unsigned. - (local $x i32) - (block $out - (loop $loop - (local.set $x - (i32.add - (local.get $x) - (i32.const 1) - ) - ) - (if - (i32.gt_u - (local.get $x) - (i32.const 100) - ) - (then - (br $out) - ) - ) - ;; x > 0 && x <= 100 here (but we need loops mode to get both). - (drop - (i32.gt_u - (local.get $x) - (i32.const 0) - ) - ) - (drop - (i32.le_u - (local.get $x) - (i32.const 100) - ) - ) - (br $loop) - ) - ) - ) ) From a319bc1628506c525a50f99b633728f1025f01b4 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 4 Aug 2026 15:11:43 -0700 Subject: [PATCH 3/8] simpl --- src/passes/ConstraintAnalysis.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/passes/ConstraintAnalysis.cpp b/src/passes/ConstraintAnalysis.cpp index da557d0b21f..e2dddb9d67d 100644 --- a/src/passes/ConstraintAnalysis.cpp +++ b/src/passes/ConstraintAnalysis.cpp @@ -322,7 +322,7 @@ struct ConstraintAnalysis if (auto branch = getBranchConstraints(block, out); branch && checkRelevancy(*branch)) { auto sentConstraints = constraints; - applyBranchConstraints(*branch, sentConstraints); + sentConstraints.approximateAnd(branch->local, branch->constraint); #if CONSTRAINT_DEBUG std::cout << block << " sending branch to " << out << " with sent constraints: " << sentConstraints << '\n'; @@ -506,9 +506,9 @@ struct ConstraintAnalysis } // The only binary operation we match is an increment (x + 1), and we do - // not always want to apply it: only in loops mode, and even then, only - // when we are allowed to keep working (see above). - if (set->value->is() && (!loops || !maxWorkLeft)) { + // not always want to apply it: only when we are allowed to keep working + // (see above). + if (set->value->is() && !maxWorkLeft) { constraints.setProvesNothing(set->index); return; } From 9cd4af1c51906b8d0d3b73e922857fc9814e3399 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 4 Aug 2026 15:19:14 -0700 Subject: [PATCH 4/8] FIX --- src/ir/constraint.cpp | 1 - test/gtest/constraint.cpp | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/ir/constraint.cpp b/src/ir/constraint.cpp index 0489d87b618..bd7270e78dc 100644 --- a/src/ir/constraint.cpp +++ b/src/ir/constraint.cpp @@ -571,7 +571,6 @@ void BasicBlockConstraintMap::set(Index index, Expression* value) { switch (c.op) { // x == N, x++ => x == N+1. case Eq: - // TODO: overflows here and below c.term = Term(N->add(Literal::makeFromInt32(1, N->type))); continue; // x >= N, x++ => x > N diff --git a/test/gtest/constraint.cpp b/test/gtest/constraint.cpp index 260f3cd7dfd..5cbb0688b40 100644 --- a/test/gtest/constraint.cpp +++ b/test/gtest/constraint.cpp @@ -607,6 +607,22 @@ TEST(ConstraintTest, TestIncrement) { map.set(0, &add); check(map.get(0), {LeU, Literal(int32_t(6))}); + // $0 <= max_signed, $0++ => nothing, because it would overflow + map.set(0, {LeS, Literal::makeSignedMax(Type::i32)}); + map.set(0, &add); + EXPECT_TRUE(map.get(0).provesNothing()); + + // $0 <= max_unsigned, $0++ => nothing, because it would overflow + map.set(0, {LeU, Literal::makeUnsignedMax(Type::i32)}); + map.set(0, &add); + EXPECT_TRUE(map.get(0).provesNothing()); + + // However, an unsigned operation on the signed max is fine. + map.set(0, {LeU, Literal::makeSignedMax(Type::i32)}); + map.set(0, &add); + auto one = Literal::makeFromInt32(1, Type::i32); + check(map.get(0), {LeU, Literal::makeSignedMax(Type::i32).add(one)}); + // Multiple constraints at once: // $0 >= 10 && $0 < 20, $0++ => $0 > 10 && $0 <= 20 map.set(0, {GeS, Literal(int32_t(10))}); From a9a9405a459491bb20245fdece032c0d4f1bdcce Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 4 Aug 2026 15:25:19 -0700 Subject: [PATCH 5/8] FIX2 --- src/ir/constraint.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/ir/constraint.cpp b/src/ir/constraint.cpp index bd7270e78dc..9359f4693e2 100644 --- a/src/ir/constraint.cpp +++ b/src/ir/constraint.cpp @@ -534,7 +534,6 @@ void BasicBlockConstraintMap::set(Index index, } } -// Set the value in an expression to a local, replacing anything before. void BasicBlockConstraintMap::set(Index index, Expression* value) { using namespace Match; using namespace Abstract; From 9d8fe3aa9626d2a5a500ac6697cbaabd154587e6 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 4 Aug 2026 15:26:36 -0700 Subject: [PATCH 6/8] FIX --- .../lit/passes/constraint-analysis-loops.wast | 226 +++++++++++++++++- 1 file changed, 223 insertions(+), 3 deletions(-) diff --git a/test/lit/passes/constraint-analysis-loops.wast b/test/lit/passes/constraint-analysis-loops.wast index fcc88a3316a..8545fa734e1 100644 --- a/test/lit/passes/constraint-analysis-loops.wast +++ b/test/lit/passes/constraint-analysis-loops.wast @@ -1,10 +1,9 @@ ;; 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 +;; RUN: wasm-opt %s --constraint-analysis -all -S -o - | filecheck %s (module - ;; CHECK: (import "a" "b" (func $import (type $2) (result i32))) - ;; LOOPS: (import "a" "b" (func $import (type $2) (result i32))) + ;; CHECK: (import "a" "b" (func $import (type $1) (result i32))) (import "a" "b" (func $import (result i32))) ;; CHECK: (func $infinite-loop (type $0) @@ -64,4 +63,225 @@ ) ) ) + + ;; CHECK: (func $bound (type $0) + ;; CHECK-NEXT: (local $x i32) + ;; 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: (local.set $x + ;; CHECK-NEXT: (call $import) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.gt_s + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.le_s + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 100) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (br $loop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $bound + (local $x i32) + (loop $loop + ;; We arrive at the loop top with {x == 0} || {x > 0 && x <= 100}. Those + ;; OR into {x >= 0 && x <= 100} - that is, the x == 0 and x > 0 combine + ;; into x >= 0. + (drop + (i32.ge_s + (local.get $x) + (i32.const 0) + ) + ) + (drop + (i32.le_s + (local.get $x) + (i32.const 100) + ) + ) + ;; Set $x to an unknown value before applying the constraints below on the + ;; way back to the loop top. + (local.set $x + (call $import) + ) + (if + (i32.gt_s + (local.get $x) + (i32.const 0) + ) + (then + (if + (i32.le_s + (local.get $x) + (i32.const 100) + ) + (then + (br $loop) + ) + ) + ) + ) + ) + ) + + ;; CHECK: (func $bound-flipped-ifs (type $0) + ;; CHECK-NEXT: (local $x i32) + ;; 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: (local.set $x + ;; CHECK-NEXT: (call $import) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.le_s + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 100) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.gt_s + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (br $loop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $bound-flipped-ifs + (local $x i32) + ;; As above, but with the ifs flipped. We optimize the same way. + (loop $loop + (drop + (i32.ge_s + (local.get $x) + (i32.const 0) + ) + ) + (drop + (i32.le_s + (local.get $x) + (i32.const 100) + ) + ) + (local.set $x + (call $import) + ) + (if + (i32.le_s + (local.get $x) + (i32.const 100) + ) + (then + (if + (i32.gt_s + (local.get $x) + (i32.const 0) + ) + (then + (br $loop) + ) + ) + ) + ) + ) + ) + + ;; CHECK: (func $bound-nonconstant-no (type $2) (param $p i32) + ;; CHECK-NEXT: (local $x i32) + ;; CHECK-NEXT: (loop $loop + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.ge_s + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (local.get $p) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.le_s + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (local.get $p) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $x + ;; CHECK-NEXT: (call $import) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.gt_s + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (local.get $p) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.le_s + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 100) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (br $loop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $bound-nonconstant-no (param $p i32) + (local $x i32) + ;; As above, but rather than zero we have an unknown param $p. + (loop $loop + ;; We can infer nothing here, as p is unknown, and x might be 0 or <= 100. + (drop + (i32.ge_s + (local.get $x) + (local.get $p) + ) + ) + (drop + (i32.le_s + (local.get $x) + (local.get $p) + ) + ) + (local.set $x + (call $import) + ) + (if + (i32.gt_s + (local.get $x) + (local.get $p) + ) + (then + (if + (i32.le_s + (local.get $x) + (i32.const 100) + ) + (then + (br $loop) + ) + ) + ) + ) + ) + ) ) From eb4fc78d33a6715c4b7e2bbe2d6956f5e1c4056f Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 4 Aug 2026 15:42:21 -0700 Subject: [PATCH 7/8] FIX --- test/gtest/constraint.cpp | 48 +++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/test/gtest/constraint.cpp b/test/gtest/constraint.cpp index 5cbb0688b40..cad0b0b678e 100644 --- a/test/gtest/constraint.cpp +++ b/test/gtest/constraint.cpp @@ -568,67 +568,67 @@ TEST(ConstraintTest, TestIncrement) { add.right = &c; // $0 = 0, $1 = $0 + 1, so $1 = 1 (and $0 is unchanged). - map.set(0, {Eq, Literal(int32_t(0))}); + map.set(0, {Eq, {Literal(int32_t(0))}}); map.set(1, &add); - check(map.get(0), {Eq, Literal(int32_t(0))}); - check(map.get(1), {Eq, Literal(int32_t(1))}); + check(map.get(0), {Eq, {Literal(int32_t(0))}}); + check(map.get(1), {Eq, {Literal(int32_t(1))}}); // $0 = $0 + 1, where $0 was 0, so it is now 1. map.set(0, &add); - check(map.get(0), {Eq, Literal(int32_t(1))}); + check(map.get(0), {Eq, {Literal(int32_t(1))}}); // $0 >= 5, $0++ => $0 > 5 (signed) - map.set(0, {GeS, Literal(int32_t(5))}); + map.set(0, {GeS, {Literal(int32_t(5))}}); map.set(0, &add); - check(map.get(0), {GtS, Literal(int32_t(5))}); + check(map.get(0), {GtS, {Literal(int32_t(5))}}); // Ditto, unsigned - map.set(0, {GeU, Literal(int32_t(5))}); + map.set(0, {GeU, {Literal(int32_t(5))}}); map.set(0, &add); - check(map.get(0), {GtU, Literal(int32_t(5))}); + check(map.get(0), {GtU, {Literal(int32_t(5))}}); // $0 < 5, $0++ => $0 <= 5 (signed) - map.set(0, {LtS, Literal(int32_t(5))}); + map.set(0, {LtS, {Literal(int32_t(5))}}); map.set(0, &add); - check(map.get(0), {LeS, Literal(int32_t(5))}); + check(map.get(0), {LeS, {Literal(int32_t(5))}}); // Ditto, unsigned - map.set(0, {LtU, Literal(int32_t(5))}); + map.set(0, {LtU, {Literal(int32_t(5))}}); map.set(0, &add); - check(map.get(0), {LeU, Literal(int32_t(5))}); + check(map.get(0), {LeU, {Literal(int32_t(5))}}); // $0 <= 5, $0++ => $0 <= 6 (signed) - map.set(0, {LeS, Literal(int32_t(5))}); + map.set(0, {LeS, {Literal(int32_t(5))}}); map.set(0, &add); - check(map.get(0), {LeS, Literal(int32_t(6))}); + check(map.get(0), {LeS, {Literal(int32_t(6))}}); // Ditto, unsigned - map.set(0, {LeU, Literal(int32_t(5))}); + map.set(0, {LeU, {Literal(int32_t(5))}}); map.set(0, &add); - check(map.get(0), {LeU, Literal(int32_t(6))}); + check(map.get(0), {LeU, {Literal(int32_t(6))}}); // $0 <= max_signed, $0++ => nothing, because it would overflow - map.set(0, {LeS, Literal::makeSignedMax(Type::i32)}); + map.set(0, {LeS, {Literal::makeSignedMax(Type::i32)}}); map.set(0, &add); EXPECT_TRUE(map.get(0).provesNothing()); // $0 <= max_unsigned, $0++ => nothing, because it would overflow - map.set(0, {LeU, Literal::makeUnsignedMax(Type::i32)}); + map.set(0, {LeU, {Literal::makeUnsignedMax(Type::i32)}}); map.set(0, &add); EXPECT_TRUE(map.get(0).provesNothing()); // However, an unsigned operation on the signed max is fine. - map.set(0, {LeU, Literal::makeSignedMax(Type::i32)}); + map.set(0, {LeU, {Literal::makeSignedMax(Type::i32)}}); map.set(0, &add); auto one = Literal::makeFromInt32(1, Type::i32); - check(map.get(0), {LeU, Literal::makeSignedMax(Type::i32).add(one)}); + check(map.get(0), {LeU, {Literal::makeSignedMax(Type::i32).add(one)}}); // Multiple constraints at once: // $0 >= 10 && $0 < 20, $0++ => $0 > 10 && $0 <= 20 - map.set(0, {GeS, Literal(int32_t(10))}); - map.approximateAnd(0, {LtS, Literal(int32_t(20))}); + map.set(0, {GeS, {Literal(int32_t(10))}}); + map.approximateAnd(0, {LtS, {Literal(int32_t(20))}}); map.set(0, &add); EXPECT_EQ(map.get(0), - (AndedConstraintSet{{GtS, Literal(int32_t(10))}, - {LeS, Literal(int32_t(20))}})); + (AndedConstraintSet{{GtS, {Literal(int32_t(10))}}, + {LeS, {Literal(int32_t(20))}}})); } From b368c1c6ab663f91afd9a33fbe633916a37035a9 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Tue, 4 Aug 2026 16:16:53 -0700 Subject: [PATCH 8/8] SIMPL --- src/ir/constraint.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ir/constraint.cpp b/src/ir/constraint.cpp index 9359f4693e2..e837e21ca71 100644 --- a/src/ir/constraint.cpp +++ b/src/ir/constraint.cpp @@ -570,7 +570,7 @@ void BasicBlockConstraintMap::set(Index index, Expression* value) { switch (c.op) { // x == N, x++ => x == N+1. case Eq: - c.term = Term(N->add(Literal::makeFromInt32(1, N->type))); + *N = N->add(Literal::makeFromInt32(1, N->type)); continue; // x >= N, x++ => x > N case GeS: