[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