[llvm] [InstCombine] Fold lshr C1, (add X, -C2) -> lshr (C1 << C2), X (PR #222351)

Julian Gabriel via llvm-commits llvm-commits at lists.llvm.org
Mon Sep 21 14:00:19 PDT 2026


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

>From 170bf73b7249317ef1169b83656195403928f45f Mon Sep 17 00:00:00 2001
From: Julian Gabriel <juliangabriel570 at gmail.com>
Date: Wed, 9 Sep 2026 17:02:59 +0200
Subject: [PATCH 1/2] [InstCombine] Fold pre-shift of constant with negative
 offset when shift operand is in-range

Relax the pre-shift fold in commonShiftTransforms:
  C << (X - offset) -> (C >> offset) << X
  C >> (X - offset) -> (C << offset) >> X

Previously, this fold required exact on lshr/ashr or nsw/nuw on shl
to guarantee correctness for out-of-range shifts. If the shift operand
X is known to be less than BitWidth (via computeKnownBits), no poison
is introduced and the fold can be safely performed without these flags.

Fixes #222281
---
 .../InstCombine/InstCombineShifts.cpp         |  54 ++++++---
 llvm/test/Transforms/InstCombine/shift-add.ll | 106 ++++++++++++++++++
 2 files changed, 144 insertions(+), 16 deletions(-)

diff --git a/llvm/lib/Transforms/InstCombine/InstCombineShifts.cpp b/llvm/lib/Transforms/InstCombine/InstCombineShifts.cpp
index 7e32abcd6e9ba5..ad34bd17dabf81 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineShifts.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineShifts.cpp
@@ -474,31 +474,53 @@ Instruction *InstCombinerImpl::commonShiftTransforms(BinaryOperator &I) {
         (-*AddC).ult(BitWidth)) {
       unsigned PosOffset = (-*AddC).getZExtValue();
 
-      auto isSuitableForPreShift = [PosOffset, &I, AC]() {
+      auto isRoundtrippable = [PosOffset, &I, AC]() {
         switch (I.getOpcode()) {
         default:
           return false;
         case Instruction::Shl:
-          return (I.hasNoSignedWrap() || I.hasNoUnsignedWrap()) &&
-                 AC->eq(AC->lshr(PosOffset).shl(PosOffset));
+          return AC->eq(AC->lshr(PosOffset).shl(PosOffset));
         case Instruction::LShr:
-          return I.isExact() && AC->eq(AC->shl(PosOffset).lshr(PosOffset));
+          return AC->eq(AC->shl(PosOffset).lshr(PosOffset));
         case Instruction::AShr:
-          return I.isExact() && AC->eq(AC->shl(PosOffset).ashr(PosOffset));
+          return AC->eq(AC->shl(PosOffset).ashr(PosOffset));
         }
       };
-      if (isSuitableForPreShift()) {
-        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) {
-          NewShiftOp->setHasNoUnsignedWrap(I.hasNoUnsignedWrap());
-        } else {
-          NewShiftOp->setIsExact();
+
+      if (isRoundtrippable()) {
+        // The shift flags (exact/nsw/nuw) guarantee correctness for any
+        // shift amount. Without them, we need X to be known less than
+        // BitWidth to avoid converting a defined result to poison.
+        bool HasShiftFlags;
+        switch (I.getOpcode()) {
+        default:
+          HasShiftFlags = false;
+          break;
+        case Instruction::Shl:
+          HasShiftFlags = I.hasNoSignedWrap() || I.hasNoUnsignedWrap();
+          break;
+        case Instruction::LShr:
+        case Instruction::AShr:
+          HasShiftFlags = I.isExact();
+          break;
+        }
+
+        if (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 (HasShiftFlags) {
+            if (I.getOpcode() == Instruction::Shl) {
+              NewShiftOp->setHasNoUnsignedWrap(I.hasNoUnsignedWrap());
+            } else {
+              NewShiftOp->setIsExact();
+            }
+          }
+          return NewShiftOp;
         }
-        return NewShiftOp;
       }
     }
 
diff --git a/llvm/test/Transforms/InstCombine/shift-add.ll b/llvm/test/Transforms/InstCombine/shift-add.ll
index 81cbc2ac23b5f2..d55e73a8416280 100644
--- a/llvm/test/Transforms/InstCombine/shift-add.ll
+++ b/llvm/test/Transforms/InstCombine/shift-add.ll
@@ -804,3 +804,109 @@ 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:    [[R:%.*]] = lshr i32 4, [[MASKED]]
+; 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:    [[R:%.*]] = shl nuw nsw i32 3, [[MASKED]]
+; 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:    [[R:%.*]] = ashr i32 -8, [[MASKED]]
+; 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:    [[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>
+  %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:    [[R:%.*]] = lshr exact i32 512, [[Z]]
+; 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
+}
+

>From 06ac6ffc0077ff0fa79257a6af93af32a82c14c6 Mon Sep 17 00:00:00 2001
From: Julian Gabriel <juliangabriel570 at gmail.com>
Date: Mon, 21 Sep 2026 22:23:03 +0200
Subject: [PATCH 2/2] Simplify pre-shift fold and add boundary tests

Merge the flag and roundtrip checks into a single switch, document the
X u< BitWidth condition, and add tests for the BitWidth boundary.
---
 .../InstCombine/InstCombineShifts.cpp         | 80 ++++++++-----------
 llvm/test/Transforms/InstCombine/shift-add.ll | 37 +++++++++
 2 files changed, 71 insertions(+), 46 deletions(-)

diff --git a/llvm/lib/Transforms/InstCombine/InstCombineShifts.cpp b/llvm/lib/Transforms/InstCombine/InstCombineShifts.cpp
index ad34bd17dabf81..6f0dd0bc4f5108 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineShifts.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineShifts.cpp
@@ -469,58 +469,46 @@ 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 isRoundtrippable = [PosOffset, &I, AC]() {
-        switch (I.getOpcode()) {
-        default:
-          return false;
-        case Instruction::Shl:
-          return AC->eq(AC->lshr(PosOffset).shl(PosOffset));
-        case Instruction::LShr:
-          return AC->eq(AC->shl(PosOffset).lshr(PosOffset));
-        case Instruction::AShr:
-          return AC->eq(AC->shl(PosOffset).ashr(PosOffset));
-        }
-      };
-
-      if (isRoundtrippable()) {
-        // The shift flags (exact/nsw/nuw) guarantee correctness for any
-        // shift amount. Without them, we need X to be known less than
-        // BitWidth to avoid converting a defined result to poison.
-        bool HasShiftFlags;
-        switch (I.getOpcode()) {
-        default:
-          HasShiftFlags = false;
-          break;
-        case Instruction::Shl:
-          HasShiftFlags = I.hasNoSignedWrap() || I.hasNoUnsignedWrap();
-          break;
-        case Instruction::LShr:
-        case Instruction::AShr:
-          HasShiftFlags = I.isExact();
-          break;
-        }
+      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 (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 (HasShiftFlags) {
-            if (I.getOpcode() == Instruction::Shl) {
-              NewShiftOp->setHasNoUnsignedWrap(I.hasNoUnsignedWrap());
-            } else {
-              NewShiftOp->setIsExact();
-            }
-          }
-          return NewShiftOp;
-        }
+      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)
+          NewShiftOp->setHasNoUnsignedWrap(I.hasNoUnsignedWrap());
+        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 d55e73a8416280..6439ec0f51af87 100644
--- a/llvm/test/Transforms/InstCombine/shift-add.ll
+++ b/llvm/test/Transforms/InstCombine/shift-add.ll
@@ -910,3 +910,40 @@ define i32 @lshr_add_negative_no_exact_overflow(i32 range(i32 28, 32) %x) {
   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:    [[R:%.*]] = lshr i32 4, [[MASKED]]
+; 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
+}



More information about the llvm-commits mailing list