[llvm] [SCEV] Infer no-wrap flags for shl by a constant. (PR #208434)

Florian Hahn via llvm-commits llvm-commits at lists.llvm.org
Fri Jul 17 05:44:35 PDT 2026


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

>From 57716156392193de41332bf2243a8f03a942536d Mon Sep 17 00:00:00 2001
From: Florian Hahn <flo at fhahn.com>
Date: Thu, 9 Jul 2026 12:34:20 +0100
Subject: [PATCH 1/3] [IndVars] Add tests for shl flag strenghtening.

---
 .../IndVarSimplify/strengthen-overflow.ll     | 120 ++++++++++++++++++
 1 file changed, 120 insertions(+)

diff --git a/llvm/test/Transforms/IndVarSimplify/strengthen-overflow.ll b/llvm/test/Transforms/IndVarSimplify/strengthen-overflow.ll
index 2fd4032eec23b..6bf1eb9fcaaea 100644
--- a/llvm/test/Transforms/IndVarSimplify/strengthen-overflow.ll
+++ b/llvm/test/Transforms/IndVarSimplify/strengthen-overflow.ll
@@ -333,5 +333,125 @@ bb3:                                              ; preds = %bb1
   ret void
 }
 
+define void @test_shl_const_bound(ptr %p) {
+; CHECK-LABEL: @test_shl_const_bound(
+; CHECK-NEXT:  entry:
+; CHECK-NEXT:    br label [[LOOP:%.*]]
+; CHECK:       loop:
+; CHECK-NEXT:    [[IV:%.*]] = phi i64 [ 0, [[ENTRY:%.*]] ], [ [[IV_NEXT:%.*]], [[LOOP]] ]
+; CHECK-NEXT:    [[OFF:%.*]] = shl i64 [[IV]], 2
+; CHECK-NEXT:    [[GEP:%.*]] = getelementptr i8, ptr [[P:%.*]], i64 [[OFF]]
+; CHECK-NEXT:    store i8 0, ptr [[GEP]], align 1
+; CHECK-NEXT:    [[IV_NEXT]] = add nuw nsw i64 [[IV]], 1
+; CHECK-NEXT:    [[EC:%.*]] = icmp eq i64 [[IV_NEXT]], 100
+; CHECK-NEXT:    br i1 [[EC]], label [[EXIT:%.*]], label [[LOOP]]
+; CHECK:       exit:
+; CHECK-NEXT:    ret void
+;
+entry:
+  br label %loop
+
+loop:
+  %iv = phi i64 [ 0, %entry ], [ %iv.next, %loop ]
+  %off = shl i64 %iv, 2
+  %gep = getelementptr i8, ptr %p, i64 %off
+  store i8 0, ptr %gep
+  %iv.next = add i64 %iv, 1
+  %ec = icmp eq i64 %iv.next, 100
+  br i1 %ec, label %exit, label %loop
+
+exit:
+  ret void
+}
+
+define void @test_shl_bw_minus_one(ptr %p) {
+; CHECK-LABEL: @test_shl_bw_minus_one(
+; CHECK-NEXT:  entry:
+; CHECK-NEXT:    br label [[LOOP:%.*]]
+; CHECK:       loop:
+; CHECK-NEXT:    [[IV:%.*]] = phi i8 [ 0, [[ENTRY:%.*]] ], [ [[IV_NEXT:%.*]], [[LOOP]] ]
+; CHECK-NEXT:    [[OFF:%.*]] = shl i8 [[IV]], 7
+; CHECK-NEXT:    store i8 [[OFF]], ptr [[P:%.*]], align 1
+; CHECK-NEXT:    [[IV_NEXT]] = add nuw nsw i8 [[IV]], 1
+; CHECK-NEXT:    [[EC:%.*]] = icmp eq i8 [[IV_NEXT]], 2
+; CHECK-NEXT:    br i1 [[EC]], label [[EXIT:%.*]], label [[LOOP]]
+; CHECK:       exit:
+; CHECK-NEXT:    ret void
+;
+entry:
+  br label %loop
+
+loop:
+  %iv = phi i8 [ 0, %entry ], [ %iv.next, %loop ]
+  %off = shl i8 %iv, 7
+  store i8 %off, ptr %p
+  %iv.next = add i8 %iv, 1
+  %ec = icmp eq i8 %iv.next, 2
+  br i1 %ec, label %exit, label %loop
+
+exit:
+  ret void
+}
+
+define void @test_shl_unbounded(ptr %p, i64 %n) {
+; CHECK-LABEL: @test_shl_unbounded(
+; CHECK-NEXT:  entry:
+; CHECK-NEXT:    br label [[LOOP:%.*]]
+; CHECK:       loop:
+; CHECK-NEXT:    [[IV:%.*]] = phi i64 [ 0, [[ENTRY:%.*]] ], [ [[IV_NEXT:%.*]], [[LOOP]] ]
+; CHECK-NEXT:    [[OFF:%.*]] = shl i64 [[IV]], 2
+; CHECK-NEXT:    [[GEP:%.*]] = getelementptr i8, ptr [[P:%.*]], i64 [[OFF]]
+; CHECK-NEXT:    store i8 0, ptr [[GEP]], align 1
+; CHECK-NEXT:    [[IV_NEXT]] = add i64 [[IV]], 1
+; CHECK-NEXT:    [[EC:%.*]] = icmp eq i64 [[IV_NEXT]], [[N:%.*]]
+; CHECK-NEXT:    br i1 [[EC]], label [[EXIT:%.*]], label [[LOOP]]
+; CHECK:       exit:
+; CHECK-NEXT:    ret void
+;
+entry:
+  br label %loop
+
+loop:
+  %iv = phi i64 [ 0, %entry ], [ %iv.next, %loop ]
+  %off = shl i64 %iv, 2
+  %gep = getelementptr i8, ptr %p, i64 %off
+  store i8 0, ptr %gep
+  %iv.next = add i64 %iv, 1
+  %ec = icmp eq i64 %iv.next, %n
+  br i1 %ec, label %exit, label %loop
+
+exit:
+  ret void
+}
+
+define void @test_shl_nuw_only(ptr %p) {
+; CHECK-LABEL: @test_shl_nuw_only(
+; CHECK-NEXT:  entry:
+; CHECK-NEXT:    br label [[LOOP:%.*]]
+; CHECK:       loop:
+; CHECK-NEXT:    [[IV:%.*]] = phi i8 [ 0, [[ENTRY:%.*]] ], [ [[IV_NEXT:%.*]], [[LOOP]] ]
+; CHECK-NEXT:    [[OFF:%.*]] = shl i8 [[IV]], 1
+; CHECK-NEXT:    store i8 [[OFF]], ptr [[P:%.*]], align 1
+; CHECK-NEXT:    [[IV_NEXT]] = add nuw i8 [[IV]], 1
+; CHECK-NEXT:    [[EC:%.*]] = icmp eq i8 [[IV_NEXT]], -128
+; CHECK-NEXT:    br i1 [[EC]], label [[EXIT:%.*]], label [[LOOP]]
+; CHECK:       exit:
+; CHECK-NEXT:    ret void
+;
+entry:
+  br label %loop
+
+loop:
+  %iv = phi i8 [ 0, %entry ], [ %iv.next, %loop ]
+  %off = shl i8 %iv, 1
+  store i8 %off, ptr %p
+  %iv.next = add i8 %iv, 1
+  %ec = icmp eq i8 %iv.next, -128
+  br i1 %ec, label %exit, label %loop
+
+exit:
+  ret void
+}
+
 !0 = !{i32 0, i32 2}
 !1 = !{i32 0, i32 42}

>From 09642b4263c0958c07e3a88e96462ae77b433637 Mon Sep 17 00:00:00 2001
From: Florian Hahn <flo at fhahn.com>
Date: Sun, 5 Jul 2026 07:27:21 +0100
Subject: [PATCH 2/3] [SCEV] Infer no-wrap flags for shl by a constant.

Treat `shl %a, C` as `mul %a, 1 << C` in getStrengthenedNoWrapFlagsFromBinOp
if possible.

Only transfer NSW from shl to mul, if the shift amount is < Bitwidth -
1, otherwise the result would be more poisonous.

Alive2 proofs: https://alive2.llvm.org/ce/z/iBPpkv
---
 llvm/lib/Analysis/ScalarEvolution.cpp         | 31 +++++++++++++------
 .../Transforms/IndVarSimplify/drop-exact.ll   |  4 +--
 .../test/Transforms/IndVarSimplify/pr66066.ll |  2 +-
 .../IndVarSimplify/strengthen-overflow.ll     |  6 ++--
 .../single-early-exit-deref-assumptions.ll    |  2 +-
 5 files changed, 28 insertions(+), 17 deletions(-)

diff --git a/llvm/lib/Analysis/ScalarEvolution.cpp b/llvm/lib/Analysis/ScalarEvolution.cpp
index af27a7897865b..286fffe4c01ac 100644
--- a/llvm/lib/Analysis/ScalarEvolution.cpp
+++ b/llvm/lib/Analysis/ScalarEvolution.cpp
@@ -2529,26 +2529,37 @@ ScalarEvolution::getStrengthenedNoWrapFlagsFromBinOp(
 
   bool Deduced = false;
 
-  if (OBO->getOpcode() != Instruction::Add &&
-      OBO->getOpcode() != Instruction::Sub &&
-      OBO->getOpcode() != Instruction::Mul)
-    return std::nullopt;
-
+  Instruction::BinaryOps Opcode = (Instruction::BinaryOps)OBO->getOpcode();
   const SCEV *LHS = getSCEV(OBO->getOperand(0));
   const SCEV *RHS = getSCEV(OBO->getOperand(1));
 
+  bool CanUseNSW = true;
+  const APInt *ShiftAmt;
+  // Treat `shl %a, C` as `mul %a, 1 << C`.
+  if (match(OBO, m_Shl(m_Value(), m_APInt(ShiftAmt)))) {
+    unsigned BitWidth = ShiftAmt->getBitWidth();
+    if (ShiftAmt->uge(BitWidth))
+      return std::nullopt;
+    // NSW only transfers if the shift amount is < BitWidth - 1, otherwise the
+    // result is more poisonous.
+    CanUseNSW = ShiftAmt->ult(BitWidth - 1);
+    Opcode = Instruction::Mul;
+    RHS = getConstant(APInt::getOneBitSet(BitWidth, ShiftAmt->getZExtValue()));
+  } else if (Opcode != Instruction::Add && Opcode != Instruction::Sub &&
+             Opcode != Instruction::Mul) {
+    return std::nullopt;
+  }
+
   const Instruction *CtxI =
       UseContextForNoWrapFlagInference ? dyn_cast<Instruction>(OBO) : nullptr;
   if (!OBO->hasNoUnsignedWrap() &&
-      willNotOverflow((Instruction::BinaryOps)OBO->getOpcode(),
-                      /* Signed */ false, LHS, RHS, CtxI)) {
+      willNotOverflow(Opcode, /* Signed */ false, LHS, RHS, CtxI)) {
     Flags = ScalarEvolution::setFlags(Flags, SCEV::FlagNUW);
     Deduced = true;
   }
 
-  if (!OBO->hasNoSignedWrap() &&
-      willNotOverflow((Instruction::BinaryOps)OBO->getOpcode(),
-                      /* Signed */ true, LHS, RHS, CtxI)) {
+  if (CanUseNSW && !OBO->hasNoSignedWrap() &&
+      willNotOverflow(Opcode, /* Signed */ true, LHS, RHS, CtxI)) {
     Flags = ScalarEvolution::setFlags(Flags, SCEV::FlagNSW);
     Deduced = true;
   }
diff --git a/llvm/test/Transforms/IndVarSimplify/drop-exact.ll b/llvm/test/Transforms/IndVarSimplify/drop-exact.ll
index fb8027df74ee7..90129628496cc 100644
--- a/llvm/test/Transforms/IndVarSimplify/drop-exact.ll
+++ b/llvm/test/Transforms/IndVarSimplify/drop-exact.ll
@@ -15,7 +15,7 @@ define void @drop_exact(ptr %p, ptr %p1) {
 ; CHECK-NEXT:    [[TMP13:%.*]] = phi i32 [ -47436, [[BB:%.*]] ], [ [[TMP15:%.*]], [[BB12]] ]
 ; CHECK-NEXT:    [[TMP14:%.*]] = phi i32 [ 0, [[BB]] ], [ [[TMP42:%.*]], [[BB12]] ]
 ; CHECK-NEXT:    [[TMP15]] = add nsw i32 [[TMP13]], -1
-; CHECK-NEXT:    [[TMP16:%.*]] = shl i32 [[TMP15]], 1
+; CHECK-NEXT:    [[TMP16:%.*]] = shl nsw i32 [[TMP15]], 1
 ; CHECK-NEXT:    [[TMP17:%.*]] = sub nsw i32 42831, [[TMP16]]
 ; CHECK-NEXT:    [[TMP19:%.*]] = lshr i32 [[TMP17]], 1
 ; CHECK-NEXT:    [[TMP20:%.*]] = urem i32 [[TMP19]], 250
@@ -62,7 +62,7 @@ define void @dont_drop_exact(ptr %p, ptr %p1) {
 ; CHECK-NEXT:    [[TMP13:%.*]] = phi i32 [ -47436, [[BB:%.*]] ], [ [[TMP15:%.*]], [[BB12]] ]
 ; CHECK-NEXT:    [[TMP14:%.*]] = phi i32 [ 0, [[BB]] ], [ [[TMP42:%.*]], [[BB12]] ]
 ; CHECK-NEXT:    [[TMP15]] = add nsw i32 [[TMP13]], -1
-; CHECK-NEXT:    [[TMP16:%.*]] = shl i32 [[TMP15]], 1
+; CHECK-NEXT:    [[TMP16:%.*]] = shl nsw i32 [[TMP15]], 1
 ; CHECK-NEXT:    [[TMP17:%.*]] = sub nsw i32 42831, [[TMP16]]
 ; CHECK-NEXT:    [[TMP19:%.*]] = lshr i32 [[TMP17]], 1
 ; CHECK-NEXT:    [[TMP20:%.*]] = urem i32 [[TMP19]], 250
diff --git a/llvm/test/Transforms/IndVarSimplify/pr66066.ll b/llvm/test/Transforms/IndVarSimplify/pr66066.ll
index cfd29876b302d..5bb0d8371b3e3 100644
--- a/llvm/test/Transforms/IndVarSimplify/pr66066.ll
+++ b/llvm/test/Transforms/IndVarSimplify/pr66066.ll
@@ -9,7 +9,7 @@ define void @test() {
 ; CHECK:       loop:
 ; CHECK-NEXT:    [[IV:%.*]] = phi i8 [ 1, [[ENTRY:%.*]] ], [ [[IV_DEC:%.*]], [[LOOP]] ]
 ; CHECK-NEXT:    [[IV_DEC]] = add nsw i8 [[IV]], -1
-; CHECK-NEXT:    [[SHL:%.*]] = shl i8 [[IV]], 7
+; CHECK-NEXT:    [[SHL:%.*]] = shl nuw i8 [[IV]], 7
 ; CHECK-NEXT:    call void @use(i8 [[SHL]])
 ; CHECK-NEXT:    [[CMP1:%.*]] = icmp eq i8 [[SHL]], 0
 ; CHECK-NEXT:    br i1 [[CMP1]], label [[EXIT:%.*]], label [[LOOP]]
diff --git a/llvm/test/Transforms/IndVarSimplify/strengthen-overflow.ll b/llvm/test/Transforms/IndVarSimplify/strengthen-overflow.ll
index 6bf1eb9fcaaea..4a230b781d2db 100644
--- a/llvm/test/Transforms/IndVarSimplify/strengthen-overflow.ll
+++ b/llvm/test/Transforms/IndVarSimplify/strengthen-overflow.ll
@@ -339,7 +339,7 @@ define void @test_shl_const_bound(ptr %p) {
 ; CHECK-NEXT:    br label [[LOOP:%.*]]
 ; CHECK:       loop:
 ; CHECK-NEXT:    [[IV:%.*]] = phi i64 [ 0, [[ENTRY:%.*]] ], [ [[IV_NEXT:%.*]], [[LOOP]] ]
-; CHECK-NEXT:    [[OFF:%.*]] = shl i64 [[IV]], 2
+; CHECK-NEXT:    [[OFF:%.*]] = shl nuw nsw i64 [[IV]], 2
 ; CHECK-NEXT:    [[GEP:%.*]] = getelementptr i8, ptr [[P:%.*]], i64 [[OFF]]
 ; CHECK-NEXT:    store i8 0, ptr [[GEP]], align 1
 ; CHECK-NEXT:    [[IV_NEXT]] = add nuw nsw i64 [[IV]], 1
@@ -370,7 +370,7 @@ define void @test_shl_bw_minus_one(ptr %p) {
 ; CHECK-NEXT:    br label [[LOOP:%.*]]
 ; CHECK:       loop:
 ; CHECK-NEXT:    [[IV:%.*]] = phi i8 [ 0, [[ENTRY:%.*]] ], [ [[IV_NEXT:%.*]], [[LOOP]] ]
-; CHECK-NEXT:    [[OFF:%.*]] = shl i8 [[IV]], 7
+; CHECK-NEXT:    [[OFF:%.*]] = shl nuw i8 [[IV]], 7
 ; CHECK-NEXT:    store i8 [[OFF]], ptr [[P:%.*]], align 1
 ; CHECK-NEXT:    [[IV_NEXT]] = add nuw nsw i8 [[IV]], 1
 ; CHECK-NEXT:    [[EC:%.*]] = icmp eq i8 [[IV_NEXT]], 2
@@ -430,7 +430,7 @@ define void @test_shl_nuw_only(ptr %p) {
 ; CHECK-NEXT:    br label [[LOOP:%.*]]
 ; CHECK:       loop:
 ; CHECK-NEXT:    [[IV:%.*]] = phi i8 [ 0, [[ENTRY:%.*]] ], [ [[IV_NEXT:%.*]], [[LOOP]] ]
-; CHECK-NEXT:    [[OFF:%.*]] = shl i8 [[IV]], 1
+; CHECK-NEXT:    [[OFF:%.*]] = shl nuw i8 [[IV]], 1
 ; CHECK-NEXT:    store i8 [[OFF]], ptr [[P:%.*]], align 1
 ; CHECK-NEXT:    [[IV_NEXT]] = add nuw i8 [[IV]], 1
 ; CHECK-NEXT:    [[EC:%.*]] = icmp eq i8 [[IV_NEXT]], -128
diff --git a/llvm/test/Transforms/LoopVectorize/single-early-exit-deref-assumptions.ll b/llvm/test/Transforms/LoopVectorize/single-early-exit-deref-assumptions.ll
index 8be66cc4c8ef8..b73720c126d03 100644
--- a/llvm/test/Transforms/LoopVectorize/single-early-exit-deref-assumptions.ll
+++ b/llvm/test/Transforms/LoopVectorize/single-early-exit-deref-assumptions.ll
@@ -299,7 +299,7 @@ define i64 @early_exit_alignment_and_deref_known_via_assumption_n_not_zero_i16_p
 ; CHECK-NEXT:  [[ENTRY:.*]]:
 ; CHECK-NEXT:    call void @llvm.assume(i1 true) [ "align"(ptr [[A]], i64 2) ]
 ; CHECK-NEXT:    [[N_EXT:%.*]] = zext i32 [[N]] to i64
-; CHECK-NEXT:    [[MUL:%.*]] = shl i64 [[N_EXT]], 1
+; CHECK-NEXT:    [[MUL:%.*]] = shl nuw nsw i64 [[N_EXT]], 1
 ; CHECK-NEXT:    call void @llvm.assume(i1 true) [ "dereferenceable"(ptr [[A]], i64 [[MUL]]) ]
 ; CHECK-NEXT:    [[A_END:%.*]] = getelementptr i8, ptr [[A]], i64 [[MUL]]
 ; CHECK-NEXT:    [[PRE:%.*]] = icmp eq i32 [[N]], 0

>From d07e045f52b4e2d87d56f760e92182ae8fc325ee Mon Sep 17 00:00:00 2001
From: Florian Hahn <flo at fhahn.com>
Date: Thu, 9 Jul 2026 18:47:06 +0100
Subject: [PATCH 3/3] !fixup clarify comment

---
 llvm/lib/Analysis/ScalarEvolution.cpp | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/llvm/lib/Analysis/ScalarEvolution.cpp b/llvm/lib/Analysis/ScalarEvolution.cpp
index 286fffe4c01ac..390e1c9abdc04 100644
--- a/llvm/lib/Analysis/ScalarEvolution.cpp
+++ b/llvm/lib/Analysis/ScalarEvolution.cpp
@@ -2540,8 +2540,8 @@ ScalarEvolution::getStrengthenedNoWrapFlagsFromBinOp(
     unsigned BitWidth = ShiftAmt->getBitWidth();
     if (ShiftAmt->uge(BitWidth))
       return std::nullopt;
-    // NSW only transfers if the shift amount is < BitWidth - 1, otherwise the
-    // result is more poisonous.
+    // NSW only transfers if the shift amount is < BitWidth - 1, as INT_MIN * -1
+    // overflows.
     CanUseNSW = ShiftAmt->ult(BitWidth - 1);
     Opcode = Instruction::Mul;
     RHS = getConstant(APInt::getOneBitSet(BitWidth, ShiftAmt->getZExtValue()));



More information about the llvm-commits mailing list