[llvm] [InstCombine] Allow pre-shift fold with negative offset when shift amount is in range (PR #222351)

Julian Gabriel via llvm-commits llvm-commits at lists.llvm.org
Mon Sep 21 22:55:51 PDT 2026


https://github.com/ParcivalLTD updated https://github.com/llvm/llvm-project/pull/222351

>From 62023a1becd1fe6dea6ef6594b34efac4cf86fde Mon Sep 17 00:00:00 2001
From: Julian Gabriel <juliangabriel570 at gmail.com>
Date: Tue, 22 Sep 2026 00:34:12 +0200
Subject: [PATCH 1/2] Pre-commit tests for pre-shift fold with in-range shift
 amount (NFC)

Co-Authored-By: Claude Opus 5 <noreply at anthropic.com>
Claude-Session: https://claude.ai/code/session_01JdbUE5JXAZRUy8su8Wo5fh
---
 llvm/test/Transforms/InstCombine/shift-add.ll | 149 ++++++++++++++++++
 1 file changed, 149 insertions(+)

diff --git a/llvm/test/Transforms/InstCombine/shift-add.ll b/llvm/test/Transforms/InstCombine/shift-add.ll
index 81cbc2ac23b5f..1fae2b692c503 100644
--- a/llvm/test/Transforms/InstCombine/shift-add.ll
+++ b/llvm/test/Transforms/InstCombine/shift-add.ll
@@ -804,3 +804,152 @@ define <2 x i8> @lshr_fold_or_disjoint_cnt_out_of_bounds(<2 x i8> %x) {
   %r = lshr <2 x i8> <i8 2, i8 3>, %a
   ret <2 x i8> %r
 }
+
+; Pre-shift fold without exact/nsw/nuw flags, guarded by known range of X.
+
+define i32 @lshr_add_negative_no_exact_known_range(i32 %x) {
+; CHECK-LABEL: @lshr_add_negative_no_exact_known_range(
+; CHECK-NEXT:    [[MASKED:%.*]] = and i32 [[X:%.*]], 15
+; CHECK-NEXT:    [[A:%.*]] = add nsw i32 [[MASKED]], -1
+; CHECK-NEXT:    [[R:%.*]] = lshr i32 2, [[A]]
+; CHECK-NEXT:    ret i32 [[R]]
+;
+  %masked = and i32 %x, 15
+  %a = add i32 %masked, -1
+  %r = lshr i32 2, %a
+  ret i32 %r
+}
+
+define i32 @shl_add_negative_no_flags_known_range(i32 %x) {
+; CHECK-LABEL: @shl_add_negative_no_flags_known_range(
+; CHECK-NEXT:    [[MASKED:%.*]] = and i32 [[X:%.*]], 15
+; CHECK-NEXT:    [[A:%.*]] = add nsw i32 [[MASKED]], -2
+; CHECK-NEXT:    [[R:%.*]] = shl i32 12, [[A]]
+; CHECK-NEXT:    ret i32 [[R]]
+;
+  %masked = and i32 %x, 15
+  %a = add i32 %masked, -2
+  %r = shl i32 12, %a
+  ret i32 %r
+}
+
+define i32 @ashr_add_negative_no_exact_known_range(i32 %x) {
+; CHECK-LABEL: @ashr_add_negative_no_exact_known_range(
+; CHECK-NEXT:    [[MASKED:%.*]] = and i32 [[X:%.*]], 15
+; CHECK-NEXT:    [[A:%.*]] = add nsw i32 [[MASKED]], -1
+; CHECK-NEXT:    [[R:%.*]] = ashr i32 -4, [[A]]
+; CHECK-NEXT:    ret i32 [[R]]
+;
+  %masked = and i32 %x, 15
+  %a = add i32 %masked, -1
+  %r = ashr i32 -4, %a
+  ret i32 %r
+}
+
+define <2 x i32> @lshr_add_negative_no_exact_known_range_vec(<2 x i32> %x) {
+; CHECK-LABEL: @lshr_add_negative_no_exact_known_range_vec(
+; CHECK-NEXT:    [[MASKED:%.*]] = and <2 x i32> [[X:%.*]], splat (i32 15)
+; CHECK-NEXT:    [[A:%.*]] = add nsw <2 x i32> [[MASKED]], splat (i32 -1)
+; CHECK-NEXT:    [[R:%.*]] = lshr <2 x i32> splat (i32 2), [[A]]
+; CHECK-NEXT:    ret <2 x i32> [[R]]
+;
+  %masked = and <2 x i32> %x, <i32 15, i32 15>
+  %a = add <2 x i32> %masked, <i32 -1, i32 -1>
+  %r = lshr <2 x i32> <i32 2, i32 2>, %a
+  ret <2 x i32> %r
+}
+
+; Motivating example from issue #222281
+define i32 @from_sub_issue222281(i8 %x) {
+; CHECK-LABEL: @from_sub_issue222281(
+; CHECK-NEXT:    [[M:%.*]] = and i8 [[X:%.*]], 120
+; CHECK-NEXT:    [[V:%.*]] = or disjoint i8 [[M]], -128
+; CHECK-NEXT:    [[TZ:%.*]] = call range(i8 3, 8) i8 @llvm.cttz.i8(i8 [[V]], i1 true)
+; CHECK-NEXT:    [[Z:%.*]] = zext nneg i8 [[TZ]] to i32
+; CHECK-NEXT:    [[S:%.*]] = add nsw i32 [[Z]], -3
+; CHECK-NEXT:    [[R:%.*]] = lshr i32 64, [[S]]
+; CHECK-NEXT:    ret i32 [[R]]
+;
+  %m = and i8 %x, -8
+  %v = or i8 %m, -128
+  %tz = call i8 @llvm.cttz.i8(i8 %v, i1 true)
+  %z = zext nneg i8 %tz to i32
+  %s = sub nuw nsw i32 %z, 3
+  %r = lshr i32 64, %s
+  ret i32 %r
+}
+
+; negative test: X range is unknown, fold should not fire without exact
+define i32 @lshr_add_negative_no_exact_unknown_range(i32 %x) {
+; CHECK-LABEL: @lshr_add_negative_no_exact_unknown_range(
+; CHECK-NEXT:    [[A:%.*]] = add i32 [[X:%.*]], -1
+; CHECK-NEXT:    [[R:%.*]] = lshr i32 2, [[A]]
+; CHECK-NEXT:    ret i32 [[R]]
+;
+  %a = add i32 %x, -1
+  %r = lshr i32 2, %a
+  ret i32 %r
+}
+
+; negative test: constant not roundtrippable (3 >> 1 loses bit 0 for shl)
+define i32 @shl_add_negative_no_flags_not_roundtrippable(i32 range(i32 1, 16) %x) {
+; CHECK-LABEL: @shl_add_negative_no_flags_not_roundtrippable(
+; CHECK-NEXT:    [[A:%.*]] = add nsw i32 [[X:%.*]], -1
+; CHECK-NEXT:    [[R:%.*]] = shl i32 3, [[A]]
+; CHECK-NEXT:    ret i32 [[R]]
+;
+  %a = add nsw i32 %x, -1
+  %r = shl i32 3, %a
+  ret i32 %r
+}
+
+; negative test: constant not roundtrippable (64 << 28 overflows 32-bit integer for lshr)
+define i32 @lshr_add_negative_no_exact_overflow(i32 range(i32 28, 32) %x) {
+; CHECK-LABEL: @lshr_add_negative_no_exact_overflow(
+; CHECK-NEXT:    [[A:%.*]] = add nsw i32 [[X:%.*]], -28
+; CHECK-NEXT:    [[R:%.*]] = lshr exact i32 64, [[A]]
+; CHECK-NEXT:    ret i32 [[R]]
+;
+  %a = add nsw i32 %x, -28
+  %r = lshr i32 64, %a
+  ret i32 %r
+}
+
+; Boundary: X is at most 31 (BitWidth - 1), so the fold is valid.
+define i32 @lshr_add_negative_no_exact_max_bitwidth_minus_one(i32 %x) {
+; CHECK-LABEL: @lshr_add_negative_no_exact_max_bitwidth_minus_one(
+; CHECK-NEXT:    [[MASKED:%.*]] = and i32 [[X:%.*]], 31
+; CHECK-NEXT:    [[A:%.*]] = add nsw i32 [[MASKED]], -1
+; CHECK-NEXT:    [[R:%.*]] = lshr i32 2, [[A]]
+; CHECK-NEXT:    ret i32 [[R]]
+;
+  %masked = and i32 %x, 31
+  %a = add i32 %masked, -1
+  %r = lshr i32 2, %a
+  ret i32 %r
+}
+
+; negative test: X may be 32 (BitWidth), where the original shift is defined
+; but the pre-shifted one would be poison.
+define i32 @lshr_add_negative_no_exact_max_bitwidth(i32 range(i32 1, 33) %x) {
+; CHECK-LABEL: @lshr_add_negative_no_exact_max_bitwidth(
+; CHECK-NEXT:    [[A:%.*]] = add nsw i32 [[X:%.*]], -1
+; CHECK-NEXT:    [[R:%.*]] = lshr i32 2, [[A]]
+; CHECK-NEXT:    ret i32 [[R]]
+;
+  %a = add nsw i32 %x, -1
+  %r = lshr i32 2, %a
+  ret i32 %r
+}
+
+; negative test: same boundary for shl.
+define i32 @shl_add_negative_no_flags_max_bitwidth(i32 range(i32 2, 34) %x) {
+; CHECK-LABEL: @shl_add_negative_no_flags_max_bitwidth(
+; CHECK-NEXT:    [[A:%.*]] = add nsw i32 [[X:%.*]], -2
+; CHECK-NEXT:    [[R:%.*]] = shl i32 12, [[A]]
+; CHECK-NEXT:    ret i32 [[R]]
+;
+  %a = add nsw i32 %x, -2
+  %r = shl i32 12, %a
+  ret i32 %r
+}

>From 1c0fb0eca12c6081028735dc5e55763960f6a14d Mon Sep 17 00:00:00 2001
From: Julian Gabriel <juliangabriel570 at gmail.com>
Date: Tue, 22 Sep 2026 00:34:34 +0200
Subject: [PATCH 2/2] Allow pre-shift of constant with negative offset when
 shift amount is in range

commonShiftTransforms folds C << (X - K) --> (C >> K) << X and
C >> (X - K) --> (C << K) >> X when no set bits of C are lost, but so
far also required nuw/nsw (shl) or exact (lshr/ashr) on the original
shift. Additionally allow the fold when X u< BitWidth is known.

If X < K, the original shift amount wraps and the original is poison
while the new shift is defined, which is a valid refinement. The only
problematic case is BitWidth <= X < BitWidth + K, where a defined
value would become poison; the flags excluded it before, and now
X u< BitWidth does as well.

Fixes #222281.

Co-Authored-By: Claude Opus 5 <noreply at anthropic.com>
Claude-Session: https://claude.ai/code/session_01JdbUE5JXAZRUy8su8Wo5fh
---
 .../InstCombine/InstCombineShifts.cpp         | 46 +++++++++++--------
 llvm/test/Transforms/InstCombine/shift-add.ll | 18 +++-----
 2 files changed, 34 insertions(+), 30 deletions(-)

diff --git a/llvm/lib/Transforms/InstCombine/InstCombineShifts.cpp b/llvm/lib/Transforms/InstCombine/InstCombineShifts.cpp
index 7e32abcd6e9ba..6f0dd0bc4f510 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineShifts.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineShifts.cpp
@@ -469,35 +469,45 @@ Instruction *InstCombinerImpl::commonShiftTransforms(BinaryOperator &I) {
     // C << (X - AddC) --> (C >> AddC) << X
     // and
     // C >> (X - AddC) --> (C << AddC) >> X
+    // This requires that no set bits of C are lost by the pre-shift, and that
+    // X is a valid shift amount whenever (X - AddC) is. The latter is implied
+    // by nuw/nsw/exact on the original shift, or holds if X u< BitWidth.
     const APInt *AddC;
     if (match(Op1, m_Add(m_Value(A), m_APInt(AddC))) && AddC->isNegative() &&
         (-*AddC).ult(BitWidth)) {
       unsigned PosOffset = (-*AddC).getZExtValue();
 
-      auto isSuitableForPreShift = [PosOffset, &I, AC]() {
-        switch (I.getOpcode()) {
-        default:
-          return false;
-        case Instruction::Shl:
-          return (I.hasNoSignedWrap() || I.hasNoUnsignedWrap()) &&
-                 AC->eq(AC->lshr(PosOffset).shl(PosOffset));
-        case Instruction::LShr:
-          return I.isExact() && AC->eq(AC->shl(PosOffset).lshr(PosOffset));
-        case Instruction::AShr:
-          return I.isExact() && AC->eq(AC->shl(PosOffset).ashr(PosOffset));
-        }
-      };
-      if (isSuitableForPreShift()) {
+      bool HasShiftFlags;
+      bool IsRoundtrippable;
+      switch (I.getOpcode()) {
+      default:
+        llvm_unreachable("Unexpected shift opcode");
+      case Instruction::Shl:
+        HasShiftFlags = I.hasNoSignedWrap() || I.hasNoUnsignedWrap();
+        IsRoundtrippable = AC->eq(AC->lshr(PosOffset).shl(PosOffset));
+        break;
+      case Instruction::LShr:
+        HasShiftFlags = I.isExact();
+        IsRoundtrippable = AC->eq(AC->shl(PosOffset).lshr(PosOffset));
+        break;
+      case Instruction::AShr:
+        HasShiftFlags = I.isExact();
+        IsRoundtrippable = AC->eq(AC->shl(PosOffset).ashr(PosOffset));
+        break;
+      }
+
+      if (IsRoundtrippable &&
+          (HasShiftFlags ||
+           computeKnownBits(A, &I).getMaxValue().ult(BitWidth))) {
         Constant *NewC = ConstantInt::get(Ty, I.getOpcode() == Instruction::Shl
                                                   ? AC->lshr(PosOffset)
                                                   : AC->shl(PosOffset));
         BinaryOperator *NewShiftOp =
             BinaryOperator::Create(I.getOpcode(), NewC, A);
-        if (I.getOpcode() == Instruction::Shl) {
+        if (I.getOpcode() == Instruction::Shl)
           NewShiftOp->setHasNoUnsignedWrap(I.hasNoUnsignedWrap());
-        } else {
-          NewShiftOp->setIsExact();
-        }
+        else
+          NewShiftOp->setIsExact(I.isExact());
         return NewShiftOp;
       }
     }
diff --git a/llvm/test/Transforms/InstCombine/shift-add.ll b/llvm/test/Transforms/InstCombine/shift-add.ll
index 1fae2b692c503..6439ec0f51af8 100644
--- a/llvm/test/Transforms/InstCombine/shift-add.ll
+++ b/llvm/test/Transforms/InstCombine/shift-add.ll
@@ -810,8 +810,7 @@ define <2 x i8> @lshr_fold_or_disjoint_cnt_out_of_bounds(<2 x i8> %x) {
 define i32 @lshr_add_negative_no_exact_known_range(i32 %x) {
 ; CHECK-LABEL: @lshr_add_negative_no_exact_known_range(
 ; CHECK-NEXT:    [[MASKED:%.*]] = and i32 [[X:%.*]], 15
-; CHECK-NEXT:    [[A:%.*]] = add nsw i32 [[MASKED]], -1
-; CHECK-NEXT:    [[R:%.*]] = lshr i32 2, [[A]]
+; CHECK-NEXT:    [[R:%.*]] = lshr i32 4, [[MASKED]]
 ; CHECK-NEXT:    ret i32 [[R]]
 ;
   %masked = and i32 %x, 15
@@ -823,8 +822,7 @@ define i32 @lshr_add_negative_no_exact_known_range(i32 %x) {
 define i32 @shl_add_negative_no_flags_known_range(i32 %x) {
 ; CHECK-LABEL: @shl_add_negative_no_flags_known_range(
 ; CHECK-NEXT:    [[MASKED:%.*]] = and i32 [[X:%.*]], 15
-; CHECK-NEXT:    [[A:%.*]] = add nsw i32 [[MASKED]], -2
-; CHECK-NEXT:    [[R:%.*]] = shl i32 12, [[A]]
+; CHECK-NEXT:    [[R:%.*]] = shl nuw nsw i32 3, [[MASKED]]
 ; CHECK-NEXT:    ret i32 [[R]]
 ;
   %masked = and i32 %x, 15
@@ -836,8 +834,7 @@ define i32 @shl_add_negative_no_flags_known_range(i32 %x) {
 define i32 @ashr_add_negative_no_exact_known_range(i32 %x) {
 ; CHECK-LABEL: @ashr_add_negative_no_exact_known_range(
 ; CHECK-NEXT:    [[MASKED:%.*]] = and i32 [[X:%.*]], 15
-; CHECK-NEXT:    [[A:%.*]] = add nsw i32 [[MASKED]], -1
-; CHECK-NEXT:    [[R:%.*]] = ashr i32 -4, [[A]]
+; CHECK-NEXT:    [[R:%.*]] = ashr i32 -8, [[MASKED]]
 ; CHECK-NEXT:    ret i32 [[R]]
 ;
   %masked = and i32 %x, 15
@@ -849,8 +846,7 @@ define i32 @ashr_add_negative_no_exact_known_range(i32 %x) {
 define <2 x i32> @lshr_add_negative_no_exact_known_range_vec(<2 x i32> %x) {
 ; CHECK-LABEL: @lshr_add_negative_no_exact_known_range_vec(
 ; CHECK-NEXT:    [[MASKED:%.*]] = and <2 x i32> [[X:%.*]], splat (i32 15)
-; CHECK-NEXT:    [[A:%.*]] = add nsw <2 x i32> [[MASKED]], splat (i32 -1)
-; CHECK-NEXT:    [[R:%.*]] = lshr <2 x i32> splat (i32 2), [[A]]
+; CHECK-NEXT:    [[R:%.*]] = lshr <2 x i32> splat (i32 4), [[MASKED]]
 ; CHECK-NEXT:    ret <2 x i32> [[R]]
 ;
   %masked = and <2 x i32> %x, <i32 15, i32 15>
@@ -866,8 +862,7 @@ define i32 @from_sub_issue222281(i8 %x) {
 ; CHECK-NEXT:    [[V:%.*]] = or disjoint i8 [[M]], -128
 ; CHECK-NEXT:    [[TZ:%.*]] = call range(i8 3, 8) i8 @llvm.cttz.i8(i8 [[V]], i1 true)
 ; CHECK-NEXT:    [[Z:%.*]] = zext nneg i8 [[TZ]] to i32
-; CHECK-NEXT:    [[S:%.*]] = add nsw i32 [[Z]], -3
-; CHECK-NEXT:    [[R:%.*]] = lshr i32 64, [[S]]
+; CHECK-NEXT:    [[R:%.*]] = lshr exact i32 512, [[Z]]
 ; CHECK-NEXT:    ret i32 [[R]]
 ;
   %m = and i8 %x, -8
@@ -919,8 +914,7 @@ define i32 @lshr_add_negative_no_exact_overflow(i32 range(i32 28, 32) %x) {
 define i32 @lshr_add_negative_no_exact_max_bitwidth_minus_one(i32 %x) {
 ; CHECK-LABEL: @lshr_add_negative_no_exact_max_bitwidth_minus_one(
 ; CHECK-NEXT:    [[MASKED:%.*]] = and i32 [[X:%.*]], 31
-; CHECK-NEXT:    [[A:%.*]] = add nsw i32 [[MASKED]], -1
-; CHECK-NEXT:    [[R:%.*]] = lshr i32 2, [[A]]
+; CHECK-NEXT:    [[R:%.*]] = lshr i32 4, [[MASKED]]
 ; CHECK-NEXT:    ret i32 [[R]]
 ;
   %masked = and i32 %x, 31



More information about the llvm-commits mailing list