[llvm] Extending LoopVersioningLICM to handle cases where loopbound is invarint (PR #192794)

Manish Srivastava via llvm-commits llvm-commits at lists.llvm.org
Sat Apr 18 07:53:15 PDT 2026


https://github.com/mk-srivastava created https://github.com/llvm/llvm-project/pull/192794

The changes extending the LoopVersioningLICM for the cases discussed in the discussion: https://github.com/llvm/llvm-project/issues/112060

>From 83035c2b497d076f7dc806eeeaa6e56a2559f9ab Mon Sep 17 00:00:00 2001
From: Manish Srivastava <manish.srivastava at amd.com>
Date: Fri, 17 Apr 2026 12:37:43 +0530
Subject: [PATCH] Extending LoopVersioningLICM to handle cases where loopbound
 is invariant

---
 .../llvm/Transforms/Utils/LoopVersioning.h    |   7 +-
 .../Transforms/Scalar/LoopVersioningLICM.cpp  | 344 +++++++++++++++++-
 llvm/lib/Transforms/Utils/LoopVersioning.cpp  |  44 ++-
 .../dynamic-bound-array-element.ll            | 139 +++++++
 .../dynamic-bound-chained-loads.ll            |  86 +++++
 .../dynamic-bound-fcmp-exit.ll                |  57 +++
 .../dynamic-bound-lambda.ll                   | 142 ++++++++
 .../dynamic-bound-loop-varying.ll             |  79 ++++
 .../dynamic-bound-simple.ll                   | 139 +++++++
 .../dynamic-bound-stored-pointer.ll           |  72 ++++
 .../dynamic-bound-two-loads.ll                | 158 ++++++++
 .../dynamic-bound-volatile-load.ll            |  67 ++++
 12 files changed, 1307 insertions(+), 27 deletions(-)
 create mode 100644 llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-array-element.ll
 create mode 100644 llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-chained-loads.ll
 create mode 100644 llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-fcmp-exit.ll
 create mode 100644 llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-lambda.ll
 create mode 100644 llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-loop-varying.ll
 create mode 100644 llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-simple.ll
 create mode 100644 llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-stored-pointer.ll
 create mode 100644 llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-two-loads.ll
 create mode 100644 llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-volatile-load.ll

diff --git a/llvm/include/llvm/Transforms/Utils/LoopVersioning.h b/llvm/include/llvm/Transforms/Utils/LoopVersioning.h
index ea4fe27c90f5c..ec07fb9259b6a 100644
--- a/llvm/include/llvm/Transforms/Utils/LoopVersioning.h
+++ b/llvm/include/llvm/Transforms/Utils/LoopVersioning.h
@@ -61,11 +61,14 @@ class LoopVersioning {
   ///        analyze L
   ///        if versioning is necessary version L
   ///        transform L
-  void versionLoop() { versionLoop(findDefsUsedOutsideOfLoop(VersionedLoop)); }
+  void versionLoop(Instruction *UserRuntimeCheck = nullptr) { versionLoop(findDefsUsedOutsideOfLoop(VersionedLoop), UserRuntimeCheck); }
 
   /// Same but if the client has already precomputed the set of values
   /// used outside the loop, this API will allows passing that.
-  void versionLoop(const SmallVectorImpl<Instruction *> &DefsUsedOutside);
+  /// \p UserMemRuntimeCheck allows the caller to supply a pre-built runtime
+  /// check instruction instead of having LoopVersioning generate one.
+  void versionLoop(const SmallVectorImpl<Instruction *> &DefsUsedOutside,
+                   Instruction *UserMemRuntimeCheck = nullptr);
 
   /// Returns the versioned loop.  Control flows here if pointers in the
   /// loop don't alias (i.e. all memchecks passed).  (This loop is actually the
diff --git a/llvm/lib/Transforms/Scalar/LoopVersioningLICM.cpp b/llvm/lib/Transforms/Scalar/LoopVersioningLICM.cpp
index 3aed643ee8065..8d973eb32189a 100644
--- a/llvm/lib/Transforms/Scalar/LoopVersioningLICM.cpp
+++ b/llvm/lib/Transforms/Scalar/LoopVersioningLICM.cpp
@@ -70,19 +70,23 @@
 #include "llvm/Analysis/LoopPass.h"
 #include "llvm/Analysis/OptimizationRemarkEmitter.h"
 #include "llvm/Analysis/ScalarEvolution.h"
+#include "llvm/Analysis/ScalarEvolutionExpressions.h"
 #include "llvm/IR/Dominators.h"
 #include "llvm/IR/Instruction.h"
 #include "llvm/IR/Instructions.h"
 #include "llvm/IR/LLVMContext.h"
 #include "llvm/IR/MDBuilder.h"
 #include "llvm/IR/Metadata.h"
+#include "llvm/IR/Module.h"
 #include "llvm/IR/Value.h"
+#include "llvm/IR/Verifier.h"
 #include "llvm/Support/Casting.h"
 #include "llvm/Support/CommandLine.h"
 #include "llvm/Support/Debug.h"
 #include "llvm/Support/raw_ostream.h"
 #include "llvm/Transforms/Utils/LoopUtils.h"
 #include "llvm/Transforms/Utils/LoopVersioning.h"
+#include "llvm/Transforms/Utils/ScalarEvolutionExpander.h"
 #include <cassert>
 
 using namespace llvm;
@@ -116,8 +120,8 @@ struct LoopVersioningLICM {
   LoopVersioningLICM(AliasAnalysis *AA, ScalarEvolution *SE,
                      OptimizationRemarkEmitter *ORE,
                      LoopAccessInfoManager &LAIs, LoopInfo &LI,
-                     Loop *CurLoop)
-      : AA(AA), SE(SE), LAIs(LAIs), LI(LI), CurLoop(CurLoop),
+                     AssumptionCache *AC, Loop *CurLoop)
+      : AA(AA), SE(SE), LAIs(LAIs), LI(LI), AC(AC), CurLoop(CurLoop),
         LoopDepthThreshold(LVLoopDepthThreshold),
         InvariantThreshold(LVInvarThreshold), ORE(ORE) {}
 
@@ -138,9 +142,17 @@ struct LoopVersioningLICM {
 
   LoopInfo &LI;
 
+  AssumptionCache *AC;
+
   // The current loop we are working on.
   Loop *CurLoop;
 
+  // Dynamic bound marker.
+  bool IsDynamicBound = false;
+
+  // Hoistable dependencies
+  SmallVector<Instruction *, 16> HoistedDependencies;
+
   // Maximum loop nest threshold
   unsigned LoopDepthThreshold;
 
@@ -156,6 +168,9 @@ struct LoopVersioningLICM {
   // Read only loop marker.
   bool IsReadOnlyLoop = true;
 
+  // Placeholder instruction for inserting runtime checks in the preheader.
+  Instruction *PlaceholderForRuntimeChecks = nullptr;
+
   // OptimizationRemarkEmitter
   OptimizationRemarkEmitter *ORE;
 
@@ -165,6 +180,11 @@ struct LoopVersioningLICM {
   bool legalLoopMemoryAccesses();
   bool isLoopAlreadyVisited();
   bool instructionSafeForVersioning(Instruction *I);
+  bool isSafeToHoistBoundDep(Instruction *I,
+                              const SmallPtrSetImpl<Value *> &ModifiedPtrs);
+  Instruction *insertRuntimeCheckPlaceholder(Loop *L);
+  bool generateAndAddRuntimeChecks(DominatorTree *DT);
+  void printLoopsWithPreheaders(Loop *VersionedLoop, Loop *NonVersionedLoop);
 };
 
 } // end anonymous namespace
@@ -213,12 +233,165 @@ bool LoopVersioningLICM::legalLoopStructure() {
   // to generate the bound checks.
   const SCEV *ExitCount = SE->getBackedgeTakenCount(CurLoop);
   if (isa<SCEVCouldNotCompute>(ExitCount)) {
+    IsDynamicBound = true;
     LLVM_DEBUG(dbgs() << "    loop does not have trip count\n");
-    return false;
+    LLVM_DEBUG(dbgs() << "    checking the possibility of dynamic trip count\n");
+
+    HoistedDependencies.clear();
+
+    // Check if the loop trip count is dynamic
+    BasicBlock *ExitBlock = CurLoop->getExitingBlock();
+    assert(ExitBlock && "Exiting block guaranteed by earlier getExitingBlock() check");
+    CondBrInst *ExitBranch = dyn_cast<CondBrInst>(ExitBlock->getTerminator());
+    if (!ExitBranch) {
+      LLVM_DEBUG(dbgs() << "    loop exit condition is not a conditional branch\n");
+      return false;
+    }
+    ICmpInst *ExitCmp = dyn_cast<ICmpInst>(ExitBranch->getCondition());
+    if (!ExitCmp) {
+      LLVM_DEBUG(dbgs() << "    loop exit condition is not an icmp instruction\n");
+      return false;
+    }
+    
+    PHINode *IndVar = CurLoop->getInductionVariable(*SE);
+    if (!IndVar) {
+      LLVM_DEBUG(dbgs() << "    unable to find loop induction variable\n");
+      return false;
+    }
+    LLVM_DEBUG(dbgs() << "    Induction variable: " << *IndVar << "\n");
+
+    Value *StepInst = IndVar->getIncomingValueForBlock(CurLoop->getLoopLatch());
+
+    auto IsIVOrStep = [&](Value *V) {
+      return V == IndVar || V == StepInst;
+    };
+
+    Value *DynamicUpperBound = nullptr;
+    if (IsIVOrStep(ExitCmp->getOperand(0))) {
+      DynamicUpperBound = ExitCmp->getOperand(1);
+    }
+    else if (IsIVOrStep(ExitCmp->getOperand(1))) {
+      DynamicUpperBound = ExitCmp->getOperand(0);
+    }
+    else {
+      LLVM_DEBUG(dbgs() << "    unable to identify dynamic bound operand\n");
+      return false;
+    }
+    LLVM_DEBUG(dbgs() << "    Dynamic upper bound: " << *DynamicUpperBound
+                      << "\n");
+    
+    SmallVector<Instruction *, 16> Worklist;
+    
+    SmallPtrSet<Value *, 16> ModifiedPtrs;
+    for (BasicBlock *BB : CurLoop->getBlocks()) {
+      for (Instruction &I : *BB) {
+        if (StoreInst *SI = dyn_cast<StoreInst>(&I))
+          ModifiedPtrs.insert(SI->getPointerOperand());
+      }
+    }
+
+    if (Instruction *I = dyn_cast<Instruction>(DynamicUpperBound)) {
+      Worklist.push_back(I);
+    }
+    
+    SmallPtrSet<Instruction *, 16> Visited;
+
+    while(!Worklist.empty()) {
+      Instruction *I = Worklist.pop_back_val();
+      if (!Visited.insert(I).second)
+        continue;
+
+      if (!CurLoop->contains(I->getParent()))
+        continue;
+
+      if (!isSafeToHoistBoundDep(I, ModifiedPtrs))
+        return false;
+
+      for (Use &U : I->operands()) {
+        if (Instruction *OpI = dyn_cast<Instruction>(U.get())) {
+          if (CurLoop->contains(OpI)) {
+            Worklist.push_back(OpI);
+          }
+        }
+      }
+
+      HoistedDependencies.push_back(I);
+    }
+    return true;
   }
   return true;
 }
 
+/// Check if an instruction in the dynamic bound dependency chain is safe to
+/// hoist. Only side-effect-free arithmetic, bitwise, cast, comparison, GEP,
+/// and simple load instructions are allowed.
+bool LoopVersioningLICM::isSafeToHoistBoundDep(
+    Instruction *I, const SmallPtrSetImpl<Value *> &ModifiedPtrs) {
+  switch (I->getOpcode()) {
+  case Instruction::Load: {
+    LoadInst *LI = cast<LoadInst>(I);
+    if (!LI->isSimple()) {
+      LLVM_DEBUG(dbgs() << "    Found a non-simple load, as a dependency"
+                           " of exit condition\n");
+      return false;
+    }
+    Value *Ptr = LI->getPointerOperand();
+    if (ModifiedPtrs.count(Ptr)) {
+      LLVM_DEBUG(dbgs() << "    Bound candidate is invalid - pointer is"
+                           " stored to within loop: " << *LI << "\n");
+      return false;
+    }
+    return true;
+  }
+  // Integer arithmetic
+  case Instruction::Add:
+  case Instruction::Sub:
+  case Instruction::Mul:
+  case Instruction::UDiv:
+  case Instruction::SDiv:
+  case Instruction::URem:
+  case Instruction::SRem:
+  // Floating-point arithmetic
+  case Instruction::FAdd:
+  case Instruction::FSub:
+  case Instruction::FMul:
+  case Instruction::FDiv:
+  case Instruction::FRem:
+  case Instruction::FNeg:
+  // Bitwise operations
+  case Instruction::Shl:
+  case Instruction::LShr:
+  case Instruction::AShr:
+  case Instruction::And:
+  case Instruction::Or:
+  case Instruction::Xor:
+  // Integer casts
+  case Instruction::SExt:
+  case Instruction::ZExt:
+  case Instruction::Trunc:
+  // FP casts
+  case Instruction::FPExt:
+  case Instruction::FPTrunc:
+  case Instruction::FPToSI:
+  case Instruction::FPToUI:
+  case Instruction::SIToFP:
+  case Instruction::UIToFP:
+  // Pointer / bitcast
+  case Instruction::BitCast:
+  case Instruction::IntToPtr:
+  case Instruction::PtrToInt:
+  case Instruction::GetElementPtr:
+  // Comparisons
+  case Instruction::ICmp:
+  case Instruction::FCmp:
+    return true;
+  default:
+    LLVM_DEBUG(dbgs() << "    Unsupported instruction in bound dependency"
+                         " chain: " << *I << "\n");
+    return false;
+  }
+}
+
 /// Check memory accesses in loop and confirms it's good for
 /// LoopVersioningLICM.
 bool LoopVersioningLICM::legalLoopMemoryAccesses() {
@@ -413,7 +586,7 @@ bool LoopVersioningLICM::legalLoopInstructions() {
     LLVM_DEBUG(dbgs() << "    Found a read-only loop!\n");
     return false;
   }
-  // Profitability check:
+  // Profitablity check:
   // Check invariant threshold, should be in limit.
   if (InvariantCounter * 100 < InvariantThreshold * LoadAndStoreCounter) {
     LLVM_DEBUG(
@@ -507,39 +680,193 @@ bool LoopVersioningLICM::isLegalForVersioning() {
   return true;
 }
 
+Instruction *LoopVersioningLICM::insertRuntimeCheckPlaceholder(Loop *L) {
+  // Create a preheader for our checks
+  BasicBlock *Preheader = L->getLoopPreheader();
+  if (!Preheader)
+    return nullptr;
+
+  // Determine the insertion point.
+  Instruction *InsertPt = Preheader->getTerminator();
+  LLVMContext &Context = CurLoop->getHeader()->getParent()->getContext();
+  // Create the comparison.
+  Instruction *Placeholder = nullptr;
+  Placeholder = ICmpInst::Create(Instruction::ICmp, ICmpInst::ICMP_EQ,
+                                 ConstantInt::get(Type::getInt32Ty(Context), 1),
+                                 ConstantInt::get(Type::getInt32Ty(Context), 1),
+                                 "runtime.check.placeholder", InsertPt->getIterator());
+
+  return Placeholder;
+}
+
+bool LoopVersioningLICM::generateAndAddRuntimeChecks(DominatorTree *DT) {
+  BasicBlock *RuntimeCheckBB = PlaceholderForRuntimeChecks->getParent();
+
+  const auto &RtPtrChecking = *LAI->getRuntimePointerChecking();
+
+  SCEVExpander Exp1(*RtPtrChecking.getSE(), "induction");
+  
+  auto RPC = LAI->getRuntimePointerChecking();
+
+  Value *MemRuntimeChecks = llvm::addRuntimeChecks(PlaceholderForRuntimeChecks, 
+    CurLoop, RPC->getChecks(), Exp1);
+
+  SCEVExpander Exp2(*SE, "scev.check");
+  
+  const SCEVPredicate &Preds(LAI->getPSE().getPredicate());
+  Value *SCEVRuntimeChecks = Exp2.expandCodeForPredicate(&Preds, PlaceholderForRuntimeChecks);
+
+  IRBuilder<InstSimplifyFolder> Builder(RuntimeCheckBB->getContext(), 
+                InstSimplifyFolder(RuntimeCheckBB->getModule()->getDataLayout()));
+  
+  Value *RuntimeCheck = nullptr;
+  if (MemRuntimeChecks && SCEVRuntimeChecks) {
+    Builder.SetInsertPoint(PlaceholderForRuntimeChecks);
+    RuntimeCheck = Builder.CreateOr(MemRuntimeChecks, SCEVRuntimeChecks, "lver.safe");
+  } else {
+    RuntimeCheck = MemRuntimeChecks ? MemRuntimeChecks : SCEVRuntimeChecks;
+  }
+
+  RuntimeCheckBB->setName(CurLoop->getHeader()->getName() + ".lver.check");
+
+  CondBrInst *BI = dyn_cast<CondBrInst>(RuntimeCheckBB->getTerminator());
+  assert(BI && "Expected conditional branch in runtime check block");
+
+  PlaceholderForRuntimeChecks->replaceAllUsesWith(RuntimeCheck);
+  PlaceholderForRuntimeChecks->eraseFromParent();
+
+  return RuntimeCheck != nullptr;
+}
+
+void LoopVersioningLICM::printLoopsWithPreheaders(
+    Loop *VersionedLoop, Loop *NonVersionedLoop) {
+  std::string DL;
+  if (CurLoop) {
+    raw_string_ostream OS(DL);
+    if (const DebugLoc LoopDbgLoc = CurLoop->getStartLoc())
+      LoopDbgLoc.print(OS);
+    else
+      OS << CurLoop->getHeader()->getParent()->getParent()->getModuleIdentifier();
+  }
+
+  auto printLoop = [&DL](Loop *L){
+    if (!L)
+      return;
+    dbgs() << ">>>>>>>>>>  Loop: ";
+    dbgs() << "Loop starts at: " << DL;
+    dbgs() << "\n";
+    BasicBlock *Header = L->getHeader();
+    BasicBlock *Preheader = L->getLoopPreheader();
+    if (Preheader)
+      Header = Preheader;
+    dbgs() << "Check :" << *Header->getSinglePredecessor() <<"\n"; 
+    if (Preheader) {
+      dbgs() << "Preheader: " << *Preheader << "\n";
+    } else {
+      dbgs() << "No preheader\n";
+    }
+    L->dumpVerbose();
+    dbgs() << "<<<<<<<<<<<<<< Loop: ";
+    dbgs() << "Loop starts at: " << DL;
+    dbgs() << "\n";
+  };
+  
+  dbgs() << "\n--- NonVersioned Loop ---\n";
+  printLoop(NonVersionedLoop);
+  dbgs() << "\n--- Versioned Loop ---\n";
+  printLoop(VersionedLoop);
+}
+
 bool LoopVersioningLICM::run(DominatorTree *DT) {
   // Do not do the transformation if disabled by metadata.
   if (hasLICMVersioningTransformation(CurLoop) & TM_Disable)
     return false;
-
   bool Changed = false;
-
   // Check feasiblity of LoopVersioningLICM.
   // If versioning found to be feasible and beneficial then proceed
   // else simply return, by cleaning up memory.
   if (isLegalForVersioning()) {
+    PlaceholderForRuntimeChecks = nullptr;
+    if (IsDynamicBound) {
+      PlaceholderForRuntimeChecks = insertRuntimeCheckPlaceholder(CurLoop);
+      if (!PlaceholderForRuntimeChecks) {
+        LLVM_DEBUG(dbgs() << "No runtime check created\n");
+        return false;
+      }
+    }
     // Do loop versioning.
     // Create memcheck for memory accessed inside loop.
     // Clone original loop, and set blocks properly.
     LoopVersioning LVer(*LAI, LAI->getRuntimePointerChecking()->getChecks(),
                         CurLoop, &LI, DT, SE);
-    LVer.versionLoop();
+    LVer.versionLoop(PlaceholderForRuntimeChecks);
+    
+    // Add the extra checks for dynamic bounds
+    if (IsDynamicBound) {
+      for (auto p = HoistedDependencies.rbegin(); p != HoistedDependencies.rend(); ++p) {
+        Instruction *I = *p;
+        if (isa<LoadInst>(I)) {
+          // Clone the load to the preheader instead of moving it. The original
+          // dead load is intentionally kept in the loop body so that the subsequent LAA recomputation sees 
+          // the load and generates runtime pointer checks for its address range. The cleanup passes will erase it later. 
+          LoadInst *LI = cast<LoadInst>(I);
+          LoadInst *NewLI = new LoadInst(
+            LI->getType(), LI->getPointerOperand(), LI->getName() + ".hoisted",
+            false, LI->getAlign(), LI->getOrdering(), LI->getSyncScopeID(),
+            PlaceholderForRuntimeChecks->getIterator()
+          );
+
+          NewLI->copyMetadata(*I);
+          I->replaceAllUsesWith(NewLI);
+        } else {
+          I->moveBefore(PlaceholderForRuntimeChecks->getIterator());
+        }
+      }
+
+// #ifdef EXPENSIVE_CHECKS
+      assert(!verifyFunction(*LVer.getVersionedLoop()->getHeader()->getParent(), &dbgs())
+             && "Verification failure after dynamic bounds");
+// #endif
+
+      SE->forgetLoop(CurLoop);
+      // Recompute LoopAccessInfo after hoisting the dynamic bound
+      LoopAccessInfoManager FreshLAIs(*SE, *AA, *DT, LI, nullptr, nullptr, AC);
+      LAI = &FreshLAIs.getInfo(*CurLoop);
+
+      generateAndAddRuntimeChecks(DT);
+
+// #ifdef EXPENSIVE_CHECKS
+      assert(!verifyFunction(*LVer.getVersionedLoop()->getHeader()->getParent(), &dbgs())
+             && "Verification failure after runtime checks");
+// #endif
+
+      // LAI isn't used anymore, so clear it to avoid dangling reference
+      LAI = nullptr;
+    }
     // Set Loop Versioning metaData for original loop.
     addStringMetadataToLoop(LVer.getNonVersionedLoop(), LICMVersioningMetaData);
     // Set Loop Versioning metaData for version loop.
     addStringMetadataToLoop(LVer.getVersionedLoop(), LICMVersioningMetaData);
+
     // Set "llvm.mem.parallel_loop_access" metaData to versioned loop.
     // FIXME: "llvm.mem.parallel_loop_access" annotates memory access
     // instructions, not loops.
     addStringMetadataToLoop(LVer.getVersionedLoop(),
                             "llvm.mem.parallel_loop_access");
+
     // Update version loop with aggressive aliasing assumption.
     LVer.annotateLoopWithNoAlias();
     Changed = true;
+
+    LLVM_DEBUG(dbgs() << "Successfully transformed the code\n");
+    LLVM_DEBUG(printLoopsWithPreheaders(LVer.getVersionedLoop(),
+                                       LVer.getNonVersionedLoop()));
   }
   return Changed;
 }
 
+namespace llvm {
+
 PreservedAnalyses LoopVersioningLICMPass::run(Loop &L, LoopAnalysisManager &AM,
                                               LoopStandardAnalysisResults &LAR,
                                               LPMUpdater &U) {
@@ -550,7 +877,8 @@ PreservedAnalyses LoopVersioningLICMPass::run(Loop &L, LoopAnalysisManager &AM,
   OptimizationRemarkEmitter ORE(F);
 
   LoopAccessInfoManager LAIs(*SE, *AA, *DT, LAR.LI, nullptr, nullptr, &LAR.AC);
-  if (!LoopVersioningLICM(AA, SE, &ORE, LAIs, LAR.LI, &L).run(DT))
+  if (!LoopVersioningLICM(AA, SE, &ORE, LAIs, LAR.LI, &LAR.AC, &L).run(DT))
     return PreservedAnalyses::all();
   return getLoopPassPreservedAnalyses();
 }
+} // namespace llvm
diff --git a/llvm/lib/Transforms/Utils/LoopVersioning.cpp b/llvm/lib/Transforms/Utils/LoopVersioning.cpp
index b90466a8c49cf..6696e3c6a6b0e 100644
--- a/llvm/lib/Transforms/Utils/LoopVersioning.cpp
+++ b/llvm/lib/Transforms/Utils/LoopVersioning.cpp
@@ -48,7 +48,7 @@ LoopVersioning::LoopVersioning(const LoopAccessInfo &LAI,
       LAI(LAI), LI(LI), DT(DT), SE(SE) {}
 
 void LoopVersioning::versionLoop(
-    const SmallVectorImpl<Instruction *> &DefsUsedOutside) {
+    const SmallVectorImpl<Instruction *> &DefsUsedOutside, Instruction *UserRuntimeCheck) {
   assert(VersionedLoop->getUniqueExitBlock() && "No single exit block");
   assert(VersionedLoop->isLoopSimplifyForm() &&
          "Loop is not in loop-simplify form");
@@ -59,27 +59,37 @@ void LoopVersioning::versionLoop(
 
   // Add the memcheck in the original preheader (this is empty initially).
   BasicBlock *RuntimeCheckBB = VersionedLoop->getLoopPreheader();
-  const auto &RtPtrChecking = *LAI.getRuntimePointerChecking();
-
-  SCEVExpander Exp2(*RtPtrChecking.getSE(), "induction");
-  MemRuntimeCheck = addRuntimeChecks(RuntimeCheckBB->getTerminator(),
-                                     VersionedLoop, AliasChecks, Exp2);
 
-  SCEVExpander Exp(*SE, "scev.check");
-  SCEVRuntimeCheck =
-      Exp.expandCodeForPredicate(&Preds, RuntimeCheckBB->getTerminator());
+  if (!UserRuntimeCheck) {
+    const auto &RtPtrChecking = *LAI.getRuntimePointerChecking();
+
+    SCEVExpander Exp2(*RtPtrChecking.getSE(), "induction");
+    MemRuntimeCheck = addRuntimeChecks(RuntimeCheckBB->getTerminator(),
+                                      VersionedLoop, AliasChecks, Exp2);
+    SCEVExpander Exp(*SE, "scev.check");
+    SCEVRuntimeCheck =
+        Exp.expandCodeForPredicate(&Preds, RuntimeCheckBB->getTerminator());
+
+    IRBuilder<InstSimplifyFolder> Builder(
+        RuntimeCheckBB->getContext(),
+        InstSimplifyFolder(RuntimeCheckBB->getDataLayout()));
+    if (MemRuntimeCheck && SCEVRuntimeCheck) {
+      Builder.SetInsertPoint(RuntimeCheckBB->getTerminator());
+      RuntimeCheck =
+          Builder.CreateOr(MemRuntimeCheck, SCEVRuntimeCheck, "lver.safe");
+    } else
+      RuntimeCheck = MemRuntimeCheck ? MemRuntimeCheck : SCEVRuntimeCheck;
+
+    Exp.eraseDeadInstructions(SCEVRuntimeCheck);
+  }
+  else {
+    RuntimeCheck = UserRuntimeCheck;
+    RuntimeCheckBB = UserRuntimeCheck->getParent();
+  }
 
   IRBuilder<InstSimplifyFolder> Builder(
       RuntimeCheckBB->getContext(),
       InstSimplifyFolder(RuntimeCheckBB->getDataLayout()));
-  if (MemRuntimeCheck && SCEVRuntimeCheck) {
-    Builder.SetInsertPoint(RuntimeCheckBB->getTerminator());
-    RuntimeCheck =
-        Builder.CreateOr(MemRuntimeCheck, SCEVRuntimeCheck, "lver.safe");
-  } else
-    RuntimeCheck = MemRuntimeCheck ? MemRuntimeCheck : SCEVRuntimeCheck;
-
-  Exp.eraseDeadInstructions(SCEVRuntimeCheck);
 
   assert(RuntimeCheck && "called even though we don't need "
                          "any runtime checks");
diff --git a/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-array-element.ll b/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-array-element.ll
new file mode 100644
index 0000000000000..bb844c2dee94e
--- /dev/null
+++ b/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-array-element.ll
@@ -0,0 +1,139 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 5
+; RUN: opt < %s -S -passes='loop-versioning-licm' | FileCheck %s
+;
+; TEST10: Loop bound loaded from an array element A[2], where A is also
+; written inside the loop. The load and store use different indices, so
+; pointer identity does not flag a conflict, but runtime checks will
+; guard correctness.
+; Verify the load is hoisted and runtime checks are generated.
+;
+;   void foo(int *A, int *B, int *C, int *Len) {
+;     for (int i = 0; i < A[2]; i++)
+;       A[i] = B[i] + C[i];
+;   }
+;
+
+define dso_local void @foo(ptr noundef captures(none) %A, ptr noundef readonly captures(none) %B, ptr noundef readonly captures(none) %C, ptr noundef readnone captures(none) %Len) #0 {
+; CHECK-LABEL: define dso_local void @foo(
+; CHECK-SAME: ptr noundef captures(none) [[A:%.*]], ptr noundef readonly captures(none) [[B:%.*]], ptr noundef readonly captures(none) [[C:%.*]], ptr noundef readnone captures(none) [[LEN:%.*]]) #[[ATTR0:[0-9]+]] {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[ARRAYIDX:%.*]] = getelementptr inbounds nuw i8, ptr [[A]], i64 8
+; CHECK-NEXT:    [[TMP0:%.*]] = load i32, ptr [[ARRAYIDX]], align 4, !tbaa [[TBAA0:![0-9]+]]
+; CHECK-NEXT:    [[CMP11:%.*]] = icmp sgt i32 [[TMP0]], 0
+; CHECK-NEXT:    br i1 [[CMP11]], label %[[FOR_BODY_LVER_CHECK:.*]], label %[[FOR_COND_CLEANUP:.*]]
+; CHECK:       [[FOR_BODY_LVER_CHECK]]:
+; CHECK-NEXT:    [[DOTHOISTED:%.*]] = load i32, ptr [[ARRAYIDX]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[TMP1:%.*]] = sext i32 [[DOTHOISTED]] to i64
+; CHECK-NEXT:    [[TMP2:%.*]] = shl nsw i64 [[TMP1]], 2
+; CHECK-NEXT:    [[SCEVGEP:%.*]] = getelementptr i8, ptr [[A]], i64 [[TMP2]]
+; CHECK-NEXT:    [[SCEVGEP2:%.*]] = getelementptr i8, ptr [[B]], i64 [[TMP2]]
+; CHECK-NEXT:    [[SCEVGEP3:%.*]] = getelementptr i8, ptr [[C]], i64 [[TMP2]]
+; CHECK-NEXT:    [[BOUND0:%.*]] = icmp ult ptr [[A]], [[SCEVGEP2]]
+; CHECK-NEXT:    [[BOUND1:%.*]] = icmp ult ptr [[B]], [[SCEVGEP]]
+; CHECK-NEXT:    [[FOUND_CONFLICT:%.*]] = and i1 [[BOUND0]], [[BOUND1]]
+; CHECK-NEXT:    [[BOUND04:%.*]] = icmp ult ptr [[A]], [[SCEVGEP3]]
+; CHECK-NEXT:    [[BOUND15:%.*]] = icmp ult ptr [[C]], [[SCEVGEP]]
+; CHECK-NEXT:    [[FOUND_CONFLICT6:%.*]] = and i1 [[BOUND04]], [[BOUND15]]
+; CHECK-NEXT:    [[CONFLICT_RDX:%.*]] = or i1 [[FOUND_CONFLICT]], [[FOUND_CONFLICT6]]
+; CHECK-NEXT:    br i1 [[CONFLICT_RDX]], label %[[FOR_BODY_PH_LVER_ORIG:.*]], label %[[FOR_BODY_PH:.*]]
+; CHECK:       [[FOR_BODY_PH_LVER_ORIG]]:
+; CHECK-NEXT:    br label %[[FOR_BODY_LVER_ORIG:.*]]
+; CHECK:       [[FOR_BODY_LVER_ORIG]]:
+; CHECK-NEXT:    [[INDVARS_IV_LVER_ORIG:%.*]] = phi i64 [ [[INDVARS_IV_NEXT_LVER_ORIG:%.*]], %[[FOR_BODY_LVER_ORIG]] ], [ 0, %[[FOR_BODY_PH_LVER_ORIG]] ]
+; CHECK-NEXT:    [[ARRAYIDX1_LVER_ORIG:%.*]] = getelementptr inbounds nuw i32, ptr [[B]], i64 [[INDVARS_IV_LVER_ORIG]]
+; CHECK-NEXT:    [[TMP3:%.*]] = load i32, ptr [[ARRAYIDX1_LVER_ORIG]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[ARRAYIDX3_LVER_ORIG:%.*]] = getelementptr inbounds nuw i32, ptr [[C]], i64 [[INDVARS_IV_LVER_ORIG]]
+; CHECK-NEXT:    [[TMP4:%.*]] = load i32, ptr [[ARRAYIDX3_LVER_ORIG]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[ADD_LVER_ORIG:%.*]] = add nsw i32 [[TMP4]], [[TMP3]]
+; CHECK-NEXT:    [[ARRAYIDX5_LVER_ORIG:%.*]] = getelementptr inbounds nuw i32, ptr [[A]], i64 [[INDVARS_IV_LVER_ORIG]]
+; CHECK-NEXT:    store i32 [[ADD_LVER_ORIG]], ptr [[ARRAYIDX5_LVER_ORIG]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[INDVARS_IV_NEXT_LVER_ORIG]] = add nuw nsw i64 [[INDVARS_IV_LVER_ORIG]], 1
+; CHECK-NEXT:    [[TMP5:%.*]] = load i32, ptr [[ARRAYIDX]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[TMP6:%.*]] = sext i32 [[TMP5]] to i64
+; CHECK-NEXT:    [[CMP_LVER_ORIG:%.*]] = icmp slt i64 [[INDVARS_IV_NEXT_LVER_ORIG]], [[TMP6]]
+; CHECK-NEXT:    br i1 [[CMP_LVER_ORIG]], label %[[FOR_BODY_LVER_ORIG]], label %[[FOR_COND_CLEANUP_LOOPEXIT_LOOPEXIT:.*]], !llvm.loop [[LOOP4:![0-9]+]]
+; CHECK:       [[FOR_BODY_PH]]:
+; CHECK-NEXT:    br label %[[FOR_BODY:.*]]
+; CHECK:       [[FOR_COND_CLEANUP_LOOPEXIT_LOOPEXIT]]:
+; CHECK-NEXT:    br label %[[FOR_COND_CLEANUP_LOOPEXIT:.*]]
+; CHECK:       [[FOR_COND_CLEANUP_LOOPEXIT_LOOPEXIT1:.*]]:
+; CHECK-NEXT:    br label %[[FOR_COND_CLEANUP_LOOPEXIT]]
+; CHECK:       [[FOR_COND_CLEANUP_LOOPEXIT]]:
+; CHECK-NEXT:    br label %[[FOR_COND_CLEANUP]]
+; CHECK:       [[FOR_COND_CLEANUP]]:
+; CHECK-NEXT:    ret void
+; CHECK:       [[FOR_BODY]]:
+; CHECK-NEXT:    [[INDVARS_IV:%.*]] = phi i64 [ [[INDVARS_IV_NEXT:%.*]], %[[FOR_BODY]] ], [ 0, %[[FOR_BODY_PH]] ]
+; CHECK-NEXT:    [[ARRAYIDX1:%.*]] = getelementptr inbounds nuw i32, ptr [[B]], i64 [[INDVARS_IV]]
+; CHECK-NEXT:    [[TMP7:%.*]] = load i32, ptr [[ARRAYIDX1]], align 4, !tbaa [[TBAA0]], !alias.scope [[META8:![0-9]+]]
+; CHECK-NEXT:    [[ARRAYIDX3:%.*]] = getelementptr inbounds nuw i32, ptr [[C]], i64 [[INDVARS_IV]]
+; CHECK-NEXT:    [[TMP8:%.*]] = load i32, ptr [[ARRAYIDX3]], align 4, !tbaa [[TBAA0]], !alias.scope [[META11:![0-9]+]]
+; CHECK-NEXT:    [[ADD:%.*]] = add nsw i32 [[TMP8]], [[TMP7]]
+; CHECK-NEXT:    [[ARRAYIDX5:%.*]] = getelementptr inbounds nuw i32, ptr [[A]], i64 [[INDVARS_IV]]
+; CHECK-NEXT:    store i32 [[ADD]], ptr [[ARRAYIDX5]], align 4, !tbaa [[TBAA0]], !alias.scope [[META13:![0-9]+]], !noalias [[META15:![0-9]+]]
+; CHECK-NEXT:    [[INDVARS_IV_NEXT]] = add nuw nsw i64 [[INDVARS_IV]], 1
+; CHECK-NEXT:    [[TMP9:%.*]] = load i32, ptr [[ARRAYIDX]], align 4, !tbaa [[TBAA0]], !alias.scope [[META16:![0-9]+]]
+; CHECK-NEXT:    [[CMP:%.*]] = icmp slt i64 [[INDVARS_IV_NEXT]], [[TMP1]]
+; CHECK-NEXT:    br i1 [[CMP]], label %[[FOR_BODY]], label %[[FOR_COND_CLEANUP_LOOPEXIT_LOOPEXIT1]], !llvm.loop [[LOOP18:![0-9]+]]
+;
+entry:
+  %arrayidx = getelementptr inbounds nuw i8, ptr %A, i64 8
+  %0 = load i32, ptr %arrayidx, align 4, !tbaa !5
+  %cmp11 = icmp sgt i32 %0, 0
+  br i1 %cmp11, label %for.body, label %for.cond.cleanup
+
+for.cond.cleanup:
+  ret void
+
+for.body:
+  %indvars.iv = phi i64 [ %indvars.iv.next, %for.body ], [ 0, %entry ]
+  %arrayidx1 = getelementptr inbounds nuw i32, ptr %B, i64 %indvars.iv
+  %1 = load i32, ptr %arrayidx1, align 4, !tbaa !5
+  %arrayidx3 = getelementptr inbounds nuw i32, ptr %C, i64 %indvars.iv
+  %2 = load i32, ptr %arrayidx3, align 4, !tbaa !5
+  %add = add nsw i32 %2, %1
+  %arrayidx5 = getelementptr inbounds nuw i32, ptr %A, i64 %indvars.iv
+  store i32 %add, ptr %arrayidx5, align 4, !tbaa !5
+  %indvars.iv.next = add nuw nsw i64 %indvars.iv, 1
+  %3 = load i32, ptr %arrayidx, align 4, !tbaa !5
+  %4 = sext i32 %3 to i64
+  %cmp = icmp slt i64 %indvars.iv.next, %4
+  br i1 %cmp, label %for.body, label %for.cond.cleanup, !llvm.loop !9
+}
+
+attributes #0 = { mustprogress nofree norecurse nosync nounwind memory(argmem: readwrite) uwtable }
+
+!0 = !{i32 1, !"wchar_size", i32 4}
+!1 = !{i32 8, !"PIC Level", i32 2}
+!2 = !{i32 7, !"PIE Level", i32 2}
+!3 = !{i32 7, !"uwtable", i32 2}
+!4 = !{!"clang version 21.1.8"}
+!5 = !{!6, !6, i64 0, i64 4}
+!6 = !{!7, i64 4, !"int"}
+!7 = !{!8, i64 1, !"omnipotent char"}
+!8 = !{!"Simple C++ TBAA"}
+!9 = distinct !{!9, !10, !11}
+!10 = !{!"llvm.loop.mustprogress"}
+!11 = !{!"llvm.loop.unroll.disable"}
+;.
+; CHECK: [[TBAA0]] = !{[[META1:![0-9]+]], [[META1]], i64 0, i64 4}
+; CHECK: [[META1]] = !{[[META2:![0-9]+]], i64 4, !"int"}
+; CHECK: [[META2]] = !{[[META3:![0-9]+]], i64 1, !"omnipotent char"}
+; CHECK: [[META3]] = !{!"Simple C++ TBAA"}
+; CHECK: [[LOOP4]] = distinct !{[[LOOP4]], [[META5:![0-9]+]], [[META6:![0-9]+]], [[META7:![0-9]+]]}
+; CHECK: [[META5]] = !{!"llvm.loop.mustprogress"}
+; CHECK: [[META6]] = !{!"llvm.loop.unroll.disable"}
+; CHECK: [[META7]] = !{!"llvm.loop.licm_versioning.disable", i32 0}
+; CHECK: [[META8]] = !{[[META9:![0-9]+]]}
+; CHECK: [[META9]] = distinct !{[[META9]], [[META10:![0-9]+]]}
+; CHECK: [[META10]] = distinct !{[[META10]], !"LVerDomain"}
+; CHECK: [[META11]] = !{[[META12:![0-9]+]]}
+; CHECK: [[META12]] = distinct !{[[META12]], [[META10]]}
+; CHECK: [[META13]] = !{[[META14:![0-9]+]]}
+; CHECK: [[META14]] = distinct !{[[META14]], [[META10]]}
+; CHECK: [[META15]] = !{[[META9]], [[META12]]}
+; CHECK: [[META16]] = !{[[META17:![0-9]+]]}
+; CHECK: [[META17]] = distinct !{[[META17]], [[META10]]}
+; CHECK: [[LOOP18]] = distinct !{[[LOOP18]], [[META5]], [[META6]], [[META7]], [[META19:![0-9]+]]}
+; CHECK: [[META19]] = !{!"llvm.mem.parallel_loop_access", i32 0}
+;.
diff --git a/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-chained-loads.ll b/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-chained-loads.ll
new file mode 100644
index 0000000000000..dbff9edccbf59
--- /dev/null
+++ b/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-chained-loads.ll
@@ -0,0 +1,86 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py
+; RUN: opt < %s -S -passes='loop-versioning-licm' -licm-versioning-invariant-threshold=15 | FileCheck %s
+;
+; Negative test: the upper bound is **PtrLen (a chained / indirect load).
+; The worklist correctly traces both loads, but LoopAccessAnalysis cannot
+; compute SCEV bounds for the indirect load (load i32, ptr %ptr) because
+; %ptr itself is loaded inside the loop.  The pass must reject the loop.
+;
+; C source:
+;   void foo(int *A, int *B, int *C, int **PtrLen) {
+;     for (int i = 0; i < **PtrLen; i++)
+;       A[i] = B[i] + C[i];
+;   }
+
+define dso_local void @foo(ptr noundef writeonly captures(none) %A, ptr noundef readonly captures(none) %B, ptr noundef readonly captures(none) %C, ptr noundef readonly captures(none) %PtrLen) #0 {
+; CHECK-LABEL: @foo(
+; CHECK-NEXT:  entry:
+; CHECK-NEXT:    [[PTR0:%.*]] = load ptr, ptr [[PTRLEN:%.*]], align 8, !tbaa [[TBAA0:![0-9]+]]
+; CHECK-NEXT:    [[VAL0:%.*]] = load i32, ptr [[PTR0]], align 4, !tbaa [[TBAA5:![0-9]+]]
+; CHECK-NEXT:    [[CMP9:%.*]] = icmp sgt i32 [[VAL0]], 0
+; CHECK-NEXT:    br i1 [[CMP9]], label [[FOR_BODY_PREHEADER:%.*]], label [[FOR_COND_CLEANUP:%.*]]
+; CHECK:       for.body.preheader:
+; CHECK-NEXT:    br label [[FOR_BODY:%.*]]
+; CHECK:       for.cond.cleanup.loopexit:
+; CHECK-NEXT:    br label [[FOR_COND_CLEANUP]]
+; CHECK:       for.cond.cleanup:
+; CHECK-NEXT:    ret void
+; CHECK:       for.body:
+; CHECK-NEXT:    [[INDVARS_IV:%.*]] = phi i64 [ [[INDVARS_IV_NEXT:%.*]], [[FOR_BODY]] ], [ 0, [[FOR_BODY_PREHEADER]] ]
+; CHECK-NEXT:    [[ARRAYIDX:%.*]] = getelementptr inbounds nuw i32, ptr [[B:%.*]], i64 [[INDVARS_IV]]
+; CHECK-NEXT:    [[TMP0:%.*]] = load i32, ptr [[ARRAYIDX]], align 4, !tbaa [[TBAA5]]
+; CHECK-NEXT:    [[ARRAYIDX2:%.*]] = getelementptr inbounds nuw i32, ptr [[C:%.*]], i64 [[INDVARS_IV]]
+; CHECK-NEXT:    [[TMP1:%.*]] = load i32, ptr [[ARRAYIDX2]], align 4, !tbaa [[TBAA5]]
+; CHECK-NEXT:    [[ADD:%.*]] = add nsw i32 [[TMP1]], [[TMP0]]
+; CHECK-NEXT:    [[ARRAYIDX4:%.*]] = getelementptr inbounds nuw i32, ptr [[A:%.*]], i64 [[INDVARS_IV]]
+; CHECK-NEXT:    store i32 [[ADD]], ptr [[ARRAYIDX4]], align 4, !tbaa [[TBAA5]]
+; CHECK-NEXT:    [[INDVARS_IV_NEXT]] = add nuw nsw i64 [[INDVARS_IV]], 1
+; CHECK-NEXT:    [[PTR:%.*]] = load ptr, ptr [[PTRLEN]], align 8, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[VAL:%.*]] = load i32, ptr [[PTR]], align 4, !tbaa [[TBAA5]]
+; CHECK-NEXT:    [[TMP2:%.*]] = sext i32 [[VAL]] to i64
+; CHECK-NEXT:    [[CMP:%.*]] = icmp slt i64 [[INDVARS_IV_NEXT]], [[TMP2]]
+; CHECK-NEXT:    br i1 [[CMP]], label [[FOR_BODY]], label [[FOR_COND_CLEANUP_LOOPEXIT:%.*]], !llvm.loop [[LOOP7:![0-9]+]]
+;
+entry:
+  %ptr0 = load ptr, ptr %PtrLen, align 8, !tbaa !5
+  %val0 = load i32, ptr %ptr0, align 4, !tbaa !10
+  %cmp9 = icmp sgt i32 %val0, 0
+  br i1 %cmp9, label %for.body, label %for.cond.cleanup
+
+for.cond.cleanup:
+  ret void
+
+for.body:
+  %indvars.iv = phi i64 [ %indvars.iv.next, %for.body ], [ 0, %entry ]
+  %arrayidx = getelementptr inbounds nuw i32, ptr %B, i64 %indvars.iv
+  %1 = load i32, ptr %arrayidx, align 4, !tbaa !10
+  %arrayidx2 = getelementptr inbounds nuw i32, ptr %C, i64 %indvars.iv
+  %2 = load i32, ptr %arrayidx2, align 4, !tbaa !10
+  %add = add nsw i32 %2, %1
+  %arrayidx4 = getelementptr inbounds nuw i32, ptr %A, i64 %indvars.iv
+  store i32 %add, ptr %arrayidx4, align 4, !tbaa !10
+  %indvars.iv.next = add nuw nsw i64 %indvars.iv, 1
+  %ptr = load ptr, ptr %PtrLen, align 8, !tbaa !5
+  %val = load i32, ptr %ptr, align 4, !tbaa !10
+  %3 = sext i32 %val to i64
+  %cmp = icmp slt i64 %indvars.iv.next, %3
+  br i1 %cmp, label %for.body, label %for.cond.cleanup, !llvm.loop !12
+}
+
+attributes #0 = { mustprogress nofree norecurse nosync nounwind memory(argmem: readwrite) uwtable }
+
+!0 = !{i32 1, !"wchar_size", i32 4}
+!1 = !{i32 8, !"PIC Level", i32 2}
+!2 = !{i32 7, !"PIE Level", i32 2}
+!3 = !{i32 7, !"uwtable", i32 2}
+!4 = !{!"clang version 21.1.8"}
+!5 = !{!6, !6, i64 0, i64 8}
+!6 = !{!7, i64 8, !"p1 int"}
+!7 = !{!8, i64 8, !"any pointer"}
+!8 = !{!9, i64 1, !"omnipotent char"}
+!9 = !{!"Simple C++ TBAA"}
+!10 = !{!11, !11, i64 0, i64 4}
+!11 = !{!8, i64 4, !"int"}
+!12 = distinct !{!12, !13, !14}
+!13 = !{!"llvm.loop.mustprogress"}
+!14 = !{!"llvm.loop.unroll.disable"}
diff --git a/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-fcmp-exit.ll b/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-fcmp-exit.ll
new file mode 100644
index 0000000000000..0424258284f30
--- /dev/null
+++ b/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-fcmp-exit.ll
@@ -0,0 +1,57 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py
+; RUN: opt < %s -S -passes='loop-versioning-licm' | FileCheck %s
+;
+; LoopVersioningLICM shouldn't kick in at all if the exit condition is an fcmp (not icmp).
+; SCEV cannot compute the trip count of a float-bounded loop, so the
+; pass enters the dynamic-bound path.  It then rejects because the
+; branch condition is an FCmpInst, not an ICmpInst.
+;
+;   void foo(float *A, float *B, float *Limit) {
+;     for (float x = 0.0f; x < *Limit; x += 1.0f)
+;       A[(int)x] = B[(int)x];
+;   }
+
+define dso_local void @foo(ptr noundef %A, ptr noundef readonly %B, ptr noundef readonly %Limit) #0 {
+; CHECK-LABEL: @foo(
+; 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:    [[FIV:%.*]] = phi float [ 0.000000e+00, [[ENTRY]] ], [ [[FIV_NEXT:%.*]], [[FOR_BODY]] ]
+; CHECK-NEXT:    [[GEP_B:%.*]] = getelementptr inbounds i32, ptr [[B:%.*]], i64 [[IV]]
+; CHECK-NEXT:    [[B:%.*]] = load i32, ptr [[GEP_B]], align 4
+; CHECK-NEXT:    [[GEP_A:%.*]] = getelementptr inbounds i32, ptr [[A:%.*]], i64 [[IV]]
+; CHECK-NEXT:    store i32 [[B]], ptr [[GEP_A]], align 4
+; CHECK-NEXT:    [[IV_NEXT]] = add nuw nsw i64 [[IV]], 1
+; CHECK-NEXT:    [[FIV_NEXT]] = fadd float [[FIV]], 1.000000e+00
+; CHECK-NEXT:    [[LIM:%.*]] = load float, ptr [[LIMIT:%.*]], align 4
+; CHECK-NEXT:    [[CMP:%.*]] = fcmp olt float [[FIV_NEXT]], [[LIM]]
+; CHECK-NEXT:    br i1 [[CMP]], label [[FOR_BODY]], label [[FOR_EXIT:%.*]], !llvm.loop [[LOOP0:![0-9]+]]
+; CHECK:       for.exit:
+; CHECK-NEXT:    ret void
+;
+entry:
+  br label %for.body
+
+for.body:
+  %iv = phi i64 [ 0, %entry ], [ %iv.next, %for.body ]
+  %fiv = phi float [ 0.0, %entry ], [ %fiv.next, %for.body ]
+  %gep.b = getelementptr inbounds i32, ptr %B, i64 %iv
+  %b = load i32, ptr %gep.b, align 4
+  %gep.a = getelementptr inbounds i32, ptr %A, i64 %iv
+  store i32 %b, ptr %gep.a, align 4
+  %iv.next = add nuw nsw i64 %iv, 1
+  %fiv.next = fadd float %fiv, 1.0
+  %lim = load float, ptr %Limit, align 4
+  %cmp = fcmp olt float %fiv.next, %lim
+  br i1 %cmp, label %for.body, label %for.exit, !llvm.loop !0
+
+for.exit:
+  ret void
+}
+
+attributes #0 = { mustprogress nofree norecurse nosync nounwind memory(argmem: readwrite) uwtable }
+
+!0 = distinct !{!0, !1, !2}
+!1 = !{!"llvm.loop.mustprogress"}
+!2 = !{!"llvm.loop.unroll.disable"}
diff --git a/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-lambda.ll b/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-lambda.ll
new file mode 100644
index 0000000000000..60bbe5a49ef71
--- /dev/null
+++ b/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-lambda.ll
@@ -0,0 +1,142 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 5
+; RUN: opt < %s -S -passes='loop-versioning-licm' | FileCheck %s
+;
+; Lambda with loop bound loaded from pointer (*len).
+; Verify the load is hoisted and runtime checks are generated.
+;
+;   void foo(int *A, int *B, int *C, int *D, int *len) {
+;     auto func = [&]() {
+;       for (int i = 0; i < *len; i++)
+;         A[i] = B[i] * C[i];
+;     };
+;     func();
+;   }
+;
+
+define dso_local void @foo(ptr noundef writeonly captures(none) %A, ptr noundef readonly captures(none) %B, ptr noundef readonly captures(none) %C, ptr noundef readnone captures(none) %D, ptr noundef readonly captures(none) %len) #0 {
+; CHECK-LABEL: define dso_local void @foo(
+; CHECK-SAME: ptr noundef writeonly captures(none) [[A:%.*]], ptr noundef readonly captures(none) [[B:%.*]], ptr noundef readonly captures(none) [[C:%.*]], ptr noundef readnone captures(none) [[D:%.*]], ptr noundef readonly captures(none) [[LEN:%.*]]) #[[ATTR0:[0-9]+]] {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[TMP0:%.*]] = load i32, ptr [[LEN]], align 4, !tbaa [[TBAA0:![0-9]+]]
+; CHECK-NEXT:    [[CMP10_I:%.*]] = icmp sgt i32 [[TMP0]], 0
+; CHECK-NEXT:    br i1 [[CMP10_I]], label %[[FOR_BODY_I_LVER_CHECK:.*]], label %[[EXIT:.*]]
+; CHECK:       [[FOR_BODY_I_LVER_CHECK]]:
+; CHECK-NEXT:    [[DOTHOISTED:%.*]] = load i32, ptr [[LEN]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[TMP1:%.*]] = sext i32 [[DOTHOISTED]] to i64
+; CHECK-NEXT:    [[TMP2:%.*]] = shl nsw i64 [[TMP1]], 2
+; CHECK-NEXT:    [[SCEVGEP:%.*]] = getelementptr i8, ptr [[A]], i64 [[TMP2]]
+; CHECK-NEXT:    [[SCEVGEP2:%.*]] = getelementptr i8, ptr [[B]], i64 [[TMP2]]
+; CHECK-NEXT:    [[SCEVGEP3:%.*]] = getelementptr i8, ptr [[C]], i64 [[TMP2]]
+; CHECK-NEXT:    [[SCEVGEP4:%.*]] = getelementptr i8, ptr [[LEN]], i64 4
+; CHECK-NEXT:    [[BOUND0:%.*]] = icmp ult ptr [[A]], [[SCEVGEP2]]
+; CHECK-NEXT:    [[BOUND1:%.*]] = icmp ult ptr [[B]], [[SCEVGEP]]
+; CHECK-NEXT:    [[FOUND_CONFLICT:%.*]] = and i1 [[BOUND0]], [[BOUND1]]
+; CHECK-NEXT:    [[BOUND05:%.*]] = icmp ult ptr [[A]], [[SCEVGEP3]]
+; CHECK-NEXT:    [[BOUND16:%.*]] = icmp ult ptr [[C]], [[SCEVGEP]]
+; CHECK-NEXT:    [[FOUND_CONFLICT7:%.*]] = and i1 [[BOUND05]], [[BOUND16]]
+; CHECK-NEXT:    [[CONFLICT_RDX:%.*]] = or i1 [[FOUND_CONFLICT]], [[FOUND_CONFLICT7]]
+; CHECK-NEXT:    [[BOUND08:%.*]] = icmp ult ptr [[A]], [[SCEVGEP4]]
+; CHECK-NEXT:    [[BOUND19:%.*]] = icmp ult ptr [[LEN]], [[SCEVGEP]]
+; CHECK-NEXT:    [[FOUND_CONFLICT10:%.*]] = and i1 [[BOUND08]], [[BOUND19]]
+; CHECK-NEXT:    [[CONFLICT_RDX11:%.*]] = or i1 [[CONFLICT_RDX]], [[FOUND_CONFLICT10]]
+; CHECK-NEXT:    br i1 [[CONFLICT_RDX11]], label %[[FOR_BODY_I_PH_LVER_ORIG:.*]], label %[[FOR_BODY_I_PH:.*]]
+; CHECK:       [[FOR_BODY_I_PH_LVER_ORIG]]:
+; CHECK-NEXT:    br label %[[FOR_BODY_I_LVER_ORIG:.*]]
+; CHECK:       [[FOR_BODY_I_LVER_ORIG]]:
+; CHECK-NEXT:    [[INDVARS_IV_I_LVER_ORIG:%.*]] = phi i64 [ [[INDVARS_IV_NEXT_I_LVER_ORIG:%.*]], %[[FOR_BODY_I_LVER_ORIG]] ], [ 0, %[[FOR_BODY_I_PH_LVER_ORIG]] ]
+; CHECK-NEXT:    [[ARRAYIDX_I_LVER_ORIG:%.*]] = getelementptr inbounds nuw i32, ptr [[B]], i64 [[INDVARS_IV_I_LVER_ORIG]]
+; CHECK-NEXT:    [[TMP3:%.*]] = load i32, ptr [[ARRAYIDX_I_LVER_ORIG]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[ARRAYIDX3_I_LVER_ORIG:%.*]] = getelementptr inbounds nuw i32, ptr [[C]], i64 [[INDVARS_IV_I_LVER_ORIG]]
+; CHECK-NEXT:    [[TMP4:%.*]] = load i32, ptr [[ARRAYIDX3_I_LVER_ORIG]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[MUL_I_LVER_ORIG:%.*]] = mul nsw i32 [[TMP4]], [[TMP3]]
+; CHECK-NEXT:    [[ARRAYIDX5_I_LVER_ORIG:%.*]] = getelementptr inbounds nuw i32, ptr [[A]], i64 [[INDVARS_IV_I_LVER_ORIG]]
+; CHECK-NEXT:    store i32 [[MUL_I_LVER_ORIG]], ptr [[ARRAYIDX5_I_LVER_ORIG]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[INDVARS_IV_NEXT_I_LVER_ORIG]] = add nuw nsw i64 [[INDVARS_IV_I_LVER_ORIG]], 1
+; CHECK-NEXT:    [[TMP5:%.*]] = load i32, ptr [[LEN]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[TMP6:%.*]] = sext i32 [[TMP5]] to i64
+; CHECK-NEXT:    [[CMP_I_LVER_ORIG:%.*]] = icmp slt i64 [[INDVARS_IV_NEXT_I_LVER_ORIG]], [[TMP6]]
+; CHECK-NEXT:    br i1 [[CMP_I_LVER_ORIG]], label %[[FOR_BODY_I_LVER_ORIG]], label %[[EXIT_LOOPEXIT_LOOPEXIT:.*]], !llvm.loop [[LOOP4:![0-9]+]]
+; CHECK:       [[FOR_BODY_I_PH]]:
+; CHECK-NEXT:    br label %[[FOR_BODY_I:.*]]
+; CHECK:       [[FOR_BODY_I]]:
+; CHECK-NEXT:    [[INDVARS_IV_I:%.*]] = phi i64 [ [[INDVARS_IV_NEXT_I:%.*]], %[[FOR_BODY_I]] ], [ 0, %[[FOR_BODY_I_PH]] ]
+; CHECK-NEXT:    [[ARRAYIDX_I:%.*]] = getelementptr inbounds nuw i32, ptr [[B]], i64 [[INDVARS_IV_I]]
+; CHECK-NEXT:    [[TMP7:%.*]] = load i32, ptr [[ARRAYIDX_I]], align 4, !tbaa [[TBAA0]], !alias.scope [[META8:![0-9]+]]
+; CHECK-NEXT:    [[ARRAYIDX3_I:%.*]] = getelementptr inbounds nuw i32, ptr [[C]], i64 [[INDVARS_IV_I]]
+; CHECK-NEXT:    [[TMP8:%.*]] = load i32, ptr [[ARRAYIDX3_I]], align 4, !tbaa [[TBAA0]], !alias.scope [[META11:![0-9]+]]
+; CHECK-NEXT:    [[MUL_I:%.*]] = mul nsw i32 [[TMP8]], [[TMP7]]
+; CHECK-NEXT:    [[ARRAYIDX5_I:%.*]] = getelementptr inbounds nuw i32, ptr [[A]], i64 [[INDVARS_IV_I]]
+; CHECK-NEXT:    store i32 [[MUL_I]], ptr [[ARRAYIDX5_I]], align 4, !tbaa [[TBAA0]], !alias.scope [[META13:![0-9]+]], !noalias [[META15:![0-9]+]]
+; CHECK-NEXT:    [[INDVARS_IV_NEXT_I]] = add nuw nsw i64 [[INDVARS_IV_I]], 1
+; CHECK-NEXT:    [[TMP9:%.*]] = load i32, ptr [[LEN]], align 4, !tbaa [[TBAA0]], !alias.scope [[META17:![0-9]+]]
+; CHECK-NEXT:    [[CMP_I:%.*]] = icmp slt i64 [[INDVARS_IV_NEXT_I]], [[TMP1]]
+; CHECK-NEXT:    br i1 [[CMP_I]], label %[[FOR_BODY_I]], label %[[EXIT_LOOPEXIT_LOOPEXIT1:.*]], !llvm.loop [[LOOP18:![0-9]+]]
+; CHECK:       [[EXIT_LOOPEXIT_LOOPEXIT]]:
+; CHECK-NEXT:    br label %[[EXIT_LOOPEXIT:.*]]
+; CHECK:       [[EXIT_LOOPEXIT_LOOPEXIT1]]:
+; CHECK-NEXT:    br label %[[EXIT_LOOPEXIT]]
+; CHECK:       [[EXIT_LOOPEXIT]]:
+; CHECK-NEXT:    br label %[[EXIT]]
+; CHECK:       [[EXIT]]:
+; CHECK-NEXT:    ret void
+;
+entry:
+  %0 = load i32, ptr %len, align 4, !tbaa !5
+  %cmp10.i = icmp sgt i32 %0, 0
+  br i1 %cmp10.i, label %for.body.i, label %exit
+
+for.body.i:
+  %indvars.iv.i = phi i64 [ %indvars.iv.next.i, %for.body.i ], [ 0, %entry ]
+  %arrayidx.i = getelementptr inbounds nuw i32, ptr %B, i64 %indvars.iv.i
+  %1 = load i32, ptr %arrayidx.i, align 4, !tbaa !5
+  %arrayidx3.i = getelementptr inbounds nuw i32, ptr %C, i64 %indvars.iv.i
+  %2 = load i32, ptr %arrayidx3.i, align 4, !tbaa !5
+  %mul.i = mul nsw i32 %2, %1
+  %arrayidx5.i = getelementptr inbounds nuw i32, ptr %A, i64 %indvars.iv.i
+  store i32 %mul.i, ptr %arrayidx5.i, align 4, !tbaa !5
+  %indvars.iv.next.i = add nuw nsw i64 %indvars.iv.i, 1
+  %3 = load i32, ptr %len, align 4, !tbaa !5
+  %4 = sext i32 %3 to i64
+  %cmp.i = icmp slt i64 %indvars.iv.next.i, %4
+  br i1 %cmp.i, label %for.body.i, label %exit, !llvm.loop !9
+
+exit:
+  ret void
+}
+
+attributes #0 = { mustprogress nofree norecurse nosync nounwind memory(argmem: readwrite) uwtable }
+
+!0 = !{i32 1, !"wchar_size", i32 4}
+!1 = !{i32 8, !"PIC Level", i32 2}
+!2 = !{i32 7, !"PIE Level", i32 2}
+!3 = !{i32 7, !"uwtable", i32 2}
+!4 = !{!"clang version 21.1.8"}
+!5 = !{!6, !6, i64 0, i64 4}
+!6 = !{!7, i64 4, !"int"}
+!7 = !{!8, i64 1, !"omnipotent char"}
+!8 = !{!"Simple C++ TBAA"}
+!9 = distinct !{!9, !10, !11}
+!10 = !{!"llvm.loop.mustprogress"}
+!11 = !{!"llvm.loop.unroll.disable"}
+;.
+; CHECK: [[TBAA0]] = !{[[META1:![0-9]+]], [[META1]], i64 0, i64 4}
+; CHECK: [[META1]] = !{[[META2:![0-9]+]], i64 4, !"int"}
+; CHECK: [[META2]] = !{[[META3:![0-9]+]], i64 1, !"omnipotent char"}
+; CHECK: [[META3]] = !{!"Simple C++ TBAA"}
+; CHECK: [[LOOP4]] = distinct !{[[LOOP4]], [[META5:![0-9]+]], [[META6:![0-9]+]], [[META7:![0-9]+]]}
+; CHECK: [[META5]] = !{!"llvm.loop.mustprogress"}
+; CHECK: [[META6]] = !{!"llvm.loop.unroll.disable"}
+; CHECK: [[META7]] = !{!"llvm.loop.licm_versioning.disable", i32 0}
+; CHECK: [[META8]] = !{[[META9:![0-9]+]]}
+; CHECK: [[META9]] = distinct !{[[META9]], [[META10:![0-9]+]]}
+; CHECK: [[META10]] = distinct !{[[META10]], !"LVerDomain"}
+; CHECK: [[META11]] = !{[[META12:![0-9]+]]}
+; CHECK: [[META12]] = distinct !{[[META12]], [[META10]]}
+; CHECK: [[META13]] = !{[[META14:![0-9]+]]}
+; CHECK: [[META14]] = distinct !{[[META14]], [[META10]]}
+; CHECK: [[META15]] = !{[[META9]], [[META12]], [[META16:![0-9]+]]}
+; CHECK: [[META16]] = distinct !{[[META16]], [[META10]]}
+; CHECK: [[META17]] = !{[[META16]]}
+; CHECK: [[LOOP18]] = distinct !{[[LOOP18]], [[META5]], [[META6]], [[META7]], [[META19:![0-9]+]]}
+; CHECK: [[META19]] = !{!"llvm.mem.parallel_loop_access", i32 0}
+;.
diff --git a/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-loop-varying.ll b/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-loop-varying.ll
new file mode 100644
index 0000000000000..30d155a84e0bd
--- /dev/null
+++ b/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-loop-varying.ll
@@ -0,0 +1,79 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py
+; RUN: opt < %s -S -passes='loop-versioning-licm' | FileCheck %s
+;
+; LoopVersioningLICM shouldn't kick in at all if the bound is Len[i], which depends on the
+; induction variable hence not invariant.  The dependency chain traces back to a loop header
+; PHI, so the pass will reject this loop instead of versioning it.
+;
+;   void foo(int *A, int *B, int *C, int *Len) {
+;     for (int i = 0; i < Len[i]; i++)
+;       A[i] = B[i] + C[i];
+;   }
+
+define dso_local void @foo(ptr noundef writeonly captures(none) %A, ptr noundef readonly captures(none) %B, ptr noundef readonly captures(none) %C, ptr noundef readonly captures(none) %Len) #0 {
+; CHECK-LABEL: @foo(
+; CHECK-NEXT:  entry:
+; CHECK-NEXT:    [[TMP0:%.*]] = load i32, ptr [[LEN:%.*]], align 4, !tbaa [[TBAA0:![0-9]+]]
+; CHECK-NEXT:    [[CMP12:%.*]] = icmp sgt i32 [[TMP0]], 0
+; CHECK-NEXT:    br i1 [[CMP12]], label [[FOR_BODY_PREHEADER:%.*]], label [[FOR_COND_CLEANUP:%.*]]
+; CHECK:       for.body.preheader:
+; CHECK-NEXT:    br label [[FOR_BODY:%.*]]
+; CHECK:       for.cond.cleanup.loopexit:
+; CHECK-NEXT:    br label [[FOR_COND_CLEANUP]]
+; CHECK:       for.cond.cleanup:
+; CHECK-NEXT:    ret void
+; CHECK:       for.body:
+; CHECK-NEXT:    [[INDVARS_IV:%.*]] = phi i64 [ [[INDVARS_IV_NEXT:%.*]], [[FOR_BODY]] ], [ 0, [[FOR_BODY_PREHEADER]] ]
+; CHECK-NEXT:    [[ARRAYIDX2:%.*]] = getelementptr inbounds nuw i32, ptr [[B:%.*]], i64 [[INDVARS_IV]]
+; CHECK-NEXT:    [[TMP1:%.*]] = load i32, ptr [[ARRAYIDX2]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[ARRAYIDX4:%.*]] = getelementptr inbounds nuw i32, ptr [[C:%.*]], i64 [[INDVARS_IV]]
+; CHECK-NEXT:    [[TMP2:%.*]] = load i32, ptr [[ARRAYIDX4]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[ADD:%.*]] = add nsw i32 [[TMP2]], [[TMP1]]
+; CHECK-NEXT:    [[ARRAYIDX6:%.*]] = getelementptr inbounds nuw i32, ptr [[A:%.*]], i64 [[INDVARS_IV]]
+; CHECK-NEXT:    store i32 [[ADD]], ptr [[ARRAYIDX6]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[INDVARS_IV_NEXT]] = add nuw nsw i64 [[INDVARS_IV]], 1
+; CHECK-NEXT:    [[ARRAYIDX:%.*]] = getelementptr inbounds nuw i32, ptr [[LEN]], i64 [[INDVARS_IV_NEXT]]
+; CHECK-NEXT:    [[TMP3:%.*]] = load i32, ptr [[ARRAYIDX]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[TMP4:%.*]] = sext i32 [[TMP3]] to i64
+; CHECK-NEXT:    [[CMP:%.*]] = icmp slt i64 [[INDVARS_IV_NEXT]], [[TMP4]]
+; CHECK-NEXT:    br i1 [[CMP]], label [[FOR_BODY]], label [[FOR_COND_CLEANUP_LOOPEXIT:%.*]], !llvm.loop [[LOOP4:![0-9]+]]
+;
+entry:
+  %0 = load i32, ptr %Len, align 4, !tbaa !5
+  %cmp12 = icmp sgt i32 %0, 0
+  br i1 %cmp12, label %for.body, label %for.cond.cleanup
+
+for.cond.cleanup:
+  ret void
+
+for.body:
+  %indvars.iv = phi i64 [ %indvars.iv.next, %for.body ], [ 0, %entry ]
+  %arrayidx2 = getelementptr inbounds nuw i32, ptr %B, i64 %indvars.iv
+  %1 = load i32, ptr %arrayidx2, align 4, !tbaa !5
+  %arrayidx4 = getelementptr inbounds nuw i32, ptr %C, i64 %indvars.iv
+  %2 = load i32, ptr %arrayidx4, align 4, !tbaa !5
+  %add = add nsw i32 %2, %1
+  %arrayidx6 = getelementptr inbounds nuw i32, ptr %A, i64 %indvars.iv
+  store i32 %add, ptr %arrayidx6, align 4, !tbaa !5
+  %indvars.iv.next = add nuw nsw i64 %indvars.iv, 1
+  %arrayidx = getelementptr inbounds nuw i32, ptr %Len, i64 %indvars.iv.next
+  %3 = load i32, ptr %arrayidx, align 4, !tbaa !5
+  %4 = sext i32 %3 to i64
+  %cmp = icmp slt i64 %indvars.iv.next, %4
+  br i1 %cmp, label %for.body, label %for.cond.cleanup, !llvm.loop !9
+}
+
+attributes #0 = { mustprogress nofree norecurse nosync nounwind memory(argmem: readwrite) uwtable }
+
+!0 = !{i32 1, !"wchar_size", i32 4}
+!1 = !{i32 8, !"PIC Level", i32 2}
+!2 = !{i32 7, !"PIE Level", i32 2}
+!3 = !{i32 7, !"uwtable", i32 2}
+!4 = !{!"clang version 21.1.8"}
+!5 = !{!6, !6, i64 0, i64 4}
+!6 = !{!7, i64 4, !"int"}
+!7 = !{!8, i64 1, !"omnipotent char"}
+!8 = !{!"Simple C++ TBAA"}
+!9 = distinct !{!9, !10, !11}
+!10 = !{!"llvm.loop.mustprogress"}
+!11 = !{!"llvm.loop.unroll.disable"}
diff --git a/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-simple.ll b/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-simple.ll
new file mode 100644
index 0000000000000..6f73c1013c047
--- /dev/null
+++ b/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-simple.ll
@@ -0,0 +1,139 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 5
+; RUN: opt < %s -S -passes='loop-versioning-licm' | FileCheck %s
+;
+; Simple loop with loop bound loaded from a pointer (*Len).
+; Verify the load is hoisted and runtime checks are generated.
+;
+;   void foo(int *A, int *B, int *C, int *Len) {
+;     for (int i = 0; i < *Len; i++)
+;       A[i] = B[i] + C[i];
+;   }
+;
+
+define dso_local void @foo(ptr noundef writeonly captures(none) %A, ptr noundef readonly captures(none) %B, ptr noundef readonly captures(none) %C, ptr noundef readonly captures(none) %Len) #0 {
+; CHECK-LABEL: define dso_local void @foo(
+; CHECK-SAME: ptr noundef writeonly captures(none) [[A:%.*]], ptr noundef readonly captures(none) [[B:%.*]], ptr noundef readonly captures(none) [[C:%.*]], ptr noundef readonly captures(none) [[LEN:%.*]]) #[[ATTR0:[0-9]+]] {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[TMP0:%.*]] = load i32, ptr [[LEN]], align 4, !tbaa [[TBAA0:![0-9]+]]
+; CHECK-NEXT:    [[CMP9:%.*]] = icmp sgt i32 [[TMP0]], 0
+; CHECK-NEXT:    br i1 [[CMP9]], label %[[FOR_BODY_LVER_CHECK:.*]], label %[[FOR_COND_CLEANUP:.*]]
+; CHECK:       [[FOR_BODY_LVER_CHECK]]:
+; CHECK-NEXT:    [[DOTHOISTED:%.*]] = load i32, ptr [[LEN]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[TMP1:%.*]] = sext i32 [[DOTHOISTED]] to i64
+; CHECK-NEXT:    [[TMP2:%.*]] = shl nsw i64 [[TMP1]], 2
+; CHECK-NEXT:    [[SCEVGEP:%.*]] = getelementptr i8, ptr [[A]], i64 [[TMP2]]
+; CHECK-NEXT:    [[SCEVGEP2:%.*]] = getelementptr i8, ptr [[B]], i64 [[TMP2]]
+; CHECK-NEXT:    [[SCEVGEP3:%.*]] = getelementptr i8, ptr [[C]], i64 [[TMP2]]
+; CHECK-NEXT:    [[SCEVGEP4:%.*]] = getelementptr i8, ptr [[LEN]], i64 4
+; CHECK-NEXT:    [[BOUND0:%.*]] = icmp ult ptr [[A]], [[SCEVGEP2]]
+; CHECK-NEXT:    [[BOUND1:%.*]] = icmp ult ptr [[B]], [[SCEVGEP]]
+; CHECK-NEXT:    [[FOUND_CONFLICT:%.*]] = and i1 [[BOUND0]], [[BOUND1]]
+; CHECK-NEXT:    [[BOUND05:%.*]] = icmp ult ptr [[A]], [[SCEVGEP3]]
+; CHECK-NEXT:    [[BOUND16:%.*]] = icmp ult ptr [[C]], [[SCEVGEP]]
+; CHECK-NEXT:    [[FOUND_CONFLICT7:%.*]] = and i1 [[BOUND05]], [[BOUND16]]
+; CHECK-NEXT:    [[CONFLICT_RDX:%.*]] = or i1 [[FOUND_CONFLICT]], [[FOUND_CONFLICT7]]
+; CHECK-NEXT:    [[BOUND08:%.*]] = icmp ult ptr [[A]], [[SCEVGEP4]]
+; CHECK-NEXT:    [[BOUND19:%.*]] = icmp ult ptr [[LEN]], [[SCEVGEP]]
+; CHECK-NEXT:    [[FOUND_CONFLICT10:%.*]] = and i1 [[BOUND08]], [[BOUND19]]
+; CHECK-NEXT:    [[CONFLICT_RDX11:%.*]] = or i1 [[CONFLICT_RDX]], [[FOUND_CONFLICT10]]
+; CHECK-NEXT:    br i1 [[CONFLICT_RDX11]], label %[[FOR_BODY_PH_LVER_ORIG:.*]], label %[[FOR_BODY_PH:.*]]
+; CHECK:       [[FOR_BODY_PH_LVER_ORIG]]:
+; CHECK-NEXT:    br label %[[FOR_BODY_LVER_ORIG:.*]]
+; CHECK:       [[FOR_BODY_LVER_ORIG]]:
+; CHECK-NEXT:    [[INDVARS_IV_LVER_ORIG:%.*]] = phi i64 [ [[INDVARS_IV_NEXT_LVER_ORIG:%.*]], %[[FOR_BODY_LVER_ORIG]] ], [ 0, %[[FOR_BODY_PH_LVER_ORIG]] ]
+; CHECK-NEXT:    [[ARRAYIDX_LVER_ORIG:%.*]] = getelementptr inbounds nuw i32, ptr [[B]], i64 [[INDVARS_IV_LVER_ORIG]]
+; CHECK-NEXT:    [[TMP3:%.*]] = load i32, ptr [[ARRAYIDX_LVER_ORIG]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[ARRAYIDX2_LVER_ORIG:%.*]] = getelementptr inbounds nuw i32, ptr [[C]], i64 [[INDVARS_IV_LVER_ORIG]]
+; CHECK-NEXT:    [[TMP4:%.*]] = load i32, ptr [[ARRAYIDX2_LVER_ORIG]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[ADD_LVER_ORIG:%.*]] = add nsw i32 [[TMP4]], [[TMP3]]
+; CHECK-NEXT:    [[ARRAYIDX4_LVER_ORIG:%.*]] = getelementptr inbounds nuw i32, ptr [[A]], i64 [[INDVARS_IV_LVER_ORIG]]
+; CHECK-NEXT:    store i32 [[ADD_LVER_ORIG]], ptr [[ARRAYIDX4_LVER_ORIG]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[INDVARS_IV_NEXT_LVER_ORIG]] = add nuw nsw i64 [[INDVARS_IV_LVER_ORIG]], 1
+; CHECK-NEXT:    [[TMP5:%.*]] = load i32, ptr [[LEN]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[TMP6:%.*]] = sext i32 [[TMP5]] to i64
+; CHECK-NEXT:    [[CMP_LVER_ORIG:%.*]] = icmp slt i64 [[INDVARS_IV_NEXT_LVER_ORIG]], [[TMP6]]
+; CHECK-NEXT:    br i1 [[CMP_LVER_ORIG]], label %[[FOR_BODY_LVER_ORIG]], label %[[FOR_COND_CLEANUP_LOOPEXIT_LOOPEXIT:.*]], !llvm.loop [[LOOP4:![0-9]+]]
+; CHECK:       [[FOR_BODY_PH]]:
+; CHECK-NEXT:    br label %[[FOR_BODY:.*]]
+; CHECK:       [[FOR_COND_CLEANUP_LOOPEXIT_LOOPEXIT]]:
+; CHECK-NEXT:    br label %[[FOR_COND_CLEANUP_LOOPEXIT:.*]]
+; CHECK:       [[FOR_COND_CLEANUP_LOOPEXIT_LOOPEXIT1:.*]]:
+; CHECK-NEXT:    br label %[[FOR_COND_CLEANUP_LOOPEXIT]]
+; CHECK:       [[FOR_COND_CLEANUP_LOOPEXIT]]:
+; CHECK-NEXT:    br label %[[FOR_COND_CLEANUP]]
+; CHECK:       [[FOR_COND_CLEANUP]]:
+; CHECK-NEXT:    ret void
+; CHECK:       [[FOR_BODY]]:
+; CHECK-NEXT:    [[INDVARS_IV:%.*]] = phi i64 [ [[INDVARS_IV_NEXT:%.*]], %[[FOR_BODY]] ], [ 0, %[[FOR_BODY_PH]] ]
+; CHECK-NEXT:    [[ARRAYIDX:%.*]] = getelementptr inbounds nuw i32, ptr [[B]], i64 [[INDVARS_IV]]
+; CHECK-NEXT:    [[TMP7:%.*]] = load i32, ptr [[ARRAYIDX]], align 4, !tbaa [[TBAA0]], !alias.scope [[META8:![0-9]+]]
+; CHECK-NEXT:    [[ARRAYIDX2:%.*]] = getelementptr inbounds nuw i32, ptr [[C]], i64 [[INDVARS_IV]]
+; CHECK-NEXT:    [[TMP8:%.*]] = load i32, ptr [[ARRAYIDX2]], align 4, !tbaa [[TBAA0]], !alias.scope [[META11:![0-9]+]]
+; CHECK-NEXT:    [[ADD:%.*]] = add nsw i32 [[TMP8]], [[TMP7]]
+; CHECK-NEXT:    [[ARRAYIDX4:%.*]] = getelementptr inbounds nuw i32, ptr [[A]], i64 [[INDVARS_IV]]
+; CHECK-NEXT:    store i32 [[ADD]], ptr [[ARRAYIDX4]], align 4, !tbaa [[TBAA0]], !alias.scope [[META13:![0-9]+]], !noalias [[META15:![0-9]+]]
+; CHECK-NEXT:    [[INDVARS_IV_NEXT]] = add nuw nsw i64 [[INDVARS_IV]], 1
+; CHECK-NEXT:    [[TMP9:%.*]] = load i32, ptr [[LEN]], align 4, !tbaa [[TBAA0]], !alias.scope [[META17:![0-9]+]]
+; CHECK-NEXT:    [[CMP:%.*]] = icmp slt i64 [[INDVARS_IV_NEXT]], [[TMP1]]
+; CHECK-NEXT:    br i1 [[CMP]], label %[[FOR_BODY]], label %[[FOR_COND_CLEANUP_LOOPEXIT_LOOPEXIT1]], !llvm.loop [[LOOP18:![0-9]+]]
+;
+entry:
+  %0 = load i32, ptr %Len, align 4, !tbaa !5
+  %cmp9 = icmp sgt i32 %0, 0
+  br i1 %cmp9, label %for.body, label %for.cond.cleanup
+
+for.cond.cleanup:
+  ret void
+
+for.body:
+  %indvars.iv = phi i64 [ %indvars.iv.next, %for.body ], [ 0, %entry ]
+  %arrayidx = getelementptr inbounds nuw i32, ptr %B, i64 %indvars.iv
+  %1 = load i32, ptr %arrayidx, align 4, !tbaa !5
+  %arrayidx2 = getelementptr inbounds nuw i32, ptr %C, i64 %indvars.iv
+  %2 = load i32, ptr %arrayidx2, align 4, !tbaa !5
+  %add = add nsw i32 %2, %1
+  %arrayidx4 = getelementptr inbounds nuw i32, ptr %A, i64 %indvars.iv
+  store i32 %add, ptr %arrayidx4, align 4, !tbaa !5
+  %indvars.iv.next = add nuw nsw i64 %indvars.iv, 1
+  %3 = load i32, ptr %Len, align 4, !tbaa !5
+  %4 = sext i32 %3 to i64
+  %cmp = icmp slt i64 %indvars.iv.next, %4
+  br i1 %cmp, label %for.body, label %for.cond.cleanup, !llvm.loop !9
+}
+
+attributes #0 = { mustprogress nofree norecurse nosync nounwind memory(argmem: readwrite) uwtable }
+
+!0 = !{i32 1, !"wchar_size", i32 4}
+!1 = !{i32 8, !"PIC Level", i32 2}
+!2 = !{i32 7, !"PIE Level", i32 2}
+!3 = !{i32 7, !"uwtable", i32 2}
+!4 = !{!"clang version 21.1.8"}
+!5 = !{!6, !6, i64 0, i64 4}
+!6 = !{!7, i64 4, !"int"}
+!7 = !{!8, i64 1, !"omnipotent char"}
+!8 = !{!"Simple C++ TBAA"}
+!9 = distinct !{!9, !10, !11}
+!10 = !{!"llvm.loop.mustprogress"}
+!11 = !{!"llvm.loop.unroll.disable"}
+;.
+; CHECK: [[TBAA0]] = !{[[META1:![0-9]+]], [[META1]], i64 0, i64 4}
+; CHECK: [[META1]] = !{[[META2:![0-9]+]], i64 4, !"int"}
+; CHECK: [[META2]] = !{[[META3:![0-9]+]], i64 1, !"omnipotent char"}
+; CHECK: [[META3]] = !{!"Simple C++ TBAA"}
+; CHECK: [[LOOP4]] = distinct !{[[LOOP4]], [[META5:![0-9]+]], [[META6:![0-9]+]], [[META7:![0-9]+]]}
+; CHECK: [[META5]] = !{!"llvm.loop.mustprogress"}
+; CHECK: [[META6]] = !{!"llvm.loop.unroll.disable"}
+; CHECK: [[META7]] = !{!"llvm.loop.licm_versioning.disable", i32 0}
+; CHECK: [[META8]] = !{[[META9:![0-9]+]]}
+; CHECK: [[META9]] = distinct !{[[META9]], [[META10:![0-9]+]]}
+; CHECK: [[META10]] = distinct !{[[META10]], !"LVerDomain"}
+; CHECK: [[META11]] = !{[[META12:![0-9]+]]}
+; CHECK: [[META12]] = distinct !{[[META12]], [[META10]]}
+; CHECK: [[META13]] = !{[[META14:![0-9]+]]}
+; CHECK: [[META14]] = distinct !{[[META14]], [[META10]]}
+; CHECK: [[META15]] = !{[[META9]], [[META12]], [[META16:![0-9]+]]}
+; CHECK: [[META16]] = distinct !{[[META16]], [[META10]]}
+; CHECK: [[META17]] = !{[[META16]]}
+; CHECK: [[LOOP18]] = distinct !{[[LOOP18]], [[META5]], [[META6]], [[META7]], [[META19:![0-9]+]]}
+; CHECK: [[META19]] = !{!"llvm.mem.parallel_loop_access", i32 0}
+;.
diff --git a/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-stored-pointer.ll b/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-stored-pointer.ll
new file mode 100644
index 0000000000000..8d1366d14fbef
--- /dev/null
+++ b/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-stored-pointer.ll
@@ -0,0 +1,72 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py
+; RUN: opt < %s -S -passes='loop-versioning-licm' | FileCheck %s
+;
+; The bound pointer %Len is also written to inside the loop body with a computed value, 
+; so the bound changes each iteration and cannot be hoisted.  The ModifiedPtrs check detects that the same
+; pointer is used as both a store target and the bound load's address.
+;
+;   void foo(int *A, int *B, int *C, int *Len) {
+;     for (int i = 0; i < *Len; i++) {
+;       A[i] = B[i] + C[i];
+;       *Len = B[i] + C[i];   // overwrite bound with computed value
+;     }
+;   }
+
+define dso_local void @foo(ptr noundef %A, ptr noundef readonly %B, ptr noundef readonly %C, ptr noundef %Len) #0 {
+; CHECK-LABEL: @foo(
+; CHECK-NEXT:  entry:
+; CHECK-NEXT:    [[LEN0:%.*]] = load i32, ptr [[LEN:%.*]], align 4
+; CHECK-NEXT:    [[CMP0:%.*]] = icmp sgt i32 [[LEN0]], 0
+; CHECK-NEXT:    br i1 [[CMP0]], label [[FOR_BODY_PREHEADER:%.*]], label [[FOR_COND_CLEANUP:%.*]]
+; CHECK:       for.body.preheader:
+; CHECK-NEXT:    br label [[FOR_BODY:%.*]]
+; CHECK:       for.cond.cleanup.loopexit:
+; CHECK-NEXT:    br label [[FOR_COND_CLEANUP]]
+; CHECK:       for.cond.cleanup:
+; CHECK-NEXT:    ret void
+; CHECK:       for.body:
+; CHECK-NEXT:    [[INDVARS_IV:%.*]] = phi i64 [ [[INDVARS_IV_NEXT:%.*]], [[FOR_BODY]] ], [ 0, [[FOR_BODY_PREHEADER]] ]
+; CHECK-NEXT:    [[ARRAYIDX:%.*]] = getelementptr inbounds nuw i32, ptr [[B:%.*]], i64 [[INDVARS_IV]]
+; CHECK-NEXT:    [[TMP0:%.*]] = load i32, ptr [[ARRAYIDX]], align 4
+; CHECK-NEXT:    [[ARRAYIDX2:%.*]] = getelementptr inbounds nuw i32, ptr [[C:%.*]], i64 [[INDVARS_IV]]
+; CHECK-NEXT:    [[TMP1:%.*]] = load i32, ptr [[ARRAYIDX2]], align 4
+; CHECK-NEXT:    [[ADD:%.*]] = add nsw i32 [[TMP1]], [[TMP0]]
+; CHECK-NEXT:    [[ARRAYIDX4:%.*]] = getelementptr inbounds nuw i32, ptr [[A:%.*]], i64 [[INDVARS_IV]]
+; CHECK-NEXT:    store i32 [[ADD]], ptr [[ARRAYIDX4]], align 4
+; CHECK-NEXT:    store i32 [[ADD]], ptr [[LEN]], align 4
+; CHECK-NEXT:    [[INDVARS_IV_NEXT]] = add nuw nsw i64 [[INDVARS_IV]], 1
+; CHECK-NEXT:    [[LEN_RELOAD:%.*]] = load i32, ptr [[LEN]], align 4
+; CHECK-NEXT:    [[EXT:%.*]] = sext i32 [[LEN_RELOAD]] to i64
+; CHECK-NEXT:    [[CMP:%.*]] = icmp slt i64 [[INDVARS_IV_NEXT]], [[EXT]]
+; CHECK-NEXT:    br i1 [[CMP]], label [[FOR_BODY]], label [[FOR_COND_CLEANUP_LOOPEXIT:%.*]], !llvm.loop [[LOOP0:![0-9]+]]
+;
+entry:
+  %len0 = load i32, ptr %Len, align 4
+  %cmp0 = icmp sgt i32 %len0, 0
+  br i1 %cmp0, label %for.body, label %for.cond.cleanup
+
+for.cond.cleanup:
+  ret void
+
+for.body:
+  %indvars.iv = phi i64 [ %indvars.iv.next, %for.body ], [ 0, %entry ]
+  %arrayidx = getelementptr inbounds nuw i32, ptr %B, i64 %indvars.iv
+  %0 = load i32, ptr %arrayidx, align 4
+  %arrayidx2 = getelementptr inbounds nuw i32, ptr %C, i64 %indvars.iv
+  %1 = load i32, ptr %arrayidx2, align 4
+  %add = add nsw i32 %1, %0
+  %arrayidx4 = getelementptr inbounds nuw i32, ptr %A, i64 %indvars.iv
+  store i32 %add, ptr %arrayidx4, align 4
+  store i32 %add, ptr %Len, align 4
+  %indvars.iv.next = add nuw nsw i64 %indvars.iv, 1
+  %len = load i32, ptr %Len, align 4
+  %ext = sext i32 %len to i64
+  %cmp = icmp slt i64 %indvars.iv.next, %ext
+  br i1 %cmp, label %for.body, label %for.cond.cleanup, !llvm.loop !0
+}
+
+attributes #0 = { mustprogress nofree norecurse nosync nounwind memory(argmem: readwrite) uwtable }
+
+!0 = distinct !{!0, !1, !2}
+!1 = !{!"llvm.loop.mustprogress"}
+!2 = !{!"llvm.loop.unroll.disable"}
diff --git a/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-two-loads.ll b/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-two-loads.ll
new file mode 100644
index 0000000000000..e07aa60a9aa33
--- /dev/null
+++ b/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-two-loads.ll
@@ -0,0 +1,158 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py UTC_ARGS: --version 5
+; RUN: opt < %s -S -passes='loop-versioning-licm' | FileCheck %s
+;
+; Loop bound is *Len1 + *Len2.  Two loads and an add are hoisted to the preheader for the dynamic bound.
+; hoisted to the preheader for the dynamic bound.
+; Verify both loads are hoisted, the add is hoisted, and runtime checks are generated.
+;
+;   void foo(int *A, int *B, int *C, int *Len1, int *Len2) {
+;     for (int i = 0; i < *Len1 + *Len2; i++)
+;       A[i] = B[i] + C[i];
+;   }
+;
+
+define dso_local void @foo(ptr noundef writeonly captures(none) %A, ptr noundef readonly captures(none) %B, ptr noundef readonly captures(none) %C, ptr noundef readonly captures(none) %Len1, ptr noundef readonly captures(none) %Len2) #0 {
+; CHECK-LABEL: define dso_local void @foo(
+; CHECK-SAME: ptr noundef writeonly captures(none) [[A:%.*]], ptr noundef readonly captures(none) [[B:%.*]], ptr noundef readonly captures(none) [[C:%.*]], ptr noundef readonly captures(none) [[LEN1:%.*]], ptr noundef readonly captures(none) [[LEN2:%.*]]) #[[ATTR0:[0-9]+]] {
+; CHECK-NEXT:  [[ENTRY:.*:]]
+; CHECK-NEXT:    [[TMP0:%.*]] = load i32, ptr [[LEN1]], align 4, !tbaa [[TBAA0:![0-9]+]]
+; CHECK-NEXT:    [[TMP1:%.*]] = load i32, ptr [[LEN2]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[ADD10:%.*]] = add nsw i32 [[TMP1]], [[TMP0]]
+; CHECK-NEXT:    [[CMP11:%.*]] = icmp sgt i32 [[ADD10]], 0
+; CHECK-NEXT:    br i1 [[CMP11]], label %[[FOR_BODY_LVER_CHECK:.*]], label %[[FOR_COND_CLEANUP:.*]]
+; CHECK:       [[FOR_BODY_LVER_CHECK]]:
+; CHECK-NEXT:    [[DOTHOISTED:%.*]] = load i32, ptr [[LEN2]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[DOTHOISTED2:%.*]] = load i32, ptr [[LEN1]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[ADD:%.*]] = add i32 [[DOTHOISTED]], [[DOTHOISTED2]]
+; CHECK-NEXT:    [[TMP2:%.*]] = sext i32 [[ADD]] to i64
+; CHECK-NEXT:    [[TMP3:%.*]] = shl nsw i64 [[TMP2]], 2
+; CHECK-NEXT:    [[SCEVGEP:%.*]] = getelementptr i8, ptr [[A]], i64 [[TMP3]]
+; CHECK-NEXT:    [[SCEVGEP3:%.*]] = getelementptr i8, ptr [[B]], i64 [[TMP3]]
+; CHECK-NEXT:    [[SCEVGEP4:%.*]] = getelementptr i8, ptr [[C]], i64 [[TMP3]]
+; CHECK-NEXT:    [[SCEVGEP5:%.*]] = getelementptr i8, ptr [[LEN1]], i64 4
+; CHECK-NEXT:    [[SCEVGEP6:%.*]] = getelementptr i8, ptr [[LEN2]], i64 4
+; CHECK-NEXT:    [[BOUND0:%.*]] = icmp ult ptr [[A]], [[SCEVGEP3]]
+; CHECK-NEXT:    [[BOUND1:%.*]] = icmp ult ptr [[B]], [[SCEVGEP]]
+; CHECK-NEXT:    [[FOUND_CONFLICT:%.*]] = and i1 [[BOUND0]], [[BOUND1]]
+; CHECK-NEXT:    [[BOUND07:%.*]] = icmp ult ptr [[A]], [[SCEVGEP4]]
+; CHECK-NEXT:    [[BOUND18:%.*]] = icmp ult ptr [[C]], [[SCEVGEP]]
+; CHECK-NEXT:    [[FOUND_CONFLICT9:%.*]] = and i1 [[BOUND07]], [[BOUND18]]
+; CHECK-NEXT:    [[CONFLICT_RDX:%.*]] = or i1 [[FOUND_CONFLICT]], [[FOUND_CONFLICT9]]
+; CHECK-NEXT:    [[BOUND010:%.*]] = icmp ult ptr [[A]], [[SCEVGEP5]]
+; CHECK-NEXT:    [[BOUND111:%.*]] = icmp ult ptr [[LEN1]], [[SCEVGEP]]
+; CHECK-NEXT:    [[FOUND_CONFLICT12:%.*]] = and i1 [[BOUND010]], [[BOUND111]]
+; CHECK-NEXT:    [[CONFLICT_RDX13:%.*]] = or i1 [[CONFLICT_RDX]], [[FOUND_CONFLICT12]]
+; CHECK-NEXT:    [[BOUND014:%.*]] = icmp ult ptr [[A]], [[SCEVGEP6]]
+; CHECK-NEXT:    [[BOUND115:%.*]] = icmp ult ptr [[LEN2]], [[SCEVGEP]]
+; CHECK-NEXT:    [[FOUND_CONFLICT16:%.*]] = and i1 [[BOUND014]], [[BOUND115]]
+; CHECK-NEXT:    [[CONFLICT_RDX17:%.*]] = or i1 [[CONFLICT_RDX13]], [[FOUND_CONFLICT16]]
+; CHECK-NEXT:    br i1 [[CONFLICT_RDX17]], label %[[FOR_BODY_PH_LVER_ORIG:.*]], label %[[FOR_BODY_PH:.*]]
+; CHECK:       [[FOR_BODY_PH_LVER_ORIG]]:
+; CHECK-NEXT:    br label %[[FOR_BODY_LVER_ORIG:.*]]
+; CHECK:       [[FOR_BODY_LVER_ORIG]]:
+; CHECK-NEXT:    [[INDVARS_IV_LVER_ORIG:%.*]] = phi i64 [ [[INDVARS_IV_NEXT_LVER_ORIG:%.*]], %[[FOR_BODY_LVER_ORIG]] ], [ 0, %[[FOR_BODY_PH_LVER_ORIG]] ]
+; CHECK-NEXT:    [[ARRAYIDX_LVER_ORIG:%.*]] = getelementptr inbounds nuw i32, ptr [[B]], i64 [[INDVARS_IV_LVER_ORIG]]
+; CHECK-NEXT:    [[TMP4:%.*]] = load i32, ptr [[ARRAYIDX_LVER_ORIG]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[ARRAYIDX2_LVER_ORIG:%.*]] = getelementptr inbounds nuw i32, ptr [[C]], i64 [[INDVARS_IV_LVER_ORIG]]
+; CHECK-NEXT:    [[TMP5:%.*]] = load i32, ptr [[ARRAYIDX2_LVER_ORIG]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[ADD3_LVER_ORIG:%.*]] = add nsw i32 [[TMP5]], [[TMP4]]
+; CHECK-NEXT:    [[ARRAYIDX5_LVER_ORIG:%.*]] = getelementptr inbounds nuw i32, ptr [[A]], i64 [[INDVARS_IV_LVER_ORIG]]
+; CHECK-NEXT:    store i32 [[ADD3_LVER_ORIG]], ptr [[ARRAYIDX5_LVER_ORIG]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[INDVARS_IV_NEXT_LVER_ORIG]] = add nuw nsw i64 [[INDVARS_IV_LVER_ORIG]], 1
+; CHECK-NEXT:    [[TMP6:%.*]] = load i32, ptr [[LEN1]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[TMP7:%.*]] = load i32, ptr [[LEN2]], align 4, !tbaa [[TBAA0]]
+; CHECK-NEXT:    [[ADD_LVER_ORIG:%.*]] = add nsw i32 [[TMP7]], [[TMP6]]
+; CHECK-NEXT:    [[TMP8:%.*]] = sext i32 [[ADD_LVER_ORIG]] to i64
+; CHECK-NEXT:    [[CMP_LVER_ORIG:%.*]] = icmp slt i64 [[INDVARS_IV_NEXT_LVER_ORIG]], [[TMP8]]
+; CHECK-NEXT:    br i1 [[CMP_LVER_ORIG]], label %[[FOR_BODY_LVER_ORIG]], label %[[FOR_COND_CLEANUP_LOOPEXIT_LOOPEXIT:.*]], !llvm.loop [[LOOP4:![0-9]+]]
+; CHECK:       [[FOR_BODY_PH]]:
+; CHECK-NEXT:    br label %[[FOR_BODY:.*]]
+; CHECK:       [[FOR_COND_CLEANUP_LOOPEXIT_LOOPEXIT]]:
+; CHECK-NEXT:    br label %[[FOR_COND_CLEANUP_LOOPEXIT:.*]]
+; CHECK:       [[FOR_COND_CLEANUP_LOOPEXIT_LOOPEXIT1:.*]]:
+; CHECK-NEXT:    br label %[[FOR_COND_CLEANUP_LOOPEXIT]]
+; CHECK:       [[FOR_COND_CLEANUP_LOOPEXIT]]:
+; CHECK-NEXT:    br label %[[FOR_COND_CLEANUP]]
+; CHECK:       [[FOR_COND_CLEANUP]]:
+; CHECK-NEXT:    ret void
+; CHECK:       [[FOR_BODY]]:
+; CHECK-NEXT:    [[INDVARS_IV:%.*]] = phi i64 [ [[INDVARS_IV_NEXT:%.*]], %[[FOR_BODY]] ], [ 0, %[[FOR_BODY_PH]] ]
+; CHECK-NEXT:    [[ARRAYIDX:%.*]] = getelementptr inbounds nuw i32, ptr [[B]], i64 [[INDVARS_IV]]
+; CHECK-NEXT:    [[TMP9:%.*]] = load i32, ptr [[ARRAYIDX]], align 4, !tbaa [[TBAA0]], !alias.scope [[META8:![0-9]+]]
+; CHECK-NEXT:    [[ARRAYIDX2:%.*]] = getelementptr inbounds nuw i32, ptr [[C]], i64 [[INDVARS_IV]]
+; CHECK-NEXT:    [[TMP10:%.*]] = load i32, ptr [[ARRAYIDX2]], align 4, !tbaa [[TBAA0]], !alias.scope [[META11:![0-9]+]]
+; CHECK-NEXT:    [[ADD3:%.*]] = add nsw i32 [[TMP10]], [[TMP9]]
+; CHECK-NEXT:    [[ARRAYIDX5:%.*]] = getelementptr inbounds nuw i32, ptr [[A]], i64 [[INDVARS_IV]]
+; CHECK-NEXT:    store i32 [[ADD3]], ptr [[ARRAYIDX5]], align 4, !tbaa [[TBAA0]], !alias.scope [[META13:![0-9]+]], !noalias [[META15:![0-9]+]]
+; CHECK-NEXT:    [[INDVARS_IV_NEXT]] = add nuw nsw i64 [[INDVARS_IV]], 1
+; CHECK-NEXT:    [[TMP11:%.*]] = load i32, ptr [[LEN1]], align 4, !tbaa [[TBAA0]], !alias.scope [[META18:![0-9]+]]
+; CHECK-NEXT:    [[TMP12:%.*]] = load i32, ptr [[LEN2]], align 4, !tbaa [[TBAA0]], !alias.scope [[META19:![0-9]+]]
+; CHECK-NEXT:    [[CMP:%.*]] = icmp slt i64 [[INDVARS_IV_NEXT]], [[TMP2]]
+; CHECK-NEXT:    br i1 [[CMP]], label %[[FOR_BODY]], label %[[FOR_COND_CLEANUP_LOOPEXIT_LOOPEXIT1]], !llvm.loop [[LOOP20:![0-9]+]]
+;
+entry:
+  %0 = load i32, ptr %Len1, align 4, !tbaa !5
+  %1 = load i32, ptr %Len2, align 4, !tbaa !5
+  %add10 = add nsw i32 %1, %0
+  %cmp11 = icmp sgt i32 %add10, 0
+  br i1 %cmp11, label %for.body, label %for.cond.cleanup
+
+for.cond.cleanup:
+  ret void
+
+for.body:
+  %indvars.iv = phi i64 [ %indvars.iv.next, %for.body ], [ 0, %entry ]
+  %arrayidx = getelementptr inbounds nuw i32, ptr %B, i64 %indvars.iv
+  %2 = load i32, ptr %arrayidx, align 4, !tbaa !5
+  %arrayidx2 = getelementptr inbounds nuw i32, ptr %C, i64 %indvars.iv
+  %3 = load i32, ptr %arrayidx2, align 4, !tbaa !5
+  %add3 = add nsw i32 %3, %2
+  %arrayidx5 = getelementptr inbounds nuw i32, ptr %A, i64 %indvars.iv
+  store i32 %add3, ptr %arrayidx5, align 4, !tbaa !5
+  %indvars.iv.next = add nuw nsw i64 %indvars.iv, 1
+  %4 = load i32, ptr %Len1, align 4, !tbaa !5
+  %5 = load i32, ptr %Len2, align 4, !tbaa !5
+  %add = add nsw i32 %5, %4
+  %6 = sext i32 %add to i64
+  %cmp = icmp slt i64 %indvars.iv.next, %6
+  br i1 %cmp, label %for.body, label %for.cond.cleanup, !llvm.loop !9
+}
+
+attributes #0 = { mustprogress nofree norecurse nosync nounwind memory(argmem: readwrite) uwtable }
+
+!0 = !{i32 1, !"wchar_size", i32 4}
+!1 = !{i32 8, !"PIC Level", i32 2}
+!2 = !{i32 7, !"PIE Level", i32 2}
+!3 = !{i32 7, !"uwtable", i32 2}
+!4 = !{!"clang version 21.1.8"}
+!5 = !{!6, !6, i64 0, i64 4}
+!6 = !{!7, i64 4, !"int"}
+!7 = !{!8, i64 1, !"omnipotent char"}
+!8 = !{!"Simple C++ TBAA"}
+!9 = distinct !{!9, !10, !11}
+!10 = !{!"llvm.loop.mustprogress"}
+!11 = !{!"llvm.loop.unroll.disable"}
+;.
+; CHECK: [[TBAA0]] = !{[[META1:![0-9]+]], [[META1]], i64 0, i64 4}
+; CHECK: [[META1]] = !{[[META2:![0-9]+]], i64 4, !"int"}
+; CHECK: [[META2]] = !{[[META3:![0-9]+]], i64 1, !"omnipotent char"}
+; CHECK: [[META3]] = !{!"Simple C++ TBAA"}
+; CHECK: [[LOOP4]] = distinct !{[[LOOP4]], [[META5:![0-9]+]], [[META6:![0-9]+]], [[META7:![0-9]+]]}
+; CHECK: [[META5]] = !{!"llvm.loop.mustprogress"}
+; CHECK: [[META6]] = !{!"llvm.loop.unroll.disable"}
+; CHECK: [[META7]] = !{!"llvm.loop.licm_versioning.disable", i32 0}
+; CHECK: [[META8]] = !{[[META9:![0-9]+]]}
+; CHECK: [[META9]] = distinct !{[[META9]], [[META10:![0-9]+]]}
+; CHECK: [[META10]] = distinct !{[[META10]], !"LVerDomain"}
+; CHECK: [[META11]] = !{[[META12:![0-9]+]]}
+; CHECK: [[META12]] = distinct !{[[META12]], [[META10]]}
+; CHECK: [[META13]] = !{[[META14:![0-9]+]]}
+; CHECK: [[META14]] = distinct !{[[META14]], [[META10]]}
+; CHECK: [[META15]] = !{[[META9]], [[META12]], [[META16:![0-9]+]], [[META17:![0-9]+]]}
+; CHECK: [[META16]] = distinct !{[[META16]], [[META10]]}
+; CHECK: [[META17]] = distinct !{[[META17]], [[META10]]}
+; CHECK: [[META18]] = !{[[META16]]}
+; CHECK: [[META19]] = !{[[META17]]}
+; CHECK: [[LOOP20]] = distinct !{[[LOOP20]], [[META5]], [[META6]], [[META7]], [[META21:![0-9]+]]}
+; CHECK: [[META21]] = !{!"llvm.mem.parallel_loop_access", i32 0}
+;.
diff --git a/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-volatile-load.ll b/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-volatile-load.ll
new file mode 100644
index 0000000000000..2b53fef5db62e
--- /dev/null
+++ b/llvm/test/Transforms/LoopVersioningLICM/dynamic-bound-volatile-load.ll
@@ -0,0 +1,67 @@
+; NOTE: Assertions have been autogenerated by utils/update_test_checks.py
+; RUN: opt < %s -S -passes='loop-versioning-licm' | FileCheck %s
+;
+; LoopVersioningLICM shouldn't kick in at all if the bound is loaded via a volatile (non-simple) load.
+; The pass rejects the loop because volatile loads cannot be hoisted.
+;
+;   void foo(int *A, int *B, int *C, volatile int *Len) {
+;     for (int i = 0; i < *Len; i++)
+;       A[i] = B[i] + C[i];
+;   }
+
+define dso_local void @foo(ptr noundef writeonly %A, ptr noundef readonly %B, ptr noundef readonly %C, ptr noundef %Len) #0 {
+; CHECK-LABEL: @foo(
+; CHECK-NEXT:  entry:
+; CHECK-NEXT:    [[LEN0:%.*]] = load volatile i32, ptr [[LEN:%.*]], align 4
+; CHECK-NEXT:    [[CMP0:%.*]] = icmp sgt i32 [[LEN0]], 0
+; CHECK-NEXT:    br i1 [[CMP0]], label [[FOR_BODY_PREHEADER:%.*]], label [[FOR_COND_CLEANUP:%.*]]
+; CHECK:       for.body.preheader:
+; CHECK-NEXT:    br label [[FOR_BODY:%.*]]
+; CHECK:       for.cond.cleanup.loopexit:
+; CHECK-NEXT:    br label [[FOR_COND_CLEANUP]]
+; CHECK:       for.cond.cleanup:
+; CHECK-NEXT:    ret void
+; CHECK:       for.body:
+; CHECK-NEXT:    [[INDVARS_IV:%.*]] = phi i64 [ [[INDVARS_IV_NEXT:%.*]], [[FOR_BODY]] ], [ 0, [[FOR_BODY_PREHEADER]] ]
+; CHECK-NEXT:    [[ARRAYIDX:%.*]] = getelementptr inbounds nuw i32, ptr [[B:%.*]], i64 [[INDVARS_IV]]
+; CHECK-NEXT:    [[TMP0:%.*]] = load i32, ptr [[ARRAYIDX]], align 4
+; CHECK-NEXT:    [[ARRAYIDX2:%.*]] = getelementptr inbounds nuw i32, ptr [[C:%.*]], i64 [[INDVARS_IV]]
+; CHECK-NEXT:    [[TMP1:%.*]] = load i32, ptr [[ARRAYIDX2]], align 4
+; CHECK-NEXT:    [[ADD:%.*]] = add nsw i32 [[TMP1]], [[TMP0]]
+; CHECK-NEXT:    [[ARRAYIDX4:%.*]] = getelementptr inbounds nuw i32, ptr [[A:%.*]], i64 [[INDVARS_IV]]
+; CHECK-NEXT:    store i32 [[ADD]], ptr [[ARRAYIDX4]], align 4
+; CHECK-NEXT:    [[INDVARS_IV_NEXT]] = add nuw nsw i64 [[INDVARS_IV]], 1
+; CHECK-NEXT:    [[LEN_RELOAD:%.*]] = load volatile i32, ptr [[LEN]], align 4
+; CHECK-NEXT:    [[EXT:%.*]] = sext i32 [[LEN_RELOAD]] to i64
+; CHECK-NEXT:    [[CMP:%.*]] = icmp slt i64 [[INDVARS_IV_NEXT]], [[EXT]]
+; CHECK-NEXT:    br i1 [[CMP]], label [[FOR_BODY]], label [[FOR_COND_CLEANUP_LOOPEXIT:%.*]], !llvm.loop [[LOOP0:![0-9]+]]
+;
+entry:
+  %len0 = load volatile i32, ptr %Len, align 4
+  %cmp0 = icmp sgt i32 %len0, 0
+  br i1 %cmp0, label %for.body, label %for.cond.cleanup
+
+for.cond.cleanup:
+  ret void
+
+for.body:
+  %indvars.iv = phi i64 [ %indvars.iv.next, %for.body ], [ 0, %entry ]
+  %arrayidx = getelementptr inbounds nuw i32, ptr %B, i64 %indvars.iv
+  %0 = load i32, ptr %arrayidx, align 4
+  %arrayidx2 = getelementptr inbounds nuw i32, ptr %C, i64 %indvars.iv
+  %1 = load i32, ptr %arrayidx2, align 4
+  %add = add nsw i32 %1, %0
+  %arrayidx4 = getelementptr inbounds nuw i32, ptr %A, i64 %indvars.iv
+  store i32 %add, ptr %arrayidx4, align 4
+  %indvars.iv.next = add nuw nsw i64 %indvars.iv, 1
+  %len = load volatile i32, ptr %Len, align 4
+  %ext = sext i32 %len to i64
+  %cmp = icmp slt i64 %indvars.iv.next, %ext
+  br i1 %cmp, label %for.body, label %for.cond.cleanup, !llvm.loop !0
+}
+
+attributes #0 = { mustprogress nofree norecurse nosync nounwind memory(argmem: readwrite) uwtable }
+
+!0 = distinct !{!0, !1, !2}
+!1 = !{!"llvm.loop.mustprogress"}
+!2 = !{!"llvm.loop.unroll.disable"}



More information about the llvm-commits mailing list