[llvm] [InstCombine] Merge a chain of equality icmps over a contiguous range (PR #207601)

via llvm-commits llvm-commits at lists.llvm.org
Sun Jul 5 10:24:55 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-llvm-transforms

Author: Igor Shevlyakov (ishevlyakov)

<details>
<summary>Changes</summary>

Fixes #<!-- -->207579.

A tree of `and`/`or` of equality range checks over a common operand (e.g. `a != 1 && a != 2 && ... && a != 8`) should collapse to one range comparison. When the constants appear in a scrambled order, the greedy pairwise fold pairs non-adjacent values first, triggering the bit-mask idiom `(x & -3) != 1`, which then blocks further range merging and leaves a partially-folded chain.

This adds an order-independent fold that walks the same-opcode one-use tree, collects the equality leaves over a common value, decomposes each leaf (including the `(x & M) == C` mask form and `(x + C1) == C`) back into point ranges, and emits a single comparison when their exact union is one contiguous range. Decomposing the mask form is what makes it insensitive to the order the pairwise fold ran in.

It runs only after the existing pairwise fold fails and fires only on a contiguous collapse, so it never regresses a case that fold already handles. Exhaustively verified over i8: every folded chain agrees with the original for all inputs.

Prepared with AI assistance per the [LLVM AI Tool Policy](https://llvm.org/docs/AIToolPolicy.html); not a "good first issue".

---
Full diff: https://github.com/llvm/llvm-project/pull/207601.diff


3 Files Affected:

- (modified) llvm/lib/Transforms/InstCombine/InstCombineAndOrXor.cpp (+132) 
- (modified) llvm/lib/Transforms/InstCombine/InstCombineInternal.h (+4) 
- (added) llvm/test/Transforms/InstCombine/and-or-icmp-chain.ll (+101) 


``````````diff
diff --git a/llvm/lib/Transforms/InstCombine/InstCombineAndOrXor.cpp b/llvm/lib/Transforms/InstCombine/InstCombineAndOrXor.cpp
index b6f4a55c07e8a..5a4b00574972a 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineAndOrXor.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineAndOrXor.cpp
@@ -1407,6 +1407,132 @@ Value *InstCombinerImpl::foldAndOrOfICmpsUsingRanges(ICmpInst *ICmp1,
   return Builder.CreateICmp(NewPred, NewV, ConstantInt::get(Ty, NewC));
 }
 
+/// Append to \p Pieces the ranges of a common operand \p V where one chain leaf
+/// is false (for `and`) or true (for `or`) - i.e. the set {X == C}. Handles
+/// `x != C`/`x == C`, looking through `(Y & M)` and `(Y + C1)`. Returns false
+/// if the leaf isn't a supported equality range check.
+static bool matchICmpChainLeaf(ICmpInst *Cmp, bool IsAnd, Value *&V,
+                               SmallVectorImpl<ConstantRange> &Pieces) {
+  // The opposite equality would select everything but a point, not a small set.
+  CmpInst::Predicate Want = IsAnd ? ICmpInst::ICMP_NE : ICmpInst::ICMP_EQ;
+  if (Cmp->getPredicate() != Want)
+    return false;
+
+  const APInt *C;
+  if (!match(Cmp->getOperand(1), m_APInt(C)))
+    return false;
+  Value *X = Cmp->getOperand(0);
+
+  // (Y & M) == C: Y matches C on M's kept bits and is free on the cleared bits.
+  const APInt *M;
+  Value *Y;
+  if (match(X, m_And(m_Value(Y), m_APInt(M)))) {
+    if (!(*C & ~*M).isZero())
+      return false; // no solution: C has bits outside the mask
+    APInt Free = ~*M;
+    if (Free.popcount() > 4)
+      return false; // bound the subset enumeration
+    V = Y;
+    for (APInt Sub = Free;; Sub = (Sub - 1) & Free) {
+      Pieces.push_back(ConstantRange(*C | Sub));
+      if (Sub.isZero())
+        break;
+    }
+    return true;
+  }
+
+  // (Y + C1) == C  <=>  Y == C - C1.
+  const APInt *C1;
+  if (match(X, m_AddLike(m_Value(Y), m_APInt(C1)))) {
+    V = Y;
+    Pieces.push_back(ConstantRange(*C - *C1));
+    return true;
+  }
+
+  V = X;
+  Pieces.push_back(ConstantRange(*C));
+  return true;
+}
+
+/// Fold a tree of `and`/`or` of equality range-check icmps over a common
+/// operand into a single comparison when their combined range is contiguous.
+/// The pairwise fold misses this when scrambled constants get paired
+/// non-adjacently and locked into a bit-mask form (e.g. `(x & -3) != 1`);
+/// decomposing that form back into points (in matchICmpChainLeaf) makes this
+/// order-independent.
+Value *InstCombinerImpl::foldAndOrOfICmpChain(BinaryOperator &I, bool IsAnd) {
+  Instruction::BinaryOps Opc = IsAnd ? Instruction::And : Instruction::Or;
+  assert(I.getOpcode() == Opc && "Wrong opcode for chain fold");
+
+  // Collect the icmp leaves. Internal nodes must be one-use so replacing the
+  // root removes the whole tree (guaranteed net win).
+  SmallVector<Value *, 8> Worklist = {I.getOperand(0), I.getOperand(1)};
+  SmallVector<ICmpInst *, 8> Leaves;
+  while (!Worklist.empty()) {
+    Value *Op = Worklist.pop_back_val();
+    if (auto *BO = dyn_cast<BinaryOperator>(Op))
+      if (BO->getOpcode() == Opc && BO->hasOneUse()) {
+        Worklist.push_back(BO->getOperand(0));
+        Worklist.push_back(BO->getOperand(1));
+        continue;
+      }
+    auto *Cmp = dyn_cast<ICmpInst>(Op);
+    if (!Cmp)
+      return nullptr;
+    Leaves.push_back(Cmp);
+    if (Leaves.size() > 32)
+      return nullptr; // bound pathological trees
+  }
+  // Runs only after the pairwise fold failed, and fires only on a contiguous
+  // collapse, so two leaves is fine: it just decomposes a locked-in bit-mask.
+  if (Leaves.size() < 2)
+    return nullptr;
+
+  Value *V = nullptr;
+  SmallVector<ConstantRange, 16> Pieces;
+  for (ICmpInst *Cmp : Leaves) {
+    Value *LeafV = nullptr;
+    if (!matchICmpChainLeaf(Cmp, IsAnd, LeafV, Pieces))
+      return nullptr;
+    if (!V)
+      V = LeafV;
+    else if (V != LeafV)
+      return nullptr;
+  }
+
+  for (const ConstantRange &R : Pieces)
+    if (R.isWrappedSet() || R.isEmptySet() || R.isFullSet())
+      return nullptr;
+
+  // Sort and merge the pieces; bail unless they form a single contiguous range.
+  llvm::sort(Pieces, [](const ConstantRange &A, const ConstantRange &B) {
+    return A.getLower().ult(B.getLower());
+  });
+  APInt Lo = Pieces.front().getLower(), Hi = Pieces.front().getUpper();
+  for (const ConstantRange &R : ArrayRef(Pieces).drop_front()) {
+    if (R.getLower().ugt(Hi))
+      return nullptr; // gap between pieces
+    if (R.getUpper().ugt(Hi))
+      Hi = R.getUpper();
+  }
+
+  ConstantRange CR(Lo, Hi);
+  if (CR.isFullSet() || CR.isEmptySet())
+    return nullptr;
+  // CR is the false set for `and` (De Morgan); invert to the true set.
+  if (IsAnd)
+    CR = CR.inverse();
+
+  Type *Ty = V->getType();
+  CmpInst::Predicate NewPred;
+  APInt NewC, Offset;
+  CR.getEquivalentICmp(NewPred, NewC, Offset);
+  Value *NewV = V;
+  if (Offset != 0)
+    NewV = Builder.CreateAdd(NewV, ConstantInt::get(Ty, Offset));
+  return Builder.CreateICmp(NewPred, NewV, ConstantInt::get(Ty, NewC));
+}
+
 /// Matches canonical form of isnan, fcmp ord x, 0
 static bool matchIsNotNaN(FCmpInst::Predicate P, Value *LHS, Value *RHS) {
   return P == FCmpInst::FCMP_ORD && match(RHS, m_AnyZeroFP());
@@ -2797,6 +2923,9 @@ Instruction *InstCombinerImpl::visitAnd(BinaryOperator &I) {
           foldBooleanAndOr(Op0, Op1, I, /*IsAnd=*/true, /*IsLogical=*/false))
     return replaceInstUsesWith(I, Res);
 
+  if (Value *Res = foldAndOrOfICmpChain(I, /*IsAnd=*/true))
+    return replaceInstUsesWith(I, Res);
+
   if (match(Op1, m_OneUse(m_LogicalAnd(m_Value(X), m_Value(Y))))) {
     bool IsLogical = isa<SelectInst>(Op1);
     if (auto *V = reassociateBooleanAndOr(Op0, X, Y, I, /*IsAnd=*/true,
@@ -4397,6 +4526,9 @@ Instruction *InstCombinerImpl::visitOr(BinaryOperator &I) {
           foldBooleanAndOr(Op0, Op1, I, /*IsAnd=*/false, /*IsLogical=*/false))
     return replaceInstUsesWith(I, Res);
 
+  if (Value *Res = foldAndOrOfICmpChain(I, /*IsAnd=*/false))
+    return replaceInstUsesWith(I, Res);
+
   if (match(Op1, m_OneUse(m_LogicalOr(m_Value(X), m_Value(Y))))) {
     bool IsLogical = isa<SelectInst>(Op1);
     if (auto *V = reassociateBooleanAndOr(Op0, X, Y, I, /*IsAnd=*/false,
diff --git a/llvm/lib/Transforms/InstCombine/InstCombineInternal.h b/llvm/lib/Transforms/InstCombine/InstCombineInternal.h
index 8b759e701da60..cf0cea9b9e931 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineInternal.h
+++ b/llvm/lib/Transforms/InstCombine/InstCombineInternal.h
@@ -415,6 +415,10 @@ class LLVM_LIBRARY_VISIBILITY InstCombinerImpl final
   Value *foldAndOrOfICmpsUsingRanges(ICmpInst *ICmp1, ICmpInst *ICmp2,
                                      bool IsAnd);
 
+  /// Fold a tree of `and`/`or` of equality range-check icmps over a common
+  /// operand into a single comparison when their combined range is contiguous.
+  Value *foldAndOrOfICmpChain(BinaryOperator &I, bool IsAnd);
+
   /// Optimize (fcmp)&(fcmp) or (fcmp)|(fcmp).
   /// NOTE: Unlike most of instcombine, this returns a Value which should
   /// already be inserted into the function.
diff --git a/llvm/test/Transforms/InstCombine/and-or-icmp-chain.ll b/llvm/test/Transforms/InstCombine/and-or-icmp-chain.ll
new file mode 100644
index 0000000000000..58a3860f97199
--- /dev/null
+++ b/llvm/test/Transforms/InstCombine/and-or-icmp-chain.ll
@@ -0,0 +1,101 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py
+; RUN: opt < %s -passes=instcombine -S | FileCheck %s
+
+; Tests for folding a tree of and/or of equality range-check icmps over a common
+; operand into a single comparison when the constants form a contiguous range,
+; independent of the order in which they appear (see #207579).
+
+; and (x != c_i) over a contiguous set, scrambled order -> single range check.
+; a not in [1,8]
+define i1 @and_ne_chain_8(i32 %a) {
+; CHECK-LABEL: @and_ne_chain_8(
+; CHECK-NEXT:    [[TMP1:%.*]] = add i32 [[A:%.*]], -9
+; CHECK-NEXT:    [[A7:%.*]] = icmp ult i32 [[TMP1]], -8
+; CHECK-NEXT:    ret i1 [[A7]]
+;
+  %v1 = icmp ne i32 %a, 3
+  %v2 = icmp ne i32 %a, 1
+  %v3 = icmp ne i32 %a, 4
+  %v4 = icmp ne i32 %a, 2
+  %v5 = icmp ne i32 %a, 7
+  %v6 = icmp ne i32 %a, 5
+  %v7 = icmp ne i32 %a, 8
+  %v8 = icmp ne i32 %a, 6
+  %a1 = and i1 %v1, %v2
+  %a2 = and i1 %a1, %v3
+  %a3 = and i1 %a2, %v4
+  %a4 = and i1 %a3, %v5
+  %a5 = and i1 %a4, %v6
+  %a6 = and i1 %a5, %v7
+  %a7 = and i1 %a6, %v8
+  ret i1 %a7
+}
+
+; or (x == c_i) over a contiguous set, scrambled order -> single range check.
+; a in {1,2,3}
+define i1 @or_eq_chain_3(i32 %a) {
+; CHECK-LABEL: @or_eq_chain_3(
+; CHECK-NEXT:    [[TMP1:%.*]] = add i32 [[A:%.*]], -1
+; CHECK-NEXT:    [[O2:%.*]] = icmp ult i32 [[TMP1]], 3
+; CHECK-NEXT:    ret i1 [[O2]]
+;
+  %v1 = icmp eq i32 %a, 3
+  %v2 = icmp eq i32 %a, 1
+  %v3 = icmp eq i32 %a, 2
+  %o1 = or i1 %v1, %v2
+  %o2 = or i1 %o1, %v3
+  ret i1 %o2
+}
+
+; Negative test: a gap at 3 means the set is not contiguous, so it must not fold
+; to a single comparison.
+define i1 @and_ne_chain_gap(i32 %a) {
+; CHECK-LABEL: @and_ne_chain_gap(
+; CHECK-NEXT:    [[V3:%.*]] = icmp ne i32 [[A:%.*]], 4
+; CHECK-NEXT:    [[TMP1:%.*]] = add i32 [[A]], -3
+; CHECK-NEXT:    [[A1:%.*]] = icmp ult i32 [[TMP1]], -2
+; CHECK-NEXT:    [[A2:%.*]] = and i1 [[A1]], [[V3]]
+; CHECK-NEXT:    ret i1 [[A2]]
+;
+  %v1 = icmp ne i32 %a, 1
+  %v2 = icmp ne i32 %a, 2
+  %v3 = icmp ne i32 %a, 4
+  %a1 = and i1 %v1, %v2
+  %a2 = and i1 %a1, %v3
+  ret i1 %a2
+}
+
+; Negative test: an internal node with an extra use can't be removed, so the
+; chain must not collapse.
+define i1 @and_ne_chain_multiuse(i32 %a, ptr %p) {
+; CHECK-LABEL: @and_ne_chain_multiuse(
+; CHECK-NEXT:    [[TMP1:%.*]] = add i32 [[A:%.*]], -3
+; CHECK-NEXT:    [[A1:%.*]] = icmp ult i32 [[TMP1]], -2
+; CHECK-NEXT:    store i1 [[A1]], ptr [[P:%.*]], align 1
+; CHECK-NEXT:    [[TMP2:%.*]] = add i32 [[A]], -4
+; CHECK-NEXT:    [[A2:%.*]] = icmp ult i32 [[TMP2]], -3
+; CHECK-NEXT:    ret i1 [[A2]]
+;
+  %v1 = icmp ne i32 %a, 1
+  %v2 = icmp ne i32 %a, 2
+  %v3 = icmp ne i32 %a, 3
+  %a1 = and i1 %v1, %v2
+  store i1 %a1, ptr %p
+  %a2 = and i1 %a1, %v3
+  ret i1 %a2
+}
+
+; Vector splat variant of the contiguous and-chain.
+define <2 x i1> @and_ne_chain_vec(<2 x i32> %a) {
+; CHECK-LABEL: @and_ne_chain_vec(
+; CHECK-NEXT:    [[TMP1:%.*]] = add <2 x i32> [[A:%.*]], splat (i32 -4)
+; CHECK-NEXT:    [[A2:%.*]] = icmp ult <2 x i32> [[TMP1]], splat (i32 -3)
+; CHECK-NEXT:    ret <2 x i1> [[A2]]
+;
+  %v1 = icmp ne <2 x i32> %a, splat(i32 1)
+  %v2 = icmp ne <2 x i32> %a, splat(i32 2)
+  %v3 = icmp ne <2 x i32> %a, splat(i32 3)
+  %a1 = and <2 x i1> %v1, %v2
+  %a2 = and <2 x i1> %a1, %v3
+  ret <2 x i1> %a2
+}

``````````

</details>


https://github.com/llvm/llvm-project/pull/207601


More information about the llvm-commits mailing list