[llvm] [SCEV] Generalize A + zext(-A + B) fold to A + zext(C + X) (PR #209160)

Aleksandr Popov via llvm-commits llvm-commits at lists.llvm.org
Tue Jul 14 02:35:52 PDT 2026


https://github.com/aleks-tmb updated https://github.com/llvm/llvm-project/pull/209160

>From 45c9703b0d287814d182f4d31a4a64ac6eb08d7e Mon Sep 17 00:00:00 2001
From: Aleksandr Popov <apopov at azul.com>
Date: Mon, 13 Jul 2026 11:40:29 +0000
Subject: [PATCH] [SCEV] Generalize A + zext(-A + B) fold to A + zext(C + X)

Extend the existing fold `A + zext(-A + B) -> zext(B)` to the more
general form `A + zext(C + X) -> (A + sext(C)) + zext(X)`, where C is
any negative constant and A is a constant so that `A + sext(C)` folds
to a single wide constant.

Correctness still relies on NUW of `(-C) + (C + X)`, which proves that
`C + X` does not wrap below zero, so `zext(C + X) == sext(C) + zext(X)`.

Related to #208778
---
 llvm/lib/Analysis/ScalarEvolution.cpp         | 30 ++++++++++-----
 .../ScalarEvolution/zext-add-nsw-fold.ll      |  8 ++--
 .../IndVarSimplify/scev-update-loop-opt.ll    |  6 +--
 .../single_early_exit_zext_trip_count.ll      | 38 ++++++++++++++++---
 4 files changed, 59 insertions(+), 23 deletions(-)

diff --git a/llvm/lib/Analysis/ScalarEvolution.cpp b/llvm/lib/Analysis/ScalarEvolution.cpp
index 134be6ac097e0..c332a40f3db13 100644
--- a/llvm/lib/Analysis/ScalarEvolution.cpp
+++ b/llvm/lib/Analysis/ScalarEvolution.cpp
@@ -2809,17 +2809,27 @@ const SCEV *ScalarEvolution::getAddExpr(SmallVectorImpl<SCEVUse> &Ops,
       }
     }
 
-    // Try to push the constant operand into a ZExt: A + zext (-A + B) -> zext
-    // (B), if trunc (A) + -A + B  does not unsigned-wrap.
-    const SCEVAddExpr *InnerAdd;
-    if (match(B, m_scev_ZExt(m_scev_Add(InnerAdd)))) {
-      const SCEV *NarrowA = getTruncateExpr(A, InnerAdd->getType());
-      if (NarrowA == getNegativeSCEV(InnerAdd->getOperand(0)) &&
-          getZeroExtendExpr(NarrowA, B->getType()) == A &&
-          hasFlags(StrengthenNoWrapFlags(this, scAddExpr, {NarrowA, InnerAdd},
-                                         SCEV::FlagAnyWrap),
+    // Push a negative constant addend out of a ZExt when the inner add is
+    // provably non-negative in the narrow type:
+    //
+    //   A + zext(C + X)  ->  WideAC + zext(X)   [WideAC = A + sext(C)]
+    //
+    // Require A to be a constant so that `A + sext(C)` folds into a single wide
+    // constant, actually simplifying the expression.
+    const SCEVAddExpr *Add;
+    if (isa<SCEVConstant>(A) && match(B, m_scev_ZExt(m_scev_Add(Add)))) {
+      const auto *CInner = dyn_cast<SCEVConstant>(Add->getOperand(0));
+      if (CInner && CInner->getAPInt().isNegative() &&
+          // NUW on `(-C) + (C + X) = X` proves that `C + X` did not wrap
+          // below zero, so `zext(C + X) == sext(C) + zext(X)`.
+          hasFlags(StrengthenNoWrapFlags(
+                       this, scAddExpr, {getConstant(-CInner->getAPInt()), Add},
+                       SCEV::FlagAnyWrap),
                    SCEV::FlagNUW)) {
-        return getZeroExtendExpr(getAddExpr(NarrowA, InnerAdd), B->getType());
+        SmallVector<SCEVUse, 4> XOps(drop_begin(Add->operands()));
+        const SCEV *X = getAddExpr(XOps);
+        return getAddExpr(A, getSignExtendExpr(CInner, B->getType()),
+                          getZeroExtendExpr(X, B->getType()));
       }
     }
   }
diff --git a/llvm/test/Analysis/ScalarEvolution/zext-add-nsw-fold.ll b/llvm/test/Analysis/ScalarEvolution/zext-add-nsw-fold.ll
index 79fbf9fd5be65..753ba3e92ea6b 100644
--- a/llvm/test/Analysis/ScalarEvolution/zext-add-nsw-fold.ll
+++ b/llvm/test/Analysis/ScalarEvolution/zext-add-nsw-fold.ll
@@ -107,7 +107,7 @@ define i64 @mul_umax_fold_round_up(i8 %x) {
 ; CHECK-NEXT:    %add = add nsw i32 -10, %mul
 ; CHECK-NEXT:    --> (-10 + (4 * (3 umax (zext i8 %x to i32)))<nuw><nsw>)<nsw> U: [2,1011) S: [2,1011)
 ; CHECK-NEXT:    %ext = zext i32 %add to i64
-; CHECK-NEXT:    --> (2 + (zext i32 (-12 + (4 * (3 umax (zext i8 %x to i32)))<nuw><nsw>)<nsw> to i64))<nuw><nsw> U: [2,1011) S: [2,1011)
+; CHECK-NEXT:    --> (-10 + (4 * (3 umax (zext i8 %x to i64)))<nuw><nsw>)<nsw> U: [2,1011) S: [2,1011)
 ; CHECK-NEXT:  Determining loop execution counts for: @mul_umax_fold_round_up
 ;
   %zx = zext i8 %x to i32
@@ -333,7 +333,7 @@ define i64 @mul_umax_fold_negative_a_non_cancelling(i8 %x) {
 ; CHECK-NEXT:    %add = add nsw i32 -5, %mul
 ; CHECK-NEXT:    --> (-5 + (4 * (3 umax (zext i8 %x to i32)))<nuw><nsw>)<nsw> U: [7,1016) S: [7,1016)
 ; CHECK-NEXT:    %ext = zext i32 %add to i64
-; CHECK-NEXT:    --> (3 + (zext i32 (-8 + (4 * (3 umax (zext i8 %x to i32)))<nuw><nsw>)<nsw> to i64))<nuw><nsw> U: [7,1016) S: [7,1016)
+; CHECK-NEXT:    --> (-5 + (4 * (3 umax (zext i8 %x to i64)))<nuw><nsw>)<nsw> U: [7,1016) S: [7,1016)
 ; CHECK-NEXT:  Determining loop execution counts for: @mul_umax_fold_negative_a_non_cancelling
 ;
   %zx = zext i8 %x to i32
@@ -361,9 +361,9 @@ define i128 @cascading_zext_fold(i8 %x) {
 ; CHECK-NEXT:    %add32 = add nsw i32 -10, %mul
 ; CHECK-NEXT:    --> (-10 + (4 * (3 umax (zext i8 %x to i32)))<nuw><nsw>)<nsw> U: [2,1011) S: [2,1011)
 ; CHECK-NEXT:    %ext64 = zext i32 %add32 to i64
-; CHECK-NEXT:    --> (2 + (zext i32 (-12 + (4 * (3 umax (zext i8 %x to i32)))<nuw><nsw>)<nsw> to i64))<nuw><nsw> U: [2,1011) S: [2,1011)
+; CHECK-NEXT:    --> (-10 + (4 * (3 umax (zext i8 %x to i64)))<nuw><nsw>)<nsw> U: [2,1011) S: [2,1011)
 ; CHECK-NEXT:    %ext128 = zext i64 %ext64 to i128
-; CHECK-NEXT:    --> (2 + (zext i32 (-12 + (4 * (3 umax (zext i8 %x to i32)))<nuw><nsw>)<nsw> to i128))<nuw><nsw> U: [2,1011) S: [2,1011)
+; CHECK-NEXT:    --> (-10 + (4 * (3 umax (zext i8 %x to i128)))<nuw><nsw>)<nsw> U: [2,1011) S: [2,1011)
 ; CHECK-NEXT:  Determining loop execution counts for: @cascading_zext_fold
 ;
   %zx = zext i8 %x to i32
diff --git a/llvm/test/Transforms/IndVarSimplify/scev-update-loop-opt.ll b/llvm/test/Transforms/IndVarSimplify/scev-update-loop-opt.ll
index f716796745fb7..01060490bb0f3 100644
--- a/llvm/test/Transforms/IndVarSimplify/scev-update-loop-opt.ll
+++ b/llvm/test/Transforms/IndVarSimplify/scev-update-loop-opt.ll
@@ -1,4 +1,4 @@
-; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 6
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --check-globals none --version 6
 ; RUN: opt %s -passes="loop(loop-idiom,indvars,loop-deletion,loop-unroll-full)" -S | FileCheck %s
 ; REQUIRES: asserts
 
@@ -10,15 +10,15 @@ define void @loop_limit_test(i32 %conv5, i1 %cmp13, i1 %cmp20, i1 %cmp27, i1 %cm
 ; CHECK-NEXT:  [[ENTRY:.*:]]
 ; CHECK-NEXT:    [[TMP0:%.*]] = add i32 [[CONV5]], 1
 ; CHECK-NEXT:    [[TMP1:%.*]] = zext i32 [[TMP0]] to i64
-; CHECK-NEXT:    [[TMP2:%.*]] = zext i32 [[CONV5]] to i64
 ; CHECK-NEXT:    br label %[[FOR_COND:.*]]
 ; CHECK:       [[FOR_COND_LOOPEXIT:.*]]:
 ; CHECK-NEXT:    br label %[[FOR_COND]]
 ; CHECK:       [[FOR_COND]]:
+; CHECK-NEXT:    [[TMP2:%.*]] = zext i32 [[CONV5]] to i64
 ; CHECK-NEXT:    br label %[[FOR_COND2:.*]]
 ; CHECK:       [[FOR_COND2]]:
 ; CHECK-NEXT:    [[INDVARS_IV:%.*]] = phi i64 [ [[INDVARS_IV_NEXT:%.*]], %[[FOR_COND_CLEANUP14:.*]] ], [ 0, %[[FOR_COND]] ]
-; CHECK-NEXT:    [[CMP6:%.*]] = icmp samesign ult i64 [[INDVARS_IV]], [[TMP2]]
+; CHECK-NEXT:    [[CMP6:%.*]] = icmp ne i64 [[INDVARS_IV]], [[TMP2]]
 ; CHECK-NEXT:    br i1 [[CMP6]], label %[[FOR_COND9_PREHEADER:.*]], label %[[FOR_COND_LOOPEXIT]]
 ; CHECK:       [[FOR_COND9_PREHEADER]]:
 ; CHECK-NEXT:    br label %[[FOR_COND9:.*]]
diff --git a/llvm/test/Transforms/LoopVectorize/single_early_exit_zext_trip_count.ll b/llvm/test/Transforms/LoopVectorize/single_early_exit_zext_trip_count.ll
index 59fe0321c0112..fb591fc3787b8 100644
--- a/llvm/test/Transforms/LoopVectorize/single_early_exit_zext_trip_count.ll
+++ b/llvm/test/Transforms/LoopVectorize/single_early_exit_zext_trip_count.ll
@@ -28,17 +28,43 @@ define void @test1(ptr %p) {
 ; CHECK:       [[PREHEADER]]:
 ; CHECK-NEXT:    [[EXIT_32:%.*]] = add nsw i32 [[LEN]], -1
 ; CHECK-NEXT:    [[EXIT:%.*]] = zext i32 [[EXIT_32]] to i64
+; CHECK-NEXT:    [[TMP0:%.*]] = add nuw nsw i64 [[EXIT]], 1
+; CHECK-NEXT:    [[MIN_ITERS_CHECK:%.*]] = icmp ult i64 [[TMP0]], 4
+; CHECK-NEXT:    br i1 [[MIN_ITERS_CHECK]], label %[[SCALAR_PH:.*]], label %[[VECTOR_PH:.*]]
+; CHECK:       [[VECTOR_PH]]:
+; CHECK-NEXT:    [[N_MOD_VF:%.*]] = urem i64 [[TMP0]], 4
+; CHECK-NEXT:    [[N_VEC:%.*]] = sub i64 [[TMP0]], [[N_MOD_VF]]
 ; CHECK-NEXT:    br label %[[LOOP:.*]]
 ; CHECK:       [[LOOP]]:
-; CHECK-NEXT:    [[IV:%.*]] = phi i64 [ [[IV_NEXT:%.*]], %[[LATCH:.*]] ], [ 0, %[[PREHEADER]] ]
+; CHECK-NEXT:    [[IV:%.*]] = phi i64 [ 0, %[[VECTOR_PH]] ], [ [[INDEX_NEXT:%.*]], %[[VECTOR_BODY_INTERIM:.*]] ]
 ; CHECK-NEXT:    [[ELEM_PTR:%.*]] = getelementptr inbounds nuw i8, ptr [[BASE]], i64 [[IV]]
-; CHECK-NEXT:    [[ELEM:%.*]] = load i8, ptr [[ELEM_PTR]], align 1
+; CHECK-NEXT:    [[WIDE_LOAD:%.*]] = load <4 x i8>, ptr [[ELEM_PTR]], align 1
+; CHECK-NEXT:    [[TMP2:%.*]] = icmp eq <4 x i8> [[WIDE_LOAD]], zeroinitializer
+; CHECK-NEXT:    [[TMP3:%.*]] = freeze <4 x i1> [[TMP2]]
+; CHECK-NEXT:    [[TMP4:%.*]] = call i1 @llvm.vector.reduce.or.v4i1(<4 x i1> [[TMP3]])
+; CHECK-NEXT:    [[INDEX_NEXT]] = add nuw i64 [[IV]], 4
+; CHECK-NEXT:    [[TMP5:%.*]] = icmp eq i64 [[INDEX_NEXT]], [[N_VEC]]
+; CHECK-NEXT:    br i1 [[TMP4]], label %[[VECTOR_EARLY_EXIT:.*]], label %[[VECTOR_BODY_INTERIM]]
+; CHECK:       [[VECTOR_BODY_INTERIM]]:
+; CHECK-NEXT:    br i1 [[TMP5]], label %[[MIDDLE_BLOCK:.*]], label %[[LOOP]], !llvm.loop [[LOOP1:![0-9]+]]
+; CHECK:       [[MIDDLE_BLOCK]]:
+; CHECK-NEXT:    [[CMP_N:%.*]] = icmp eq i64 [[TMP0]], [[N_VEC]]
+; CHECK-NEXT:    br i1 [[CMP_N]], label %[[RET_LOOPEXIT:.*]], label %[[SCALAR_PH]]
+; CHECK:       [[VECTOR_EARLY_EXIT]]:
+; CHECK-NEXT:    br label %[[RET_LOOPEXIT]]
+; CHECK:       [[SCALAR_PH]]:
+; CHECK-NEXT:    [[BC_RESUME_VAL:%.*]] = phi i64 [ [[N_VEC]], %[[MIDDLE_BLOCK]] ], [ 0, %[[PREHEADER]] ]
+; CHECK-NEXT:    br label %[[LOOP2:.*]]
+; CHECK:       [[LOOP2]]:
+; CHECK-NEXT:    [[IV1:%.*]] = phi i64 [ [[IV_NEXT:%.*]], %[[LATCH:.*]] ], [ [[BC_RESUME_VAL]], %[[SCALAR_PH]] ]
+; CHECK-NEXT:    [[ELEM_PTR1:%.*]] = getelementptr inbounds nuw i8, ptr [[BASE]], i64 [[IV1]]
+; CHECK-NEXT:    [[ELEM:%.*]] = load i8, ptr [[ELEM_PTR1]], align 1
 ; CHECK-NEXT:    [[IS_TARGET:%.*]] = icmp eq i8 [[ELEM]], 0
-; CHECK-NEXT:    br i1 [[IS_TARGET]], label %[[RET_LOOPEXIT:.*]], label %[[LATCH]]
+; CHECK-NEXT:    br i1 [[IS_TARGET]], label %[[RET_LOOPEXIT]], label %[[LATCH]]
 ; CHECK:       [[LATCH]]:
-; CHECK-NEXT:    [[IV_NEXT]] = add nuw nsw i64 [[IV]], 1
-; CHECK-NEXT:    [[LOOP_COND:%.*]] = icmp ult i64 [[IV]], [[EXIT]]
-; CHECK-NEXT:    br i1 [[LOOP_COND]], label %[[LOOP]], label %[[RET_LOOPEXIT]]
+; CHECK-NEXT:    [[IV_NEXT]] = add nuw nsw i64 [[IV1]], 1
+; CHECK-NEXT:    [[LOOP_COND:%.*]] = icmp ult i64 [[IV1]], [[EXIT]]
+; CHECK-NEXT:    br i1 [[LOOP_COND]], label %[[LOOP2]], label %[[RET_LOOPEXIT]], !llvm.loop [[LOOP4:![0-9]+]]
 ; CHECK:       [[RET_LOOPEXIT]]:
 ; CHECK-NEXT:    br label %[[RET]]
 ; CHECK:       [[RET]]:



More information about the llvm-commits mailing list