[llvm] [ValueTracking] Prove single-index inbounds GEP into known-size alloc… (PR #185301)
Weiwen He via llvm-commits
llvm-commits at lists.llvm.org
Sun Mar 8 09:05:47 PDT 2026
https://github.com/he-weiwen created https://github.com/llvm/llvm-project/pull/185301
While investigating #183956, I found that InstCombine couldn't remove the `freeze` on `%3 = freeze ptr getelementptr inbounds nuw (i8, ptr @d, i64 8)`, blocking alias analysis for SLP scheduler .
This patch teaches `isGuaranteedNotToBeUndefOrPoison` that a single-index inbounds GEP with a constant offset in an alloca or global is not poison when the offset is in within the allocated range. This enables InstCombine to remove the freezes.
Restricted to single-index ones to avoid reasoning about multi-step inbounds/nuw/nusw violations. This should be sufficient as InstCombine seems to fold multi-step GEPs into single-index ones anyway.
Alive2 proofs: https://alive2.llvm.org/ce/z/bVeUXp
>From fb5b81b68ec70d48f67165a7be5c56d888bb128d Mon Sep 17 00:00:00 2001
From: he-weiwen <he.weiwen at outlook.com>
Date: Fri, 6 Mar 2026 14:43:44 +0000
Subject: [PATCH] [ValueTracking] Prove single-index inbounds GEP into
known-size allocation is non-poison
---
llvm/lib/Analysis/ValueTracking.cpp | 37 +++++++++++
.../Attributor/call-simplify-pointer-info.ll | 14 +++--
.../freeze-gep-inbounds-known-allocation.ll | 63 +++++++++++++++++++
.../Transforms/InstCombine/select-and-or.ll | 2 +-
4 files changed, 109 insertions(+), 7 deletions(-)
create mode 100644 llvm/test/Transforms/InstCombine/freeze-gep-inbounds-known-allocation.ll
diff --git a/llvm/lib/Analysis/ValueTracking.cpp b/llvm/lib/Analysis/ValueTracking.cpp
index 3c499862642db..18aed556bc594 100644
--- a/llvm/lib/Analysis/ValueTracking.cpp
+++ b/llvm/lib/Analysis/ValueTracking.cpp
@@ -7799,6 +7799,43 @@ static bool isGuaranteedNotToBeUndefOrPoison(
isa<Function>(StrippedV) || isa<ConstantPointerNull>(StrippedV))
return true;
+ // A single-index inbounds GEP into a known-size allocation cannot produce
+ // poison if the constant byte offset is non-negative and within the allocated
+ // range. We restrict to single-index to avoid reasoning about per-step
+ // inbounds violations in multi-index GEPs. This extends the zero-offset case
+ // above.
+ if (auto *GEP = dyn_cast<GEPOperator>(V);
+ GEP && GEP->isInBounds() && GEP->getNumIndices() == 1) {
+ auto *Base =
+ GEP->getPointerOperand()->stripPointerCastsSameRepresentation();
+ auto *CI = dyn_cast<ConstantInt>(*GEP->idx_begin());
+ const DataLayout *DL = nullptr;
+ if (auto *AI = dyn_cast<AllocaInst>(Base))
+ DL = &AI->getDataLayout();
+ else if (auto *GV = dyn_cast<GlobalVariable>(Base))
+ DL = &GV->getDataLayout();
+ if (DL && CI) {
+ unsigned IdxWidth = DL->getIndexTypeSizeInBits(V->getType());
+ APInt Idx = CI->getValue();
+ bool TruncChangesSign =
+ Idx.getBitWidth() > IdxWidth && !Idx.isSignedIntN(IdxWidth);
+ TypeSize ElemSizeTS = DL->getTypeAllocSize(GEP->getSourceElementType());
+ if (!TruncChangesSign && !ElemSizeTS.isScalable()) {
+ Idx = Idx.sextOrTrunc(IdxWidth);
+ APInt ElemSize(IdxWidth, ElemSizeTS.getKnownMinValue());
+ bool Overflow = false;
+ APInt Offset = Idx.smul_ov(ElemSize, Overflow);
+ if (!Overflow && Offset.isNonNegative()) {
+ bool CanBeNull, CanBeFreed;
+ uint64_t DerefBytes =
+ Base->getPointerDereferenceableBytes(*DL, CanBeNull, CanBeFreed);
+ if (DerefBytes != 0 && Offset.ule(DerefBytes))
+ return true;
+ }
+ }
+ }
+ }
+
auto OpCheck = [&](const Value *V) {
return isGuaranteedNotToBeUndefOrPoison(V, AC, CtxI, DT, Depth + 1, Kind);
};
diff --git a/llvm/test/Transforms/Attributor/call-simplify-pointer-info.ll b/llvm/test/Transforms/Attributor/call-simplify-pointer-info.ll
index 1d435815d89e3..f8d48f29cd0ba 100644
--- a/llvm/test/Transforms/Attributor/call-simplify-pointer-info.ll
+++ b/llvm/test/Transforms/Attributor/call-simplify-pointer-info.ll
@@ -8,7 +8,7 @@ define internal i8 @read_arg(ptr %p) {
; CGSCC-LABEL: define {{[^@]+}}@read_arg
; CGSCC-SAME: (ptr nofree noundef nonnull readonly captures(none) dereferenceable(1022) [[P:%.*]]) #[[ATTR0:[0-9]+]] {
; CGSCC-NEXT: entry:
-; CGSCC-NEXT: [[L:%.*]] = load i8, ptr [[P]], align 1
+; CGSCC-NEXT: [[L:%.*]] = load i8, ptr [[P]], align 1, !invariant.load [[META0:![0-9]+]]
; CGSCC-NEXT: ret i8 [[L]]
;
entry:
@@ -22,7 +22,7 @@ define internal i8 @read_arg_index(ptr %p, i64 %index) {
; CGSCC-SAME: (ptr nofree noundef nonnull readonly align 16 captures(none) dereferenceable(1024) [[P:%.*]]) #[[ATTR0]] {
; CGSCC-NEXT: entry:
; CGSCC-NEXT: [[G:%.*]] = getelementptr inbounds i8, ptr [[P]], i64 2
-; CGSCC-NEXT: [[L:%.*]] = load i8, ptr [[G]], align 1
+; CGSCC-NEXT: [[L:%.*]] = load i8, ptr [[G]], align 1, !invariant.load [[META0]]
; CGSCC-NEXT: ret i8 [[L]]
;
entry:
@@ -281,14 +281,16 @@ entry:
ret i8 %r
}
+;.
+; CGSCC: attributes #[[ATTR0]] = { mustprogress nofree norecurse nosync nounwind willreturn memory(argmem: read) }
+; CGSCC: attributes #[[ATTR1]] = { mustprogress nofree nosync nounwind willreturn memory(none) }
+; CGSCC: attributes #[[ATTR2]] = { mustprogress nofree nosync nounwind willreturn memory(argmem: read) }
+; CGSCC: attributes #[[ATTR3]] = { nofree willreturn memory(read) }
;.
; TUNIT: attributes #[[ATTR0]] = { mustprogress nofree norecurse nosync nounwind willreturn memory(none) }
; TUNIT: attributes #[[ATTR1]] = { mustprogress nofree norecurse nosync nounwind willreturn memory(argmem: read) }
; TUNIT: attributes #[[ATTR2]] = { nofree nosync nounwind willreturn memory(read) }
; TUNIT: attributes #[[ATTR3]] = { nofree norecurse nosync nounwind willreturn memory(read) }
;.
-; CGSCC: attributes #[[ATTR0]] = { mustprogress nofree norecurse nosync nounwind willreturn memory(argmem: read) }
-; CGSCC: attributes #[[ATTR1]] = { mustprogress nofree nosync nounwind willreturn memory(none) }
-; CGSCC: attributes #[[ATTR2]] = { mustprogress nofree nosync nounwind willreturn memory(argmem: read) }
-; CGSCC: attributes #[[ATTR3]] = { nofree willreturn memory(read) }
+; CGSCC: [[META0]] = !{}
;.
diff --git a/llvm/test/Transforms/InstCombine/freeze-gep-inbounds-known-allocation.ll b/llvm/test/Transforms/InstCombine/freeze-gep-inbounds-known-allocation.ll
new file mode 100644
index 0000000000000..971879e6df258
--- /dev/null
+++ b/llvm/test/Transforms/InstCombine/freeze-gep-inbounds-known-allocation.ll
@@ -0,0 +1,63 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py
+; RUN: opt < %s -passes=instcombine -S | FileCheck %s
+
+ at g = global [6 x i64] zeroinitializer, align 16
+
+define ptr @freeze_gep_inbounds_global() {
+; CHECK-LABEL: @freeze_gep_inbounds_global(
+; CHECK-NEXT: ret ptr getelementptr inbounds nuw (i8, ptr @g, i64 8)
+;
+ %fr = freeze ptr getelementptr inbounds (i8, ptr @g, i64 8)
+ ret ptr %fr
+}
+
+define ptr @freeze_gep_inbounds_alloca() {
+; CHECK-LABEL: @freeze_gep_inbounds_alloca(
+; CHECK-NEXT: [[A:%.*]] = alloca [32 x i8], align 1
+; CHECK-NEXT: [[GEP:%.*]] = getelementptr inbounds nuw i8, ptr [[A]], i64 16
+; CHECK-NEXT: ret ptr [[GEP]]
+;
+ %a = alloca [32 x i8], align 1
+ %gep = getelementptr inbounds i8, ptr %a, i64 16
+ %fr = freeze ptr %gep
+ ret ptr %fr
+}
+
+define ptr @freeze_gep_inbounds_global_boundary() {
+; CHECK-LABEL: @freeze_gep_inbounds_global_boundary(
+; CHECK-NEXT: ret ptr getelementptr inbounds nuw (i8, ptr @g, i64 48)
+;
+ %fr = freeze ptr getelementptr inbounds (i8, ptr @g, i64 48)
+ ret ptr %fr
+}
+
+define ptr @freeze_gep_inbounds_global_oob() {
+; CHECK-LABEL: @freeze_gep_inbounds_global_oob(
+; CHECK-NEXT: [[FR:%.*]] = freeze ptr getelementptr inbounds nuw (i8, ptr @g, i64 49)
+; CHECK-NEXT: ret ptr [[FR]]
+;
+ %fr = freeze ptr getelementptr inbounds (i8, ptr @g, i64 49)
+ ret ptr %fr
+}
+
+define ptr @freeze_gep_inbounds_global_negative() {
+; CHECK-LABEL: @freeze_gep_inbounds_global_negative(
+; CHECK-NEXT: [[FR:%.*]] = freeze ptr getelementptr inbounds (i8, ptr @g, i64 -1)
+; CHECK-NEXT: ret ptr [[FR]]
+;
+ %fr = freeze ptr getelementptr inbounds (i8, ptr @g, i64 -1)
+ ret ptr %fr
+}
+
+define ptr @freeze_gep_inbounds_alloca_variable_offset(i64 %n) {
+; CHECK-LABEL: @freeze_gep_inbounds_alloca_variable_offset(
+; CHECK-NEXT: [[A:%.*]] = alloca [32 x i8], align 1
+; CHECK-NEXT: [[N_FR:%.*]] = freeze i64 [[N:%.*]]
+; CHECK-NEXT: [[GEP:%.*]] = getelementptr i8, ptr [[A]], i64 [[N_FR]]
+; CHECK-NEXT: ret ptr [[GEP]]
+;
+ %a = alloca [32 x i8], align 1
+ %gep = getelementptr inbounds i8, ptr %a, i64 %n
+ %fr = freeze ptr %gep
+ ret ptr %fr
+}
diff --git a/llvm/test/Transforms/InstCombine/select-and-or.ll b/llvm/test/Transforms/InstCombine/select-and-or.ll
index 7ae250f04f29e..24df8a280baac 100644
--- a/llvm/test/Transforms/InstCombine/select-and-or.ll
+++ b/llvm/test/Transforms/InstCombine/select-and-or.ll
@@ -478,7 +478,7 @@ define i1 @demorgan_select_infloop2(i1 %L) {
; CHECK-LABEL: @demorgan_select_infloop2(
; CHECK-NEXT: [[NOT_L:%.*]] = xor i1 [[L:%.*]], true
; CHECK-NEXT: [[CMP2:%.*]] = icmp ne ptr getelementptr inbounds nuw (i8, ptr @g2, i64 2), @g1
-; CHECK-NEXT: [[C15:%.*]] = select i1 [[NOT_L]], i1 [[CMP2]], i1 false
+; CHECK-NEXT: [[C15:%.*]] = and i1 [[CMP2]], [[NOT_L]]
; CHECK-NEXT: ret i1 [[C15]]
;
%not.L = xor i1 %L, true
More information about the llvm-commits
mailing list