diff --git a/src/ir/constraint.cpp b/src/ir/constraint.cpp index 868618bd9f2..7cc20d7e636 100644 --- a/src/ir/constraint.cpp +++ b/src/ir/constraint.cpp @@ -747,8 +747,21 @@ std::optional LocalConstraint::parse(Expression* curr) { if (auto* unary = curr->dynCast()) { if (Abstract::getUnary(unary->value->type, Abstract::EqZ) == unary->op) { + // EqZ of EqZ means a check that the value is *not* zero. + if (auto* nested = unary->value->dynCast()) { + if (Abstract::getUnary(nested->value->type, Abstract::EqZ) == + nested->op) { + if (auto* get = nested->value->dynCast()) { + auto value = Literal::makeZero(get->type); + return LocalConstraint{get->index, + Constraint{Abstract::Ne, {value}}}; + } + } + } + return parseEqZArgument(unary->value); } + return {}; } @@ -807,17 +820,107 @@ std::optional LocalConstraint::parse(Expression* curr) { return {}; } -std::optional -LocalConstraint::parseCondition(Expression* curr) { +ParsedAndedConstraints ParsedAndedConstraints::parse(Expression* curr) { + // The final return value. + ParsedAndedConstraints ret; + + // Starting from |curr|, parse and recurse into sub-trees: when we see an AND, + // we push both children as further work. + SmallVector work; + work.push_back(curr); + while (!work.empty()) { + auto* curr = work.back(); + work.pop_back(); + + auto parsed = LocalConstraint::parse(curr); + if (parsed) { + ret.push_back(*parsed); + continue; + } + + if (auto* binary = curr->dynCast()) { + // An AND can be recursively processed: both sides must be true. + if (Abstract::getBinary(binary->left->type, Abstract::And) == + binary->op) { + work.push_back(binary->left); + work.push_back(binary->right); + continue; + } + // TODO: support OR + } + + // We failed to parse this. + ret.hasUnknown = true; + } + + return ret; +} + +ParsedAndedConstraints +ParsedAndedConstraints::parseCondition(Expression* curr) { // A get by itself is a check for not being null. if (auto* get = curr->dynCast()) { auto value = Literal::makeZero(get->type); - return LocalConstraint{get->index, Constraint{Abstract::Ne, {value}}}; + return {LocalConstraint{get->index, Constraint{Abstract::Ne, {value}}}}; } // Otherwise, parse normally. return parse(curr); -}; +} + +void ParsedAndedConstraints::negate() { + if (empty()) { + return; + } + + if (hasUnknown) { + // This includes things we don't know about, and don't know how to negate. + clear(); + return; + } + + // The input is a list of constraints all applying at once, A & B & C. The + // negation is !A | !B | !C, but we cannot express a general OR like that, + // except in the simple case where they all talk about the same local: then + // we can at least approximateOr them all into one constraint. + auto& self = *this; + for (Index i = 1; i < size(); i++) { + if (self[i].local != self[0].local) { + // They refer to different locals. Give up. + clear(); + return; + } + } + + // Negate them before the OR. + for (auto& pair : self) { + pair.constraint = pair.constraint.negate(); + } + + if (size() == 1) { + // The simple case of 1 doesn't need any more work. + return; + } + + // Do the OR. + AndedConstraintSet anded; + anded.set(self[0].constraint); + for (Index i = 1; i < size(); i++) { + anded.approximateOr({self[i].constraint}); + if (anded.provesNothing()) { + // We have nothing useful here. + clear(); + return; + } + } + + // Return only the OR'ed result. + auto local = self[0].local; + clear(); + for (auto& c : anded) { + emplace_back(local, c); + } +} void LocalConstraint::flip() { auto other = std::get(constraint.term); @@ -1152,6 +1255,11 @@ std::ostream& operator<<(std::ostream& o, const Constraint& c) { return o; } +std::ostream& operator<<(std::ostream& o, const LocalConstraint& c) { + o << "LocalConstraint{$" << c.local << ", " << c.constraint << '}'; + return o; +} + std::ostream& operator<<(std::ostream& o, const AndedConstraintSet& set) { if (set.provesEverything()) { o << "AndedConstraintSet(contradiction)"; diff --git a/src/ir/constraint.h b/src/ir/constraint.h index d699fc4c424..2e15bec5ff0 100644 --- a/src/ir/constraint.h +++ b/src/ir/constraint.h @@ -32,6 +32,7 @@ #include "ir/abstract.h" #include "support/inplace_vector.h" +#include "support/small_vector.h" #include "support/span.h" #include "support/utilities.h" #include "wasm.h" @@ -235,6 +236,12 @@ struct LocalConstraint { Index local; Constraint constraint; + LocalConstraint() = default; + LocalConstraint(Index local, Constraint constraint) + : local(local), constraint(std::move(constraint)) {} + + bool operator==(const LocalConstraint&) const = default; + // Try to parse BinaryenIR into a local to which a constraint is applied. For // example // @@ -246,15 +253,43 @@ struct LocalConstraint { // static std::optional parse(Expression* curr); - // Parse in a condition context, i.e., where (local.get $x) is the same as - // $x != 0 (e.g., in an if condition, or a br_on ref). - static std::optional parseCondition(Expression* curr); - // Reverse the constraint. The constraint's term must, of course, be another // local. void flip(); }; +// A utility to parse BinaryenIR into locals and constraints on them. This is +// similar to LocalConstraint::parse, but that parses a single constraint, while +// this can handle a list of ANDed ones: +// +// (i32.and (..A..) (..B..)) +// +// parses into [ A, B ]. +// +// We also set a field |hasUnknown| if we saw things we could not parse. E.g. +// +// (i32.and (call $unknown) (i32.eqz (local.get $x))) +// +// This parses into [ $x == 0 ] and sets hasUnknown=true. Even if there are +// unknown things, we do know that definitely $x == 0 at least, which is useful +// in some cases. +struct ParsedAndedConstraints : public SmallVector { + using SmallVector::SmallVector; + + bool hasUnknown = false; + + static ParsedAndedConstraints parse(Expression* curr); + + // Parse in a condition context, i.e., where (local.get $x) is the same as + // $x != 0 (e.g., in an if condition, or a br_on ref). + static ParsedAndedConstraints parseCondition(Expression* curr); + + // Negate the entire list of constraints. If we fail to generate something + // that can be represented as a list of ANDed constraints, the list will be + // empty (i.e., we can prove nothing). + void negate(); +}; + // A map of locals and their constraints, representing the state at a basic // block. We use the following representation: // @@ -366,6 +401,7 @@ struct BasicBlockConstraintMap { }; std::ostream& operator<<(std::ostream& o, const Constraint& c); +std::ostream& operator<<(std::ostream& o, const LocalConstraint& c); std::ostream& operator<<(std::ostream& o, const AndedConstraintSet& set); } // namespace wasm::constraint diff --git a/src/passes/ConstraintAnalysis.cpp b/src/passes/ConstraintAnalysis.cpp index bd2d9a14867..9e275665e9e 100644 --- a/src/passes/ConstraintAnalysis.cpp +++ b/src/passes/ConstraintAnalysis.cpp @@ -66,6 +66,8 @@ // function analysis). // +#include + #include "cfg/cfg-traversal.h" #include "ir/constraint.h" #include "ir/drop.h" @@ -146,11 +148,12 @@ struct ConstraintAnalysis void maybeMarkRelevant(Expression* curr) { // If this parses into a constraint on a local, that local is relevant. - if (auto parsed = LocalConstraint::parseCondition(curr); - parsed && isRelevantType(getFunction()->getLocalType(parsed->local))) { - relevantLocals[parsed->local] = true; - if (auto* other = std::get_if(&parsed->constraint.term)) { - relevantLocals[*other] = true; + for (auto& pair : ParsedAndedConstraints::parseCondition(curr)) { + if (isRelevantType(getFunction()->getLocalType(pair.local))) { + relevantLocals[pair.local] = true; + if (auto* other = std::get_if(&pair.constraint.term)) { + relevantLocals[*other] = true; + } } } } @@ -371,9 +374,9 @@ struct ConstraintAnalysis // Find the constraints sent to this specific successor, if there is a // branch, and use them. if (auto branch = getBranchConstraints(block, out); - branch && checkRelevancy(*branch)) { + filterRelevant(branch), !branch.empty()) { auto sentConstraints = constraints; - applyBranchConstraints(*branch, sentConstraints); + applyBranchConstraints(branch, sentConstraints); #if CONSTRAINT_DEBUG std::cout << block << " sending branch to " << out << " with sent constraints: " << sentConstraints << '\n'; @@ -441,6 +444,9 @@ struct ConstraintAnalysis void optimizeExpression(Expression** currp, const BasicBlockConstraintMap& constraints) { auto* curr = *currp; + // Note that we don't need to try to parse a series of constraints with + // ParsedAndedConstraints: if there is a tree of ANDed things, we will + // simply optimize it as we walk it, each time handling one. auto parsed = LocalConstraint::parse(curr); if (!parsed) { return; @@ -472,8 +478,8 @@ struct ConstraintAnalysis // Given a predecessor and one of its successors, find new constraints that // can be added due to the flow to that specific successor. - std::optional getBranchConstraints(BasicBlock* pred, - BasicBlock* succ) { + ParsedAndedConstraints getBranchConstraints(BasicBlock* pred, + BasicBlock* succ) { auto* brancher = pred->contents.brancher; if (!brancher) { return {}; @@ -502,32 +508,31 @@ struct ConstraintAnalysis return {}; } - std::optional getConstraintsFromIf(If* iff, - bool physicalSuccessor) { - auto parsed = LocalConstraint::parseCondition(iff->condition); - if (parsed && !physicalSuccessor) { + ParsedAndedConstraints getConstraintsFromIf(If* iff, bool physicalSuccessor) { + auto parsed = ParsedAndedConstraints::parseCondition(iff->condition); + if (!physicalSuccessor) { // We are in the ifFalse, so negate the condition. - parsed->constraint = parsed->constraint.negate(); + parsed.negate(); } return parsed; } - std::optional - getConstraintsFromBreak(Break* br, bool physicalSuccessor) { + ParsedAndedConstraints getConstraintsFromBreak(Break* br, + bool physicalSuccessor) { // We get here when there is more than one successor, so there must be a // condition. assert(br->condition); - auto parsed = LocalConstraint::parseCondition(br->condition); - if (parsed && physicalSuccessor) { + auto parsed = ParsedAndedConstraints::parseCondition(br->condition); + if (physicalSuccessor) { // The branch was not taken, so negate the condition. - parsed->constraint = parsed->constraint.negate(); + parsed.negate(); } return parsed; } - std::optional - getConstraintsFromBrOn(BrOn* brOn, bool physicalSuccessor) { + ParsedAndedConstraints getConstraintsFromBrOn(BrOn* brOn, + bool physicalSuccessor) { // The constraint on that local depends on the op. // TODO: Handle BrOnCast* etc using subtyping operations. if (brOn->op != BrOnNull && brOn->op != BrOnNonNull) { @@ -537,10 +542,10 @@ struct ConstraintAnalysis // parseCondition can parse more things than a local.get, which is all we // handle here, but there is no other valid IR that can appear there, so we // can reuse it. - auto parsed = LocalConstraint::parseCondition(brOn->ref); + auto parsed = ParsedAndedConstraints::parseCondition(brOn->ref); // Negate depending on the op and (similar to Break) the successor. - if (parsed && ((brOn->op == BrOnNull) ^ physicalSuccessor)) { - parsed->constraint = parsed->constraint.negate(); + if ((brOn->op == BrOnNull) ^ physicalSuccessor) { + parsed.negate(); } return parsed; } @@ -664,17 +669,31 @@ struct ConstraintAnalysis return true; } + // Filters out constraints on irrelevant locals. + void filterRelevant(ParsedAndedConstraints& parsed) { + parsed.erase(std::remove_if(parsed.begin(), + parsed.end(), + [&](const LocalConstraint& pair) { + return !checkRelevancy(pair); + }), + parsed.end()); + } + // Apply branch constraints to the current set of constraints. - void applyBranchConstraints(const LocalConstraint& branch, + void applyBranchConstraints(const ParsedAndedConstraints& branch, BasicBlockConstraintMap& constraints) { - // Extend the range of values in the "jump ahead" manner described in the - // top-level comment. - if (applyBranchRangeExtensionToConstraints(branch, constraints)) { - return; - } + for (auto& pair : branch) { + // Extend the range of values in the "jump ahead" manner described in the + // top-level comment. + if (!applyBranchRangeExtensionToConstraints(pair, constraints)) { + // Otherwise, apply the constraint normally. + constraints.approximateAnd(pair.local, pair.constraint); + } - // Otherwise, apply the constraint normally. - constraints.approximateAnd(branch.local, branch.constraint); + if (constraints.unreachable) { + return; + } + } } bool diff --git a/src/support/inplace_vector.h b/src/support/inplace_vector.h index cebf6f112fb..5f0d3628ebb 100644 --- a/src/support/inplace_vector.h +++ b/src/support/inplace_vector.h @@ -154,7 +154,6 @@ template class inplace_vector { ConstIterator(const Iterator& other) : wasm::ParentIndexIterator*, ConstIterator>{ other.parent, other.index} {} - ConstIterator(const ConstIterator& other) = default; const T& operator*() const { return (*this->parent)[this->index]; } const T* operator->() const { return &(*this->parent)[this->index]; } diff --git a/src/support/small_vector.h b/src/support/small_vector.h index 6a7a6ec89ec..93c8899bc59 100644 --- a/src/support/small_vector.h +++ b/src/support/small_vector.h @@ -175,7 +175,6 @@ template class SmallVector { Iterator(SmallVector* parent, size_t index) : ParentIndexIterator*, Iterator>{parent, index} {} - Iterator(const Iterator& other) = default; T& operator*() const { return (*this->parent)[this->index]; } }; @@ -189,7 +188,6 @@ template class SmallVector { ConstIterator(const SmallVector* parent, size_t index) : ParentIndexIterator*, ConstIterator>{parent, index} {} - ConstIterator(const ConstIterator& other) = default; const T& operator*() const { return (*this->parent)[this->index]; } }; diff --git a/test/gtest/constraint.cpp b/test/gtest/constraint.cpp index ae0a77d61a3..d02aae4c329 100644 --- a/test/gtest/constraint.cpp +++ b/test/gtest/constraint.cpp @@ -1,7 +1,8 @@ #include -#include "ir/constraint.h" #include "ir/abstract.h" +#include "ir/constraint.h" +#include "wasm-builder.h" #include "gtest/gtest.h" using namespace wasm; @@ -1644,3 +1645,409 @@ TEST(ConstraintTest, EqualTermPairs) { sGtUContra.approximateAnd(eq1); EXPECT_TRUE(sGtUContra.provesEverything()); } + +TEST(ConstraintTest, ParseUnaryEqZ) { + Module wasm; + Builder builder(wasm); + + // 1. Single i32.eqz of a local.get: parsed as x == 0. + { + auto* expr = + builder.makeUnary(EqZInt32, builder.makeLocalGet(0, Type::i32)); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_FALSE(parsed.hasUnknown); + ASSERT_EQ(parsed.size(), 1); + EXPECT_EQ(parsed[0], + (LocalConstraint{0, Constraint{Eq, {Literal(int32_t(0))}}})); + } + + // 2. Single i64.eqz of a local.get: parsed as x == 0_i64. + { + auto* expr = + builder.makeUnary(EqZInt64, builder.makeLocalGet(1, Type::i64)); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_FALSE(parsed.hasUnknown); + ASSERT_EQ(parsed.size(), 1); + EXPECT_EQ(parsed[0], + (LocalConstraint{1, Constraint{Eq, {Literal(int64_t(0))}}})); + } + + // 3. Nested eqz of eqz: parsed as x != 0. + { + auto* inner = + builder.makeUnary(EqZInt32, builder.makeLocalGet(0, Type::i32)); + auto* expr = builder.makeUnary(EqZInt32, inner); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_FALSE(parsed.hasUnknown); + ASSERT_EQ(parsed.size(), 1); + EXPECT_EQ(parsed[0], + (LocalConstraint{0, Constraint{Ne, {Literal(int32_t(0))}}})); + } + + // 4. Nested eqz of eqz with 64-bit inner: parsed as x != 0_i64. + { + auto* inner = + builder.makeUnary(EqZInt64, builder.makeLocalGet(1, Type::i64)); + auto* expr = builder.makeUnary(EqZInt32, inner); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_FALSE(parsed.hasUnknown); + ASSERT_EQ(parsed.size(), 1); + EXPECT_EQ(parsed[0], + (LocalConstraint{1, Constraint{Ne, {Literal(int64_t(0))}}})); + } + + // 5. eqz of non-local.get (e.g. call): unhandled, sets hasUnknown. + { + auto* call = builder.makeCall("foo", {}, Type::i32); + auto* expr = builder.makeUnary(EqZInt32, call); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_TRUE(parsed.empty()); + EXPECT_TRUE(parsed.hasUnknown); + } + + // 6. Double eqz where inner value is not a local.get: sets hasUnknown. + { + auto* call = builder.makeCall("foo", {}, Type::i32); + auto* inner = builder.makeUnary(EqZInt32, call); + auto* expr = builder.makeUnary(EqZInt32, inner); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_TRUE(parsed.empty()); + EXPECT_TRUE(parsed.hasUnknown); + } + + // 7. Unary operation that is not EqZ: sets hasUnknown. + { + auto* expr = + builder.makeUnary(ClzInt32, builder.makeLocalGet(0, Type::i32)); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_TRUE(parsed.empty()); + EXPECT_TRUE(parsed.hasUnknown); + } +} + +TEST(ConstraintTest, ParseRefIsNull) { + Module wasm; + Builder builder(wasm); + auto anyref = Type(HeapType::any, Nullable); + + // 1. ref.is_null of local.get: parsed as x == null. + { + auto* expr = builder.makeRefIsNull(builder.makeLocalGet(0, anyref)); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_FALSE(parsed.hasUnknown); + ASSERT_EQ(parsed.size(), 1); + EXPECT_EQ( + parsed[0], + (LocalConstraint{0, Constraint{Eq, {Literal::makeNull(HeapType::any)}}})); + } + + // 2. ref.is_null of non-local.get (e.g. call): unhandled, sets hasUnknown. + { + auto* call = builder.makeCall("foo", {}, anyref); + auto* expr = builder.makeRefIsNull(call); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_TRUE(parsed.empty()); + EXPECT_TRUE(parsed.hasUnknown); + } + + // 3. ref.is_null of constant: unhandled, sets hasUnknown. + { + auto* expr = builder.makeRefIsNull(builder.makeRefNull(HeapType::any)); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_TRUE(parsed.empty()); + EXPECT_TRUE(parsed.hasUnknown); + } +} + +TEST(ConstraintTest, ParseBinary) { + Module wasm; + Builder builder(wasm); + + // 1. Binary comparison with constant on right: parsed as local constraint. + { + auto* expr = builder.makeBinary(EqInt32, + builder.makeLocalGet(0, Type::i32), + builder.makeConst(Literal(int32_t(42)))); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_FALSE(parsed.hasUnknown); + ASSERT_EQ(parsed.size(), 1); + EXPECT_EQ(parsed[0], + (LocalConstraint{0, Constraint{Eq, {Literal(int32_t(42))}}})); + } + + // 2. Binary comparison with local.get on right: parsed as local constraint + { + auto* expr = builder.makeBinary(NeInt32, + builder.makeLocalGet(0, Type::i32), + builder.makeLocalGet(1, Type::i32)); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_FALSE(parsed.hasUnknown); + ASSERT_EQ(parsed.size(), 1); + EXPECT_EQ(parsed[0], (LocalConstraint{0, Constraint{Ne, {Index(1)}}})); + } + + // 3. All relational binary operators: + for (auto [wasmOp, abstractOp] : { + std::pair{EqInt32, Eq}, + std::pair{NeInt32, Ne}, + std::pair{LtSInt32, LtS}, + std::pair{LtUInt32, LtU}, + std::pair{LeSInt32, LeS}, + std::pair{LeUInt32, LeU}, + std::pair{GtSInt32, GtS}, + std::pair{GtUInt32, GtU}, + std::pair{GeSInt32, GeS}, + std::pair{GeUInt32, GeU}, + }) { + auto* expr = builder.makeBinary(wasmOp, + builder.makeLocalGet(0, Type::i32), + builder.makeConst(Literal(int32_t(5)))); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_FALSE(parsed.hasUnknown); + ASSERT_EQ(parsed.size(), 1); + EXPECT_EQ( + parsed[0], + (LocalConstraint{0, Constraint{abstractOp, {Literal(int32_t(5))}}})); + } + + // 64-bit comparison: + { + auto* expr = builder.makeBinary(LtSInt64, + builder.makeLocalGet(0, Type::i64), + builder.makeConst(Literal(int64_t(100)))); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_FALSE(parsed.hasUnknown); + ASSERT_EQ(parsed.size(), 1); + EXPECT_EQ(parsed[0], + (LocalConstraint{0, Constraint{LtS, {Literal(int64_t(100))}}})); + } + + // 4. Comparison where right is not a term (e.g. call): unhandled, sets + // hasUnknown. + { + auto* call = builder.makeCall("foo", {}, Type::i32); + auto* expr = + builder.makeBinary(EqInt32, builder.makeLocalGet(0, Type::i32), call); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_TRUE(parsed.empty()); + EXPECT_TRUE(parsed.hasUnknown); + } + + // 5. Comparison where left is not a local.get: unhandled, sets hasUnknown. + { + auto* expr = builder.makeBinary(EqInt32, + builder.makeConst(Literal(int32_t(1))), + builder.makeConst(Literal(int32_t(2)))); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_TRUE(parsed.empty()); + EXPECT_TRUE(parsed.hasUnknown); + } + // Ditto, local on right. + { + auto* expr = builder.makeBinary(EqInt32, + builder.makeConst(Literal(int32_t(1))), + builder.makeLocalGet(0, Type::i32)); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_TRUE(parsed.empty()); + EXPECT_TRUE(parsed.hasUnknown); + } + + // 6. Binary operation that is not a comparison or AND (e.g. Add, Sub, Mul, + // Or, Xor): sets hasUnknown. + for (auto op : {AddInt32, SubInt32, MulInt32, OrInt32, XorInt32}) { + auto* expr = builder.makeBinary(op, + builder.makeLocalGet(0, Type::i32), + builder.makeConst(Literal(int32_t(1)))); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_TRUE(parsed.empty()); + EXPECT_TRUE(parsed.hasUnknown); + } +} + +TEST(ConstraintTest, ParseRefEq) { + Module wasm; + Builder builder(wasm); + auto anyref = Type(HeapType::any, Nullable); + + // 1. ref.eq with local.get on both sides. + { + auto* expr = builder.makeRefEq(builder.makeLocalGet(0, anyref), + builder.makeLocalGet(1, anyref)); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_FALSE(parsed.hasUnknown); + ASSERT_EQ(parsed.size(), 1); + EXPECT_EQ(parsed[0], (LocalConstraint{0, Constraint{Eq, {Index(1)}}})); + } + + // 2. ref.eq with local.get and ref.null. + { + auto* expr = builder.makeRefEq(builder.makeLocalGet(0, anyref), + builder.makeRefNull(HeapType::any)); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_FALSE(parsed.hasUnknown); + ASSERT_EQ(parsed.size(), 1); + EXPECT_EQ( + parsed[0], + (LocalConstraint{0, Constraint{Eq, {Literal::makeNull(HeapType::any)}}})); + } + + // 3. ref.eq where left is not a local.get (e.g. null on left): sets + // hasUnknown. + { + auto* expr = builder.makeRefEq(builder.makeRefNull(HeapType::any), + builder.makeLocalGet(0, anyref)); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_TRUE(parsed.empty()); + EXPECT_TRUE(parsed.hasUnknown); + } + + // 4. ref.eq where right is not a term (e.g. call): sets hasUnknown. + { + auto* call = builder.makeCall("foo", {}, anyref); + auto* expr = builder.makeRefEq(builder.makeLocalGet(0, anyref), call); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_TRUE(parsed.empty()); + EXPECT_TRUE(parsed.hasUnknown); + } +} + +TEST(ConstraintTest, ParseAnd) { + Module wasm; + Builder builder(wasm); + + // 1. AND over two valid comparisons: both constraints returned. + { + auto* left = builder.makeBinary(EqInt32, + builder.makeLocalGet(0, Type::i32), + builder.makeConst(Literal(int32_t(1)))); + auto* right = builder.makeBinary(EqInt32, + builder.makeLocalGet(1, Type::i32), + builder.makeConst(Literal(int32_t(2)))); + auto* expr = builder.makeBinary(AndInt32, left, right); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_FALSE(parsed.hasUnknown); + ASSERT_EQ(parsed.size(), 2); + // Work stack processes right then left. + EXPECT_EQ(parsed[0], + (LocalConstraint{1, Constraint{Eq, {Literal(int32_t(2))}}})); + EXPECT_EQ(parsed[1], + (LocalConstraint{0, Constraint{Eq, {Literal(int32_t(1))}}})); + } + + // 2. AND with known constraint and unknown expression (call): Parses the + // constraint and also sets hasUnknown. + { + auto* left = builder.makeBinary(EqInt32, + builder.makeLocalGet(0, Type::i32), + builder.makeConst(Literal(int32_t(10)))); + auto* right = builder.makeCall("unknown", {}, Type::i32); + auto* expr = builder.makeBinary(AndInt32, left, right); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_TRUE(parsed.hasUnknown); + ASSERT_EQ(parsed.size(), 1); + EXPECT_EQ(parsed[0], + (LocalConstraint{0, Constraint{Eq, {Literal(int32_t(10))}}})); + } + + // 3. Same as above with unknown on left: + { + auto* left = builder.makeCall("unknown", {}, Type::i32); + auto* right = builder.makeBinary(EqInt32, + builder.makeLocalGet(0, Type::i32), + builder.makeConst(Literal(int32_t(10)))); + auto* expr = builder.makeBinary(AndInt32, left, right); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_TRUE(parsed.hasUnknown); + ASSERT_EQ(parsed.size(), 1); + EXPECT_EQ(parsed[0], + (LocalConstraint{0, Constraint{Eq, {Literal(int32_t(10))}}})); + } + + // 4. AND with known constraint and unhandled binary op (e.g. Add): + { + auto* left = builder.makeBinary(EqInt32, + builder.makeLocalGet(0, Type::i32), + builder.makeConst(Literal(int32_t(1)))); + auto* right = builder.makeBinary(AddInt32, + builder.makeLocalGet(1, Type::i32), + builder.makeConst(Literal(int32_t(2)))); + auto* expr = builder.makeBinary(AndInt32, left, right); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_TRUE(parsed.hasUnknown); + ASSERT_EQ(parsed.size(), 1); + EXPECT_EQ(parsed[0], + (LocalConstraint{0, Constraint{Eq, {Literal(int32_t(1))}}})); + } + + // 5. AND with known constraint and unhandled unary op (e.g. Clz): + { + auto* left = builder.makeBinary(EqInt32, + builder.makeLocalGet(0, Type::i32), + builder.makeConst(Literal(int32_t(1)))); + auto* right = + builder.makeUnary(ClzInt32, builder.makeLocalGet(1, Type::i32)); + auto* expr = builder.makeBinary(AndInt32, left, right); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_TRUE(parsed.hasUnknown); + ASSERT_EQ(parsed.size(), 1); + EXPECT_EQ(parsed[0], + (LocalConstraint{0, Constraint{Eq, {Literal(int32_t(1))}}})); + } + + // 6. AND with known constraint and unhandled eqz argument: + { + auto* left = builder.makeBinary(EqInt32, + builder.makeLocalGet(0, Type::i32), + builder.makeConst(Literal(int32_t(1)))); + auto* right = + builder.makeUnary(EqZInt32, builder.makeCall("foo", {}, Type::i32)); + auto* expr = builder.makeBinary(AndInt32, left, right); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_TRUE(parsed.hasUnknown); + ASSERT_EQ(parsed.size(), 1); + EXPECT_EQ(parsed[0], + (LocalConstraint{0, Constraint{Eq, {Literal(int32_t(1))}}})); + } + + // 7. AND with two unknowns: + { + auto* left = builder.makeCall("foo", {}, Type::i32); + auto* right = builder.makeCall("bar", {}, Type::i32); + auto* expr = builder.makeBinary(AndInt32, left, right); + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_TRUE(parsed.hasUnknown); + EXPECT_TRUE(parsed.empty()); + } +} + +TEST(ConstraintTest, ParseOtherUnknowns) { + Module wasm; + Builder builder(wasm); + + // General expression types not handled by parse: + for (Expression* expr : { + (Expression*)builder.makeCall("foo", {}, Type::i32), + (Expression*)builder.makeConst(Literal(int32_t(42))), + (Expression*)builder.makeLocalGet(0, Type::i32), + (Expression*)builder.makeNop(), + (Expression*)builder.makeBlock(), + }) { + auto parsed = ParsedAndedConstraints::parse(expr); + EXPECT_TRUE(parsed.empty()); + EXPECT_TRUE(parsed.hasUnknown); + } + + // Contrast parse vs parseCondition for a bare local.get: + // parse treats it as an unhandled expression (unknown), while + // parseCondition recognizes it as x != 0 in a condition context. + auto* get = builder.makeLocalGet(0, Type::i32); + auto parsedAsExpr = ParsedAndedConstraints::parse(get); + EXPECT_TRUE(parsedAsExpr.empty()); + EXPECT_TRUE(parsedAsExpr.hasUnknown); + + auto parsedAsCondition = ParsedAndedConstraints::parseCondition(get); + EXPECT_FALSE(parsedAsCondition.hasUnknown); + ASSERT_EQ(parsedAsCondition.size(), 1); + EXPECT_EQ(parsedAsCondition[0], + (LocalConstraint{0, Constraint{Ne, {Literal(int32_t(0))}}})); +} diff --git a/test/lit/passes/constraint-analysis.wast b/test/lit/passes/constraint-analysis.wast index d5590395f5b..c180cec1d6d 100644 --- a/test/lit/passes/constraint-analysis.wast +++ b/test/lit/passes/constraint-analysis.wast @@ -13,6 +13,10 @@ ;; OPTIN: (type $array (array (mut i32))) (type $array (array (mut i32))) + ;; CHECK: (import "a" "b" (func $import (type $3) (result i32))) + ;; OPTIN: (import "a" "b" (func $import (type $3) (result i32))) + (import "a" "b" (func $import (result i32))) + ;; CHECK: (func $simple (type $1) ;; CHECK-NEXT: (local $x i32) ;; CHECK-NEXT: (local.set $x @@ -1731,7 +1735,7 @@ ) ) - ;; CHECK: (func $br_on_null (type $5) (param $param anyref) + ;; CHECK: (func $br_on_null (type $6) (param $param anyref) ;; CHECK-NEXT: (block $block ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (ref.is_null @@ -1752,7 +1756,7 @@ ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; OPTIN: (func $br_on_null (type $5) (param $param anyref) + ;; OPTIN: (func $br_on_null (type $6) (param $param anyref) ;; OPTIN-NEXT: (block $block ;; OPTIN-NEXT: (drop ;; OPTIN-NEXT: (ref.is_null @@ -1802,7 +1806,7 @@ ) ) - ;; CHECK: (func $br_on_non_null (type $5) (param $param anyref) + ;; CHECK: (func $br_on_non_null (type $6) (param $param anyref) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block $block (result (ref any)) ;; CHECK-NEXT: (drop @@ -1823,7 +1827,7 @@ ;; CHECK-NEXT: (i32.const 0) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; OPTIN: (func $br_on_non_null (type $5) (param $param anyref) + ;; OPTIN: (func $br_on_non_null (type $6) (param $param anyref) ;; OPTIN-NEXT: (drop ;; OPTIN-NEXT: (block $block (result (ref any)) ;; OPTIN-NEXT: (drop @@ -2476,7 +2480,7 @@ ) ) - ;; CHECK: (func $local-changes (type $3) (param $x i32) (param $y i32) (param $z i32) + ;; CHECK: (func $local-changes (type $4) (param $x i32) (param $y i32) (param $z i32) ;; CHECK-NEXT: (local.set $x ;; CHECK-NEXT: (local.get $y) ;; CHECK-NEXT: ) @@ -2532,7 +2536,7 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; OPTIN: (func $local-changes (type $3) (param $x i32) (param $y i32) (param $z i32) + ;; OPTIN: (func $local-changes (type $4) (param $x i32) (param $y i32) (param $z i32) ;; OPTIN-NEXT: (local.set $x ;; OPTIN-NEXT: (local.get $y) ;; OPTIN-NEXT: ) @@ -2662,7 +2666,7 @@ ) ) - ;; CHECK: (func $local-changes-2 (type $3) (param $x i32) (param $y i32) (param $z i32) + ;; CHECK: (func $local-changes-2 (type $4) (param $x i32) (param $y i32) (param $z i32) ;; CHECK-NEXT: (local.set $x ;; CHECK-NEXT: (local.get $y) ;; CHECK-NEXT: ) @@ -2718,7 +2722,7 @@ ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; OPTIN: (func $local-changes-2 (type $3) (param $x i32) (param $y i32) (param $z i32) + ;; OPTIN: (func $local-changes-2 (type $4) (param $x i32) (param $y i32) (param $z i32) ;; OPTIN-NEXT: (local.set $x ;; OPTIN-NEXT: (local.get $y) ;; OPTIN-NEXT: ) @@ -3052,7 +3056,7 @@ ) ) - ;; CHECK: (func $local-changes-ne (type $3) (param $x i32) (param $y i32) (param $z i32) + ;; CHECK: (func $local-changes-ne (type $4) (param $x i32) (param $y i32) (param $z i32) ;; CHECK-NEXT: (if ;; CHECK-NEXT: (i32.ne ;; CHECK-NEXT: (local.get $x) @@ -3104,7 +3108,7 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; OPTIN: (func $local-changes-ne (type $3) (param $x i32) (param $y i32) (param $z i32) + ;; OPTIN: (func $local-changes-ne (type $4) (param $x i32) (param $y i32) (param $z i32) ;; OPTIN-NEXT: (if ;; OPTIN-NEXT: (i32.ne ;; OPTIN-NEXT: (local.get $x) @@ -4457,7 +4461,7 @@ ) ) - ;; CHECK: (func $flipped-contradiction (type $6) (result i32) + ;; CHECK: (func $flipped-contradiction (type $3) (result i32) ;; CHECK-NEXT: (local $x i32) ;; CHECK-NEXT: (loop $loop ;; CHECK-NEXT: (br_if $loop @@ -4469,7 +4473,7 @@ ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; OPTIN: (func $flipped-contradiction (type $6) (result i32) + ;; OPTIN: (func $flipped-contradiction (type $3) (result i32) ;; OPTIN-NEXT: (local $x i32) ;; OPTIN-NEXT: (loop $loop ;; OPTIN-NEXT: (br_if $loop @@ -4503,7 +4507,7 @@ ) ) - ;; CHECK: (func $flipped-contradiction-no (type $6) (result i32) + ;; CHECK: (func $flipped-contradiction-no (type $3) (result i32) ;; CHECK-NEXT: (local $x i32) ;; CHECK-NEXT: (loop $loop (result i32) ;; CHECK-NEXT: (br_if $loop @@ -4517,7 +4521,7 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; OPTIN: (func $flipped-contradiction-no (type $6) (result i32) + ;; OPTIN: (func $flipped-contradiction-no (type $3) (result i32) ;; OPTIN-NEXT: (local $x i32) ;; OPTIN-NEXT: (loop $loop (result i32) ;; OPTIN-NEXT: (br_if $loop @@ -5025,4 +5029,889 @@ ) ) ) + + ;; CHECK: (func $eqz_eqz (type $0) (param $a i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eqz + ;; CHECK-NEXT: (i32.eqz + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPTIN: (func $eqz_eqz (type $0) (param $a i32) + ;; OPTIN-NEXT: (if + ;; OPTIN-NEXT: (local.get $a) + ;; OPTIN-NEXT: (then + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.const 1) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + (func $eqz_eqz (param $a i32) + (if + ;; !!a => a is not 0. + (i32.eqz + (i32.eqz + (local.get $a) + ) + ) + (then + (drop + ;; This is true. + (i32.ne + (local.get $a) + (i32.const 0) + ) + ) + ) + ) + ) + + ;; CHECK: (func $pair (type $2) (param $a i32) (param $b i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.and + ;; CHECK-NEXT: (i32.eqz + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $b) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; 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.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $b) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPTIN: (func $pair (type $2) (param $a i32) (param $b i32) + ;; OPTIN-NEXT: (if + ;; OPTIN-NEXT: (i32.and + ;; OPTIN-NEXT: (i32.eqz + ;; OPTIN-NEXT: (local.get $a) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (i32.eq + ;; OPTIN-NEXT: (local.get $b) + ;; OPTIN-NEXT: (i32.const 42) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (then + ;; 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.const 0) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (else + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.eqz + ;; OPTIN-NEXT: (local.get $a) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.eq + ;; OPTIN-NEXT: (local.get $b) + ;; OPTIN-NEXT: (i32.const 42) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.eq + ;; OPTIN-NEXT: (local.get $a) + ;; OPTIN-NEXT: (i32.const 42) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.const 42) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + (func $pair (param $a i32) (param $b i32) + ;; Two constraints in one if condition. + (if + (i32.and + ;; a == 0 + (i32.eqz + (local.get $a) + ) + ;; b == 42 + (i32.eq + (local.get $b) + (i32.const 42) + ) + ) + (then + ;; These are all true. + (drop + (i32.eq + (local.get $a) + (i32.const 0) + ) + ) + (drop + (i32.eq + (local.get $b) + (i32.const 42) + ) + ) + ;; This is not. + (drop + (i32.eq + (local.get $a) + (i32.const 42) + ) + ) + ) + (else + ;; The same expressions as in the (then ..). Here, at least one must be + ;; false, not not necessarily all of them, so we infer nothing. + (drop + (i32.eq + (local.get $a) + (i32.const 0) + ) + ) + (drop + (i32.eq + (local.get $b) + (i32.const 42) + ) + ) + (drop + (i32.eq + (local.get $a) + (i32.const 42) + ) + ) + ;; A silly extra instruction to stop optimize-instructions from + ;; folding the if-else arms. + (drop + (i32.const 42) + ) + ) + ) + ) + + ;; CHECK: (func $pair-same-local (type $0) (param $a i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.and + ;; CHECK-NEXT: (i32.gt_u + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.gt_u + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: (i32.const 1337) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; 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 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPTIN: (func $pair-same-local (type $0) (param $a i32) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (if (result i32) + ;; OPTIN-NEXT: (i32.and + ;; OPTIN-NEXT: (i32.gt_u + ;; OPTIN-NEXT: (local.get $a) + ;; OPTIN-NEXT: (i32.const 42) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (i32.gt_u + ;; OPTIN-NEXT: (local.get $a) + ;; OPTIN-NEXT: (i32.const 1337) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (then + ;; OPTIN-NEXT: (i32.const 1) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (else + ;; OPTIN-NEXT: (i32.const 1) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + (func $pair-same-local (param $a i32) + ;; Similar to above, but now the two ANDed conditions are about the same + ;; local + (if + (i32.and + ;; a > 42 + (i32.gt_u + (local.get $a) + (i32.const 42) + ) + ;; a > 1337 + (i32.gt_u + (local.get $a) + (i32.const 1337) + ) + ) + (then + ;; This is true. + (drop + (i32.gt_u + (local.get $a) + (i32.const 1337) + ) + ) + ) + (else + ;; The negation of a > 42 && a > 1337 is a <= 42 || a <= 1337, so we + ;; can infer a <= 1337. + (drop + (i32.le_u + (local.get $a) + (i32.const 1337) + ) + ) + ) + ) + ) + + ;; CHECK: (func $pair-same-local-bad-or (type $0) (param $a i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.and + ;; CHECK-NEXT: (i32.ne + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.ne + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: (i32.const 1337) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; 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.ne + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: (i32.const 500) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: (i32.const 1337) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: (i32.const 500) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPTIN: (func $pair-same-local-bad-or (type $0) (param $a i32) + ;; OPTIN-NEXT: (if + ;; OPTIN-NEXT: (i32.and + ;; OPTIN-NEXT: (i32.ne + ;; OPTIN-NEXT: (local.get $a) + ;; OPTIN-NEXT: (i32.const 42) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (i32.ne + ;; OPTIN-NEXT: (local.get $a) + ;; OPTIN-NEXT: (i32.const 1337) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (then + ;; 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.ne + ;; OPTIN-NEXT: (local.get $a) + ;; OPTIN-NEXT: (i32.const 500) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (else + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.eq + ;; OPTIN-NEXT: (local.get $a) + ;; OPTIN-NEXT: (i32.const 42) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.eq + ;; OPTIN-NEXT: (local.get $a) + ;; OPTIN-NEXT: (i32.const 1337) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.eq + ;; OPTIN-NEXT: (local.get $a) + ;; OPTIN-NEXT: (i32.const 500) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + (func $pair-same-local-bad-or (param $a i32) + ;; As above, but the OR we do for the else fails. + (if + (i32.and + ;; a != 42 && a != 1337 + (i32.ne + (local.get $a) + (i32.const 42) + ) + (i32.ne + (local.get $a) + (i32.const 1337) + ) + ) + (then + ;; These are true. + (drop + (i32.ne + (local.get $a) + (i32.const 42) + ) + ) + (drop + (i32.ne + (local.get $a) + (i32.const 1337) + ) + ) + ;; This is unknown. + (drop + (i32.ne + (local.get $a) + (i32.const 500) + ) + ) + ) + (else + ;; ORing a == 42 || a == 1337, we get nothing useful, and can prove + ;; nothing. TODO: we could infer a span here + (drop + (i32.eq + (local.get $a) + (i32.const 42) + ) + ) + (drop + (i32.eq + (local.get $a) + (i32.const 1337) + ) + ) + (drop + (i32.eq + (local.get $a) + (i32.const 500) + ) + ) + ) + ) + ) + + ;; CHECK: (func $pair-partial (type $0) (param $a i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.and + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $import) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPTIN: (func $pair-partial (type $0) (param $a i32) + ;; OPTIN-NEXT: (if + ;; OPTIN-NEXT: (i32.and + ;; OPTIN-NEXT: (call $import) + ;; OPTIN-NEXT: (i32.eq + ;; OPTIN-NEXT: (local.get $a) + ;; OPTIN-NEXT: (i32.const 42) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (then + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.const 1) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (else + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.eq + ;; OPTIN-NEXT: (local.get $a) + ;; OPTIN-NEXT: (i32.const 42) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.const 42) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + (func $pair-partial (param $a i32) + (if + (i32.and + ;; a == 42 + (i32.eq + (local.get $a) + (i32.const 42) + ) + ;; ANDed with something we know nothing about + (call $import) + ) + (then + ;; a must be 42, to get here. + (drop + (i32.eq + (local.get $a) + (i32.const 42) + ) + ) + ) + (else + ;; But we don't know anything about a here: we get to this place when + ;; a != 42 OR the unknown was 0, so a could be anything. + (drop + (i32.eq + (local.get $a) + (i32.const 42) + ) + ) + ;; A silly extra instruction to stop optimize-instructions from + ;; folding the if-else arms. + (drop + (i32.const 42) + ) + ) + ) + ) + + ;; CHECK: (func $several (type $11) (param $a i32) (param $b i32) (param $c i32) (param $d i32) (param $e i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.and + ;; CHECK-NEXT: (i32.and + ;; CHECK-NEXT: (i32.eqz + ;; CHECK-NEXT: (i32.eqz + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $b) + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.and + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $d) + ;; CHECK-NEXT: (i32.const 20) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $e) + ;; CHECK-NEXT: (i32.const 30) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; 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.const 1) + ;; 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.eq + ;; CHECK-NEXT: (local.get $c) + ;; CHECK-NEXT: (i32.const 40) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPTIN: (func $several (type $11) (param $a i32) (param $b i32) (param $c i32) (param $d i32) (param $e i32) + ;; OPTIN-NEXT: (if + ;; OPTIN-NEXT: (i32.and + ;; OPTIN-NEXT: (i32.and + ;; OPTIN-NEXT: (i32.eq + ;; OPTIN-NEXT: (local.get $b) + ;; OPTIN-NEXT: (i32.const 10) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (i32.ne + ;; OPTIN-NEXT: (local.get $a) + ;; OPTIN-NEXT: (i32.const 0) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (i32.and + ;; OPTIN-NEXT: (i32.eq + ;; OPTIN-NEXT: (local.get $d) + ;; OPTIN-NEXT: (i32.const 20) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (i32.eq + ;; OPTIN-NEXT: (local.get $e) + ;; OPTIN-NEXT: (i32.const 30) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (then + ;; 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.const 1) + ;; 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.eq + ;; OPTIN-NEXT: (local.get $c) + ;; OPTIN-NEXT: (i32.const 40) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + (func $several (param $a i32) (param $b i32) (param $c i32) (param $d i32) (param $e i32) + ;; Four constraints in one if condition. + (if + (i32.and + (i32.and + ;; !!a => a is not 0. + (i32.eqz + (i32.eqz + (local.get $a) + ) + ) + ;; b == 10 + (i32.eq + (local.get $b) + (i32.const 10) + ) + ) + (i32.and + ;; d == 20 + (i32.eq + (local.get $d) + (i32.const 20) + ) + ;; e == 30 + (i32.eq + (local.get $e) + (i32.const 30) + ) + ) + ) + (then + ;; These are all true. + (drop + (i32.ne + (local.get $a) + (i32.const 0) + ) + ) + (drop + (i32.eq + (local.get $b) + (i32.const 10) + ) + ) + (drop + (i32.eq + (local.get $d) + (i32.const 20) + ) + ) + (drop + (i32.eq + (local.get $e) + (i32.const 30) + ) + ) + ;; This is false. + (drop + (i32.eq + (local.get $a) + (i32.const 0) + ) + ) + ;; This local is unknown. + (drop + (i32.eq + (local.get $c) + (i32.const 40) + ) + ) + ) + ) + ) + + ;; CHECK: (func $three.contradiction.middle (type $0) (param $x i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.and + ;; CHECK-NEXT: (i32.eqz + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.and + ;; CHECK-NEXT: (i32.lt_s + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.ge_s + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPTIN: (func $three.contradiction.middle (type $0) (param $x i32) + ;; OPTIN-NEXT: (if + ;; OPTIN-NEXT: (i32.and + ;; OPTIN-NEXT: (i32.eqz + ;; OPTIN-NEXT: (local.get $x) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (i32.and + ;; OPTIN-NEXT: (i32.lt_s + ;; OPTIN-NEXT: (local.get $x) + ;; OPTIN-NEXT: (i32.const 0) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (i32.ge_s + ;; OPTIN-NEXT: (local.get $x) + ;; OPTIN-NEXT: (i32.const 0) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (then + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (unreachable) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + (func $three.contradiction.middle (param $x i32) + ;; First we set x == 0, then x < 0, then x >= 0. The first two are a + ;; contradiction. We should not error (after a contradiction, we should not + ;; apply further constraints), and can optimize away the if body. + (if + (i32.and + (i32.eqz + (local.get $x) + ) + (i32.and + (i32.lt_s + (local.get $x) + (i32.const 0) + ) + (i32.ge_s + (local.get $x) + (i32.const 0) + ) + ) + ) + (then + (drop + (i32.eqz + (local.get $x) + ) + ) + ) + ) + ) + + ;; CHECK: (func $br_if_and (type $0) (param $param i32) + ;; CHECK-NEXT: (block $block + ;; CHECK-NEXT: (br_if $block + ;; CHECK-NEXT: (i32.and + ;; CHECK-NEXT: (i32.ne + ;; CHECK-NEXT: (local.get $param) + ;; CHECK-NEXT: (i32.const 10) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.ne + ;; CHECK-NEXT: (local.get $param) + ;; CHECK-NEXT: (i32.const 20) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.and + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.ne + ;; CHECK-NEXT: (local.get $param) + ;; CHECK-NEXT: (i32.const 30) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; OPTIN: (func $br_if_and (type $0) (param $param i32) + ;; OPTIN-NEXT: (block $block + ;; OPTIN-NEXT: (br_if $block + ;; OPTIN-NEXT: (i32.and + ;; OPTIN-NEXT: (i32.ne + ;; OPTIN-NEXT: (local.get $param) + ;; OPTIN-NEXT: (i32.const 10) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (i32.ne + ;; OPTIN-NEXT: (local.get $param) + ;; OPTIN-NEXT: (i32.const 20) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (return) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.and + ;; OPTIN-NEXT: (i32.const 1) + ;; OPTIN-NEXT: (i32.const 1) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: (drop + ;; OPTIN-NEXT: (i32.ne + ;; OPTIN-NEXT: (local.get $param) + ;; OPTIN-NEXT: (i32.const 30) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + ;; OPTIN-NEXT: ) + (func $br_if_and (param $param i32) + ;; An AND in a br_if condition. + (block $block + (br_if $block + (i32.and + (i32.ne + (local.get $param) + (i32.const 10) + ) + (i32.ne + (local.get $param) + (i32.const 20) + ) + ) + ) + (return) + ) + ;; If we get here, param != 10 && param != 20. We optimize each arm of the + ;; AND here. + (drop + (i32.and + (i32.ne + (local.get $param) + (i32.const 10) + ) + (i32.ne + (local.get $param) + (i32.const 20) + ) + ) + ) + ;; This one we don't know. + (drop + (i32.ne + (local.get $param) + (i32.const 30) + ) + ) + ) )