[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