[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 00:54:53 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-llvm-transforms

Author: Ineshmcw

<details>
<summary>Changes</summary>

InstCombine currently misses simplifying this pattern to a single shl+add 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.

This happens because 'visitTrunc' in InstCombineCasts.cpp lacked a pattern match for this specific add-then-shift-right shape, falling through the existing folds (which only handled sequences like 'trunc(lshr(sext A), C)').

This patch adds a new match in 'visitTrunc' to recognize and fold this sequence into the simplified shl+add form.

Fixes #<!-- -->214339

Generalized Proof - [Alive2](https://alive2.llvm.org/ce/z/UzfB2m)

---
Full diff: https://github.com/llvm/llvm-project/pull/214562.diff


2 Files Affected:

- (modified) llvm/lib/Transforms/InstCombine/InstCombineSimplifyDemanded.cpp (+20) 
- (added) llvm/test/Transforms/InstCombine/trunc-lshr-add-shl.ll (+276) 


``````````diff
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
+}

``````````

</details>


https://github.com/llvm/llvm-project/pull/214562


More information about the llvm-commits mailing list