[llvm] [LAA][NFC] Refactor deref no-wrap check; expose broken reverse-loop bounds (PR #211960)

Aleksandr Popov via llvm-commits llvm-commits at lists.llvm.org
Sun Aug 2 14:11:05 PDT 2026


https://github.com/aleks-tmb updated https://github.com/llvm/llvm-project/pull/211960

>From 172ef03d3d19e2d7ceca01079e59b3e6834d57e9 Mon Sep 17 00:00:00 2001
From: Aleksandr Popov <apopov at azul.com>
Date: Fri, 24 Jul 2026 22:37:09 +0000
Subject: [PATCH 1/6] [LAA][NFC] Restructure deref no-wrap check; flag
 reverse-step bugs

Split evaluatePtrAddRecAtMaxBTCWillNotWrap into a two-stage form:
compute MaxOffset per direction, then apply the shared
MaxOffset <= DerefBytes check.  Rename intermediates after what
they actually hold (LowestOffset, WalkBytes, SpanBytes, MaxOffset)
and document the safety invariants above the function.

The new shape makes two long-standing off-by-EltSize bugs on the
negative-step branch visible; both are flagged with FIXMEs and
pinned by a new LIT test, to be fixed in follow-ups:

  * Lower bound is over-strict by EltSize, wrongly rejecting
    reverse loops whose last iteration reaches the base pointer.

  * Upper bound is under-counted by EltSize, accepting loops
    whose top read spills past the deref region.
---
 llvm/lib/Analysis/LoopAccessAnalysis.cpp      | 70 ++++++++------
 .../negative-step-deref-off-by-eltsize.ll     | 94 +++++++++++++++++++
 2 files changed, 136 insertions(+), 28 deletions(-)
 create mode 100644 llvm/test/Analysis/LoopAccessAnalysis/negative-step-deref-off-by-eltsize.ll

diff --git a/llvm/lib/Analysis/LoopAccessAnalysis.cpp b/llvm/lib/Analysis/LoopAccessAnalysis.cpp
index 1f13106214ea9..f59dd7cee55c3 100644
--- a/llvm/lib/Analysis/LoopAccessAnalysis.cpp
+++ b/llvm/lib/Analysis/LoopAccessAnalysis.cpp
@@ -210,6 +210,14 @@ static const SCEV *mulSCEVNoOverflow(const SCEV *A, const SCEV *B,
 
 /// Return true, if evaluating \p AR at \p MaxBTC cannot wrap, because \p AR at
 /// \p MaxBTC is guaranteed inbounds of the accessed object.
+///
+/// The accessed byte range is [LowestOffset, LowestOffset + SpanBytes), where
+///   SpanBytes    = MaxBTC * |Step| + EltSize,
+///   LowestOffset = smallest byte offset from StartPtr any iteration reaches.
+///
+/// Safety invariants (both directions):
+///   1. LowestOffset >= 0                       — no access below StartPtr.
+///   2. LowestOffset + SpanBytes <= DerefBytes  — no access past the region.
 static bool evaluatePtrAddRecAtMaxBTCWillNotWrap(
     const SCEVAddRecExpr *AR, const SCEV *MaxBTC, const SCEV *EltSize,
     ScalarEvolution &SE, const DataLayout &DL, DominatorTree *DT,
@@ -268,54 +276,60 @@ static bool evaluatePtrAddRecAtMaxBTCWillNotWrap(
   MaxBTC = SE.getNoopOrZeroExtend(MaxBTC, WiderTy);
 
   // For the computations below, make sure they don't unsigned wrap.
-  if (!SE.isKnownPredicate(CmpInst::ICMP_UGE, AR->getStart(), StartPtr))
+  // FIXME: for a negative step this holds the HIGHEST accessed address, not
+  // the lowest
+  const SCEV *LowestAddr = AR->getStart();
+  if (!SE.isKnownPredicate(CmpInst::ICMP_UGE, LowestAddr, StartPtr))
     return false;
-  const SCEV *StartOffset = SE.getNoopOrZeroExtend(
-      SE.getMinusSCEV(AR->getStart(), StartPtr), WiderTy);
+  const SCEV *LowestOffset =
+      SE.getNoopOrZeroExtend(SE.getMinusSCEV(LowestAddr, StartPtr), WiderTy);
 
   if (!LoopGuards)
     LoopGuards.emplace(ScalarEvolution::LoopGuards::collect(AR->getLoop(), SE));
   MaxBTC = SE.applyLoopGuards(MaxBTC, *LoopGuards);
 
-  const SCEV *OffsetAtLastIter =
-      mulSCEVNoOverflow(MaxBTC, SE.getAbsExpr(Step, /*IsNSW=*/false), SE);
-  if (!OffsetAtLastIter) {
+  const SCEV *AbsStep = SE.getAbsExpr(Step, /*IsNSW=*/false);
+  // Total distance (in bytes) walked between the first and the last
+  // accessed pointer; MaxBTC * |Step|.
+  const SCEV *WalkBytes = mulSCEVNoOverflow(MaxBTC, AbsStep, SE);
+  if (!WalkBytes) {
     // Re-try with constant max backedge-taken count if using the symbolic one
     // failed.
     MaxBTC = SE.getConstantMaxBackedgeTakenCount(AR->getLoop());
     if (isa<SCEVCouldNotCompute>(MaxBTC))
       return false;
-    MaxBTC = SE.getNoopOrZeroExtend(
-        MaxBTC, WiderTy);
-    OffsetAtLastIter =
-        mulSCEVNoOverflow(MaxBTC, SE.getAbsExpr(Step, /*IsNSW=*/false), SE);
-    if (!OffsetAtLastIter)
+    MaxBTC = SE.getNoopOrZeroExtend(MaxBTC, WiderTy);
+    WalkBytes = mulSCEVNoOverflow(MaxBTC, AbsStep, SE);
+    if (!WalkBytes)
       return false;
   }
 
-  const SCEV *OffsetEndBytes = addSCEVNoOverflow(
-      OffsetAtLastIter, SE.getNoopOrZeroExtend(EltSize, WiderTy), SE);
-  if (!OffsetEndBytes)
+  // Total length in bytes of the accessed range (from the first accessed
+  // byte through the end of the last access); WalkBytes + EltSize.
+  const SCEV *SpanBytes = addSCEVNoOverflow(
+      WalkBytes, SE.getNoopOrZeroExtend(EltSize, WiderTy), SE);
+  if (!SpanBytes)
     return false;
 
+  // Compute MaxOffset per direction: exclusive upper offset of the
+  // accessed range.
+  const SCEV *MaxOffset;
   if (IsKnownNonNegative) {
-    // For positive steps, check if
-    //  (AR->getStart() - StartPtr) + (MaxBTC  * Step) + EltSize <= DerefBytes,
-    // while making sure none of the computations unsigned wrap themselves.
-    const SCEV *EndBytes = addSCEVNoOverflow(StartOffset, OffsetEndBytes, SE);
-    if (!EndBytes)
+    MaxOffset = addSCEVNoOverflow(LowestOffset, SpanBytes, SE);
+    if (!MaxOffset)
       return false;
-
     DerefBytesSCEV = SE.applyLoopGuards(DerefBytesSCEV, *LoopGuards);
-    return SE.isKnownPredicate(CmpInst::ICMP_ULE, EndBytes, DerefBytesSCEV);
+  } else {
+    // FIXME: LowestOffset here is actually the HIGHEST offset (see FIXME
+    // above). Lower check is over-strict by EltSize, upper is under-counted
+    // by EltSize.
+    assert(SE.isKnownNegative(Step) && "must be known negative");
+    if (!SE.isKnownPredicate(CmpInst::ICMP_SGE, LowestOffset, SpanBytes))
+      return false;
+    MaxOffset = LowestOffset;
   }
-
-  // For negative steps check if
-  //  * StartOffset >= (MaxBTC * Step + EltSize)
-  //  * StartOffset <= DerefBytes.
-  assert(SE.isKnownNegative(Step) && "must be known negative");
-  return SE.isKnownPredicate(CmpInst::ICMP_SGE, StartOffset, OffsetEndBytes) &&
-         SE.isKnownPredicate(CmpInst::ICMP_ULE, StartOffset, DerefBytesSCEV);
+  // MaxOffset must not exceed the deref-region end.
+  return SE.isKnownPredicate(CmpInst::ICMP_ULE, MaxOffset, DerefBytesSCEV);
 }
 
 std::pair<const SCEV *, const SCEV *> llvm::getStartAndEndForAccess(
diff --git a/llvm/test/Analysis/LoopAccessAnalysis/negative-step-deref-off-by-eltsize.ll b/llvm/test/Analysis/LoopAccessAnalysis/negative-step-deref-off-by-eltsize.ll
new file mode 100644
index 0000000000000..160b5847a728a
--- /dev/null
+++ b/llvm/test/Analysis/LoopAccessAnalysis/negative-step-deref-off-by-eltsize.ll
@@ -0,0 +1,94 @@
+; RUN: opt -passes='print<access-info>' -disable-output %s 2>&1 | FileCheck %s
+
+; Reverse i32 loop over 4 elements whose access range exactly fills the
+; dereferenceable region (deref(16), reads bytes [0, 16)).
+;
+; TODO: LAA should recognise that this AR fits within the deref
+; region and produce tight bounds (Low: %A, High: %A + 16).
+;
+; Pseudocode:
+;   // A, B: at least 4 i32s dereferenceable each
+;   for (i64 i = 3; i >= 0; --i) {
+;     i32 l = A[i];       // A[3], A[2], A[1], A[0]
+;     B[i] = 0;
+;     if (l == 0) break;
+;   }
+
+define void @reverse_reaches_base(ptr dereferenceable(16) %A, ptr dereferenceable(16) %B) {
+; CHECK-LABEL: 'reverse_reaches_base'
+; CHECK:      Group GRP0:
+; CHECK-NEXT:   (Low: (-4 + inttoptr (i64 -1 to ptr))<nsw> High: (16 + %B)<nuw>)
+; CHECK-NEXT:     Member: {(12 + %B)<nuw>,+,-4}<nw><%loop>
+; CHECK:      Group GRP1:
+; CHECK-NEXT:   (Low: (-4 + inttoptr (i64 -1 to ptr))<nsw> High: (16 + %A)<nuw>)
+; CHECK-NEXT:     Member: {(12 + %A)<nuw>,+,-4}<nw><%loop>
+entry:
+  br label %loop
+
+loop:
+  %iv = phi i64 [ 3, %entry ], [ %iv.dec, %latch ]
+  %gep.A = getelementptr inbounds i32, ptr %A, i64 %iv
+  %gep.B = getelementptr inbounds i32, ptr %B, i64 %iv
+  %l = load i32, ptr %gep.A, align 4
+  store i32 0, ptr %gep.B, align 4
+  %uncntable = icmp eq i32 %l, 0
+  br i1 %uncntable, label %exit.early, label %latch
+
+latch:
+  %iv.dec = add nsw i64 %iv, -1
+  %ec = icmp eq i64 %iv, 0
+  br i1 %ec, label %exit.done, label %loop
+
+exit.early:
+  ret void
+
+exit.done:
+  ret void
+}
+
+; Reverse i32 loop whose top read spills one byte past the deref end.
+; The IR is UB by construction: top i32 read at byte 13 covers [13, 17),
+; but deref(16) only guarantees [0, 16).
+;
+; TODO: LAA should reject this AR (top access exits the deref region)
+; and fall back to the wide low bound.
+;
+; Pseudocode:
+;   for (i64 i = 13; i > 1; i -= 4) {
+;     i32 l = *(i32*)((char*)A + i);   // reads [i, i+4)
+;     *(i32*)((char*)B + i) = 0;
+;     if (l == 0) break;
+;   }
+
+define void @reverse_top_spills(ptr dereferenceable(16) %A, ptr dereferenceable(16) %B) {
+; CHECK-LABEL: 'reverse_top_spills'
+; CHECK:      Group GRP0:
+; CHECK-NEXT:   (Low: (5 + %B)<nuw> High: (17 + %B))
+; CHECK-NEXT:     Member: {(13 + %B)<nuw>,+,-4}<nw><%loop2>
+; CHECK:      Group GRP1:
+; CHECK-NEXT:   (Low: (5 + %A)<nuw> High: (17 + %A))
+; CHECK-NEXT:     Member: {(13 + %A)<nuw>,+,-4}<nw><%loop2>
+entry:
+  br label %loop2
+
+loop2:
+  %iv = phi i64 [ 13, %entry ], [ %iv.dec, %latch ]
+  %gep.A = getelementptr inbounds i8, ptr %A, i64 %iv
+  %gep.B = getelementptr inbounds i8, ptr %B, i64 %iv
+  %l = load i32, ptr %gep.A, align 1
+  store i32 0, ptr %gep.B, align 1
+  %uncntable = icmp eq i32 %l, 0
+  br i1 %uncntable, label %exit.early, label %latch
+
+latch:
+  %iv.dec = add nsw i64 %iv, -4
+  %ec = icmp eq i64 %iv, 5
+  br i1 %ec, label %exit.done, label %loop2
+
+exit.early:
+  ret void
+
+exit.done:
+  ret void
+}
+

>From b00fd96be11396413b3d5989efec7aa1dba591b5 Mon Sep 17 00:00:00 2001
From: Aleksandr Popov <apopov at azul.com>
Date: Mon, 27 Jul 2026 10:33:32 +0000
Subject: [PATCH 2/6] Clarify FIXMEs and test comment

---
 llvm/lib/Analysis/LoopAccessAnalysis.cpp            | 13 ++++++++-----
 .../negative-step-deref-off-by-eltsize.ll           |  8 ++++----
 2 files changed, 12 insertions(+), 9 deletions(-)

diff --git a/llvm/lib/Analysis/LoopAccessAnalysis.cpp b/llvm/lib/Analysis/LoopAccessAnalysis.cpp
index f59dd7cee55c3..9ff3eac135c72 100644
--- a/llvm/lib/Analysis/LoopAccessAnalysis.cpp
+++ b/llvm/lib/Analysis/LoopAccessAnalysis.cpp
@@ -276,8 +276,8 @@ static bool evaluatePtrAddRecAtMaxBTCWillNotWrap(
   MaxBTC = SE.getNoopOrZeroExtend(MaxBTC, WiderTy);
 
   // For the computations below, make sure they don't unsigned wrap.
-  // FIXME: for a negative step this holds the HIGHEST accessed address, not
-  // the lowest
+  // FIXME: for a negative step LowestAddr should be evaluated at the last
+  // iteration as AR->evaluateAtIteration(MaxBTC, SE).
   const SCEV *LowestAddr = AR->getStart();
   if (!SE.isKnownPredicate(CmpInst::ICMP_UGE, LowestAddr, StartPtr))
     return false;
@@ -320,9 +320,12 @@ static bool evaluatePtrAddRecAtMaxBTCWillNotWrap(
       return false;
     DerefBytesSCEV = SE.applyLoopGuards(DerefBytesSCEV, *LoopGuards);
   } else {
-    // FIXME: LowestOffset here is actually the HIGHEST offset (see FIXME
-    // above). Lower check is over-strict by EltSize, upper is under-counted
-    // by EltSize.
+    // FIXME: two independent off-by-EltSize bugs on this branch:
+    //  1. LowestOffset here is actually the HIGHEST offset, because
+    //     LowestAddr is computed from AR->getStart() rather than
+    //     AR->evaluateAtIteration(MaxBTC, SE) (see FIXME above).
+    //  2. The lower check is over-strict by EltSize and the upper is
+    //     under-counted by EltSize.
     assert(SE.isKnownNegative(Step) && "must be known negative");
     if (!SE.isKnownPredicate(CmpInst::ICMP_SGE, LowestOffset, SpanBytes))
       return false;
diff --git a/llvm/test/Analysis/LoopAccessAnalysis/negative-step-deref-off-by-eltsize.ll b/llvm/test/Analysis/LoopAccessAnalysis/negative-step-deref-off-by-eltsize.ll
index 160b5847a728a..b1d9a984592c8 100644
--- a/llvm/test/Analysis/LoopAccessAnalysis/negative-step-deref-off-by-eltsize.ll
+++ b/llvm/test/Analysis/LoopAccessAnalysis/negative-step-deref-off-by-eltsize.ll
@@ -47,11 +47,11 @@ exit.done:
 }
 
 ; Reverse i32 loop whose top read spills one byte past the deref end.
-; The IR is UB by construction: top i32 read at byte 13 covers [13, 17),
-; but deref(16) only guarantees [0, 16).
+; The top i32 read at byte 13 covers [13, 17), but deref(16) only
+; guarantees [0, 16) — bytes at/after 16 may or may not be dereferenceable.
 ;
-; TODO: LAA should reject this AR (top access exits the deref region)
-; and fall back to the wide low bound.
+; TODO: LAA must not assume the AR fits in the deref region and should
+; fall back to the wide low bound.
 ;
 ; Pseudocode:
 ;   for (i64 i = 13; i > 1; i -= 4) {

>From 054bd4549241e6a8d1187167393a2fe9ffdae0e7 Mon Sep 17 00:00:00 2001
From: Aleksandr Popov <apopov at azul.com>
Date: Wed, 29 Jul 2026 12:53:05 +0000
Subject: [PATCH 3/6] Revert StartOffset rename per review

---
 llvm/lib/Analysis/LoopAccessAnalysis.cpp | 31 +++++++++---------------
 1 file changed, 12 insertions(+), 19 deletions(-)

diff --git a/llvm/lib/Analysis/LoopAccessAnalysis.cpp b/llvm/lib/Analysis/LoopAccessAnalysis.cpp
index 9ff3eac135c72..d2f01c339fd29 100644
--- a/llvm/lib/Analysis/LoopAccessAnalysis.cpp
+++ b/llvm/lib/Analysis/LoopAccessAnalysis.cpp
@@ -210,14 +210,6 @@ static const SCEV *mulSCEVNoOverflow(const SCEV *A, const SCEV *B,
 
 /// Return true, if evaluating \p AR at \p MaxBTC cannot wrap, because \p AR at
 /// \p MaxBTC is guaranteed inbounds of the accessed object.
-///
-/// The accessed byte range is [LowestOffset, LowestOffset + SpanBytes), where
-///   SpanBytes    = MaxBTC * |Step| + EltSize,
-///   LowestOffset = smallest byte offset from StartPtr any iteration reaches.
-///
-/// Safety invariants (both directions):
-///   1. LowestOffset >= 0                       — no access below StartPtr.
-///   2. LowestOffset + SpanBytes <= DerefBytes  — no access past the region.
 static bool evaluatePtrAddRecAtMaxBTCWillNotWrap(
     const SCEVAddRecExpr *AR, const SCEV *MaxBTC, const SCEV *EltSize,
     ScalarEvolution &SE, const DataLayout &DL, DominatorTree *DT,
@@ -276,13 +268,14 @@ static bool evaluatePtrAddRecAtMaxBTCWillNotWrap(
   MaxBTC = SE.getNoopOrZeroExtend(MaxBTC, WiderTy);
 
   // For the computations below, make sure they don't unsigned wrap.
-  // FIXME: for a negative step LowestAddr should be evaluated at the last
-  // iteration as AR->evaluateAtIteration(MaxBTC, SE).
-  const SCEV *LowestAddr = AR->getStart();
-  if (!SE.isKnownPredicate(CmpInst::ICMP_UGE, LowestAddr, StartPtr))
+  // FIXME: for a negative step the lowest accessed address is not
+  // AR->getStart() but AR->evaluateAtIteration(MaxBTC, SE); the check below
+  // therefore compares StartPtr against the highest accessed address instead
+  // of the lowest.
+  if (!SE.isKnownPredicate(CmpInst::ICMP_UGE, AR->getStart(), StartPtr))
     return false;
-  const SCEV *LowestOffset =
-      SE.getNoopOrZeroExtend(SE.getMinusSCEV(LowestAddr, StartPtr), WiderTy);
+  const SCEV *StartOffset = SE.getNoopOrZeroExtend(
+      SE.getMinusSCEV(AR->getStart(), StartPtr), WiderTy);
 
   if (!LoopGuards)
     LoopGuards.emplace(ScalarEvolution::LoopGuards::collect(AR->getLoop(), SE));
@@ -315,21 +308,21 @@ static bool evaluatePtrAddRecAtMaxBTCWillNotWrap(
   // accessed range.
   const SCEV *MaxOffset;
   if (IsKnownNonNegative) {
-    MaxOffset = addSCEVNoOverflow(LowestOffset, SpanBytes, SE);
+    MaxOffset = addSCEVNoOverflow(StartOffset, SpanBytes, SE);
     if (!MaxOffset)
       return false;
     DerefBytesSCEV = SE.applyLoopGuards(DerefBytesSCEV, *LoopGuards);
   } else {
     // FIXME: two independent off-by-EltSize bugs on this branch:
-    //  1. LowestOffset here is actually the HIGHEST offset, because
-    //     LowestAddr is computed from AR->getStart() rather than
+    //  1. StartOffset here is actually the HIGHEST offset, because it is
+    //     computed from AR->getStart() rather than
     //     AR->evaluateAtIteration(MaxBTC, SE) (see FIXME above).
     //  2. The lower check is over-strict by EltSize and the upper is
     //     under-counted by EltSize.
     assert(SE.isKnownNegative(Step) && "must be known negative");
-    if (!SE.isKnownPredicate(CmpInst::ICMP_SGE, LowestOffset, SpanBytes))
+    if (!SE.isKnownPredicate(CmpInst::ICMP_SGE, StartOffset, SpanBytes))
       return false;
-    MaxOffset = LowestOffset;
+    MaxOffset = StartOffset;
   }
   // MaxOffset must not exceed the deref-region end.
   return SE.isKnownPredicate(CmpInst::ICMP_ULE, MaxOffset, DerefBytesSCEV);

>From 1cef1ac9099aa4eea1321069cc45e7752dff036d Mon Sep 17 00:00:00 2001
From: Aleksandr Popov <apopov at azul.com>
Date: Fri, 31 Jul 2026 12:14:44 +0000
Subject: [PATCH 4/6] Regenerate check lines for
 negative-step-deref-off-by-eltsize test

---
 .../negative-step-deref-off-by-eltsize.ll     | 64 ++++++++++++++-----
 1 file changed, 48 insertions(+), 16 deletions(-)

diff --git a/llvm/test/Analysis/LoopAccessAnalysis/negative-step-deref-off-by-eltsize.ll b/llvm/test/Analysis/LoopAccessAnalysis/negative-step-deref-off-by-eltsize.ll
index b1d9a984592c8..1fc5fe3e92fb5 100644
--- a/llvm/test/Analysis/LoopAccessAnalysis/negative-step-deref-off-by-eltsize.ll
+++ b/llvm/test/Analysis/LoopAccessAnalysis/negative-step-deref-off-by-eltsize.ll
@@ -1,3 +1,4 @@
+; NOTE: Assertions have been autogenerated by utils/update_analyze_test_checks.py UTC_ARGS: --version 6
 ; RUN: opt -passes='print<access-info>' -disable-output %s 2>&1 | FileCheck %s
 
 ; Reverse i32 loop over 4 elements whose access range exactly fills the
@@ -16,12 +17,28 @@
 
 define void @reverse_reaches_base(ptr dereferenceable(16) %A, ptr dereferenceable(16) %B) {
 ; CHECK-LABEL: 'reverse_reaches_base'
-; CHECK:      Group GRP0:
-; CHECK-NEXT:   (Low: (-4 + inttoptr (i64 -1 to ptr))<nsw> High: (16 + %B)<nuw>)
-; CHECK-NEXT:     Member: {(12 + %B)<nuw>,+,-4}<nw><%loop>
-; CHECK:      Group GRP1:
-; CHECK-NEXT:   (Low: (-4 + inttoptr (i64 -1 to ptr))<nsw> High: (16 + %A)<nuw>)
-; CHECK-NEXT:     Member: {(12 + %A)<nuw>,+,-4}<nw><%loop>
+; CHECK-NEXT:    loop:
+; CHECK-NEXT:      Memory dependences are safe with run-time checks
+; CHECK-NEXT:      Dependences:
+; CHECK-NEXT:      Run-time memory checks:
+; CHECK-NEXT:      Check 0:
+; CHECK-NEXT:        Comparing group GRP0:
+; CHECK-NEXT:          %gep.B = getelementptr inbounds i32, ptr %B, i64 %iv
+; CHECK-NEXT:        Against group GRP1:
+; CHECK-NEXT:          %gep.A = getelementptr inbounds i32, ptr %A, i64 %iv
+; CHECK-NEXT:      Grouped accesses:
+; CHECK-NEXT:        Group GRP0:
+; CHECK-NEXT:          (Low: (-4 + inttoptr (i64 -1 to ptr))<nsw> High: (16 + %B)<nuw>)
+; CHECK-NEXT:            Member: {(12 + %B)<nuw>,+,-4}<nw><%loop>
+; CHECK-NEXT:        Group GRP1:
+; CHECK-NEXT:          (Low: (-4 + inttoptr (i64 -1 to ptr))<nsw> High: (16 + %A)<nuw>)
+; CHECK-NEXT:            Member: {(12 + %A)<nuw>,+,-4}<nw><%loop>
+; CHECK-EMPTY:
+; CHECK-NEXT:      Non vectorizable stores to invariant address were not found in loop.
+; CHECK-NEXT:      SCEV assumptions:
+; CHECK-EMPTY:
+; CHECK-NEXT:      Expressions re-written:
+;
 entry:
   br label %loop
 
@@ -62,16 +79,32 @@ exit.done:
 
 define void @reverse_top_spills(ptr dereferenceable(16) %A, ptr dereferenceable(16) %B) {
 ; CHECK-LABEL: 'reverse_top_spills'
-; CHECK:      Group GRP0:
-; CHECK-NEXT:   (Low: (5 + %B)<nuw> High: (17 + %B))
-; CHECK-NEXT:     Member: {(13 + %B)<nuw>,+,-4}<nw><%loop2>
-; CHECK:      Group GRP1:
-; CHECK-NEXT:   (Low: (5 + %A)<nuw> High: (17 + %A))
-; CHECK-NEXT:     Member: {(13 + %A)<nuw>,+,-4}<nw><%loop2>
+; CHECK-NEXT:    loop:
+; CHECK-NEXT:      Memory dependences are safe with run-time checks
+; CHECK-NEXT:      Dependences:
+; CHECK-NEXT:      Run-time memory checks:
+; CHECK-NEXT:      Check 0:
+; CHECK-NEXT:        Comparing group GRP0:
+; CHECK-NEXT:          %gep.B = getelementptr inbounds i8, ptr %B, i64 %iv
+; CHECK-NEXT:        Against group GRP1:
+; CHECK-NEXT:          %gep.A = getelementptr inbounds i8, ptr %A, i64 %iv
+; CHECK-NEXT:      Grouped accesses:
+; CHECK-NEXT:        Group GRP0:
+; CHECK-NEXT:          (Low: (5 + %B)<nuw> High: (17 + %B))
+; CHECK-NEXT:            Member: {(13 + %B)<nuw>,+,-4}<nw><%loop>
+; CHECK-NEXT:        Group GRP1:
+; CHECK-NEXT:          (Low: (5 + %A)<nuw> High: (17 + %A))
+; CHECK-NEXT:            Member: {(13 + %A)<nuw>,+,-4}<nw><%loop>
+; CHECK-EMPTY:
+; CHECK-NEXT:      Non vectorizable stores to invariant address were not found in loop.
+; CHECK-NEXT:      SCEV assumptions:
+; CHECK-EMPTY:
+; CHECK-NEXT:      Expressions re-written:
+;
 entry:
-  br label %loop2
+  br label %loop
 
-loop2:
+loop:
   %iv = phi i64 [ 13, %entry ], [ %iv.dec, %latch ]
   %gep.A = getelementptr inbounds i8, ptr %A, i64 %iv
   %gep.B = getelementptr inbounds i8, ptr %B, i64 %iv
@@ -83,7 +116,7 @@ loop2:
 latch:
   %iv.dec = add nsw i64 %iv, -4
   %ec = icmp eq i64 %iv, 5
-  br i1 %ec, label %exit.done, label %loop2
+  br i1 %ec, label %exit.done, label %loop
 
 exit.early:
   ret void
@@ -91,4 +124,3 @@ exit.early:
 exit.done:
   ret void
 }
-

>From a8ca0ef444ea1eff5898e69f3b90dc38f4a0837c Mon Sep 17 00:00:00 2001
From: Aleksandr Popov <42888396+aleks-tmb at users.noreply.github.com>
Date: Sun, 2 Aug 2026 22:42:45 +0200
Subject: [PATCH 5/6] Apply suggestions from code review

Co-authored-by: Florian Hahn <flo at fhahn.com>
---
 llvm/lib/Analysis/LoopAccessAnalysis.cpp                      | 4 ++--
 .../LoopAccessAnalysis/negative-step-deref-off-by-eltsize.ll  | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/llvm/lib/Analysis/LoopAccessAnalysis.cpp b/llvm/lib/Analysis/LoopAccessAnalysis.cpp
index d2f01c339fd29..8214230b01ee8 100644
--- a/llvm/lib/Analysis/LoopAccessAnalysis.cpp
+++ b/llvm/lib/Analysis/LoopAccessAnalysis.cpp
@@ -282,8 +282,8 @@ static bool evaluatePtrAddRecAtMaxBTCWillNotWrap(
   MaxBTC = SE.applyLoopGuards(MaxBTC, *LoopGuards);
 
   const SCEV *AbsStep = SE.getAbsExpr(Step, /*IsNSW=*/false);
-  // Total distance (in bytes) walked between the first and the last
-  // accessed pointer; MaxBTC * |Step|.
+  // Total distance (in bytes) between the first and the last
+  // accessed pointer.
   const SCEV *WalkBytes = mulSCEVNoOverflow(MaxBTC, AbsStep, SE);
   if (!WalkBytes) {
     // Re-try with constant max backedge-taken count if using the symbolic one
diff --git a/llvm/test/Analysis/LoopAccessAnalysis/negative-step-deref-off-by-eltsize.ll b/llvm/test/Analysis/LoopAccessAnalysis/negative-step-deref-off-by-eltsize.ll
index 1fc5fe3e92fb5..1a53fba949749 100644
--- a/llvm/test/Analysis/LoopAccessAnalysis/negative-step-deref-off-by-eltsize.ll
+++ b/llvm/test/Analysis/LoopAccessAnalysis/negative-step-deref-off-by-eltsize.ll
@@ -1,7 +1,7 @@
 ; NOTE: Assertions have been autogenerated by utils/update_analyze_test_checks.py UTC_ARGS: --version 6
 ; RUN: opt -passes='print<access-info>' -disable-output %s 2>&1 | FileCheck %s
 
-; Reverse i32 loop over 4 elements whose access range exactly fills the
+; Reverse loop loading 4 i32 elements whose access range exactly fills the
 ; dereferenceable region (deref(16), reads bytes [0, 16)).
 ;
 ; TODO: LAA should recognise that this AR fits within the deref

>From ab6c28b831f3782f353e3e0721acb66dcbd84b75 Mon Sep 17 00:00:00 2001
From: Aleksandr Popov <apopov at azul.com>
Date: Sun, 2 Aug 2026 21:03:47 +0000
Subject: [PATCH 6/6] WalkBytes/SpanBytes to DistToLastIter/AccessedBytes

---
 llvm/lib/Analysis/LoopAccessAnalysis.cpp | 20 ++++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/llvm/lib/Analysis/LoopAccessAnalysis.cpp b/llvm/lib/Analysis/LoopAccessAnalysis.cpp
index 8214230b01ee8..c99d43dd1ccc2 100644
--- a/llvm/lib/Analysis/LoopAccessAnalysis.cpp
+++ b/llvm/lib/Analysis/LoopAccessAnalysis.cpp
@@ -284,31 +284,31 @@ static bool evaluatePtrAddRecAtMaxBTCWillNotWrap(
   const SCEV *AbsStep = SE.getAbsExpr(Step, /*IsNSW=*/false);
   // Total distance (in bytes) between the first and the last
   // accessed pointer.
-  const SCEV *WalkBytes = mulSCEVNoOverflow(MaxBTC, AbsStep, SE);
-  if (!WalkBytes) {
+  const SCEV *DistToLastIter = mulSCEVNoOverflow(MaxBTC, AbsStep, SE);
+  if (!DistToLastIter) {
     // Re-try with constant max backedge-taken count if using the symbolic one
     // failed.
     MaxBTC = SE.getConstantMaxBackedgeTakenCount(AR->getLoop());
     if (isa<SCEVCouldNotCompute>(MaxBTC))
       return false;
     MaxBTC = SE.getNoopOrZeroExtend(MaxBTC, WiderTy);
-    WalkBytes = mulSCEVNoOverflow(MaxBTC, AbsStep, SE);
-    if (!WalkBytes)
+    DistToLastIter = mulSCEVNoOverflow(MaxBTC, AbsStep, SE);
+    if (!DistToLastIter)
       return false;
   }
 
   // Total length in bytes of the accessed range (from the first accessed
-  // byte through the end of the last access); WalkBytes + EltSize.
-  const SCEV *SpanBytes = addSCEVNoOverflow(
-      WalkBytes, SE.getNoopOrZeroExtend(EltSize, WiderTy), SE);
-  if (!SpanBytes)
+  // byte through the end of the last access).
+  const SCEV *AccessedBytes = addSCEVNoOverflow(
+      DistToLastIter, SE.getNoopOrZeroExtend(EltSize, WiderTy), SE);
+  if (!AccessedBytes)
     return false;
 
   // Compute MaxOffset per direction: exclusive upper offset of the
   // accessed range.
   const SCEV *MaxOffset;
   if (IsKnownNonNegative) {
-    MaxOffset = addSCEVNoOverflow(StartOffset, SpanBytes, SE);
+    MaxOffset = addSCEVNoOverflow(StartOffset, AccessedBytes, SE);
     if (!MaxOffset)
       return false;
     DerefBytesSCEV = SE.applyLoopGuards(DerefBytesSCEV, *LoopGuards);
@@ -320,7 +320,7 @@ static bool evaluatePtrAddRecAtMaxBTCWillNotWrap(
     //  2. The lower check is over-strict by EltSize and the upper is
     //     under-counted by EltSize.
     assert(SE.isKnownNegative(Step) && "must be known negative");
-    if (!SE.isKnownPredicate(CmpInst::ICMP_SGE, StartOffset, SpanBytes))
+    if (!SE.isKnownPredicate(CmpInst::ICMP_SGE, StartOffset, AccessedBytes))
       return false;
     MaxOffset = StartOffset;
   }



More information about the llvm-commits mailing list