Add a ConstraintAnalysis pass (#8853)

This simply parses IR into constraints, flows them around, and sees
if we can prove things.

This is a minimal version, without conditional propagation etc.
diff --git a/src/ir/constraint.cpp b/src/ir/constraint.cpp
index 57420a1..32d6504 100644
--- a/src/ir/constraint.cpp
+++ b/src/ir/constraint.cpp
@@ -77,6 +77,12 @@
 } // anonymous namespace
 
 Result AndedConstraintSet::proves(const Constraint& condition) const {
+  if (provesEverything()) {
+    return True;
+  }
+  // Note we do not need to handle the provesNothing case in a special way: the
+  // loop below finds nothing.
+
   // Sometimes a single constraint is enough to determine the condition.
   for (auto& c : *this) {
     auto result = provesPair(c, condition);
@@ -92,11 +98,15 @@
 }
 
 Result AndedConstraintSet::proves(const AndedConstraintSet& other) const {
-  if (other.empty()) {
-    // The empty set of constraints is always true.
+  if (provesEverything()) {
     return True;
   }
 
+  if (other.provesEverything()) {
+    // We are not a contradiction, but other is, so we prove it false.
+    return False;
+  }
+
   bool hasUnknown = false;
 
   for (auto& c : other) {
@@ -114,6 +124,17 @@
 }
 
 void AndedConstraintSet::approximateAnd(const Constraint& c) {
+  if (provesEverything()) {
+    // Nothing to add.
+    return;
+  }
+
+  if (proves(c) == False) {
+    // We are now a contradiction.
+    isContradiction = true;
+    return;
+  }
+
   if (size() < MaxConstraints) {
     push_back(c);
     return;
@@ -125,13 +146,12 @@
 }
 
 void AndedConstraintSet::approximateOr(const AndedConstraintSet& other) {
-  // If one is empty (no constraints, everything is true, and we can prove
-  // nothing useful) then it does not add anything to the other.
-  if (empty()) {
+  // If one proves everything, the only thing that matters is the other.
+  if (provesEverything()) {
     *this = other;
     return;
   }
-  if (other.empty()) {
+  if (other.provesEverything()) {
     return;
   }
 
@@ -153,4 +173,112 @@
   clear();
 }
 
+std::optional<LocalConstraint> LocalConstraint::parse(Expression* curr) {
+  auto parseEqZArgument =
+    [&](Expression* value) -> std::optional<LocalConstraint> {
+    if (auto* get = value->dynCast<LocalGet>()) {
+      // Canonicalize EqZ to Eq of 0.
+      auto value = Literal::makeZero(get->type);
+      return LocalConstraint{get->index, Constraint{Abstract::Eq, {value}}};
+    }
+    // TODO: Recursively parse and reverse a constraint
+    return {};
+  };
+
+  if (auto* unary = curr->dynCast<Unary>()) {
+    if (Abstract::getUnary(unary->type, Abstract::EqZ) == unary->op) {
+      return parseEqZArgument(unary->value);
+    }
+    return {};
+  }
+
+  if (auto* refIsNull = curr->dynCast<RefIsNull>()) {
+    return parseEqZArgument(refIsNull->value);
+  }
+
+  // Parse a get or a constant.
+  auto parseTerm = [&](Expression* expr) -> std::optional<Term> {
+    if (auto* get = expr->dynCast<LocalGet>()) {
+      return Term{get->index};
+    }
+    if (Properties::isSingleConstantExpression(expr)) {
+      return Term{Properties::getLiteral(expr)};
+    }
+    return {};
+  };
+
+  auto parseBinaryArguments =
+    [&](Abstract::Op op,
+        Expression* left,
+        Expression* right) -> std::optional<LocalConstraint> {
+    // The left must be a get.
+    if (auto* get = left->dynCast<LocalGet>()) {
+      // The right can be any term.
+      if (auto value = parseTerm(right)) {
+        return LocalConstraint{get->index, Constraint{op, *value}};
+      }
+    }
+    return {};
+  };
+
+  if (auto* binary = curr->dynCast<Binary>()) {
+    // The operation must be one we recognize.
+    for (auto op : {Abstract::Eq, Abstract::Ne}) {
+      if (Abstract::getBinary(binary->type, op) == binary->op) {
+        return parseBinaryArguments(op, binary->left, binary->right);
+      }
+    }
+    return {};
+  }
+
+  if (auto* refEq = curr->dynCast<RefEq>()) {
+    return parseBinaryArguments(Abstract::Eq, refEq->left, refEq->right);
+  }
+
+  return {};
+}
+
+void LocalConstraintMap::approximateOr(const LocalConstraintMap& other) {
+  // Remove things only in us.
+  std::erase_if(*this, [&](const auto& item) {
+    const auto& [local, constraints] = item;
+    return !other.contains(local);
+  });
+
+  for (auto& [local, constraints] : other) {
+    (*this)[local].approximateOr(constraints);
+  }
+}
+
+std::ostream& operator<<(std::ostream& o, const Constraint& constraint) {
+  o << "Constraint{" << /*constraint.op <<*/ ", ";
+  if (auto* c = std::get_if<Literal>(&constraint.term)) {
+    o << *c;
+  } else if (auto* i = std::get_if<Index>(&constraint.term)) {
+    o << "Index(" << *i << ')';
+  }
+  o << '}';
+  return o;
+}
+
+std::ostream& operator<<(std::ostream& o,
+                         const AndedConstraintSet& constraints) {
+  if (constraints.provesEverything()) {
+    o << "AndedConstraintSet(contradiction)";
+    return o;
+  }
+  o << "AndedConstraintSet{";
+  bool first = true;
+  for (auto& constraint : constraints) {
+    if (first) {
+      first = false;
+    } else {
+      o << ", ";
+    }
+    o << constraint;
+  }
+  o << '}';
+  return o;
+}
+
 } // namespace wasm::constraint
diff --git a/src/ir/constraint.h b/src/ir/constraint.h
index 4a689ce..cf9da48 100644
--- a/src/ir/constraint.h
+++ b/src/ir/constraint.h
@@ -60,6 +60,36 @@
 // about, which looks like a local, but it could be a global or a struct field
 // or anything else in general.
 struct AndedConstraintSet : inplace_vector<Constraint, MaxConstraints> {
+  // We could represent a contradiction using two constraints that contradict
+  // each other (== 0 && != 0), but for simplicity we mark this explicitly.
+  //
+  // A contradiction is the default value here, because it represents
+  // unreachable code: if (x == 0 && x != 0) { .. unreachable ..}. That is, we
+  // assume we represent the constraints in code that has not been reached,
+  // until something changes.
+  bool isContradiction = true;
+
+  // Proving everything (even contradictions) is equivalent to being a
+  // contradiction. (This and provesNothing can be seen as the top/bottom of a
+  // poset, if one wants to think of things that way.)
+  bool provesEverything() const { return isContradiction; }
+
+  void setProvesEverything() {
+    clear();
+    isContradiction = true;
+    assert(provesEverything());
+  }
+
+  // An empty set of contradictions means we know nothing, and so anything is
+  // possible, and we can prove nothing.
+  bool provesNothing() const { return empty(); }
+
+  void setProvesNothing() {
+    clear();
+    isContradiction = false;
+    assert(provesNothing());
+  }
+
   // Check a condition against this set, that is, whether the existing
   // constraints prove that it must be true, false, or unknown: whether
   //
@@ -119,8 +149,44 @@
   //
   // If we become too imprecise, we lose the ability to imply anything useful.
   void approximateOr(const AndedConstraintSet& other);
+
+  // Set a constraint, replacing all previous state.
+  void set(const Constraint& c) {
+    setProvesNothing();
+    push_back(c);
+  }
 };
 
+// A local plus a constraint on it.
+struct LocalConstraint {
+  Index local;
+  Constraint constraint;
+
+  // Try to parse BinaryenIR into a local to which a constraint is applied. For
+  // example
+  //
+  //   (i32.eq (local.get $r) (i32.const 10))
+  //
+  // parses into
+  //
+  //   LocalConstraint($r, { x == 10 })
+  //
+  static std::optional<LocalConstraint> parse(Expression* curr);
+};
+
+// A map of locals and their constraints.
+struct LocalConstraintMap
+  : public std::unordered_map<Index, AndedConstraintSet> {
+  // Perform an OR as above. When a local only appears in one map, we treat it
+  // as if it contains a contradiction there, that is, as if the code is
+  // unreachable.
+  void approximateOr(const LocalConstraintMap& other);
+};
+
+std::ostream& operator<<(std::ostream& o, const Constraint& constraint);
+std::ostream& operator<<(std::ostream& o,
+                         const AndedConstraintSet& constraints);
+
 } // namespace wasm::constraint
 
 #endif // wasm_ir_constraint_h
diff --git a/src/passes/CMakeLists.txt b/src/passes/CMakeLists.txt
index 2dfda96..6840771 100644
--- a/src/passes/CMakeLists.txt
+++ b/src/passes/CMakeLists.txt
@@ -26,6 +26,7 @@
   CodeFolding.cpp
   ConstantFieldPropagation.cpp
   ConstHoisting.cpp
+  ConstraintAnalysis.cpp
   DataFlowOpts.cpp
   DeadArgumentElimination.cpp
   DeadArgumentElimination2.cpp
diff --git a/src/passes/ConstraintAnalysis.cpp b/src/passes/ConstraintAnalysis.cpp
new file mode 100644
index 0000000..7b1a04d
--- /dev/null
+++ b/src/passes/ConstraintAnalysis.cpp
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2026 WebAssembly Community Group participants
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+//
+// Use mathematical constraint solving to optimize. For example:
+//
+//  if (x == 10) {
+//    assert(x != 0); // redundant and can be removed.
+//  }
+//
+
+#include "cfg/cfg-traversal.h"
+#include "ir/constraint.h"
+#include "ir/drop.h"
+#include "ir/literal-utils.h"
+#include "ir/local-graph.h"
+#include "ir/properties.h"
+#include "pass.h"
+#include "support/unique_deferring_queue.h"
+#include "support/utilities.h"
+#include "wasm-builder.h"
+#include "wasm.h"
+
+namespace wasm {
+
+using namespace wasm::constraint;
+
+namespace {
+
+// In each basic block we will store the relevant operations, which are all
+// local gets and sets, branches, and uses of them.
+struct Info {
+  std::vector<Expression**> actions;
+
+  // For each local index, we track the constraints we know about it. We only do
+  // so at the start of each block, which is enough for the analysis below.
+  LocalConstraintMap startConstraints;
+};
+
+struct ConstraintAnalysis
+  : public WalkerPass<
+      CFGWalker<ConstraintAnalysis, Visitor<ConstraintAnalysis>, Info>> {
+  bool isFunctionParallel() override { return true; }
+
+  // Locals are not modified here.
+  bool requiresNonNullableLocalFixups() override { return false; }
+
+  std::unique_ptr<Pass> create() override {
+    return std::make_unique<ConstraintAnalysis>();
+  }
+
+  // Branches outside of the function can be ignored, as we only look at local
+  // state in the function.
+  bool ignoreBranchesOutsideOfFunc = true;
+
+  // Store the actions we care about.
+  void addAction() {
+    if (currBasicBlock) {
+      currBasicBlock->contents.actions.push_back(getCurrentPointer());
+    }
+  }
+
+  void visitLocalSet(LocalSet* curr) { addAction(); }
+  void visitUnary(Unary* curr) { addAction(); }
+  void visitBinary(Binary* curr) { addAction(); }
+  void visitRefEq(RefEq* curr) { addAction(); }
+  void visitRefIsNull(RefIsNull* curr) { addAction(); }
+
+  void visitFunction(Function* curr) {
+    if (!entry) {
+      // Body is unreachable, no entry block.
+      return;
+    }
+    // TODO: optimize for speed, find relevant locals etc.
+    flow();
+    optimize();
+  }
+
+  // Flow infos around until we have inferred all we can about the constraints
+  // in each location.
+  void flow() {
+    // Start from the entry. That block has incoming values - defaults - for
+    // each var.
+    auto& entryConstraints = entry->contents.startConstraints;
+    auto* func = getFunction();
+    auto numLocals = func->getNumLocals();
+    for (Index i = 0; i < numLocals; i++) {
+      auto type = func->getLocalType(i);
+      // TODO: support tuples
+      if (func->isParam(i) || type.size() != 1) {
+        // We can't do anything in this case.
+        entryConstraints[i].setProvesNothing();
+      } else if (LiteralUtils::canMakeZero(type)) {
+        // We have a default value.
+        auto value = Literal::makeZero(type);
+        entryConstraints[i].set(Constraint{Abstract::Eq, {value}});
+      } else {
+        // Otherwise, this is non-nullable, and it is unreachable until set.
+        assert(type.isNonNullable());
+        entryConstraints[i].setProvesEverything();
+      }
+    }
+
+    // Starting from the entry, keep going while we find something new.
+    UniqueDeferredQueue<BasicBlock*> work;
+    work.push(entry);
+    while (!work.empty()) {
+      auto* block = work.pop();
+
+      // Start at the top of the block, then go through, applying things.
+      LocalConstraintMap constraints = block->contents.startConstraints;
+      for (auto** currp : block->contents.actions) {
+        applyToConstraints(*currp, constraints);
+      }
+
+      // We now know the values at the end of the block. Flow it onward, and
+      // where it causes changes, queue more work.
+      for (auto* out : block->out) {
+        auto& outStartConstraints = out->contents.startConstraints;
+        auto old = outStartConstraints;
+        outStartConstraints.approximateOr(constraints);
+        if (outStartConstraints != old) {
+          work.push(out);
+        }
+      }
+    }
+  }
+
+  // After inferring all we can, apply it to optimize the code.
+  void optimize() {
+    for (auto& block : basicBlocks) {
+      // Follow the general shape of flow(): we need to see what the state is
+      // at each intermediate point inside the block. (Flowing between blocks is
+      // of course not needed at this stage.)
+      auto& constraints = block->contents.startConstraints;
+      for (auto** currp : block->contents.actions) {
+        applyToConstraints(*currp, constraints);
+        optimizeExpression(currp, constraints);
+      }
+    }
+  }
+
+  // Given an expression and the constraints on it, optimize it.
+  void optimizeExpression(Expression** currp,
+                          const LocalConstraintMap& constraints) {
+    auto* curr = *currp;
+    auto parsed = LocalConstraint::parse(curr);
+    if (!parsed) {
+      return;
+    }
+
+    auto iter = constraints.find(parsed->local);
+    if (iter == constraints.end()) {
+      return;
+    }
+    auto& localConstraints = iter->second;
+    Result result = localConstraints.proves(parsed->constraint);
+    if (result == Unknown) {
+      // If we parsed something using two locals, like x != y, we can also look
+      // for the flipped condition among y's constraints TODO
+      return;
+    }
+
+    // We know the result!
+    auto& wasm = *getModule();
+    auto value =
+      LiteralUtils::makeFromInt32(result == True ? 1 : 0, curr->type, wasm);
+    *currp = getDroppedChildrenAndAppend(
+      curr, wasm, getPassOptions(), value, DropMode::IgnoreParentEffects);
+  }
+
+  // Given an expression, apply it to the constraints. For example, a local.set
+  // sets the value for that local.
+  void applyToConstraints(Expression* curr, LocalConstraintMap& constraints) {
+    if (auto* set = curr->dynCast<LocalSet>()) {
+      auto& localConstraints = constraints[set->index];
+      if (Properties::isSingleConstantExpression(set->value)) {
+        // We know this one constraint.
+        auto value = Properties::getLiteral(set->value);
+        localConstraints.set(Constraint{Abstract::Eq, {value}});
+      } else {
+        // We know and can prove nothing.
+        localConstraints.setProvesNothing();
+      }
+    }
+  }
+};
+
+} // anonymous namespace
+
+Pass* createConstraintAnalysisPass() { return new ConstraintAnalysis(); }
+
+} // namespace wasm
diff --git a/src/passes/pass.cpp b/src/passes/pass.cpp
index 672936f..06339ca 100644
--- a/src/passes/pass.cpp
+++ b/src/passes/pass.cpp
@@ -129,6 +129,9 @@
   registerPass("cfp-reftest",
                "propagate constant struct field values, using ref.test",
                createConstantFieldPropagationRefTestPass);
+  registerPass("constraint-analysis",
+               "finds and uses mathematical constraints on locals",
+               createConstraintAnalysisPass);
   registerPass(
     "dce", "removes unreachable code", createDeadCodeEliminationPass);
   registerPass("dealign",
diff --git a/src/passes/passes.h b/src/passes/passes.h
index caeaf20..3e28946 100644
--- a/src/passes/passes.h
+++ b/src/passes/passes.h
@@ -33,6 +33,7 @@
 Pass* createConstHoistingPass();
 Pass* createConstantFieldPropagationPass();
 Pass* createConstantFieldPropagationRefTestPass();
+Pass* createConstraintAnalysisPass();
 Pass* createDAEPass();
 Pass* createDAEOptimizingPass();
 Pass* createDAE2Pass();
diff --git a/test/gtest/constraint.cpp b/test/gtest/constraint.cpp
index a8bb66f..1ee15d4 100644
--- a/test/gtest/constraint.cpp
+++ b/test/gtest/constraint.cpp
@@ -7,15 +7,17 @@
 using namespace wasm::constraint;
 
 TEST(ConstraintTest, TestEq) {
-  // Sets start empty.
-  AndedConstraintSet s;
-  EXPECT_TRUE(s.empty());
-
   // x == 5 (we use "x" for the name of the thing being compared, in these
   // comments).
   Constraint c{Eq, {Literal(int32_t(5))}};
 
-  // We can't infer anything using an empty set.
+  // Sets start as proving anything, as representing unreachable code.
+  AndedConstraintSet s;
+  EXPECT_TRUE(s.provesEverything());
+  EXPECT_EQ(s.proves(c), True);
+
+  // We can't infer anything if told so.
+  s.setProvesNothing();
   EXPECT_EQ(s.proves(c), Unknown);
 
   // If we add it, then things check out: a thing always proves itself true.
@@ -23,6 +25,10 @@
   EXPECT_EQ(s.size(), 1);
   EXPECT_EQ(s.proves(c), True);
 
+  // Ditto using set();
+  s.set(c);
+  EXPECT_EQ(s.proves(c), True);
+
   // x == 10, a different number: we can infer false.
   EXPECT_EQ(s.proves(Constraint{Eq, {Literal(int32_t(10))}}), False);
 
@@ -37,7 +43,7 @@
   AndedConstraintSet s;
   // x != 5
   Constraint c{Ne, {Literal(int32_t(5))}};
-  s.approximateAnd(c);
+  s.set(c);
 
   // Checks out versus itself.
   EXPECT_EQ(s.proves(c), True);
@@ -57,7 +63,7 @@
   // x != 5 && x != 10
   Constraint c{Ne, {Literal(int32_t(5))}};
   Constraint d{Ne, {Literal(int32_t(10))}};
-  s.approximateAnd(c);
+  s.set(c);
   s.approximateAnd(d);
 
   // Each checks out versus itself.
@@ -87,27 +93,22 @@
   EXPECT_EQ(s.proves(s), True);
 
   // Ditto after adding something.
-  s.approximateAnd(c);
+  s.set(c);
   EXPECT_EQ(s.proves(s), True);
 
   // Another set, empty.
   AndedConstraintSet t;
 
-  // Any set always proves an empty set to be true.
-  EXPECT_EQ(s.proves(t), True);
-
   // Make both sets contain the same stuff.
-  t.approximateAnd(c);
+  t.set(c);
   EXPECT_EQ(s.proves(t), True);
 
   // Now t has *different* stuff, x == 10, which given s is false.
-  t.clear();
-  t.approximateAnd(Constraint{Eq, {Literal(int32_t(10))}});
+  t.set(Constraint{Eq, {Literal(int32_t(10))}});
   EXPECT_EQ(s.proves(t), False);
 
   // Same, with x != 10. Now we know it is true.
-  t.clear();
-  t.approximateAnd(Constraint{Ne, {Literal(int32_t(10))}});
+  t.set(Constraint{Ne, {Literal(int32_t(10))}});
   EXPECT_EQ(s.proves(t), True);
 
   // In reverse, we can infer nothing: knowing x != 10 does not say if x == 5.
@@ -118,36 +119,38 @@
   // x != 5
   // x != 10
   AndedConstraintSet s;
-  s.approximateAnd(Constraint{Ne, {Literal(int32_t(5))}});
+  s.set(Constraint{Ne, {Literal(int32_t(5))}});
   s.approximateAnd(Constraint{Ne, {Literal(int32_t(10))}});
 
   // x != 20, which is unknown by s.
   AndedConstraintSet t;
-  t.approximateAnd(Constraint{Ne, {Literal(int32_t(20))}});
+  t.set(Constraint{Ne, {Literal(int32_t(20))}});
   EXPECT_EQ(s.proves(t), Unknown);
 
   // Add x == 10, which is false by s, and so the whole thing is false.
-  t.approximateAnd(Constraint{Eq, {Literal(int32_t(10))}});
+  t.set(Constraint{Eq, {Literal(int32_t(10))}});
   EXPECT_EQ(s.proves(t), False);
 }
 
 TEST(ConstraintTest, TestOrTrivial) {
   // { x == 5 }
   AndedConstraintSet s;
-  s.approximateAnd(Constraint{Eq, {Literal(int32_t(5))}});
+  s.set(Constraint{Eq, {Literal(int32_t(5))}});
 
   // { }
   AndedConstraintSet empty;
+  empty.setProvesNothing();
 
-  // Anything ORed with the empty set is unchanged.
+  // Anything ORed with the empty set becomes the empty set: if one side can
+  // prove nothing, neither can the result.
   auto t = s;
   t.approximateOr(empty);
-  EXPECT_EQ(t, s);
+  EXPECT_EQ(t, empty);
 
   // Flipped.
   t = empty;
   t.approximateOr(s);
-  EXPECT_EQ(t, s);
+  EXPECT_EQ(t, empty);
 
   // ORing with oneself changes nothing
   t = s;
@@ -158,11 +161,11 @@
 TEST(ConstraintTest, TestOrImplies) {
   // { x == 5 }
   AndedConstraintSet s;
-  s.approximateAnd(Constraint{Eq, {Literal(int32_t(5))}});
+  s.set(Constraint{Eq, {Literal(int32_t(5))}});
 
   // { x != 10 }
   AndedConstraintSet t;
-  t.approximateAnd(Constraint{Ne, {Literal(int32_t(10))}});
+  t.set(Constraint{Ne, {Literal(int32_t(10))}});
 
   // ORing these leaves us with x != 10.
   auto u = s;
@@ -184,7 +187,7 @@
   Constraint not30{Ne, {Literal(int32_t(30))}};
 
   AndedConstraintSet s;
-  s.approximateAnd(not10);
+  s.set(not10);
   s.approximateAnd(not20);
   s.approximateAnd(not30);
 
diff --git a/test/lit/help/wasm-metadce.test b/test/lit/help/wasm-metadce.test
index f106846..f74d904 100644
--- a/test/lit/help/wasm-metadce.test
+++ b/test/lit/help/wasm-metadce.test
@@ -109,6 +109,9 @@
 ;; CHECK-NEXT:   --const-hoisting                              hoist repeated constants to a
 ;; CHECK-NEXT:                                                 local
 ;; CHECK-NEXT:
+;; CHECK-NEXT:   --constraint-analysis                         finds and uses mathematical
+;; CHECK-NEXT:                                                 constraints on locals
+;; CHECK-NEXT:
 ;; CHECK-NEXT:   --dae                                         removes arguments to calls in an
 ;; CHECK-NEXT:                                                 lto-like manner
 ;; CHECK-NEXT:
diff --git a/test/lit/help/wasm-opt.test b/test/lit/help/wasm-opt.test
index 903d613..e8c9ff0 100644
--- a/test/lit/help/wasm-opt.test
+++ b/test/lit/help/wasm-opt.test
@@ -145,6 +145,9 @@
 ;; CHECK-NEXT:   --const-hoisting                              hoist repeated constants to a
 ;; CHECK-NEXT:                                                 local
 ;; CHECK-NEXT:
+;; CHECK-NEXT:   --constraint-analysis                         finds and uses mathematical
+;; CHECK-NEXT:                                                 constraints on locals
+;; CHECK-NEXT:
 ;; CHECK-NEXT:   --dae                                         removes arguments to calls in an
 ;; CHECK-NEXT:                                                 lto-like manner
 ;; CHECK-NEXT:
diff --git a/test/lit/help/wasm2js.test b/test/lit/help/wasm2js.test
index acfc47b..9192c7e 100644
--- a/test/lit/help/wasm2js.test
+++ b/test/lit/help/wasm2js.test
@@ -73,6 +73,9 @@
 ;; CHECK-NEXT:   --const-hoisting                              hoist repeated constants to a
 ;; CHECK-NEXT:                                                 local
 ;; CHECK-NEXT:
+;; CHECK-NEXT:   --constraint-analysis                         finds and uses mathematical
+;; CHECK-NEXT:                                                 constraints on locals
+;; CHECK-NEXT:
 ;; CHECK-NEXT:   --dae                                         removes arguments to calls in an
 ;; CHECK-NEXT:                                                 lto-like manner
 ;; CHECK-NEXT:
diff --git a/test/lit/passes/constraint-analysis.wast b/test/lit/passes/constraint-analysis.wast
new file mode 100644
index 0000000..cac599c
--- /dev/null
+++ b/test/lit/passes/constraint-analysis.wast
@@ -0,0 +1,798 @@
+;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited.
+
+;; RUN: wasm-opt %s --constraint-analysis -all -S -o - | filecheck %s
+
+;; Also run after optimize-instructions, which canonicalizes the order of
+;; things like binary children. We want to see that this pass optimizes the
+;; IR that optimize-instructions emits.
+
+;; RUN: wasm-opt %s --optimize-instructions --constraint-analysis -all -S -o - | filecheck %s --check-prefix=OPTIN
+
+(module
+  ;; CHECK:      (func $simple (type $1)
+  ;; CHECK-NEXT:  (local $x i32)
+  ;; CHECK-NEXT:  (local.set $x
+  ;; CHECK-NEXT:   (i32.const 10)
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (drop
+  ;; CHECK-NEXT:   (i32.const 1)
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (drop
+  ;; CHECK-NEXT:   (i32.const 0)
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (drop
+  ;; CHECK-NEXT:   (i32.const 0)
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (drop
+  ;; CHECK-NEXT:   (i32.const 1)
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT: )
+  ;; OPTIN:      (func $simple (type $1)
+  ;; OPTIN-NEXT:  (local $x i32)
+  ;; OPTIN-NEXT:  (local.set $x
+  ;; OPTIN-NEXT:   (i32.const 10)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (drop
+  ;; OPTIN-NEXT:   (i32.const 1)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (drop
+  ;; OPTIN-NEXT:   (i32.const 0)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (drop
+  ;; OPTIN-NEXT:   (i32.const 0)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (drop
+  ;; OPTIN-NEXT:   (i32.const 1)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT: )
+  (func $simple
+    (local $x i32)
+    ;; Set x to 10, and then compare it to 10 and 20 using == and !=, all in a
+    ;; single basic block.
+    (local.set $x
+      (i32.const 10)
+    )
+    (drop
+      (i32.eq
+        (local.get $x)
+        (i32.const 10)
+      )
+    )
+    (drop
+      (i32.eq
+        (local.get $x)
+        (i32.const 20)
+      )
+    )
+    (drop
+      (i32.ne
+        (local.get $x)
+        (i32.const 10)
+      )
+    )
+    (drop
+      (i32.ne
+        (local.get $x)
+        (i32.const 20)
+      )
+    )
+  )
+
+  ;; CHECK:      (func $unknown (type $1)
+  ;; CHECK-NEXT:  (local $x i32)
+  ;; CHECK-NEXT:  (local.set $x
+  ;; CHECK-NEXT:   (i32.add
+  ;; CHECK-NEXT:    (i32.const 10)
+  ;; CHECK-NEXT:    (i32.const 20)
+  ;; CHECK-NEXT:   )
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (drop
+  ;; CHECK-NEXT:   (i32.eq
+  ;; CHECK-NEXT:    (local.get $x)
+  ;; CHECK-NEXT:    (i32.const 30)
+  ;; CHECK-NEXT:   )
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (local.set $x
+  ;; CHECK-NEXT:   (i32.div_s
+  ;; CHECK-NEXT:    (i32.const 1337)
+  ;; CHECK-NEXT:    (i32.const 42)
+  ;; CHECK-NEXT:   )
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (drop
+  ;; CHECK-NEXT:   (i32.eq
+  ;; CHECK-NEXT:    (local.get $x)
+  ;; CHECK-NEXT:    (i32.const 31)
+  ;; CHECK-NEXT:   )
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT: )
+  ;; OPTIN:      (func $unknown (type $1)
+  ;; OPTIN-NEXT:  (local $x i32)
+  ;; OPTIN-NEXT:  (local.set $x
+  ;; OPTIN-NEXT:   (i32.const 30)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (drop
+  ;; OPTIN-NEXT:   (i32.const 1)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (local.set $x
+  ;; OPTIN-NEXT:   (i32.div_u
+  ;; OPTIN-NEXT:    (i32.const 1337)
+  ;; OPTIN-NEXT:    (i32.const 42)
+  ;; OPTIN-NEXT:   )
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (drop
+  ;; OPTIN-NEXT:   (i32.eq
+  ;; OPTIN-NEXT:    (local.get $x)
+  ;; OPTIN-NEXT:    (i32.const 31)
+  ;; OPTIN-NEXT:   )
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT: )
+  (func $unknown
+    (local $x i32)
+    ;; Set x to an add. We can only optimize this if optimize-instructions first
+    ;; simplifies it to a constant.
+    (local.set $x
+      (i32.add
+        (i32.const 10)
+        (i32.const 20)
+      )
+    )
+    (drop
+      (i32.eq
+        (local.get $x)
+        (i32.const 30)
+      )
+    )
+    ;; When optimize-instructions does not help, we infer nothing.
+    (local.set $x
+      (i32.div_s
+        (i32.const 1337)
+        (i32.const 42)
+      )
+    )
+    (drop
+      (i32.eq
+        (local.get $x)
+        (i32.const 31)
+      )
+    )
+  )
+
+  ;; CHECK:      (func $multi-local (type $1)
+  ;; CHECK-NEXT:  (local $x i32)
+  ;; CHECK-NEXT:  (local $y i32)
+  ;; CHECK-NEXT:  (local.set $x
+  ;; CHECK-NEXT:   (i32.const 10)
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (local.set $y
+  ;; CHECK-NEXT:   (i32.const 20)
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (drop
+  ;; CHECK-NEXT:   (i32.const 1)
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (drop
+  ;; CHECK-NEXT:   (i32.const 1)
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (local.set $x
+  ;; CHECK-NEXT:   (i32.const 15)
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (drop
+  ;; CHECK-NEXT:   (i32.const 0)
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (drop
+  ;; CHECK-NEXT:   (i32.const 1)
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (drop
+  ;; CHECK-NEXT:   (i32.const 1)
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT: )
+  ;; OPTIN:      (func $multi-local (type $1)
+  ;; OPTIN-NEXT:  (local $x i32)
+  ;; OPTIN-NEXT:  (local $y i32)
+  ;; OPTIN-NEXT:  (local.set $x
+  ;; OPTIN-NEXT:   (i32.const 10)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (local.set $y
+  ;; OPTIN-NEXT:   (i32.const 20)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (drop
+  ;; OPTIN-NEXT:   (i32.const 1)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (drop
+  ;; OPTIN-NEXT:   (i32.const 1)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (local.set $x
+  ;; OPTIN-NEXT:   (i32.const 15)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (drop
+  ;; OPTIN-NEXT:   (i32.const 0)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (drop
+  ;; OPTIN-NEXT:   (i32.const 1)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (drop
+  ;; OPTIN-NEXT:   (i32.const 1)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT: )
+  (func $multi-local
+    (local $x i32)
+    (local $y i32)
+    ;; x is 10, y is 20
+    (local.set $x
+      (i32.const 10)
+    )
+    (local.set $y
+      (i32.const 20)
+    )
+    ;; Verify those values.
+    (drop
+      (i32.eq
+        (local.get $x)
+        (i32.const 10)
+      )
+    )
+    (drop
+      (i32.eq
+        (local.get $y)
+        (i32.const 20)
+      )
+    )
+    ;; Overwrite x to 15.
+    (local.set $x
+      (i32.const 15)
+    )
+    (drop
+      (i32.eq
+        (local.get $x)
+        (i32.const 10)
+      )
+    )
+    (drop
+      (i32.eq
+        (local.get $x)
+        (i32.const 15)
+      )
+    )
+    ;; y is unchanged.
+    (drop
+      (i32.eq
+        (local.get $y)
+        (i32.const 20)
+      )
+    )
+  )
+
+  ;; CHECK:      (func $multi-block (type $0) (param $param i32)
+  ;; CHECK-NEXT:  (local $x i32)
+  ;; CHECK-NEXT:  (local.set $x
+  ;; CHECK-NEXT:   (i32.const 10)
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (if
+  ;; CHECK-NEXT:   (local.get $param)
+  ;; CHECK-NEXT:   (then
+  ;; CHECK-NEXT:    (drop
+  ;; CHECK-NEXT:     (i32.const 1)
+  ;; CHECK-NEXT:    )
+  ;; CHECK-NEXT:   )
+  ;; CHECK-NEXT:   (else
+  ;; CHECK-NEXT:    (drop
+  ;; CHECK-NEXT:     (i32.const 0)
+  ;; CHECK-NEXT:    )
+  ;; CHECK-NEXT:   )
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (drop
+  ;; CHECK-NEXT:   (i32.const 1)
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT: )
+  ;; OPTIN:      (func $multi-block (type $0) (param $param i32)
+  ;; OPTIN-NEXT:  (local $x i32)
+  ;; OPTIN-NEXT:  (local.set $x
+  ;; OPTIN-NEXT:   (i32.const 10)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (drop
+  ;; OPTIN-NEXT:   (if (result i32)
+  ;; OPTIN-NEXT:    (local.get $param)
+  ;; OPTIN-NEXT:    (then
+  ;; OPTIN-NEXT:     (i32.const 1)
+  ;; OPTIN-NEXT:    )
+  ;; OPTIN-NEXT:    (else
+  ;; OPTIN-NEXT:     (i32.const 0)
+  ;; OPTIN-NEXT:    )
+  ;; OPTIN-NEXT:   )
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (drop
+  ;; OPTIN-NEXT:   (i32.const 1)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT: )
+  (func $multi-block (param $param i32)
+    (local $x i32)
+    (local.set $x
+      (i32.const 10)
+    )
+    ;; We can infer into these basic blocks.
+    (if
+      (local.get $param)
+      (then
+        (drop
+          (i32.eq
+            (local.get $x)
+            (i32.const 10)
+          )
+        )
+      )
+      (else
+        (drop
+          (i32.ne           ;; also test a not-equals here; result is 0
+            (local.get $x)
+            (i32.const 10)
+          )
+        )
+      )
+    )
+    ;; We can infer after the merge.
+    (drop
+      (i32.eq
+        (local.get $x)
+        (i32.const 10)
+      )
+    )
+  )
+
+  ;; CHECK:      (func $multi-block-split (type $0) (param $param i32)
+  ;; CHECK-NEXT:  (local $x i32)
+  ;; CHECK-NEXT:  (if
+  ;; CHECK-NEXT:   (local.get $param)
+  ;; CHECK-NEXT:   (then
+  ;; CHECK-NEXT:    (local.set $x
+  ;; CHECK-NEXT:     (i32.const 10)
+  ;; CHECK-NEXT:    )
+  ;; CHECK-NEXT:   )
+  ;; CHECK-NEXT:   (else
+  ;; CHECK-NEXT:    (local.set $x
+  ;; CHECK-NEXT:     (i32.const 20)
+  ;; CHECK-NEXT:    )
+  ;; CHECK-NEXT:   )
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (drop
+  ;; CHECK-NEXT:   (i32.eq
+  ;; CHECK-NEXT:    (local.get $x)
+  ;; CHECK-NEXT:    (i32.const 10)
+  ;; CHECK-NEXT:   )
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (drop
+  ;; CHECK-NEXT:   (i32.eq
+  ;; CHECK-NEXT:    (local.get $x)
+  ;; CHECK-NEXT:    (i32.const 20)
+  ;; CHECK-NEXT:   )
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT: )
+  ;; OPTIN:      (func $multi-block-split (type $0) (param $param i32)
+  ;; OPTIN-NEXT:  (local $x i32)
+  ;; OPTIN-NEXT:  (local.set $x
+  ;; OPTIN-NEXT:   (if (result i32)
+  ;; OPTIN-NEXT:    (local.get $param)
+  ;; OPTIN-NEXT:    (then
+  ;; OPTIN-NEXT:     (i32.const 10)
+  ;; OPTIN-NEXT:    )
+  ;; OPTIN-NEXT:    (else
+  ;; OPTIN-NEXT:     (i32.const 20)
+  ;; OPTIN-NEXT:    )
+  ;; OPTIN-NEXT:   )
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (drop
+  ;; OPTIN-NEXT:   (i32.eq
+  ;; OPTIN-NEXT:    (local.get $x)
+  ;; OPTIN-NEXT:    (i32.const 10)
+  ;; OPTIN-NEXT:   )
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (drop
+  ;; OPTIN-NEXT:   (i32.eq
+  ;; OPTIN-NEXT:    (local.get $x)
+  ;; OPTIN-NEXT:    (i32.const 20)
+  ;; OPTIN-NEXT:   )
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT: )
+  (func $multi-block-split (param $param i32)
+    (local $x i32)
+    (if
+      (local.get $param)
+      (then
+        (local.set $x
+          (i32.const 10)
+        )
+      )
+      (else
+        (local.set $x
+          (i32.const 20)
+        )
+      )
+    )
+    ;; x is either 10 or 20, but not known to be one or the other.
+    (drop
+      (i32.eq
+        (local.get $x)
+        (i32.const 10)
+      )
+    )
+    (drop
+      (i32.eq
+        (local.get $x)
+        (i32.const 20)
+      )
+    )
+    ;; TODO: test we can infer x >= 10 etc.
+  )
+
+  ;; CHECK:      (func $multi-block-split-2 (type $0) (param $param i32)
+  ;; CHECK-NEXT:  (local $x i32)
+  ;; CHECK-NEXT:  (local.set $x
+  ;; CHECK-NEXT:   (i32.const 10)
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (if
+  ;; CHECK-NEXT:   (local.get $param)
+  ;; CHECK-NEXT:   (then
+  ;; CHECK-NEXT:    (local.set $x
+  ;; CHECK-NEXT:     (i32.const 20)
+  ;; CHECK-NEXT:    )
+  ;; CHECK-NEXT:   )
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (drop
+  ;; CHECK-NEXT:   (i32.eq
+  ;; CHECK-NEXT:    (local.get $x)
+  ;; CHECK-NEXT:    (i32.const 10)
+  ;; CHECK-NEXT:   )
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (drop
+  ;; CHECK-NEXT:   (i32.eq
+  ;; CHECK-NEXT:    (local.get $x)
+  ;; CHECK-NEXT:    (i32.const 20)
+  ;; CHECK-NEXT:   )
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT: )
+  ;; OPTIN:      (func $multi-block-split-2 (type $0) (param $param i32)
+  ;; OPTIN-NEXT:  (local $x i32)
+  ;; OPTIN-NEXT:  (local.set $x
+  ;; OPTIN-NEXT:   (i32.const 10)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (if
+  ;; OPTIN-NEXT:   (local.get $param)
+  ;; OPTIN-NEXT:   (then
+  ;; OPTIN-NEXT:    (local.set $x
+  ;; OPTIN-NEXT:     (i32.const 20)
+  ;; OPTIN-NEXT:    )
+  ;; OPTIN-NEXT:   )
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (drop
+  ;; OPTIN-NEXT:   (i32.eq
+  ;; OPTIN-NEXT:    (local.get $x)
+  ;; OPTIN-NEXT:    (i32.const 10)
+  ;; OPTIN-NEXT:   )
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (drop
+  ;; OPTIN-NEXT:   (i32.eq
+  ;; OPTIN-NEXT:    (local.get $x)
+  ;; OPTIN-NEXT:    (i32.const 20)
+  ;; OPTIN-NEXT:   )
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT: )
+  (func $multi-block-split-2 (param $param i32)
+    (local $x i32)
+    ;; As above, but one if arm, and a set before the if.
+    (local.set $x
+      (i32.const 10)
+    )
+    (if
+      (local.get $param)
+      (then
+        (local.set $x
+          (i32.const 20)
+        )
+      )
+    )
+    ;; Again, we cannot infer.
+    (drop
+      (i32.eq
+        (local.get $x)
+        (i32.const 10)
+      )
+    )
+    (drop
+      (i32.eq
+        (local.get $x)
+        (i32.const 20)
+      )
+    )
+  )
+
+  ;; CHECK:      (func $multi-block-split-yes (type $0) (param $param i32)
+  ;; CHECK-NEXT:  (local $x i32)
+  ;; CHECK-NEXT:  (local.set $x
+  ;; CHECK-NEXT:   (i32.const 10)
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (if
+  ;; CHECK-NEXT:   (local.get $param)
+  ;; CHECK-NEXT:   (then
+  ;; CHECK-NEXT:    (local.set $x
+  ;; CHECK-NEXT:     (i32.const 10)
+  ;; CHECK-NEXT:    )
+  ;; CHECK-NEXT:   )
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (drop
+  ;; CHECK-NEXT:   (i32.const 1)
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (drop
+  ;; CHECK-NEXT:   (i32.const 0)
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT: )
+  ;; OPTIN:      (func $multi-block-split-yes (type $0) (param $param i32)
+  ;; OPTIN-NEXT:  (local $x i32)
+  ;; OPTIN-NEXT:  (local.set $x
+  ;; OPTIN-NEXT:   (i32.const 10)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (if
+  ;; OPTIN-NEXT:   (local.get $param)
+  ;; OPTIN-NEXT:   (then
+  ;; OPTIN-NEXT:    (local.set $x
+  ;; OPTIN-NEXT:     (i32.const 10)
+  ;; OPTIN-NEXT:    )
+  ;; OPTIN-NEXT:   )
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (drop
+  ;; OPTIN-NEXT:   (i32.const 1)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (drop
+  ;; OPTIN-NEXT:   (i32.const 0)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT: )
+  (func $multi-block-split-yes (param $param i32)
+    (local $x i32)
+    ;; As above, but now we set 10 again in the if arm.
+    (local.set $x
+      (i32.const 10)
+    )
+    (if
+      (local.get $param)
+      (then
+        (local.set $x
+          (i32.const 10)
+        )
+      )
+    )
+    ;; Now we can infer 1 and 0 here.
+    (drop
+      (i32.eq
+        (local.get $x)
+        (i32.const 10)
+      )
+    )
+    (drop
+      (i32.eq
+        (local.get $x)
+        (i32.const 20)
+      )
+    )
+  )
+
+  ;; CHECK:      (func $loop (type $0) (param $param i32)
+  ;; CHECK-NEXT:  (local $x i32)
+  ;; CHECK-NEXT:  (local.set $x
+  ;; CHECK-NEXT:   (i32.const 10)
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (loop $loop
+  ;; CHECK-NEXT:   (drop
+  ;; CHECK-NEXT:    (i32.const 1)
+  ;; CHECK-NEXT:   )
+  ;; CHECK-NEXT:   (drop
+  ;; CHECK-NEXT:    (i32.const 1)
+  ;; CHECK-NEXT:   )
+  ;; CHECK-NEXT:   (br_if $loop
+  ;; CHECK-NEXT:    (local.get $param)
+  ;; CHECK-NEXT:   )
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT: )
+  ;; OPTIN:      (func $loop (type $0) (param $param i32)
+  ;; OPTIN-NEXT:  (local $x i32)
+  ;; OPTIN-NEXT:  (local.set $x
+  ;; OPTIN-NEXT:   (i32.const 10)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (loop $loop
+  ;; OPTIN-NEXT:   (drop
+  ;; OPTIN-NEXT:    (i32.const 1)
+  ;; OPTIN-NEXT:   )
+  ;; OPTIN-NEXT:   (drop
+  ;; OPTIN-NEXT:    (i32.const 1)
+  ;; OPTIN-NEXT:   )
+  ;; OPTIN-NEXT:   (br_if $loop
+  ;; OPTIN-NEXT:    (local.get $param)
+  ;; OPTIN-NEXT:   )
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT: )
+  (func $loop (param $param i32)
+    (local $x i32)
+    ;; Set $x to 10 before the loop.
+    (local.set $x
+      (i32.const 10)
+    )
+    (loop $loop
+      ;; Despite the backedges, we can infer x is 10 and not 20.
+      (drop
+        (i32.eq
+          (local.get $x)
+          (i32.const 10)
+        )
+      )
+      (drop
+        (i32.ne
+          (local.get $x)
+          (i32.const 20)
+        )
+      )
+      (br_if $loop
+        (local.get $param)
+      )
+    )
+  )
+
+  ;; CHECK:      (func $loop-no (type $0) (param $param i32)
+  ;; CHECK-NEXT:  (local $x i32)
+  ;; CHECK-NEXT:  (local.set $x
+  ;; CHECK-NEXT:   (i32.const 10)
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (loop $loop
+  ;; CHECK-NEXT:   (drop
+  ;; CHECK-NEXT:    (i32.eq
+  ;; CHECK-NEXT:     (local.get $x)
+  ;; CHECK-NEXT:     (i32.const 10)
+  ;; CHECK-NEXT:    )
+  ;; CHECK-NEXT:   )
+  ;; CHECK-NEXT:   (drop
+  ;; CHECK-NEXT:    (i32.ne
+  ;; CHECK-NEXT:     (local.get $x)
+  ;; CHECK-NEXT:     (i32.const 20)
+  ;; CHECK-NEXT:    )
+  ;; CHECK-NEXT:   )
+  ;; CHECK-NEXT:   (local.set $x
+  ;; CHECK-NEXT:    (i32.const 20)
+  ;; CHECK-NEXT:   )
+  ;; CHECK-NEXT:   (br_if $loop
+  ;; CHECK-NEXT:    (local.get $param)
+  ;; CHECK-NEXT:   )
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT: )
+  ;; OPTIN:      (func $loop-no (type $0) (param $param i32)
+  ;; OPTIN-NEXT:  (local $x i32)
+  ;; OPTIN-NEXT:  (local.set $x
+  ;; OPTIN-NEXT:   (i32.const 10)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (loop $loop
+  ;; OPTIN-NEXT:   (drop
+  ;; OPTIN-NEXT:    (i32.eq
+  ;; OPTIN-NEXT:     (local.get $x)
+  ;; OPTIN-NEXT:     (i32.const 10)
+  ;; OPTIN-NEXT:    )
+  ;; OPTIN-NEXT:   )
+  ;; OPTIN-NEXT:   (drop
+  ;; OPTIN-NEXT:    (i32.ne
+  ;; OPTIN-NEXT:     (local.get $x)
+  ;; OPTIN-NEXT:     (i32.const 20)
+  ;; OPTIN-NEXT:    )
+  ;; OPTIN-NEXT:   )
+  ;; OPTIN-NEXT:   (local.set $x
+  ;; OPTIN-NEXT:    (i32.const 20)
+  ;; OPTIN-NEXT:   )
+  ;; OPTIN-NEXT:   (br_if $loop
+  ;; OPTIN-NEXT:    (local.get $param)
+  ;; OPTIN-NEXT:   )
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT: )
+  (func $loop-no (param $param i32)
+    (local $x i32)
+    ;; As above, but now with another value set in the loop. We cannot infer.
+    (local.set $x
+      (i32.const 10)
+    )
+    (loop $loop
+      (drop
+        (i32.eq
+          (local.get $x)
+          (i32.const 10)
+        )
+      )
+      (drop
+        (i32.ne
+          (local.get $x)
+          (i32.const 20)
+        )
+      )
+      (local.set $x
+        (i32.const 20)
+      )
+      (br_if $loop
+        (local.get $param)
+      )
+    )
+  )
+
+  ;; CHECK:      (func $default-var (type $0) (param $param i32)
+  ;; CHECK-NEXT:  (local $x i32)
+  ;; CHECK-NEXT:  (local $eq eqref)
+  ;; CHECK-NEXT:  (local $nn-eq (ref eq))
+  ;; CHECK-NEXT:  (drop
+  ;; CHECK-NEXT:   (i32.const 1)
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (drop
+  ;; CHECK-NEXT:   (i32.const 1)
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (drop
+  ;; CHECK-NEXT:   (i32.eq
+  ;; CHECK-NEXT:    (local.get $param)
+  ;; CHECK-NEXT:    (i32.const 0)
+  ;; CHECK-NEXT:   )
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (drop
+  ;; CHECK-NEXT:   (i32.const 1)
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT:  (drop
+  ;; CHECK-NEXT:   (i32.const 1)
+  ;; CHECK-NEXT:  )
+  ;; CHECK-NEXT: )
+  ;; OPTIN:      (func $default-var (type $0) (param $param i32)
+  ;; OPTIN-NEXT:  (local $x i32)
+  ;; OPTIN-NEXT:  (local $eq eqref)
+  ;; OPTIN-NEXT:  (local $nn-eq (ref eq))
+  ;; OPTIN-NEXT:  (drop
+  ;; OPTIN-NEXT:   (i32.const 1)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (drop
+  ;; OPTIN-NEXT:   (i32.const 1)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (drop
+  ;; OPTIN-NEXT:   (i32.eqz
+  ;; OPTIN-NEXT:    (local.get $param)
+  ;; OPTIN-NEXT:   )
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (drop
+  ;; OPTIN-NEXT:   (i32.const 1)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT:  (drop
+  ;; OPTIN-NEXT:   (i32.const 1)
+  ;; OPTIN-NEXT:  )
+  ;; OPTIN-NEXT: )
+  (func $default-var (param $param i32)
+    (local $x i32)
+    (local $eq eqref)
+    ;; A non-nullable local. We cannot add a get for it (it would not validate),
+    ;; but check we do not error.
+    (local $nn-eq (ref eq))
+    ;; locals begin with default values, so we can infer here.
+    (drop
+      (i32.eq
+        (local.get $x)
+        (i32.const 0)
+      )
+    )
+    (drop
+      (i32.eqz
+        (local.get $x)
+      )
+    )
+    ;; We can infer nothing for a param.
+    (drop
+      (i32.eq
+        (local.get $param)
+        (i32.const 0)
+      )
+    )
+    ;; We can infer a null reference.
+    (drop
+      (ref.eq
+        (local.get $eq)
+        (ref.null eq)
+      )
+    )
+    (drop
+      (ref.is_null
+        (local.get $eq)
+      )
+    )
+  )
+)