[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