[llvm] [InstSimplify] Fold value-preserving zext/sext of trunc (PR #204089)

Pengcheng Wang via llvm-commits llvm-commits at lists.llvm.org
Mon Jun 29 05:50:24 PDT 2026


https://github.com/wangpc-pp updated https://github.com/llvm/llvm-project/pull/204089

>From 3d504a4ce424ff8db07b3b7cd8bc7ace85cb4498 Mon Sep 17 00:00:00 2001
From: Pengcheng Wang <wangpengcheng.pp at bytedance.com>
Date: Mon, 15 Jun 2026 17:12:06 +0800
Subject: [PATCH 1/3] [GVN] Precommit tests

---
 .../GVN/PRE/pre-load-through-select.ll        | 50 +++++++++++++++++++
 1 file changed, 50 insertions(+)

diff --git a/llvm/test/Transforms/GVN/PRE/pre-load-through-select.ll b/llvm/test/Transforms/GVN/PRE/pre-load-through-select.ll
index dc4974e804c80..b9e818583023d 100644
--- a/llvm/test/Transforms/GVN/PRE/pre-load-through-select.ll
+++ b/llvm/test/Transforms/GVN/PRE/pre-load-through-select.ll
@@ -1148,3 +1148,53 @@ for.body:
   %res.0.lcssa = phi i32 [ 0, %entry ], [ %spec.select, %for.body ]
   ret i32 %res.0.lcssa
 }
+
+; The indvars-canonicalized form of a min-index loop: the induction variable is
+; an i64 `%iv`, `arr[i]` is addressed directly with `%iv`, while the select's
+; true value is `trunc nuw nsw %iv` and the running index is zero-extended back
+; for `arr[minloc]`.  The load through the select index should still be
+; eliminated: translating the true side yields `zext nneg (trunc nuw %iv)` which
+; folds back to `%iv`, matching the existing `arr[i]` address.
+define i32 @test_phi_select_index_loop_trunc(ptr %arr, i64 %n) {
+; CHECK-LABEL: @test_phi_select_index_loop_trunc(
+; CHECK-NEXT:  entry:
+; CHECK-NEXT:    br label [[FOR_BODY:%.*]]
+; CHECK:       for.body:
+; CHECK-NEXT:    [[IV:%.*]] = phi i64 [ 0, [[ENTRY:%.*]] ], [ [[IV_NEXT:%.*]], [[FOR_BODY]] ]
+; CHECK-NEXT:    [[MINLOC:%.*]] = phi i32 [ 0, [[ENTRY]] ], [ [[SEL:%.*]], [[FOR_BODY]] ]
+; CHECK-NEXT:    [[ARRAYIDX:%.*]] = getelementptr inbounds [4 x i8], ptr [[ARR:%.*]], i64 [[IV]]
+; CHECK-NEXT:    [[TMP1:%.*]] = load i32, ptr [[ARRAYIDX]], align 4
+; CHECK-NEXT:    [[IDXPROM:%.*]] = zext nneg i32 [[MINLOC]] to i64
+; CHECK-NEXT:    [[ARRAYIDX2:%.*]] = getelementptr inbounds [4 x i8], ptr [[ARR]], i64 [[IDXPROM]]
+; CHECK-NEXT:    [[TMP0:%.*]] = load i32, ptr [[ARRAYIDX2]], align 4
+; CHECK-NEXT:    [[CMP:%.*]] = icmp slt i32 [[TMP1]], [[TMP0]]
+; CHECK-NEXT:    [[TMP2:%.*]] = trunc nuw nsw i64 [[IV]] to i32
+; CHECK-NEXT:    [[SEL]] = select i1 [[CMP]], i32 [[TMP2]], i32 [[MINLOC]]
+; CHECK-NEXT:    [[IV_NEXT]] = add nuw nsw i64 [[IV]], 1
+; CHECK-NEXT:    [[EC:%.*]] = icmp eq i64 [[IV_NEXT]], [[N:%.*]]
+; CHECK-NEXT:    br i1 [[EC]], label [[EXIT:%.*]], label [[FOR_BODY]]
+; CHECK:       exit:
+; CHECK-NEXT:    ret i32 [[SEL]]
+;
+entry:
+  br label %for.body
+
+for.body:
+  %iv = phi i64 [ 0, %entry ], [ %iv.next, %for.body ]
+  %minloc = phi i32 [ 0, %entry ], [ %sel, %for.body ]
+  %arrayidx = getelementptr inbounds [4 x i8], ptr %arr, i64 %iv
+  %0 = load i32, ptr %arrayidx, align 4
+  %idxprom = zext nneg i32 %minloc to i64
+  %arrayidx2 = getelementptr inbounds [4 x i8], ptr %arr, i64 %idxprom
+  %1 = load i32, ptr %arrayidx2, align 4
+  %cmp = icmp slt i32 %0, %1
+  %2 = trunc nuw nsw i64 %iv to i32
+  %sel = select i1 %cmp, i32 %2, i32 %minloc
+  %iv.next = add nuw nsw i64 %iv, 1
+  %ec = icmp eq i64 %iv.next, %n
+  br i1 %ec, label %exit, label %for.body
+
+exit:
+  %r = phi i32 [ %sel, %for.body ]
+  ret i32 %r
+}

>From a4d330aaf6b13cde6ed73571727e891cc0756d1f Mon Sep 17 00:00:00 2001
From: Pengcheng Wang <wangpengcheng.pp at bytedance.com>
Date: Mon, 15 Jun 2026 17:15:51 +0800
Subject: [PATCH 2/3] [GVN] Fold value-preserving zext/sext of trunc during PHI
 translation

After indvars canonicalizes a min-index loop, the running index select
has a `trunc nuw nsw i64 %iv to i32` on its true side while `arr[i]` is
addressed directly with the i64 `%iv`. When PHI-translating the
select-dependent address along the backedge, the true side becomes
`gep base, zext nneg (trunc nuw %iv)`, which has no matching instruction
in the loop, so the select dependency was never formed and the redundant
load survived.

Teach PHITransAddr::translateSubExpr to fold a value-preserving
`zext nneg (trunc nuw X)` / `sext (trunc nsw X)` back to X (when the trunc
source type equals the extend result type and the no-wrap flags guarantee
the round-trip is identity). The translated address then matches the
existing `gep base, %iv`, allowing the load through the select index to be
eliminated -- fixing the integer min-index variant of
llvm/llvm-project#58569 (e.g. returning minloc instead of the value).

Add a regression test covering the indvars-canonicalized trunc form.

Assisted-by: TraeCli (AI assistant)
---
 llvm/lib/Analysis/InstructionSimplify.cpp     | 11 ++++
 .../GVN/PRE/pre-load-through-select.ll        | 60 +++++++++++++------
 2 files changed, 52 insertions(+), 19 deletions(-)

diff --git a/llvm/lib/Analysis/InstructionSimplify.cpp b/llvm/lib/Analysis/InstructionSimplify.cpp
index 4c05cd97c8124..52a0956f7ffd8 100644
--- a/llvm/lib/Analysis/InstructionSimplify.cpp
+++ b/llvm/lib/Analysis/InstructionSimplify.cpp
@@ -5668,6 +5668,17 @@ static Value *simplifyCastInst(unsigned CastOpc, Value *Op, Type *Ty,
       X->getType() == Ty && Ty == Q.DL.getIndexType(Ptr->getType()))
     return X;
 
+  // Fold a value-preserving zext/sext of a trunc back to the original value.
+  if (CastOpc == Instruction::ZExt || CastOpc == Instruction::SExt) {
+    if (auto *Trunc = dyn_cast<TruncInst>(Op)) {
+      Value *Src = Trunc->getOperand(0);
+      bool NoWrap = CastOpc == Instruction::ZExt ? Trunc->hasNoUnsignedWrap()
+                                                 : Trunc->hasNoSignedWrap();
+      if (Src->getType() == Ty && NoWrap)
+        return Src;
+    }
+  }
+
   return nullptr;
 }
 
diff --git a/llvm/test/Transforms/GVN/PRE/pre-load-through-select.ll b/llvm/test/Transforms/GVN/PRE/pre-load-through-select.ll
index b9e818583023d..2a2019e80323e 100644
--- a/llvm/test/Transforms/GVN/PRE/pre-load-through-select.ll
+++ b/llvm/test/Transforms/GVN/PRE/pre-load-through-select.ll
@@ -1156,25 +1156,47 @@ for.body:
 ; eliminated: translating the true side yields `zext nneg (trunc nuw %iv)` which
 ; folds back to `%iv`, matching the existing `arr[i]` address.
 define i32 @test_phi_select_index_loop_trunc(ptr %arr, i64 %n) {
-; CHECK-LABEL: @test_phi_select_index_loop_trunc(
-; CHECK-NEXT:  entry:
-; CHECK-NEXT:    br label [[FOR_BODY:%.*]]
-; CHECK:       for.body:
-; CHECK-NEXT:    [[IV:%.*]] = phi i64 [ 0, [[ENTRY:%.*]] ], [ [[IV_NEXT:%.*]], [[FOR_BODY]] ]
-; CHECK-NEXT:    [[MINLOC:%.*]] = phi i32 [ 0, [[ENTRY]] ], [ [[SEL:%.*]], [[FOR_BODY]] ]
-; CHECK-NEXT:    [[ARRAYIDX:%.*]] = getelementptr inbounds [4 x i8], ptr [[ARR:%.*]], i64 [[IV]]
-; CHECK-NEXT:    [[TMP1:%.*]] = load i32, ptr [[ARRAYIDX]], align 4
-; CHECK-NEXT:    [[IDXPROM:%.*]] = zext nneg i32 [[MINLOC]] to i64
-; CHECK-NEXT:    [[ARRAYIDX2:%.*]] = getelementptr inbounds [4 x i8], ptr [[ARR]], i64 [[IDXPROM]]
-; CHECK-NEXT:    [[TMP0:%.*]] = load i32, ptr [[ARRAYIDX2]], align 4
-; CHECK-NEXT:    [[CMP:%.*]] = icmp slt i32 [[TMP1]], [[TMP0]]
-; CHECK-NEXT:    [[TMP2:%.*]] = trunc nuw nsw i64 [[IV]] to i32
-; CHECK-NEXT:    [[SEL]] = select i1 [[CMP]], i32 [[TMP2]], i32 [[MINLOC]]
-; CHECK-NEXT:    [[IV_NEXT]] = add nuw nsw i64 [[IV]], 1
-; CHECK-NEXT:    [[EC:%.*]] = icmp eq i64 [[IV_NEXT]], [[N:%.*]]
-; CHECK-NEXT:    br i1 [[EC]], label [[EXIT:%.*]], label [[FOR_BODY]]
-; CHECK:       exit:
-; CHECK-NEXT:    ret i32 [[SEL]]
+; MDEP-LABEL: @test_phi_select_index_loop_trunc(
+; MDEP-NEXT:  entry:
+; MDEP-NEXT:    [[DOTPRE:%.*]] = load i32, ptr [[ARR:%.*]], align 4
+; MDEP-NEXT:    br label [[FOR_BODY:%.*]]
+; MDEP:       for.body:
+; MDEP-NEXT:    [[TMP0:%.*]] = phi i32 [ [[DOTPRE]], [[ENTRY:%.*]] ], [ [[TMP3:%.*]], [[FOR_BODY]] ]
+; MDEP-NEXT:    [[IV:%.*]] = phi i64 [ 0, [[ENTRY]] ], [ [[IV_NEXT:%.*]], [[FOR_BODY]] ]
+; MDEP-NEXT:    [[MINLOC:%.*]] = phi i32 [ 0, [[ENTRY]] ], [ [[SEL:%.*]], [[FOR_BODY]] ]
+; MDEP-NEXT:    [[ARRAYIDX:%.*]] = getelementptr inbounds [4 x i8], ptr [[ARR]], i64 [[IV]]
+; MDEP-NEXT:    [[TMP1:%.*]] = load i32, ptr [[ARRAYIDX]], align 4
+; MDEP-NEXT:    [[IDXPROM:%.*]] = zext nneg i32 [[MINLOC]] to i64
+; MDEP-NEXT:    [[ARRAYIDX2:%.*]] = getelementptr inbounds [4 x i8], ptr [[ARR]], i64 [[IDXPROM]]
+; MDEP-NEXT:    [[CMP:%.*]] = icmp slt i32 [[TMP1]], [[TMP0]]
+; MDEP-NEXT:    [[TMP2:%.*]] = trunc nuw nsw i64 [[IV]] to i32
+; MDEP-NEXT:    [[SEL]] = select i1 [[CMP]], i32 [[TMP2]], i32 [[MINLOC]]
+; MDEP-NEXT:    [[IV_NEXT]] = add nuw nsw i64 [[IV]], 1
+; MDEP-NEXT:    [[EC:%.*]] = icmp eq i64 [[IV_NEXT]], [[N:%.*]]
+; MDEP-NEXT:    [[TMP3]] = select i1 [[CMP]], i32 [[TMP1]], i32 [[TMP0]]
+; MDEP-NEXT:    br i1 [[EC]], label [[EXIT:%.*]], label [[FOR_BODY]]
+; MDEP:       exit:
+; MDEP-NEXT:    ret i32 [[SEL]]
+;
+; MSSA-LABEL: @test_phi_select_index_loop_trunc(
+; MSSA-NEXT:  entry:
+; MSSA-NEXT:    br label [[FOR_BODY:%.*]]
+; MSSA:       for.body:
+; MSSA-NEXT:    [[IV:%.*]] = phi i64 [ 0, [[ENTRY:%.*]] ], [ [[IV_NEXT:%.*]], [[FOR_BODY]] ]
+; MSSA-NEXT:    [[MINLOC:%.*]] = phi i32 [ 0, [[ENTRY]] ], [ [[SEL:%.*]], [[FOR_BODY]] ]
+; MSSA-NEXT:    [[ARRAYIDX:%.*]] = getelementptr inbounds [4 x i8], ptr [[ARR:%.*]], i64 [[IV]]
+; MSSA-NEXT:    [[TMP0:%.*]] = load i32, ptr [[ARRAYIDX]], align 4
+; MSSA-NEXT:    [[IDXPROM:%.*]] = zext nneg i32 [[MINLOC]] to i64
+; MSSA-NEXT:    [[ARRAYIDX2:%.*]] = getelementptr inbounds [4 x i8], ptr [[ARR]], i64 [[IDXPROM]]
+; MSSA-NEXT:    [[TMP1:%.*]] = load i32, ptr [[ARRAYIDX2]], align 4
+; MSSA-NEXT:    [[CMP:%.*]] = icmp slt i32 [[TMP0]], [[TMP1]]
+; MSSA-NEXT:    [[TMP2:%.*]] = trunc nuw nsw i64 [[IV]] to i32
+; MSSA-NEXT:    [[SEL]] = select i1 [[CMP]], i32 [[TMP2]], i32 [[MINLOC]]
+; MSSA-NEXT:    [[IV_NEXT]] = add nuw nsw i64 [[IV]], 1
+; MSSA-NEXT:    [[EC:%.*]] = icmp eq i64 [[IV_NEXT]], [[N:%.*]]
+; MSSA-NEXT:    br i1 [[EC]], label [[EXIT:%.*]], label [[FOR_BODY]]
+; MSSA:       exit:
+; MSSA-NEXT:    ret i32 [[SEL]]
 ;
 entry:
   br label %for.body

>From c74493418f8ab2fccc19d62ee944b03f7c788b66 Mon Sep 17 00:00:00 2001
From: Pengcheng Wang <wangpengcheng.pp at bytedance.com>
Date: Mon, 29 Jun 2026 11:39:32 +0800
Subject: [PATCH 3/3] [InstSimplify] Add tests for value-preserving zext/sext
 of trunc

Add direct InstSimplify coverage for the simplifyCastInst fold that
collapses `zext (trunc nuw X)` and `sext (trunc nsw X)` back to X,
including vector and negative (missing/mismatched no-wrap flag) cases.

Assisted-by: TraeCli (AI assistant)
---
 llvm/test/Transforms/InstSimplify/cast.ll | 66 +++++++++++++++++++++++
 1 file changed, 66 insertions(+)

diff --git a/llvm/test/Transforms/InstSimplify/cast.ll b/llvm/test/Transforms/InstSimplify/cast.ll
index 8178f05be5cb9..8de8d0e0e013f 100644
--- a/llvm/test/Transforms/InstSimplify/cast.ll
+++ b/llvm/test/Transforms/InstSimplify/cast.ll
@@ -56,3 +56,69 @@ define i32 @test5() {
   %add_to_int = ptrtoint ptr %add to i32                            ; 4
   ret i32 %add_to_int                                               ; 4
 }
+
+; A value-preserving zext of a no-unsigned-wrap trunc folds back to the source.
+define i64 @zext_of_trunc_nuw(i64 %x) {
+; CHECK-LABEL: @zext_of_trunc_nuw(
+; CHECK-NEXT:    ret i64 [[X:%.*]]
+;
+  %t = trunc nuw i64 %x to i32
+  %z = zext i32 %t to i64
+  ret i64 %z
+}
+
+; A value-preserving sext of a no-signed-wrap trunc folds back to the source.
+define i64 @sext_of_trunc_nsw(i64 %x) {
+; CHECK-LABEL: @sext_of_trunc_nsw(
+; CHECK-NEXT:    ret i64 [[X:%.*]]
+;
+  %t = trunc nsw i64 %x to i32
+  %s = sext i32 %t to i64
+  ret i64 %s
+}
+
+; Same folds work on vectors.
+define <2 x i64> @zext_of_trunc_nuw_vec(<2 x i64> %x) {
+; CHECK-LABEL: @zext_of_trunc_nuw_vec(
+; CHECK-NEXT:    ret <2 x i64> [[X:%.*]]
+;
+  %t = trunc nuw <2 x i64> %x to <2 x i32>
+  %z = zext <2 x i32> %t to <2 x i64>
+  ret <2 x i64> %z
+}
+
+; Negative: without nuw, zext(trunc) is not value-preserving for InstSimplify.
+define i64 @zext_of_trunc_no_flag(i64 %x) {
+; CHECK-LABEL: @zext_of_trunc_no_flag(
+; CHECK-NEXT:    [[T:%.*]] = trunc i64 [[X:%.*]] to i32
+; CHECK-NEXT:    [[Z:%.*]] = zext i32 [[T]] to i64
+; CHECK-NEXT:    ret i64 [[Z]]
+;
+  %t = trunc i64 %x to i32
+  %z = zext i32 %t to i64
+  ret i64 %z
+}
+
+; Negative: zext of an nsw-only trunc does not guarantee the unsigned round-trip.
+define i64 @zext_of_trunc_nsw_only(i64 %x) {
+; CHECK-LABEL: @zext_of_trunc_nsw_only(
+; CHECK-NEXT:    [[T:%.*]] = trunc nsw i64 [[X:%.*]] to i32
+; CHECK-NEXT:    [[Z:%.*]] = zext i32 [[T]] to i64
+; CHECK-NEXT:    ret i64 [[Z]]
+;
+  %t = trunc nsw i64 %x to i32
+  %z = zext i32 %t to i64
+  ret i64 %z
+}
+
+; Negative: sext of an nuw-only trunc does not guarantee the signed round-trip.
+define i64 @sext_of_trunc_nuw_only(i64 %x) {
+; CHECK-LABEL: @sext_of_trunc_nuw_only(
+; CHECK-NEXT:    [[T:%.*]] = trunc nuw i64 [[X:%.*]] to i32
+; CHECK-NEXT:    [[S:%.*]] = sext i32 [[T]] to i64
+; CHECK-NEXT:    ret i64 [[S]]
+;
+  %t = trunc nuw i64 %x to i32
+  %s = sext i32 %t to i64
+  ret i64 %s
+}



More information about the llvm-commits mailing list