[llvm] 4eb56cf - [InstSimplify] Fold value-preserving zext/sext of trunc (#204089)
via llvm-commits
llvm-commits at lists.llvm.org
Sun Jul 5 20:44:45 PDT 2026
Author: Pengcheng Wang
Date: 2026-07-06T03:44:39Z
New Revision: 4eb56cf497aaee55dc1346a7e1319dff33864601
URL: https://github.com/llvm/llvm-project/commit/4eb56cf497aaee55dc1346a7e1319dff33864601
DIFF: https://github.com/llvm/llvm-project/commit/4eb56cf497aaee55dc1346a7e1319dff33864601.diff
LOG: [InstSimplify] Fold value-preserving zext/sext of trunc (#204089)
Fold a value-preserving `zext (trunc nuw X)` / `sext (trunc nsw X)` back
to
`X` in `simplifyCastInst` (when the trunc source type equals the extend
result type and the no-wrap flag guarantees the round-trip is identity).
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 (see #203863), the true side becomes
`gep base, zext nneg (trunc nuw %iv)`, which has no matching instruction
in
the loop, so the select dependency is never formed and the redundant
load
survives.
Teaching `simplifyCastInst` to collapse the value-preserving
extend-of-trunc
makes the translated address fold back to `gep base, %iv`, matching the
existing `arr[i]` address, so the load through the select index is
eliminated. This fixes the integer min-index variant of
https://github.com/llvm/llvm-project/issues/58569#issuecomment-2788365047
(e.g. returning `minloc` instead of the value).
Alive2: https://alive2.llvm.org/ce/z/T5kMG8
Assisted-by: TraeCli (AI assistant)
Added:
Modified:
llvm/lib/Analysis/InstructionSimplify.cpp
llvm/test/Transforms/GVN/PRE/pre-load-through-select.ll
llvm/test/Transforms/InstSimplify/cast.ll
Removed:
################################################################################
diff --git a/llvm/lib/Analysis/InstructionSimplify.cpp b/llvm/lib/Analysis/InstructionSimplify.cpp
index fcd7f68260856..1efc01a726c37 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 dc4974e804c80..2a2019e80323e 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,75 @@ 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) {
+; 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
+
+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
+}
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