[llvm] [ConstraintElim] Derive an unsigned IV bound from a signed relational latch. (PR #222762)

Florian Hahn via llvm-commits llvm-commits at lists.llvm.org
Sat Sep 12 08:19:44 PDT 2026


https://github.com/fhahn updated https://github.com/llvm/llvm-project/pull/222762

>From 45ab67e0387763019c8e3dfb432ec78937057ca2 Mon Sep 17 00:00:00 2001
From: Florian Hahn <flo at fhahn.com>
Date: Wed, 9 Sep 2026 12:40:17 +0100
Subject: [PATCH 1/3] [ConstraintElim] Add tests for unsigned IV bounds from a
 signed latch (NFC).

Pre-commit tests for deriving an unsigned bound on the induction from a signed
relational latch condition, plus the negative case where the step may wrap.
---
 .../induction-relational-predicate-latch.ll   | 164 ++++++++++++++++++
 1 file changed, 164 insertions(+)

diff --git a/llvm/test/Transforms/ConstraintElimination/induction-relational-predicate-latch.ll b/llvm/test/Transforms/ConstraintElimination/induction-relational-predicate-latch.ll
index 803679831b988..8779f341d9546 100644
--- a/llvm/test/Transforms/ConstraintElimination/induction-relational-predicate-latch.ll
+++ b/llvm/test/Transforms/ConstraintElimination/induction-relational-predicate-latch.ll
@@ -712,3 +712,167 @@ loop.exit:
   call void @use(i1 %c)
   ret void
 }
+
+; %iv is known non-negative, so the signed condition in the latch can also be
+; translated to an unsigned bound.
+define void @signed_latch_unsigned_fact(i64 %n, i1 %header.ec) {
+; CHECK-LABEL: define void @signed_latch_unsigned_fact(
+; CHECK-SAME: i64 [[N:%.*]], i1 [[HEADER_EC:%.*]]) {
+; CHECK-NEXT:  [[ENTRY:.*]]:
+; CHECK-NEXT:    [[PRE:%.*]] = icmp ne i64 [[N]], 0
+; CHECK-NEXT:    call void @llvm.assume(i1 [[PRE]])
+; CHECK-NEXT:    br label %[[LOOP_HEADER:.*]]
+; CHECK:       [[LOOP_HEADER]]:
+; CHECK-NEXT:    [[IV:%.*]] = phi i64 [ 0, %[[ENTRY]] ], [ [[IV_NEXT:%.*]], %[[LOOP_LATCH:.*]] ]
+; CHECK-NEXT:    [[C:%.*]] = icmp ult i64 [[IV]], [[N]]
+; CHECK-NEXT:    call void @use(i1 [[C]])
+; CHECK-NEXT:    br i1 [[HEADER_EC]], label %[[EXIT:.*]], label %[[LOOP_LATCH]]
+; CHECK:       [[LOOP_LATCH]]:
+; CHECK-NEXT:    [[IV_NEXT]] = add nsw i64 [[IV]], 2
+; CHECK-NEXT:    [[EC:%.*]] = icmp slt i64 [[IV_NEXT]], [[N]]
+; CHECK-NEXT:    br i1 [[EC]], label %[[LOOP_HEADER]], label %[[EXIT]]
+; CHECK:       [[EXIT]]:
+; CHECK-NEXT:    ret void
+;
+entry:
+  %pre = icmp ne i64 %n, 0
+  call void @llvm.assume(i1 %pre)
+  br label %loop.header
+
+loop.header:
+  %iv = phi i64 [ 0, %entry ], [ %iv.next, %loop.latch ]
+  %c = icmp ult i64 %iv, %n
+  call void @use(i1 %c)
+  br i1 %header.ec, label %exit, label %loop.latch
+
+loop.latch:
+  %iv.next = add nsw i64 %iv, 2
+  %ec = icmp slt i64 %iv.next, %n
+  br i1 %ec, label %loop.header, label %exit
+
+exit:
+  ret void
+}
+
+; Same, but the step may wrap.
+define void @neg_wrapping_step_no_unsigned_fact(i64 %n, i1 %header.ec) {
+; CHECK-LABEL: define void @neg_wrapping_step_no_unsigned_fact(
+; CHECK-SAME: i64 [[N:%.*]], i1 [[HEADER_EC:%.*]]) {
+; CHECK-NEXT:  [[ENTRY:.*]]:
+; CHECK-NEXT:    [[PRE:%.*]] = icmp ne i64 [[N]], 0
+; CHECK-NEXT:    call void @llvm.assume(i1 [[PRE]])
+; CHECK-NEXT:    br label %[[LOOP_HEADER:.*]]
+; CHECK:       [[LOOP_HEADER]]:
+; CHECK-NEXT:    [[IV:%.*]] = phi i64 [ 0, %[[ENTRY]] ], [ [[IV_NEXT:%.*]], %[[LOOP_LATCH:.*]] ]
+; CHECK-NEXT:    [[C:%.*]] = icmp ult i64 [[IV]], [[N]]
+; CHECK-NEXT:    call void @use(i1 [[C]])
+; CHECK-NEXT:    br i1 [[HEADER_EC]], label %[[EXIT:.*]], label %[[LOOP_LATCH]]
+; CHECK:       [[LOOP_LATCH]]:
+; CHECK-NEXT:    [[IV_NEXT]] = add i64 [[IV]], 2
+; CHECK-NEXT:    [[EC:%.*]] = icmp slt i64 [[IV_NEXT]], [[N]]
+; CHECK-NEXT:    br i1 [[EC]], label %[[LOOP_HEADER]], label %[[EXIT]]
+; CHECK:       [[EXIT]]:
+; CHECK-NEXT:    ret void
+;
+entry:
+  %pre = icmp ne i64 %n, 0
+  call void @llvm.assume(i1 %pre)
+  br label %loop.header
+
+loop.header:
+  %iv = phi i64 [ 0, %entry ], [ %iv.next, %loop.latch ]
+  %c = icmp ult i64 %iv, %n
+  call void @use(i1 %c)
+  br i1 %header.ec, label %exit, label %loop.latch
+
+loop.latch:
+  %iv.next = add i64 %iv, 2
+  %ec = icmp slt i64 %iv.next, %n
+  br i1 %ec, label %loop.header, label %exit
+
+exit:
+  ret void
+}
+
+define void @signed_latch_unsigned_fact_variable_start(i64 %start, i64 %n, i1 %header.ec) {
+; CHECK-LABEL: define void @signed_latch_unsigned_fact_variable_start(
+; CHECK-SAME: i64 [[START:%.*]], i64 [[N:%.*]], i1 [[HEADER_EC:%.*]]) {
+; CHECK-NEXT:  [[ENTRY:.*]]:
+; CHECK-NEXT:    [[PRE:%.*]] = icmp ult i64 [[START]], [[N]]
+; CHECK-NEXT:    call void @llvm.assume(i1 [[PRE]])
+; CHECK-NEXT:    [[PRE_POS:%.*]] = icmp sge i64 [[START]], 0
+; CHECK-NEXT:    call void @llvm.assume(i1 [[PRE_POS]])
+; CHECK-NEXT:    br label %[[LOOP_HEADER:.*]]
+; CHECK:       [[LOOP_HEADER]]:
+; CHECK-NEXT:    [[IV:%.*]] = phi i64 [ [[START]], %[[ENTRY]] ], [ [[IV_NEXT:%.*]], %[[LOOP_LATCH:.*]] ]
+; CHECK-NEXT:    [[C:%.*]] = icmp ult i64 [[IV]], [[N]]
+; CHECK-NEXT:    call void @use(i1 [[C]])
+; CHECK-NEXT:    br i1 [[HEADER_EC]], label %[[EXIT:.*]], label %[[LOOP_LATCH]]
+; CHECK:       [[LOOP_LATCH]]:
+; CHECK-NEXT:    [[IV_NEXT]] = add nsw i64 [[IV]], 2
+; CHECK-NEXT:    [[EC:%.*]] = icmp slt i64 [[IV_NEXT]], [[N]]
+; CHECK-NEXT:    br i1 [[EC]], label %[[LOOP_HEADER]], label %[[EXIT]]
+; CHECK:       [[EXIT]]:
+; CHECK-NEXT:    ret void
+;
+entry:
+  %pre.1 = icmp ult i64 %start, %n
+  call void @llvm.assume(i1 %pre.1)
+  %start.pos = icmp sge i64 %start, 0
+  call void @llvm.assume(i1 %start.pos)
+  br label %loop.header
+
+loop.header:
+  %iv = phi i64 [ %start, %entry ], [ %iv.next, %loop.latch ]
+  %c = icmp ult i64 %iv, %n
+  call void @use(i1 %c)
+  br i1 %header.ec, label %exit, label %loop.latch
+
+loop.latch:
+  %iv.next = add nsw i64 %iv, 2
+  %ec = icmp slt i64 %iv.next, %n
+  br i1 %ec, label %loop.header, label %exit
+
+exit:
+  ret void
+}
+
+; Same as above, but with missing %start < %n precondition.
+define void @signed_latch_unsigned_fact_variable_start_missing_ult(i64 %start, i64 %n, i1 %header.ec) {
+; CHECK-LABEL: define void @signed_latch_unsigned_fact_variable_start_missing_ult(
+; CHECK-SAME: i64 [[START:%.*]], i64 [[N:%.*]], i1 [[HEADER_EC:%.*]]) {
+; CHECK-NEXT:  [[ENTRY:.*]]:
+; CHECK-NEXT:    [[PRE_POS:%.*]] = icmp sge i64 [[START]], 0
+; CHECK-NEXT:    call void @llvm.assume(i1 [[PRE_POS]])
+; CHECK-NEXT:    br label %[[LOOP_HEADER:.*]]
+; CHECK:       [[LOOP_HEADER]]:
+; CHECK-NEXT:    [[IV:%.*]] = phi i64 [ [[START]], %[[ENTRY]] ], [ [[IV_NEXT:%.*]], %[[LOOP_LATCH:.*]] ]
+; CHECK-NEXT:    [[C:%.*]] = icmp ult i64 [[IV]], [[N]]
+; CHECK-NEXT:    call void @use(i1 [[C]])
+; CHECK-NEXT:    br i1 [[HEADER_EC]], label %[[EXIT:.*]], label %[[LOOP_LATCH]]
+; CHECK:       [[LOOP_LATCH]]:
+; CHECK-NEXT:    [[IV_NEXT]] = add nsw i64 [[IV]], 2
+; CHECK-NEXT:    [[EC:%.*]] = icmp slt i64 [[IV_NEXT]], [[N]]
+; CHECK-NEXT:    br i1 [[EC]], label %[[LOOP_HEADER]], label %[[EXIT]]
+; CHECK:       [[EXIT]]:
+; CHECK-NEXT:    ret void
+;
+entry:
+  %start.pos = icmp sge i64 %start, 0
+  call void @llvm.assume(i1 %start.pos)
+  br label %loop.header
+
+loop.header:
+  %iv = phi i64 [ %start, %entry ], [ %iv.next, %loop.latch ]
+  %c = icmp ult i64 %iv, %n
+  call void @use(i1 %c)
+  br i1 %header.ec, label %exit, label %loop.latch
+
+loop.latch:
+  %iv.next = add nsw i64 %iv, 2
+  %ec = icmp slt i64 %iv.next, %n
+  br i1 %ec, label %loop.header, label %exit
+
+exit:
+  ret void
+}

>From 45cf0d434c323785a776458d518b9d05c90c6a91 Mon Sep 17 00:00:00 2001
From: Florian Hahn <flo at fhahn.com>
Date: Wed, 9 Sep 2026 12:43:13 +0100
Subject: [PATCH 2/3] [ConstraintElim] Derive an unsigned IV bound from a
 signed relational latch.

For a known non-negative backedge (either via isKnownNonNegative or
because the increment is increasing), 0 <=s PN <= B holds and implies
that B is non-negative as well.

Use that to translate a signed condition to the equivalent unsigned one.

Note that the general signed->unsigned rewrite cannot catch this,
because the precondition also needs rewriting to unsigned.

No llvm-opt-benchmark-nightly impact, but it can help remove runtime
checks generated by sanitizers or Swift code. An end-to-end C example is
https://clang.godbolt.org/z/vbd91MjaM.
---
 llvm/lib/Transforms/Scalar/ConstraintElimination.cpp | 12 ++++++++++++
 .../induction-relational-predicate-latch.ll          |  6 ++----
 2 files changed, 14 insertions(+), 4 deletions(-)

diff --git a/llvm/lib/Transforms/Scalar/ConstraintElimination.cpp b/llvm/lib/Transforms/Scalar/ConstraintElimination.cpp
index ed95686e9e5d9..e4fc0a2c2a4e6 100644
--- a/llvm/lib/Transforms/Scalar/ConstraintElimination.cpp
+++ b/llvm/lib/Transforms/Scalar/ConstraintElimination.cpp
@@ -1188,6 +1188,18 @@ void State::addInfoForInductions(BasicBlock &BB) {
     WorkList.push_back(FactOrCheck::getConditionFact(
         DTN, ContinuePred, PN, B, ConditionTy(ContinuePred, StartValue, B)));
 
+    // For a non-negative backedge value, 0 s<= PN s< B implies B is
+    // non-negative as well, so the same bound holds in the unsigned system.
+    if (ICmpInst::isSigned(ContinuePred)) {
+      MonotonicInfo Info = getMonotonicityInfo(*PN, Backedge);
+      if ((Info.Signed && !Info.Decreasing) ||
+          isKnownNonNegative(Backedge, BB.getDataLayout())) {
+        CmpInst::Predicate UPred = ICmpInst::getUnsignedPredicate(ContinuePred);
+        WorkList.push_back(FactOrCheck::getConditionFact(
+            DTN, UPred, PN, B, ConditionTy(UPred, StartValue, B)));
+      }
+    }
+
     // A relational latch steps past B rather than landing on it, so none of the
     // reasoning below applies.
     return;
diff --git a/llvm/test/Transforms/ConstraintElimination/induction-relational-predicate-latch.ll b/llvm/test/Transforms/ConstraintElimination/induction-relational-predicate-latch.ll
index 8779f341d9546..29101d54567e9 100644
--- a/llvm/test/Transforms/ConstraintElimination/induction-relational-predicate-latch.ll
+++ b/llvm/test/Transforms/ConstraintElimination/induction-relational-predicate-latch.ll
@@ -724,8 +724,7 @@ define void @signed_latch_unsigned_fact(i64 %n, i1 %header.ec) {
 ; CHECK-NEXT:    br label %[[LOOP_HEADER:.*]]
 ; CHECK:       [[LOOP_HEADER]]:
 ; CHECK-NEXT:    [[IV:%.*]] = phi i64 [ 0, %[[ENTRY]] ], [ [[IV_NEXT:%.*]], %[[LOOP_LATCH:.*]] ]
-; CHECK-NEXT:    [[C:%.*]] = icmp ult i64 [[IV]], [[N]]
-; CHECK-NEXT:    call void @use(i1 [[C]])
+; CHECK-NEXT:    call void @use(i1 true)
 ; CHECK-NEXT:    br i1 [[HEADER_EC]], label %[[EXIT:.*]], label %[[LOOP_LATCH]]
 ; CHECK:       [[LOOP_LATCH]]:
 ; CHECK-NEXT:    [[IV_NEXT]] = add nsw i64 [[IV]], 2
@@ -805,8 +804,7 @@ define void @signed_latch_unsigned_fact_variable_start(i64 %start, i64 %n, i1 %h
 ; CHECK-NEXT:    br label %[[LOOP_HEADER:.*]]
 ; CHECK:       [[LOOP_HEADER]]:
 ; CHECK-NEXT:    [[IV:%.*]] = phi i64 [ [[START]], %[[ENTRY]] ], [ [[IV_NEXT:%.*]], %[[LOOP_LATCH:.*]] ]
-; CHECK-NEXT:    [[C:%.*]] = icmp ult i64 [[IV]], [[N]]
-; CHECK-NEXT:    call void @use(i1 [[C]])
+; CHECK-NEXT:    call void @use(i1 true)
 ; CHECK-NEXT:    br i1 [[HEADER_EC]], label %[[EXIT:.*]], label %[[LOOP_LATCH]]
 ; CHECK:       [[LOOP_LATCH]]:
 ; CHECK-NEXT:    [[IV_NEXT]] = add nsw i64 [[IV]], 2

>From 8478e03913814cbcdfb716c491f5a33e36a62052 Mon Sep 17 00:00:00 2001
From: Florian Hahn <flo at fhahn.com>
Date: Sat, 12 Sep 2026 16:19:11 +0100
Subject: [PATCH 3/3] !fixup add SLT/SLE assert + SGT tests

---
 .../Scalar/ConstraintElimination.cpp          |  3 +
 .../induction-relational-predicate-latch.ll   | 81 +++++++++++++++++++
 2 files changed, 84 insertions(+)

diff --git a/llvm/lib/Transforms/Scalar/ConstraintElimination.cpp b/llvm/lib/Transforms/Scalar/ConstraintElimination.cpp
index e4fc0a2c2a4e6..9ceec86722759 100644
--- a/llvm/lib/Transforms/Scalar/ConstraintElimination.cpp
+++ b/llvm/lib/Transforms/Scalar/ConstraintElimination.cpp
@@ -1191,6 +1191,9 @@ void State::addInfoForInductions(BasicBlock &BB) {
     // For a non-negative backedge value, 0 s<= PN s< B implies B is
     // non-negative as well, so the same bound holds in the unsigned system.
     if (ICmpInst::isSigned(ContinuePred)) {
+      assert((ContinuePred == CmpInst::ICMP_SLT ||
+              ContinuePred == CmpInst::ICMP_SLE) &&
+             "Expected a signed less-than continuation predicate");
       MonotonicInfo Info = getMonotonicityInfo(*PN, Backedge);
       if ((Info.Signed && !Info.Decreasing) ||
           isKnownNonNegative(Backedge, BB.getDataLayout())) {
diff --git a/llvm/test/Transforms/ConstraintElimination/induction-relational-predicate-latch.ll b/llvm/test/Transforms/ConstraintElimination/induction-relational-predicate-latch.ll
index 29101d54567e9..bdc1f406fce90 100644
--- a/llvm/test/Transforms/ConstraintElimination/induction-relational-predicate-latch.ll
+++ b/llvm/test/Transforms/ConstraintElimination/induction-relational-predicate-latch.ll
@@ -874,3 +874,84 @@ loop.latch:
 exit:
   ret void
 }
+
+; Continuing on sgt does not imply an unsigned lower bound. For n = -2,
+; incrementing iv from -1 to 0 makes the unsigned comparison false.
+define void @signed_gt_latch_no_unsigned_fact(i64 %n) {
+; CHECK-LABEL: define void @signed_gt_latch_no_unsigned_fact(
+; CHECK-SAME: i64 [[N:%.*]]) {
+; CHECK-NEXT:  [[ENTRY:.*]]:
+; CHECK-NEXT:    [[PRE:%.*]] = icmp ugt i64 -1, [[N]]
+; CHECK-NEXT:    call void @llvm.assume(i1 [[PRE]])
+; CHECK-NEXT:    br label %[[LOOP_HEADER:.*]]
+; CHECK:       [[LOOP_HEADER]]:
+; CHECK-NEXT:    [[IV:%.*]] = phi i64 [ -1, %[[ENTRY]] ], [ [[IV_NEXT:%.*]], %[[LOOP_LATCH:.*]] ]
+; CHECK-NEXT:    [[C:%.*]] = icmp ugt i64 [[IV]], [[N]]
+; CHECK-NEXT:    call void @use(i1 [[C]])
+; CHECK-NEXT:    [[STOP:%.*]] = icmp eq i64 [[IV]], 1
+; CHECK-NEXT:    br i1 [[STOP]], label %[[EXIT:.*]], label %[[LOOP_LATCH]]
+; CHECK:       [[LOOP_LATCH]]:
+; CHECK-NEXT:    [[IV_NEXT]] = add nsw i64 [[IV]], 1
+; CHECK-NEXT:    [[EC:%.*]] = icmp sgt i64 [[IV_NEXT]], [[N]]
+; CHECK-NEXT:    br i1 [[EC]], label %[[LOOP_HEADER]], label %[[EXIT]]
+; CHECK:       [[EXIT]]:
+; CHECK-NEXT:    ret void
+;
+entry:
+  %pre = icmp ugt i64 -1, %n
+  call void @llvm.assume(i1 %pre)
+  br label %loop.header
+
+loop.header:
+  %iv = phi i64 [ -1, %entry ], [ %iv.next, %loop.latch ]
+  %c = icmp ugt i64 %iv, %n
+  call void @use(i1 %c)
+  %stop = icmp eq i64 %iv, 1
+  br i1 %stop, label %exit, label %loop.latch
+
+loop.latch:
+  %iv.next = add nsw i64 %iv, 1
+  %ec = icmp sgt i64 %iv.next, %n
+  br i1 %ec, label %loop.header, label %exit
+
+exit:
+  ret void
+}
+
+; Exiting on sgt instead gives a signed <= continuation, which does imply an
+; unsigned upper bound for this non-negative induction.
+define void @signed_gt_exit_unsigned_fact(i64 %n) {
+; CHECK-LABEL: define void @signed_gt_exit_unsigned_fact(
+; CHECK-SAME: i64 [[N:%.*]]) {
+; CHECK-NEXT:  [[ENTRY:.*]]:
+; CHECK-NEXT:    br label %[[LOOP_HEADER:.*]]
+; CHECK:       [[LOOP_HEADER]]:
+; CHECK-NEXT:    [[IV:%.*]] = phi i64 [ 0, %[[ENTRY]] ], [ [[IV_NEXT:%.*]], %[[LOOP_LATCH:.*]] ]
+; CHECK-NEXT:    call void @use(i1 true)
+; CHECK-NEXT:    [[STOP:%.*]] = icmp eq i64 [[IV]], 4
+; CHECK-NEXT:    br i1 [[STOP]], label %[[EXIT:.*]], label %[[LOOP_LATCH]]
+; CHECK:       [[LOOP_LATCH]]:
+; CHECK-NEXT:    [[IV_NEXT]] = add nuw nsw i64 [[IV]], 1
+; CHECK-NEXT:    [[EC:%.*]] = icmp sgt i64 [[IV_NEXT]], [[N]]
+; CHECK-NEXT:    br i1 [[EC]], label %[[EXIT]], label %[[LOOP_HEADER]]
+; CHECK:       [[EXIT]]:
+; CHECK-NEXT:    ret void
+;
+entry:
+  br label %loop.header
+
+loop.header:
+  %iv = phi i64 [ 0, %entry ], [ %iv.next, %loop.latch ]
+  %c = icmp ule i64 %iv, %n
+  call void @use(i1 %c)
+  %stop = icmp eq i64 %iv, 4
+  br i1 %stop, label %exit, label %loop.latch
+
+loop.latch:
+  %iv.next = add nsw i64 %iv, 1
+  %ec = icmp sgt i64 %iv.next, %n
+  br i1 %ec, label %exit, label %loop.header
+
+exit:
+  ret void
+}



More information about the llvm-commits mailing list