[llvm] [InstCombine] Fold trunc(lshr(add(shl(X, ShAmt), C), ShrAmt)) (PR #214562)
via llvm-commits
llvm-commits at lists.llvm.org
Tue Aug 11 01:50:27 PDT 2026
https://github.com/Ineshmcw updated https://github.com/llvm/llvm-project/pull/214562
>From 697bef34604954e97751b64f1858bae4e9ebd987 Mon Sep 17 00:00:00 2001
From: Sree Inesh Murugan Palanisamy <inesh.murugan at multicorewareinc.com>
Date: Thu, 6 Aug 2026 15:39:26 +0530
Subject: [PATCH 1/2] [InstCombine] Fold trunc(lshr(BinOp(shl(X, ShAmt), C),
ShrAmt)) (llvm#214339)
InstCombine currently misses simplifying this pattern to a single
shl+BinOp when the lshr is provably redundant. For example,
'trunc i8 (lshr (add (shl X, 12), 34816), 8)' always equals
'add (shl X, 4), 136' for every possible X. However, the chain from the
issue's zlib_deflate-derived example was left as 4 instructions instead
of 3. The same simplification also holds when the inner operator is
'or', 'xor', or 'and' instead of 'add'.
This patch adds a new case to the LShr handling in
SimplifyDemandedUseBits that recognizes this pattern for 'add', 'or',
'xor', and 'and', and folds it into the simplified shl+BinOp form.
'add' requires C's low ShrAmt bits to be zero, since it doesn't
distribute over 'lshr' unconditionally like the other three. Placing
the fold here, rather than in visitTrunc, makes it apply whenever
demanded bits are narrowed on the lshr's result, not only for trunc.
Fixes #214339
---
.../InstCombineSimplifyDemanded.cpp | 20 ++
.../InstCombine/trunc-lshr-add-shl.ll | 276 ++++++++++++++++++
2 files changed, 296 insertions(+)
create mode 100644 llvm/test/Transforms/InstCombine/trunc-lshr-add-shl.ll
diff --git a/llvm/lib/Transforms/InstCombine/InstCombineSimplifyDemanded.cpp b/llvm/lib/Transforms/InstCombine/InstCombineSimplifyDemanded.cpp
index 0a7c0375106f2..06819a6339de4 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineSimplifyDemanded.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineSimplifyDemanded.cpp
@@ -863,6 +863,26 @@ Value *InstCombinerImpl::SimplifyDemandedUseBits(Instruction *I,
X, ConstantInt::get(X->getType(), Factor->lshr(ShiftAmt)));
return InsertNewInstWith(Mul, I->getIterator());
}
+
+ // (BinOp (shl InnerX, InnerShAmt), BOC) >> ShiftAmt -->
+ // BinOp (shl InnerX, InnerShAmt - ShiftAmt), (BOC >> ShiftAmt)
+ BinaryOperator *BO;
+ Value *InnerX;
+ const APInt *InnerShAmt, *BOC;
+ if (match(I->getOperand(0), m_OneUse(m_BinOp(BO))) &&
+ match(BO->getOperand(0),
+ m_Shl(m_Value(InnerX), m_APInt(InnerShAmt))) &&
+ match(BO->getOperand(1), m_APInt(BOC)) &&
+ InnerShAmt->uge(ShiftAmt) &&
+ (BO->isBitwiseLogicOp() || (BO->getOpcode() == Instruction::Add &&
+ BOC->countr_zero() >= ShiftAmt))) {
+ Value *NewShl = Builder.CreateShl(
+ InnerX, ConstantInt::get(VTy, *InnerShAmt - ShiftAmt));
+ Constant *NewBOC = ConstantInt::get(VTy, BOC->lshr(ShiftAmt));
+ Instruction *NewBO =
+ BinaryOperator::Create(BO->getOpcode(), NewShl, NewBOC);
+ return InsertNewInstWith(NewBO, I->getIterator());
+ }
}
// Unsigned shift right.
diff --git a/llvm/test/Transforms/InstCombine/trunc-lshr-add-shl.ll b/llvm/test/Transforms/InstCombine/trunc-lshr-add-shl.ll
new file mode 100644
index 0000000000000..62abbd0284c90
--- /dev/null
+++ b/llvm/test/Transforms/InstCombine/trunc-lshr-add-shl.ll
@@ -0,0 +1,276 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 5
+; RUN: opt -S -passes=instcombine < %s | FileCheck %s
+
+; trunc(lshr(BinOp(shl(X, ShAmt), C), ShrAmt))
+; --> trunc(BinOp(shl(X, ShAmt - ShrAmt), C >> ShrAmt))
+; where BinOp is add, or, xor, or and. or/xor/and distribute over lshr
+; unconditionally; add additionally needs C's low ShrAmt bits to be zero.
+;
+; %x is the result of an opaque call in most tests below purely so the
+; RUN line's output is stable and easy to read; InstCombine's generic
+; "shrink the whole expression to a smaller type" machinery still applies
+; on top of this fold once the lshr is gone, which is expected -- it just
+; means the fold enables further cleanup, not that it's being bypassed.
+declare i32 @opaque(i32)
+declare void @use32(i32)
+
+define i8 @src_from_issue(i32 %x) {
+; CHECK-LABEL: define i8 @src_from_issue(
+; CHECK-SAME: i32 [[X:%.*]]) {
+; CHECK-NEXT: [[X_TR:%.*]] = trunc i32 [[X]] to i8
+; CHECK-NEXT: [[TMP1:%.*]] = shl i8 [[X_TR]], 4
+; CHECK-NEXT: [[V17:%.*]] = add i8 [[TMP1]], -120
+; CHECK-NEXT: ret i8 [[V17]]
+;
+ %v2 = shl i32 %x, 12
+ %v3 = add i32 %v2, 34816
+ %v16 = lshr i32 %v3, 8
+ %v17 = trunc i32 %v16 to i8
+ ret i8 %v17
+}
+
+define i8 @positive_basic(i32 %y) {
+; CHECK-LABEL: define i8 @positive_basic(
+; CHECK-SAME: i32 [[Y:%.*]]) {
+; CHECK-NEXT: [[X:%.*]] = call i32 @opaque(i32 [[Y]])
+; CHECK-NEXT: [[X_TR:%.*]] = trunc i32 [[X]] to i8
+; CHECK-NEXT: [[TMP1:%.*]] = shl i8 [[X_TR]], 4
+; CHECK-NEXT: [[V17:%.*]] = add i8 [[TMP1]], -120
+; CHECK-NEXT: ret i8 [[V17]]
+;
+ %x = call i32 @opaque(i32 %y)
+ %v2 = shl i32 %x, 12
+ %v3 = add i32 %v2, 34816
+ %v16 = lshr i32 %v3, 8
+ %v17 = trunc i32 %v16 to i8
+ ret i8 %v17
+}
+
+; ShrAmt == ShAmt: the new shift amount is 0.
+define i8 @positive_boundary_shramt_eq_shamt(i32 %y) {
+; CHECK-LABEL: define i8 @positive_boundary_shramt_eq_shamt(
+; CHECK-SAME: i32 [[Y:%.*]]) {
+; CHECK-NEXT: [[X:%.*]] = call i32 @opaque(i32 [[Y]])
+; CHECK-NEXT: [[TMP1:%.*]] = trunc i32 [[X]] to i8
+; CHECK-NEXT: [[V17:%.*]] = add i8 [[TMP1]], -120
+; CHECK-NEXT: ret i8 [[V17]]
+;
+ %x = call i32 @opaque(i32 %y)
+ %v2 = shl i32 %x, 8
+ %v3 = add i32 %v2, 34816
+ %v16 = lshr i32 %v3, 8
+ %v17 = trunc i32 %v16 to i8
+ ret i8 %v17
+}
+
+; ShrAmt == SrcWidth - DestWidth (24): still safe, right at the boundary.
+define i8 @positive_boundary_shramt_eq_srcwidth_minus_destwidth(i32 %y) {
+; CHECK-LABEL: define i8 @positive_boundary_shramt_eq_srcwidth_minus_destwidth(
+; CHECK-SAME: i32 [[Y:%.*]]) {
+; CHECK-NEXT: [[X:%.*]] = call i32 @opaque(i32 [[Y]])
+; CHECK-NEXT: [[TMP1:%.*]] = trunc i32 [[X]] to i8
+; CHECK-NEXT: [[V17:%.*]] = add i8 [[TMP1]], 1
+; CHECK-NEXT: ret i8 [[V17]]
+;
+ %x = call i32 @opaque(i32 %y)
+ %v2 = shl i32 %x, 24
+ %v3 = add i32 %v2, 16777216
+ %v16 = lshr i32 %v3, 24
+ %v17 = trunc i32 %v16 to i8
+ ret i8 %v17
+}
+
+; The fold also applies when the inner binop is or/xor/and instead of add.
+define i8 @positive_or(i32 %y) {
+; CHECK-LABEL: define i8 @positive_or(
+; CHECK-SAME: i32 [[Y:%.*]]) {
+; CHECK-NEXT: [[X:%.*]] = call i32 @opaque(i32 [[Y]])
+; CHECK-NEXT: [[X_TR:%.*]] = trunc i32 [[X]] to i8
+; CHECK-NEXT: [[TMP1:%.*]] = shl i8 [[X_TR]], 4
+; CHECK-NEXT: [[V17:%.*]] = or i8 [[TMP1]], -120
+; CHECK-NEXT: ret i8 [[V17]]
+;
+ %x = call i32 @opaque(i32 %y)
+ %v2 = shl i32 %x, 12
+ %v3 = or i32 %v2, 34816
+ %v16 = lshr i32 %v3, 8
+ %v17 = trunc i32 %v16 to i8
+ ret i8 %v17
+}
+
+define i8 @positive_xor(i32 %y) {
+; CHECK-LABEL: define i8 @positive_xor(
+; CHECK-SAME: i32 [[Y:%.*]]) {
+; CHECK-NEXT: [[X:%.*]] = call i32 @opaque(i32 [[Y]])
+; CHECK-NEXT: [[X_TR:%.*]] = trunc i32 [[X]] to i8
+; CHECK-NEXT: [[TMP1:%.*]] = shl i8 [[X_TR]], 4
+; CHECK-NEXT: [[V17:%.*]] = xor i8 [[TMP1]], -120
+; CHECK-NEXT: ret i8 [[V17]]
+;
+ %x = call i32 @opaque(i32 %y)
+ %v2 = shl i32 %x, 12
+ %v3 = xor i32 %v2, 34816
+ %v16 = lshr i32 %v3, 8
+ %v17 = trunc i32 %v16 to i8
+ ret i8 %v17
+}
+
+define i8 @positive_and(i32 %y) {
+; CHECK-LABEL: define i8 @positive_and(
+; CHECK-SAME: i32 [[Y:%.*]]) {
+; CHECK-NEXT: [[X:%.*]] = call i32 @opaque(i32 [[Y]])
+; CHECK-NEXT: [[X_TR:%.*]] = trunc i32 [[X]] to i8
+; CHECK-NEXT: [[TMP1:%.*]] = shl i8 [[X_TR]], 4
+; CHECK-NEXT: [[V17:%.*]] = and i8 [[TMP1]], -128
+; CHECK-NEXT: ret i8 [[V17]]
+;
+ %x = call i32 @opaque(i32 %y)
+ %v2 = shl i32 %x, 12
+ %v3 = and i32 %v2, 34816
+ %v16 = lshr i32 %v3, 8
+ %v17 = trunc i32 %v16 to i8
+ ret i8 %v17
+}
+
+; Unlike add, or/xor/and distribute over lshr unconditionally, so they still
+; fold even with a constant (34817, odd) whose low bits aren't aligned to
+; ShrAmt -- compare against @negative_addc_not_aligned below, which uses the
+; same constant with add and correctly does not fold.
+define i8 @positive_or_misaligned_c(i32 %y) {
+; CHECK-LABEL: define i8 @positive_or_misaligned_c(
+; CHECK-SAME: i32 [[Y:%.*]]) {
+; CHECK-NEXT: [[X:%.*]] = call i32 @opaque(i32 [[Y]])
+; CHECK-NEXT: [[X_TR:%.*]] = trunc i32 [[X]] to i8
+; CHECK-NEXT: [[TMP1:%.*]] = shl i8 [[X_TR]], 4
+; CHECK-NEXT: [[V17:%.*]] = or i8 [[TMP1]], -120
+; CHECK-NEXT: ret i8 [[V17]]
+;
+ %x = call i32 @opaque(i32 %y)
+ %v2 = shl i32 %x, 12
+ %v3 = or i32 %v2, 34817
+ %v16 = lshr i32 %v3, 8
+ %v17 = trunc i32 %v16 to i8
+ ret i8 %v17
+}
+
+define i8 @positive_xor_misaligned_c(i32 %y) {
+; CHECK-LABEL: define i8 @positive_xor_misaligned_c(
+; CHECK-SAME: i32 [[Y:%.*]]) {
+; CHECK-NEXT: [[X:%.*]] = call i32 @opaque(i32 [[Y]])
+; CHECK-NEXT: [[X_TR:%.*]] = trunc i32 [[X]] to i8
+; CHECK-NEXT: [[TMP1:%.*]] = shl i8 [[X_TR]], 4
+; CHECK-NEXT: [[V17:%.*]] = xor i8 [[TMP1]], -120
+; CHECK-NEXT: ret i8 [[V17]]
+;
+ %x = call i32 @opaque(i32 %y)
+ %v2 = shl i32 %x, 12
+ %v3 = xor i32 %v2, 34817
+ %v16 = lshr i32 %v3, 8
+ %v17 = trunc i32 %v16 to i8
+ ret i8 %v17
+}
+
+define i8 @positive_and_misaligned_c(i32 %y) {
+; CHECK-LABEL: define i8 @positive_and_misaligned_c(
+; CHECK-SAME: i32 [[Y:%.*]]) {
+; CHECK-NEXT: [[X:%.*]] = call i32 @opaque(i32 [[Y]])
+; CHECK-NEXT: [[X_TR:%.*]] = trunc i32 [[X]] to i8
+; CHECK-NEXT: [[TMP1:%.*]] = shl i8 [[X_TR]], 4
+; CHECK-NEXT: [[V17:%.*]] = and i8 [[TMP1]], -128
+; CHECK-NEXT: ret i8 [[V17]]
+;
+ %x = call i32 @opaque(i32 %y)
+ %v2 = shl i32 %x, 12
+ %v3 = and i32 %v2, 34817
+ %v16 = lshr i32 %v3, 8
+ %v17 = trunc i32 %v16 to i8
+ ret i8 %v17
+}
+
+; Negative test: ShrAmt (4) < ShAmt is fine, but here ShrAmt (8) > ShAmt (4),
+; which would require a negative shift amount, so the fold must not fire.
+define i8 @negative_shramt_gt_shamt(i32 %y) {
+; CHECK-LABEL: define i8 @negative_shramt_gt_shamt(
+; CHECK-SAME: i32 [[Y:%.*]]) {
+; CHECK-NEXT: [[X:%.*]] = call i32 @opaque(i32 [[Y]])
+; CHECK-NEXT: [[V2:%.*]] = shl i32 [[X]], 4
+; CHECK-NEXT: [[V3:%.*]] = add i32 [[V2]], 34816
+; CHECK-NEXT: [[V16:%.*]] = lshr i32 [[V3]], 8
+; CHECK-NEXT: [[V17:%.*]] = trunc i32 [[V16]] to i8
+; CHECK-NEXT: ret i8 [[V17]]
+;
+ %x = call i32 @opaque(i32 %y)
+ %v2 = shl i32 %x, 4
+ %v3 = add i32 %v2, 34816
+ %v16 = lshr i32 %v3, 8
+ %v17 = trunc i32 %v16 to i8
+ ret i8 %v17
+}
+
+; Negative test: AddC's low ShrAmt bits are not zero (34817 is not a
+; multiple of 256), so the lshr can't be distributed over the add exactly.
+; %v3 is kept alive via @use32 so an unrelated demanded-bits simplification
+; can't quietly clear that low bit before this fold's check ever runs.
+define i8 @negative_addc_not_aligned(i32 %y) {
+; CHECK-LABEL: define i8 @negative_addc_not_aligned(
+; CHECK-SAME: i32 [[Y:%.*]]) {
+; CHECK-NEXT: [[X:%.*]] = call i32 @opaque(i32 [[Y]])
+; CHECK-NEXT: [[V2:%.*]] = shl i32 [[X]], 12
+; CHECK-NEXT: [[V3:%.*]] = add i32 [[V2]], 34817
+; CHECK-NEXT: call void @use32(i32 [[V3]])
+; CHECK-NEXT: [[V16:%.*]] = lshr i32 [[V3]], 8
+; CHECK-NEXT: [[V17:%.*]] = trunc i32 [[V16]] to i8
+; CHECK-NEXT: ret i8 [[V17]]
+;
+ %x = call i32 @opaque(i32 %y)
+ %v2 = shl i32 %x, 12
+ %v3 = add i32 %v2, 34817
+ call void @use32(i32 %v3)
+ %v16 = lshr i32 %v3, 8
+ %v17 = trunc i32 %v16 to i8
+ ret i8 %v17
+}
+
+; Negative test: ShrAmt (25) > SrcWidth - DestWidth (24), even though AddC
+; (2^25) is still a multiple of 2^25. A freshly computed "X << (ShAmt -
+; ShrAmt)" would disagree with "(X << ShAmt) >> ShrAmt" in a bit that the
+; final trunc to i8 would still observe, so the fold must not fire.
+define i8 @negative_shramt_too_large(i32 %y) {
+; CHECK-LABEL: define i8 @negative_shramt_too_large(
+; CHECK-SAME: i32 [[Y:%.*]]) {
+; CHECK-NEXT: [[X:%.*]] = call i32 @opaque(i32 [[Y]])
+; CHECK-NEXT: [[X_TR:%.*]] = trunc i32 [[X]] to i8
+; CHECK-NEXT: [[TMP1:%.*]] = shl i8 [[X_TR]], 5
+; CHECK-NEXT: [[TMP2:%.*]] = and i8 [[TMP1]], 96
+; CHECK-NEXT: [[V17:%.*]] = or disjoint i8 [[TMP2]], 1
+; CHECK-NEXT: ret i8 [[V17]]
+;
+ %x = call i32 @opaque(i32 %y)
+ %v2 = shl i32 %x, 30
+ %v3 = add i32 %v2, 33554432
+ %v16 = lshr i32 %v3, 25
+ %v17 = trunc i32 %v16 to i8
+ ret i8 %v17
+}
+
+; Negative test: the lshr has another use, so replacing the trunc's operand
+; would leave the original computation behind rather than eliminating it.
+define i8 @negative_lshr_multi_use(i32 %y, ptr %p) {
+; CHECK-LABEL: define i8 @negative_lshr_multi_use(
+; CHECK-SAME: i32 [[Y:%.*]], ptr [[P:%.*]]) {
+; CHECK-NEXT: [[X:%.*]] = call i32 @opaque(i32 [[Y]])
+; CHECK-NEXT: [[V2:%.*]] = shl i32 [[X]], 12
+; CHECK-NEXT: [[V3:%.*]] = add i32 [[V2]], 34816
+; CHECK-NEXT: [[V16:%.*]] = lshr exact i32 [[V3]], 8
+; CHECK-NEXT: store i32 [[V16]], ptr [[P]], align 4
+; CHECK-NEXT: [[V17:%.*]] = trunc i32 [[V16]] to i8
+; CHECK-NEXT: ret i8 [[V17]]
+;
+ %x = call i32 @opaque(i32 %y)
+ %v2 = shl i32 %x, 12
+ %v3 = add i32 %v2, 34816
+ %v16 = lshr i32 %v3, 8
+ store i32 %v16, ptr %p
+ %v17 = trunc i32 %v16 to i8
+ ret i8 %v17
+}
>From 2b09c8764ce28d4124934e971b2b878e140ec61e Mon Sep 17 00:00:00 2001
From: Sree Inesh Murugan Palanisamy <inesh.murugan at multicorewareinc.com>
Date: Tue, 11 Aug 2026 14:18:07 +0530
Subject: [PATCH 2/2] [InstCombine] Fix instruction ordering in the new
shl+BinOp fold (llvm#214339)
The new shl was inserted via Builder.CreateShl, which uses whatever
insertion point the IRBuilder happened to be at rather than the
lshr's location. When that point came later in the block than the add
that consumes the shl, the add ended up dominating the shl instead of
the other way around, and the verifier rightly complained.
Insert the shl with InsertNewInstWith right before the lshr, same as
the add already was. Added a test with an and-mask consumer since
that's the shape that triggered it.
---
.../InstCombine/InstCombineSimplifyDemanded.cpp | 6 ++++--
.../InstCombine/trunc-lshr-add-shl.ll | 17 +++++++++++++++++
2 files changed, 21 insertions(+), 2 deletions(-)
diff --git a/llvm/lib/Transforms/InstCombine/InstCombineSimplifyDemanded.cpp b/llvm/lib/Transforms/InstCombine/InstCombineSimplifyDemanded.cpp
index 06819a6339de4..9d38d3d6893e4 100644
--- a/llvm/lib/Transforms/InstCombine/InstCombineSimplifyDemanded.cpp
+++ b/llvm/lib/Transforms/InstCombine/InstCombineSimplifyDemanded.cpp
@@ -876,8 +876,10 @@ Value *InstCombinerImpl::SimplifyDemandedUseBits(Instruction *I,
InnerShAmt->uge(ShiftAmt) &&
(BO->isBitwiseLogicOp() || (BO->getOpcode() == Instruction::Add &&
BOC->countr_zero() >= ShiftAmt))) {
- Value *NewShl = Builder.CreateShl(
- InnerX, ConstantInt::get(VTy, *InnerShAmt - ShiftAmt));
+ Value *NewShl = InsertNewInstWith(
+ BinaryOperator::CreateShl(
+ InnerX, ConstantInt::get(VTy, *InnerShAmt - ShiftAmt)),
+ I->getIterator());
Constant *NewBOC = ConstantInt::get(VTy, BOC->lshr(ShiftAmt));
Instruction *NewBO =
BinaryOperator::Create(BO->getOpcode(), NewShl, NewBOC);
diff --git a/llvm/test/Transforms/InstCombine/trunc-lshr-add-shl.ll b/llvm/test/Transforms/InstCombine/trunc-lshr-add-shl.ll
index 62abbd0284c90..463dc41ff5b6e 100644
--- a/llvm/test/Transforms/InstCombine/trunc-lshr-add-shl.ll
+++ b/llvm/test/Transforms/InstCombine/trunc-lshr-add-shl.ll
@@ -187,6 +187,23 @@ define i8 @positive_and_misaligned_c(i32 %y) {
ret i8 %v17
}
+; Regression test: an "and" mask (not just trunc) used to crash the fold
+; with a dominance violation.
+define i32 @regression_and_mask_dominance(i32 %x) {
+; CHECK-LABEL: define i32 @regression_and_mask_dominance(
+; CHECK-SAME: i32 [[X:%.*]]) {
+; CHECK-NEXT: [[TMP1:%.*]] = shl i32 [[X]], 4
+; CHECK-NEXT: [[TMP2:%.*]] = add i32 [[TMP1]], 136
+; CHECK-NEXT: [[R:%.*]] = and i32 [[TMP2]], 248
+; CHECK-NEXT: ret i32 [[R]]
+;
+ %v2 = shl i32 %x, 12
+ %v3 = add i32 %v2, 34816
+ %v16 = lshr i32 %v3, 8
+ %r = and i32 %v16, 255
+ ret i32 %r
+}
+
; Negative test: ShrAmt (4) < ShAmt is fine, but here ShrAmt (8) > ShAmt (4),
; which would require a negative shift amount, so the fold must not fire.
define i8 @negative_shramt_gt_shamt(i32 %y) {
More information about the llvm-commits
mailing list