[llvm] [Analysis] Add intra-procedural EscapeAnalysis (PR #169896)

Alexey Paznikov via llvm-commits llvm-commits at lists.llvm.org
Tue Jun 23 00:10:38 PDT 2026


https://github.com/apaznikov updated https://github.com/llvm/llvm-project/pull/169896

>From acd34b52b67cea05bffa06206d7fc839fabd5207 Mon Sep 17 00:00:00 2001
From: Alexey Paznikov <apaznikov at gmail.com>
Date: Thu, 25 Sep 2025 15:32:29 +0800
Subject: [PATCH] [Analysis] Add intra-procedural EscapeAnalysis
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This adds a new generic intraprocedural EscapeAnalysis pass. The analysis determines whether a memory allocation — stack or heap — is confined to the current function's execution scope, i.e., its address cannot become visible to other threads.

The pass is designed as a reusable compiler analysis. Its primary initial motivation and first user is ThreadSanitizer (TSan), where it eliminates redundant instrumentation for objects that areprovably thread-local. Follow-up TSan integration PR: #192945

Potential future users include alias analysis, scalar optimizations, and other analyses that benefit from thread-locality information.

The underlying algorithms and performance validation were conducted by:
Alexey Paznikov, Michael Schwarz, Andrey Kogutenko, Yaroslav Osipov, and Umang Mathur. See https://arxiv.org/abs/2512.05555
---
 llvm/include/llvm/Analysis/EscapeAnalysis.h   |  194 +++
 llvm/lib/Analysis/CMakeLists.txt              |    1 +
 llvm/lib/Analysis/EscapeAnalysis.cpp          |  798 ++++++++++
 llvm/lib/Passes/PassBuilder.cpp               |    1 +
 llvm/lib/Passes/PassRegistry.def              |    2 +
 .../EscapeAnalysis/escape-analysis.ll         | 1320 +++++++++++++++++
 .../Analysis/EscapeAnalysis/invalidation.ll   |   22 +
 .../Analysis/EscapeAnalysis/worklist-limit.ll |   25 +
 8 files changed, 2363 insertions(+)
 create mode 100644 llvm/include/llvm/Analysis/EscapeAnalysis.h
 create mode 100644 llvm/lib/Analysis/EscapeAnalysis.cpp
 create mode 100644 llvm/test/Analysis/EscapeAnalysis/escape-analysis.ll
 create mode 100644 llvm/test/Analysis/EscapeAnalysis/invalidation.ll
 create mode 100644 llvm/test/Analysis/EscapeAnalysis/worklist-limit.ll

diff --git a/llvm/include/llvm/Analysis/EscapeAnalysis.h b/llvm/include/llvm/Analysis/EscapeAnalysis.h
new file mode 100644
index 0000000000000..f200b477b3838
--- /dev/null
+++ b/llvm/include/llvm/Analysis/EscapeAnalysis.h
@@ -0,0 +1,194 @@
+//===- EscapeAnalysis.h - Intraprocedural Escape Analysis -------*- C++ -*-===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This file defines the interface for a simple, conservative intraprocedural
+// escape analysis. It is designed as a helper utility for other passes, like
+// ThreadSanitizer, to determine if an allocation escapes the context of its
+// containing function.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_ANALYSIS_ESCAPEANALYSIS_H
+#define LLVM_ANALYSIS_ESCAPEANALYSIS_H
+
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/SmallVector.h"
+#include "llvm/Analysis/CaptureTracking.h"
+#include "llvm/Analysis/LoopInfo.h"
+#include "llvm/Analysis/MemorySSA.h"
+#include "llvm/Analysis/TargetLibraryInfo.h"
+#include "llvm/IR/PassManager.h"
+#include "llvm/Support/Compiler.h"
+
+namespace llvm {
+/// Find underlying base objects for a pointer possibly produced by loads.
+///
+/// This routine walks backwards through MemorySSA clobbering definitions of
+/// simple loads to find stores that defined the loaded pointer values, and
+/// collects their base objects. Additionally, it attempts ValueTracking
+/// `getUnderlyingObjects` to peel pointer casts/GEPs/phis where profitable.
+///
+/// Collected "base" objects are:
+///  - `AllocaInst` (stack, base=stack)
+///  - `Argument` (function argument, base=arg)
+///  - `GlobalVariable` and `GlobalAlias` (base=global|alias)
+///  - `ConstantPointerNull` (base=null)
+///  - Results of known heap-allocating calls (e.g. `malloc`, `calloc`,
+///    `realloc`, `strdup`, or C\+\+ `new`) when recognized by `isAllocationFn`
+///    (i.e. appropriately annotated, e.g. with `allockind`) (base=heap).
+///
+/// If the walk encounters an unrecognized defining write, a non-simple store,
+/// a memintrinsic as a defining write, or the step budget is exceeded, the
+/// analysis conservatively treats the current value as a terminal non-base
+/// and marks the result as incomplete.
+///
+/// Contract and guarantees:
+///  - If `MSSA` is null, the analysis immediately returns with
+///    `*IsComplete == false` (if provided).
+///  - If `TLI` is null, heap allocations cannot be recognized; terminals that
+///    are calls are treated as non-bases and lead to `*IsComplete == false`.
+///  - `Result` is a set of terminal values observed (may include non-bases if
+///    the analysis is incomplete). Use `*IsComplete` to know if all are bases.
+///  - `MaxSteps` is a per-query safety valve limiting the combined number of
+///    processed worklist nodes. When exceeded, the analysis bails out and
+///    sets `*IsComplete == false`.
+LLVM_ABI void getUnderlyingObjectsThroughLoads(
+    const Value *Ptr, MemorySSA *MSSA, SmallPtrSetImpl<const Value *> &Result,
+    const TargetLibraryInfo *TLI = nullptr, LoopInfo *LI = nullptr,
+    bool *IsComplete = nullptr, unsigned MaxSteps = 10000);
+
+/// EscapeAnalysisInfo - This class implements the actual backward dataflow
+/// analysis for a function; queries are per allocation site.
+///
+/// This is a lightweight, intraprocedural and conservative analysis intended
+/// to help instrumentation passes (e.g. ThreadSanitizer) skip objects that do
+/// not escape the function scope. The main query is \c isEscaping(Value&),
+/// which answers whether an allocation site (alloca/malloc-like) may escape
+/// the current function. Results are memoized per underlying object.
+struct EscapeAnalysisInfo {
+  /// Constructs an escape analysis utility for a given function.
+  /// Requires a FunctionAnalysisManager to obtain other analyses like AA.
+  EscapeAnalysisInfo(Function &F, FunctionAnalysisManager &FAM) : F(F) {
+    TLI = &FAM.getResult<TargetLibraryAnalysis>(F);
+    MSSA = &FAM.getResult<MemorySSAAnalysis>(F).getMSSA();
+    LI = &FAM.getResult<LoopAnalysis>(F);
+  }
+  ~EscapeAnalysisInfo() = default;
+
+  /// Return true if \p Alloc may escape the function.
+  /// \param Alloc - Must be an allocation site (AllocaInst or heap allocation
+  ///                call). Passing GEPs/bitcasts is not supported; use the base
+  ///                allocation.
+  /// \returns true if the allocation escapes or if \p Alloc is not an
+  /// allocation site.
+  LLVM_ABI bool isEscaping(const Value &Alloc);
+
+  /// Print escape information for all allocations in the function
+  LLVM_ABI void print(raw_ostream &OS);
+
+  LLVM_ABI bool invalidate(Function &Fn, const PreservedAnalyses &PA,
+                           FunctionAnalysisManager::Invalidator &Inv);
+
+private:
+  Function &F;
+  DenseMap<const Value *, bool> Cache;
+
+  TargetLibraryInfo *TLI = nullptr;
+  MemorySSA *MSSA = nullptr;
+  LoopInfo *LI = nullptr;
+
+  /// Checks whether a base location is externally visible (thus escapes).
+  static bool isExternalObject(const Value *Base);
+
+  /// Custom CaptureTracker for escape analysis
+  class EscapeCaptureTracker : public CaptureTracker {
+  public:
+    EscapeCaptureTracker(EscapeAnalysisInfo &EAI,
+                         const SmallPtrSet<const Value *, 32> &ProcessingSet,
+                         bool &SawCycle)
+        : EAI(EAI), ProcessingSet(ProcessingSet), SawCycle(SawCycle) {}
+
+    void tooManyUses() override { Escaped = true; }
+    bool shouldExplore(const Use *U) override;
+    Action captured(const Use *U, UseCaptureInfo CI) override;
+    bool hasEscaped() const { return Escaped; }
+
+  private:
+    EscapeAnalysisInfo &EAI;
+    SmallPtrSet<const Value *, 32> ProcessingSet;
+    bool &SawCycle;
+    bool Escaped = false;
+
+    /// Analyze if storing to destination causes escape
+    bool doesStoreDestEscape(const Value *Dest);
+
+    /// Get indices of pointer-typed arguments that are marked 'nocapture'
+    SmallVector<unsigned, 8>
+    getNoCapturePointerArgIndices(const CallBase *CB) const;
+
+    /// Check if any of the 'nocapture' arguments can reach the query object
+    bool canEscapeViaNocaptureArgs(
+        const CallBase &CB, ArrayRef<unsigned> NoCapPtrArgs,
+        SmallPtrSetImpl<const Value *> &StorePtrOpndBases) const;
+
+    /// Check if the given clobber stems from StartMDef
+    bool stemsFromStartStore(MemoryUseOrDef *MUOD, const MemoryDef *StartMDef,
+                             MemoryLocation Loc, bool &IsComplete,
+                             MemorySSAWalker *Walker) const;
+
+    /// Walk MemorySSA forward from StartStore and:
+    ///  - collect pointer-typed Loads that may read bytes written by StartStore
+    ///  - detect calls that may export those bytes via nocapture pointer args
+    /// Sets ContentMayEscape if any call may export the bytes.
+    SmallVector<const LoadInst *, 32>
+    findStoreReadersAndExports(const StoreInst *StartStore,
+                               bool &ContentMayEscape, bool &IsComplete);
+
+    /// Analyze whether the pointer value stored by `Store` can escape
+    bool doesStoredPointerEscapeViaLoads(const StoreInst *Store);
+  };
+
+  /// Solve escape for a single allocation site using backward dataflow.
+  ///
+  /// If \p SawCycle is provided, it is set when the query encounters a
+  /// backedge into the current \p ProcessingSet. This does not by itself mean
+  /// the object escapes; it only marks a negative result as provisional so it
+  /// is not memoized before the whole local SCC is resolved.
+  bool solveEscapeFor(const Value &Ptr,
+                      SmallPtrSet<const Value *, 32> &ProcessingSet,
+                      bool *SawCycle = nullptr);
+
+  /// Helper function to detect allocation sites (malloc/new-like)
+  /// Returns true if V is an Alloca or a call to a known heap alloc function.
+  bool isAllocationSite(const Value *V);
+};
+
+/// EscapeAnalysisInfo wrapper for the new pass manager.
+class EscapeAnalysis : public AnalysisInfoMixin<EscapeAnalysis> {
+  friend AnalysisInfoMixin<EscapeAnalysis>;
+  static AnalysisKey Key;
+
+public:
+  using Result = EscapeAnalysisInfo;
+  static Result run(Function &F, FunctionAnalysisManager &FAM);
+};
+
+/// Printer pass for the \c EscapeAnalysis results.
+class EscapeAnalysisPrinterPass
+    : public PassInfoMixin<EscapeAnalysisPrinterPass> {
+  raw_ostream &OS;
+
+public:
+  explicit EscapeAnalysisPrinterPass(raw_ostream &OS) : OS(OS) {}
+  PreservedAnalyses run(Function &F, FunctionAnalysisManager &FAM) const;
+  static bool isRequired() { return true; }
+};
+
+} // end namespace llvm
+
+#endif // LLVM_ANALYSIS_ESCAPEANALYSIS_H
diff --git a/llvm/lib/Analysis/CMakeLists.txt b/llvm/lib/Analysis/CMakeLists.txt
index f3586c66cb056..219dfe2998ca3 100644
--- a/llvm/lib/Analysis/CMakeLists.txt
+++ b/llvm/lib/Analysis/CMakeLists.txt
@@ -78,6 +78,7 @@ add_llvm_component_library(LLVMAnalysis
   DXILResource.cpp
   DXILMetadataAnalysis.cpp
   EphemeralValuesCache.cpp
+  EscapeAnalysis.cpp
   FloatingPointPredicateUtils.cpp
   FunctionPropertiesAnalysis.cpp
   GlobalsModRef.cpp
diff --git a/llvm/lib/Analysis/EscapeAnalysis.cpp b/llvm/lib/Analysis/EscapeAnalysis.cpp
new file mode 100644
index 0000000000000..4ef6a5ec02143
--- /dev/null
+++ b/llvm/lib/Analysis/EscapeAnalysis.cpp
@@ -0,0 +1,798 @@
+//===- EscapeAnalysis.cpp - Intraprocedural Escape Analysis Implementation ===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This file implements the intraprocedural EscapeAnalysis helper class. The
+// analysis determines whether a memory allocation (stack or heap) can escape
+// the current function, i.e., whether its address can become visible to other
+// threads.
+//
+// Algorithm overview:
+//
+// 1. Entry point: isEscaping(V) first checks simple cases via CaptureTracking.
+//    If CaptureTracking reports the pointer as captured, the result is
+//    "escaped" immediately.
+//
+// 2. Heap allocations: isAllocationFn() identifies malloc/new-family calls.
+//    A heap allocation is treated identically to an alloca — non-escaping if
+//    no pointer to it leaks out of the function.
+//
+// 3. MemorySSA-driven look-through: getUnderlyingObjectsThroughLoads() walks
+//    the MemorySSA def-use chain backward from a load's defining access to
+//    find the underlying allocation(s). This lets the analysis see through
+//    stack slots and struct/array fields.
+//
+// 4. Indirect escape via containers (doesStoredPointerEscapeViaLoads()):
+//    When a pointer P is stored into a slot S, the analysis checks (a) whether
+//    S itself escapes, and (b) whether any load from S that was written by that
+//    specific store can further propagate P to an escaping use.
+//
+// 5. Escape criteria: an allocation escapes if its address is stored into a
+//    global, returned from the function, passed to a call that captures the
+//    pointer, cast via ptrtoint, or accessed through a volatile operation.
+//
+// 6. Worklist limit: WorklistLimit caps the number of MemorySSA nodes visited
+//    per allocation. When exceeded, the analysis conservatively reports escape.
+//
+//===----------------------------------------------------------------------===//
+
+#include "llvm/Analysis/EscapeAnalysis.h"
+
+#include "llvm/ADT/SmallString.h"
+#include "llvm/ADT/Statistic.h"
+#include "llvm/Analysis/AliasAnalysis.h"
+#include "llvm/Analysis/MemoryBuiltins.h"
+#include "llvm/Analysis/MemorySSA.h"
+#include "llvm/Analysis/ValueTracking.h"
+#include "llvm/IR/InstIterator.h"
+#include "llvm/Support/CommandLine.h"
+#include "llvm/Support/Debug.h"
+
+#define DEBUG_TYPE "escape-analysis"
+
+using namespace llvm;
+
+STATISTIC(NumAllocationsAnalyzed, "Number of allocation sites analyzed");
+STATISTIC(NumAllocationsEscaped, "Number of allocation sites found to escape");
+
+/// Per-allocation worklist cap (safety valve). If the number of processed
+/// worklist nodes exceeds this limit, the analysis bails out conservatively and
+/// considers the allocation as escaping.
+static cl::opt<unsigned> WorklistLimit(
+    "escape-analysis-worklist-limit", cl::init(1000), cl::Hidden,
+    cl::desc("Max number of worklist nodes processed per allocation; "
+             "if exceeded, assume the allocation escapes"));
+
+// getUnderlyingObjects(..., MaxLookup = 0) is assumed to mean "unbounded".
+// If upstream changes semantics, this must be revisited.
+static constexpr unsigned VTMaxLookup = 0;
+
+//===----------------------------------------------------------------------===//
+// File-local MemorySSA utilities
+//===----------------------------------------------------------------------===//
+
+namespace llvm {
+/// Add P to Worklist if it doesn't exist in Seen
+template <typename PtrT, typename SetT, typename WorklistT>
+static bool tryEnqueueIfNew(PtrT *P, SetT &Seen, WorklistT &Worklist) {
+  if (P && Seen.insert(P).second) {
+    Worklist.push_back(P);
+    return true;
+  }
+  return false;
+}
+
+/// Add incoming unvisited MemoryAccesses of a MemoryPhi to MAWorkList.
+static void appendIncomingMAs(const MemoryPhi *MPhi,
+                              SmallPtrSetImpl<MemoryAccess *> &VisitedMA,
+                              SmallVectorImpl<MemoryAccess *> &MAWorkList,
+                              MemoryLocation Loc, MemorySSAWalker *Walker,
+                              bool &IsComplete) {
+  for (unsigned Idx = 0, N = MPhi->getNumIncomingValues(); Idx != N; ++Idx) {
+    MemoryAccess *InMA = MPhi->getIncomingValue(Idx);
+    MemoryAccess *EdgeCl = Walker->getClobberingMemoryAccess(InMA, Loc);
+    if (!EdgeCl) {
+      IsComplete = false;
+      continue;
+    }
+    tryEnqueueIfNew(EdgeCl, VisitedMA, MAWorkList);
+  }
+}
+
+enum class EdgeWalkStep { Recurse, RecurseAbove, SkipSuccessors, Stop };
+
+/// Walk edge clobbering definitions starting from Start MemoryAccess.
+template <typename VisitT>
+static void walkEdgeClobbers(MemoryAccess *Start, MemorySSAWalker *Walker,
+                             MemoryLocation Loc, unsigned Limit,
+                             const VisitT &Visit, bool &IsComplete) {
+  IsComplete = true;
+  if (!Start) {
+    IsComplete = false;
+    return;
+  }
+
+  SmallVector<MemoryAccess *, 32> MAWorklist;
+  SmallPtrSet<MemoryAccess *, 32> MAVisited;
+  tryEnqueueIfNew(Start, MAVisited, MAWorklist);
+  unsigned Steps = 0;
+
+  while (!MAWorklist.empty()) {
+    if (++Steps > Limit) {
+      IsComplete = false;
+      return;
+    }
+
+    MemoryAccess *MA = MAWorklist.pop_back_val();
+
+    const EdgeWalkStep Act = Visit(MA);
+    if (Act == EdgeWalkStep::Stop)
+      return;
+    if (Act == EdgeWalkStep::SkipSuccessors)
+      continue;
+
+    if (auto *MDef = dyn_cast<MemoryDef>(MA)) {
+      // For RecurseAbove, continue the walk strictly *above* MDef. The
+      // with-location query returns MDef itself when MDef clobbers Loc, so
+      // start from its defining access to reach the next dominating clobber.
+      // Plain Recurse keeps the nearest-clobber semantics (a clobbering MDef
+      // ends the path), which callers rely on to stop at must-aliasing stores.
+      MemoryAccess *RecurseFrom =
+          Act == EdgeWalkStep::RecurseAbove ? MDef->getDefiningAccess() : MDef;
+      MemoryAccess *EdgeCl =
+          Walker->getClobberingMemoryAccess(RecurseFrom, Loc);
+      if (!EdgeCl) {
+        IsComplete = false;
+        return;
+      }
+      tryEnqueueIfNew(EdgeCl, MAVisited, MAWorklist);
+    } else if (const auto *MPhi = dyn_cast<MemoryPhi>(MA)) {
+      appendIncomingMAs(MPhi, MAVisited, MAWorklist, Loc, Walker, IsComplete);
+      if (!IsComplete)
+        return;
+    } else {
+      llvm_unreachable("Unexpected MemoryAccess kind");
+    }
+  }
+}
+
+/// Try to use ValueTracking to find underlying objects.
+static bool tryValueTracking(const Value *V, LoopInfo *LI,
+                             SmallVectorImpl<const Value *> &Work,
+                             SmallPtrSetImpl<const Value *> &Enqueued) {
+  SmallVector<const Value *, 4> Bases;
+  if (!V->getType()->isPointerTy())
+    return false; // Only pointers have underlying objects.
+
+  getUnderlyingObjects(V, Bases, LI, VTMaxLookup);
+
+  if (Bases.empty() || (Bases.size() == 1 && Bases[0] == V))
+    return false;
+
+  for (const Value *B : Bases)
+    tryEnqueueIfNew(B, Enqueued, Work);
+  return true;
+}
+
+void getUnderlyingObjectsThroughLoads(const Value *Ptr, MemorySSA *MSSA,
+                                      SmallPtrSetImpl<const Value *> &Result,
+                                      const TargetLibraryInfo *TLI,
+                                      LoopInfo *LI, bool *IsComplete,
+                                      unsigned MaxSteps) {
+  LLVM_DEBUG(dbgs() << "getUnderlyingObjectsThroughLoads: " << Ptr->getName()
+                    << "\n");
+
+  if (!Ptr->getType()->isPointerTy()) {
+    LLVM_DEBUG(dbgs() << "Input is not a pointer: " << *Ptr << "\n");
+    return; // Only pointers have underlying objects.
+  }
+
+  if (!MSSA) {
+    LLVM_DEBUG(dbgs() << "MSSA is null, marking analysis as incomplete\n");
+    if (IsComplete)
+      *IsComplete = false;
+    return;
+  }
+
+  auto AddTerminal = [&](const Value *Term) {
+    if (!Term || !Term->getType()->isPointerTy())
+      return;
+    bool IsBase = isa<AllocaInst>(Term) || isa<Argument>(Term) ||
+                  isa<GlobalVariable>(Term) || isa<GlobalAlias>(Term) ||
+                  isa<ConstantPointerNull>(Term);
+    if (!IsBase && TLI) { // Check if it's heap allocation call
+      if (const auto *CB = dyn_cast<CallBase>(Term))
+        IsBase = isAllocationFn(CB, TLI);
+    }
+    LLVM_DEBUG(dbgs() << "Mark terminal: " << *Term
+                      << " IsBase=" << (IsBase ? "yes" : "no") << "\n");
+    Result.insert(Term);
+    if (IsComplete && !IsBase) {
+      *IsComplete = false;
+      LLVM_DEBUG(dbgs() << "Marking incomplete due to non-base\n");
+    }
+  };
+
+  SmallPtrSet<const Value *, 32> ValueTrackingSeen;
+  SmallPtrSet<const Value *, 32> Seen;
+  SmallVector<const Value *, 32> Worklist;
+
+  auto Bail = [&]() {
+    if (IsComplete)
+      *IsComplete = false;
+    for (const Value *WV : Worklist)
+      AddTerminal(WV);
+  };
+
+  tryEnqueueIfNew(Ptr, Seen, Worklist);
+
+  unsigned Step = 0;
+  if (IsComplete)
+    *IsComplete = true;
+
+  MemorySSAWalker *Walker = MSSA->getSkipSelfWalker();
+
+  // Reuse the alias analysis MemorySSA was built with to decide, per defining
+  // store, whether it must-aliases the load (and thus fully defines the loaded
+  // value). Batched across the loads processed in this call; the IR is not
+  // mutated during the analysis.
+  BatchAAResults BatchAA(MSSA->getAA());
+
+  while (!Worklist.empty()) {
+    const Value *CurrPtr = Worklist.pop_back_val();
+
+    // Safety valve: if we exceed MaxSteps, bail out conservatively.
+    if (++Step > MaxSteps) {
+      LLVM_DEBUG(dbgs() << "MaxSteps exceeded at: " << *CurrPtr << "\n");
+      AddTerminal(CurrPtr);
+      Bail();
+      return;
+    }
+
+    // Try ValueTracking first (only once per value)
+    if (!isa<LoadInst>(CurrPtr) && ValueTrackingSeen.insert(CurrPtr).second &&
+        tryValueTracking(CurrPtr, LI, Worklist, Seen))
+      continue; // Successfully expanded via ValueTracking;
+
+    const auto *Load = dyn_cast<LoadInst>(CurrPtr);
+    if (!Load || !Load->isSimple()) {
+      AddTerminal(CurrPtr);
+      continue;
+    }
+
+    // Use MemorySSA's API to get the clobbering MemoryAccess.
+    MemoryAccess *Clobber = Walker->getClobberingMemoryAccess(Load);
+    const auto LoadLoc = MemoryLocation::get(Load);
+
+    // Local accumulators for Load
+    SmallVector<const Value *, 8> LocalWorklist;
+    SmallPtrSet<const Value *, 8> LocalSeen;
+
+    LocalSeen.insert(Load);
+    bool Fallback = false;
+    bool MAWalkComplete = false;
+    // Limit MemorySSA walk to half of the budget
+    const unsigned MAIterationLimit = std::max(1u, MaxSteps / 2);
+
+    walkEdgeClobbers(
+        Clobber, Walker, LoadLoc, MAIterationLimit,
+        [&](MemoryAccess *MA) -> EdgeWalkStep {
+          if (MSSA->isLiveOnEntryDef(MA)) {
+            LLVM_DEBUG(dbgs() << "LiveOnEntryDef reached, fallback\n");
+            Fallback = true;
+            return EdgeWalkStep::Stop;
+          }
+
+          if (const auto *MDef = dyn_cast<MemoryDef>(MA)) {
+            const Instruction *I = MDef->getMemoryInst();
+            assert(I && "MemoryDef must have an instruction");
+
+            if (const auto *Store = dyn_cast<StoreInst>(I)) {
+              if (!Store->isSimple()) {
+                Fallback = true;
+                return EdgeWalkStep::Stop;
+              }
+              const Value *SV = Store->getValueOperand();
+              if (SV->getType()->isPointerTy()) {
+                tryEnqueueIfNew(SV, LocalSeen, LocalWorklist);
+                // Only stop if this store provably defines the exact bytes the
+                // load reads. If it merely may-alias the load, the loaded value
+                // can also come from an earlier definition, so keep walking
+                // upward to collect those sources too; an unresolved source
+                // upstream will (soundly) mark the result incomplete.
+                if (BatchAA.isMustAlias(MemoryLocation::get(Store), LoadLoc))
+                  return EdgeWalkStep::SkipSuccessors;
+                return EdgeWalkStep::RecurseAbove;
+              }
+              LLVM_DEBUG(dbgs() << "Non-pointer store: " << *Store << "\n");
+              Fallback = true;
+              return EdgeWalkStep::Stop;
+            }
+            // NOTE: We intentionally don't consider the source in memintrinsics
+            // (memmove/memcpy): they are not semantically underlying objects.
+            // Conservatively assume escape.
+            LLVM_DEBUG(dbgs() << "Unrecognized defining write, fallback\n");
+            Fallback = true;
+            return EdgeWalkStep::Stop;
+          }
+          return EdgeWalkStep::Recurse;
+        },
+        MAWalkComplete);
+
+    if (!MAWalkComplete)
+      Fallback = true;
+
+    if (Fallback) {
+      LLVM_DEBUG(dbgs() << "Fallback: mark Load as term: " << *Load << "\n");
+      AddTerminal(Load);
+    } else {
+      for (const auto *WV : LocalWorklist)
+        tryEnqueueIfNew(WV, Seen, Worklist);
+    }
+  } // end while for Work
+  LLVM_DEBUG(dbgs() << "getUnderlyingObjectsThroughLoads: " << Ptr->getName()
+                    << " -- end\n");
+}
+
+} // end namespace llvm
+
+//===----------------------------------------------------------------------===//
+// EscapeCaptureTracker Implementation
+//===----------------------------------------------------------------------===//
+
+bool EscapeAnalysisInfo::EscapeCaptureTracker::shouldExplore(const Use *U) {
+  return true;
+}
+
+bool EscapeAnalysisInfo::isExternalObject(const Value *Base) {
+  return isa<GlobalVariable>(Base) || isa<GlobalAlias>(Base) ||
+         isa<Argument>(Base);
+}
+
+bool EscapeAnalysisInfo::EscapeCaptureTracker::doesStoreDestEscape(
+    const Value *Dest) {
+  // Find base objects for the storage location
+  SmallPtrSet<const Value *, 8> BaseObjects;
+  bool IsComplete = false;
+  getUnderlyingObjectsThroughLoads(Dest, EAI.MSSA, BaseObjects, EAI.TLI, EAI.LI,
+                                   &IsComplete);
+
+  // If bases are unknown or the walk is incomplete, be conservative.
+  if (BaseObjects.empty() || !IsComplete) {
+    LLVM_DEBUG(dbgs() << "  Store destination unknown/incomplete, escapes\n");
+    return true;
+  }
+
+  for (const Value *Base : BaseObjects) {
+    LLVM_DEBUG(dbgs() << "  Store destination Base: " << *Base << "\n");
+    if (isExternalObject(Base)) {
+      LLVM_DEBUG(dbgs() << "  Stored to external object, escapes\n");
+      return true;
+    }
+
+    // If storing to another local allocation, recursively check if it escapes
+    if (const auto *Alloca = dyn_cast<AllocaInst>(Base)) {
+      // Recurse to decide whether the target alloca itself escapes.
+      if (EAI.solveEscapeFor(*Alloca, ProcessingSet, &SawCycle)) {
+        LLVM_DEBUG(dbgs() << "  Stored to escaping alloca, escapes\n");
+        return true;
+      }
+    } else if (const auto *CB = dyn_cast<CallBase>(Base)) {
+      // Store to malloc/new call result (heap allocation).
+      if (isAllocationFn(CB, EAI.TLI)) {
+        if (EAI.solveEscapeFor(*CB, ProcessingSet, &SawCycle)) {
+          LLVM_DEBUG(dbgs() << "  Stored into escaping heap alloc, escapes\n");
+          return true; // Stored into escaping heap allocation — escapes.
+        }
+        continue; // Stored into non-escaping heap allocation — OK.
+      }
+      LLVM_DEBUG(dbgs() << "  Stored to unknown call result, escapes\n");
+      return true; // Unknown call result — escapes.
+    } else {
+      // Any other/unknown terminal means the destination is not proven local.
+      LLVM_DEBUG(dbgs() << "  Stored to unknown location, escapes\n");
+      return true;
+    }
+  }
+  return false;
+}
+
+SmallVector<unsigned, 8>
+EscapeAnalysisInfo::EscapeCaptureTracker::getNoCapturePointerArgIndices(
+    const CallBase *CB) const {
+  SmallVector<unsigned, 8> Indices;
+  for (unsigned ArgNo = 0, End = CB->arg_size(); ArgNo != End; ++ArgNo) {
+    const auto *Opnd = CB->getArgOperand(ArgNo);
+    if (!Opnd || !Opnd->getType()->isPointerTy() ||
+        CB->paramHasAttr(ArgNo, Attribute::WriteOnly))
+      continue;
+    CaptureInfo CI = CB->getCaptureInfo(ArgNo);
+    if (capturesNothing(
+            UseCaptureInfo(CI.getOtherComponents(), CI.getRetComponents())))
+      Indices.push_back(ArgNo);
+  }
+  return Indices;
+}
+
+bool EscapeAnalysisInfo::EscapeCaptureTracker::canEscapeViaNocaptureArgs(
+    const CallBase &CB, ArrayRef<unsigned> NoCapPtrArgs,
+    SmallPtrSetImpl<const Value *> &StorePtrOpndBases) const {
+  // For each nocapture arg, compute bases and check if it is the QueryObj.
+  for (unsigned ArgIdx : NoCapPtrArgs) {
+    const Value *OpV = CB.getArgOperand(ArgIdx);
+    SmallPtrSet<const Value *, 8> ArgBases;
+    bool Complete = false;
+    getUnderlyingObjectsThroughLoads(OpV, EAI.MSSA, ArgBases, EAI.TLI, EAI.LI,
+                                     &Complete);
+    if (!Complete)
+      return true;
+    for (const Value *StoreBaseObj : StorePtrOpndBases) {
+      if (ArgBases.contains(StoreBaseObj))
+        return true;
+    }
+  }
+  return false;
+}
+
+bool EscapeAnalysisInfo::EscapeCaptureTracker::stemsFromStartStore(
+    MemoryUseOrDef *MUOD, const MemoryDef *StartMDef, MemoryLocation Loc,
+    bool &IsComplete, MemorySSAWalker *Walker) const {
+  LLVM_DEBUG(dbgs() << "  stemsFromStartStore: clobber " << *MUOD << "\n");
+  MemoryAccess *Clobber =
+      Walker->getClobberingMemoryAccess(MUOD->getDefiningAccess(), Loc);
+  if (!Clobber) {
+    IsComplete = false;
+    return true;
+  }
+  bool WalkComplete = false;
+  bool Found = false;
+
+  walkEdgeClobbers(
+      Clobber, Walker, Loc, WorklistLimit,
+      [&](MemoryAccess *MA) {
+        LLVM_DEBUG(dbgs() << "    inspect MA: " << *MA << "\n");
+        if (EAI.MSSA->isLiveOnEntryDef(MA))
+          return EdgeWalkStep::SkipSuccessors; // No defining store above entry
+        if (MA == StartMDef) {
+          Found = true;
+          return EdgeWalkStep::Stop;
+        }
+        return EdgeWalkStep::Recurse;
+      },
+      WalkComplete);
+
+  if (!WalkComplete)
+    IsComplete = false;
+  return Found || !WalkComplete; // conservative on incompleteness
+}
+
+SmallVector<const LoadInst *, 32>
+EscapeAnalysisInfo::EscapeCaptureTracker::findStoreReadersAndExports(
+    const StoreInst *StartStore, bool &ContentMayEscape, bool &IsComplete) {
+  IsComplete = true;
+  ContentMayEscape = false;
+  auto *StartMDef = cast<MemoryDef>(EAI.MSSA->getMemoryAccess(StartStore));
+  LLVM_DEBUG(dbgs() << "Collecting Loads reading from Store: " << *StartStore
+                    << "\t" << *StartMDef << "\n");
+  const MemoryLocation LocDest = MemoryLocation::get(StartStore);
+  MemorySSAWalker *Walker = EAI.MSSA->getSkipSelfWalker();
+
+  // A call may export memory and cause its escape.
+  auto MayReadMemory = [](const CallBase *CB) {
+    if (!CB || CB->doesNotAccessMemory() || CB->onlyWritesMemory())
+      return false;
+    return true;
+  };
+
+  // Resolve the bases of the store's pointer operand. If the walk is
+  // incomplete we cannot reliably determine whether any nocapture argument
+  // aliases this location, so treat the stored content as potentially
+  // exported by any call that reads memory.
+  SmallPtrSet<const Value *, 8> StorePtrBases;
+  bool StorePtrComplete = true;
+  getUnderlyingObjectsThroughLoads(StartStore->getPointerOperand(), EAI.MSSA,
+                                   StorePtrBases, EAI.TLI, EAI.LI,
+                                   &StorePtrComplete);
+  if (!StorePtrComplete) {
+    IsComplete = false;
+    ContentMayEscape = true;
+    return {};
+  }
+
+  SmallVector<const LoadInst *, 32> ResLoads;
+  SmallVector<MemoryAccess *, 32> MAWorklist;
+  SmallPtrSet<MemoryAccess *, 32> MAVisited;
+  tryEnqueueIfNew(StartMDef, MAVisited, MAWorklist);
+
+  unsigned Steps = 0;
+  while (!MAWorklist.empty()) {
+    if (++Steps > WorklistLimit) {
+      IsComplete = false;
+      return {};
+    }
+
+    MemoryAccess *MA = MAWorklist.pop_back_val();
+    for (User *U : MA->users()) {
+      if (auto *MUOD = dyn_cast<MemoryUseOrDef>(U)) {
+        if (const Instruction *I = MUOD->getMemoryInst()) {
+          LLVM_DEBUG(dbgs() << "  UseOrDef: " << *I << "\n");
+          // Consider functions which can export the content behind LocDest
+          if (const auto *CB = dyn_cast<CallBase>(I); CB && MayReadMemory(CB)) {
+            if (auto NoCapArgs = getNoCapturePointerArgIndices(CB);
+                canEscapeViaNocaptureArgs(*CB, NoCapArgs, StorePtrBases) &&
+                stemsFromStartStore(MUOD, StartMDef, LocDest, IsComplete,
+                                    Walker)) {
+              LLVM_DEBUG(dbgs() << "  Call may export bytes: " << *CB << "\n");
+              ContentMayEscape = true;
+              return {};
+            }
+          } else if (auto *MU = dyn_cast<MemoryUse>(MUOD)) {
+            if (const auto *Load = dyn_cast<LoadInst>(I);
+                Load && stemsFromStartStore(MU, StartMDef, LocDest, IsComplete,
+                                            Walker)) {
+              LLVM_DEBUG(dbgs() << "  Load read from Store: " << *Load << "\n");
+              ResLoads.push_back(Load);
+            }
+            continue; // No need to enqueue further for MemoryUse
+          }
+        }
+      }
+      tryEnqueueIfNew(cast<MemoryAccess>(U), MAVisited, MAWorklist);
+    }
+  }
+  return ResLoads;
+}
+
+bool EscapeAnalysisInfo::EscapeCaptureTracker::doesStoredPointerEscapeViaLoads(
+    const StoreInst *Store) {
+  LLVM_DEBUG(dbgs() << "\n---- doesStoredPointerEscapeViaLoads " << *Store
+                    << " ----\n");
+  bool IsComplete = false;
+  bool ContentMayEscape = false;
+  const auto Loads =
+      findStoreReadersAndExports(Store, ContentMayEscape, IsComplete);
+  if (!IsComplete) {
+    LLVM_DEBUG(dbgs() << "  Incomplete load collection, escapes\n");
+    return true; // We may have missed loads -> conservatively escape
+  }
+  if (ContentMayEscape) {
+    LLVM_DEBUG(dbgs() << "  Bytes exported by call, escapes\n");
+    return true; // Bytes stored may have been exported -> conservatively escape
+  }
+
+  for (const LoadInst *Load : Loads) {
+    if (!Load->isSimple())
+      return true;
+    if (!Load->getType()->isPointerTy())
+      continue; // Loading non-pointer cannot cause escape
+    if (EAI.solveEscapeFor(*Load, ProcessingSet, &SawCycle)) {
+      LLVM_DEBUG(dbgs() << "  -> escapes via load\n");
+      return true;
+    }
+  }
+  LLVM_DEBUG(dbgs() << "  -> does not escape via loads\n");
+  return false;
+}
+
+CaptureTracker::Action
+EscapeAnalysisInfo::EscapeCaptureTracker::captured(const Use *U,
+                                                   UseCaptureInfo CI) {
+  LLVM_DEBUG(dbgs() << "\n--> Analyzing capture use: " << *U->get() << " in "
+                    << *U->getUser() << "\n");
+  const auto *I = cast<Instruction>(U->getUser());
+
+  // If CaptureTracking says this use does not capture, continue exploring.
+  if (capturesNothing(CI.UseCC)) {
+    LLVM_DEBUG(dbgs() << "    Use doesn't capture, continue\n");
+    return Continue; // CaptureTracking says it's not captured, continue
+  }
+
+  // Passthrough ops (gep/bitcast/select/phi..) should be explored transitively.
+  if (CI.isPassthrough()) {
+    LLVM_DEBUG(dbgs() << "    Passthrough operation, continue to result\n");
+    return Continue;
+  }
+
+  // Now handle special cases where CaptureTracking says it's captured,
+  // but we need more sophisticated escape analysis
+
+  if (const auto *Store = dyn_cast<StoreInst>(I)) {
+    // Check if we're storing the pointer (not storing to it)
+    if (Store->getValueOperand() == U->get()) {
+      LLVM_DEBUG(dbgs() << "==> Storing pointer value, analyze destination "
+                        << *Store->getPointerOperand() << "\n");
+      if (!Store->isSimple() ||
+          doesStoreDestEscape(Store->getPointerOperand()) ||
+          doesStoredPointerEscapeViaLoads(Store)) {
+        LLVM_DEBUG(dbgs() << "  Store to escaping destination, escapes\n");
+        Escaped = true;
+        return Stop;
+      }
+      return ContinueIgnoringReturn;
+    }
+    // If we are the destination pointer, this use does not capture the value.
+    LLVM_DEBUG(dbgs() << "    Used as store destination, doesn't escape\n");
+    return ContinueIgnoringReturn;
+  }
+
+  if (isa<ICmpInst>(I)) { // Pure comparisons of addresses do not cause escape.
+    LLVM_DEBUG(dbgs() << "    Pointer comparison, doesn't escape\n");
+    return ContinueIgnoringReturn;
+  }
+
+  // FreezeInst is semantically a passthrough (like bitcast/select), but
+  // CaptureTracking's DetermineUseCaptureKind has no explicit case for it and
+  // falls through to "default: UseCaptureInfo(All, None)". Because ResultCC is
+  // None, returning Continue would not cause CaptureTracking to follow uses of
+  // the frozen result. Instead, explicitly recurse into the freeze result so
+  // that transitive uses (e.g. storing the frozen pointer to a global) are
+  // correctly detected.
+  if (const auto *Freeze = dyn_cast<FreezeInst>(I)) {
+    LLVM_DEBUG(dbgs() << "    freeze: recursing into frozen result\n");
+    if (EAI.solveEscapeFor(*Freeze, ProcessingSet, &SawCycle)) {
+      Escaped = true;
+      return Stop;
+    }
+    return ContinueIgnoringReturn;
+  }
+
+  // After the capturesNothing and isPassthrough early-returns above, UseCC
+  // captures something — treat as escape.
+  LLVM_DEBUG(dbgs() << "  Captured by: " << *I << "\n");
+  Escaped = true;
+  return Stop;
+}
+
+bool EscapeAnalysisInfo::solveEscapeFor(
+    const Value &Ptr, SmallPtrSet<const Value *, 32> &ProcessingSet,
+    bool *SawCycle) {
+  LLVM_DEBUG(dbgs() << "Solving escape for "
+                    << (Ptr.hasName() ? Ptr.getName() : "Load") << "\n");
+  if (const auto CacheIt = Cache.find(&Ptr); CacheIt != Cache.end()) {
+    LLVM_DEBUG(dbgs() << "  Cached result: "
+                      << (CacheIt->second ? "escaped" : "not escaped") << "\n");
+    return CacheIt->second;
+  }
+
+  if (ProcessingSet.contains(&Ptr)) { // Cycle
+    LLVM_DEBUG(dbgs() << "  Cycle detected for " << Ptr.getName()
+                      << ", defer to enclosing SCC\n");
+    if (SawCycle)
+      *SawCycle = true;
+    return false;
+  }
+  ProcessingSet.insert(&Ptr);
+
+  // Recursive queries performed by EscapeCaptureTracker write into
+  // LocalSawCycle through the bool reference passed to the tracker.
+  // A 'false' result that depended on an in-progress cycle is provisional and
+  // must not be memoized, otherwise peers in the same local SCC could observe
+  // an early cached 'no-escape' result before the SCC is fully resolved.
+  bool LocalSawCycle = false;
+
+  EscapeCaptureTracker Tracker(*this, ProcessingSet, LocalSawCycle);
+
+  // Use the CaptureTracking infrastructure to analyze the allocation
+  PointerMayBeCaptured(&Ptr, &Tracker, /*MaxUsesToExplore=*/WorklistLimit);
+  const bool Escaped = Tracker.hasEscaped();
+  if (Escaped || !LocalSawCycle)
+    Cache[&Ptr] = Escaped;
+  ProcessingSet.erase(&Ptr);
+
+  if (SawCycle)
+    *SawCycle |= LocalSawCycle;
+
+  LLVM_DEBUG(dbgs() << "  Result: " << (Escaped ? "escaped" : "not escaped")
+                    << "\n");
+  return Escaped;
+}
+
+//===----------------------------------------------------------------------===//
+// EscapeAnalysis Core Implementation
+//===----------------------------------------------------------------------===//
+
+bool EscapeAnalysisInfo::isAllocationSite(const Value *V) {
+  if (isa<AllocaInst>(V))
+    return true;
+  if (const auto *CB = dyn_cast<CallBase>(V))
+    return isAllocationFn(CB, TLI);
+  return false;
+}
+
+bool EscapeAnalysisInfo::isEscaping(const Value &Alloc) {
+  if (!isAllocationSite(&Alloc)) { // Validate input
+    LLVM_DEBUG(dbgs() << "EscapeAnalysis: Not an allocation: " << Alloc
+                      << "\n");
+    return true; // Conservative: unknown things "escape"
+  }
+
+  LLVM_DEBUG(dbgs() << "EscapeAnalysis: Analyzing " << Alloc << "\n");
+  NumAllocationsAnalyzed++;
+
+  // Track allocations being processed to detect cycles
+  SmallPtrSet<const Value *, 32> ProcessingSet;
+  const bool IsEscaped = solveEscapeFor(Alloc, ProcessingSet);
+
+  if (IsEscaped)
+    NumAllocationsEscaped++;
+
+  return IsEscaped;
+}
+
+void EscapeAnalysisInfo::print(raw_ostream &OS) {
+  bool Any = false;
+  unsigned UnnamedCount = 0;
+
+  for (Instruction &I : instructions(F)) {
+    if (!isAllocationSite(&I))
+      continue;
+
+    Any = true;
+
+    // Stable symbol: use SSA name if exists, otherwise "unnamed#N".
+    StringRef Name = I.hasName() ? I.getName() : StringRef();
+    SmallString<32> Gen;
+    if (Name.empty()) {
+      ++UnnamedCount;
+      Gen += "unnamed#";
+      Gen += Twine(UnnamedCount).str();
+      Name = Gen;
+    }
+
+    const bool Esc = isEscaping(I);
+    OS << "  " << Name << " escapes: " << (Esc ? "yes" : "no") << "\n";
+  }
+
+  if (!Any)
+    OS << "  none\n";
+  OS << "\n";
+}
+
+bool EscapeAnalysisInfo::invalidate(Function &Fn, const PreservedAnalyses &PA,
+                                    FunctionAnalysisManager::Invalidator &Inv) {
+  auto Invalidate = [&]() {
+    Cache.clear();
+    MSSA = nullptr;
+    LI = nullptr;
+    TLI = nullptr;
+    return true;
+  };
+
+  auto PAC = PA.getChecker<EscapeAnalysis>();
+  if (!(PAC.preserved() || PAC.preservedSet<AllAnalysesOn<Function>>()))
+    return Invalidate();
+
+  if (Inv.invalidate<MemorySSAAnalysis>(Fn, PA) ||
+      Inv.invalidate<LoopAnalysis>(Fn, PA) ||
+      Inv.invalidate<TargetLibraryAnalysis>(Fn, PA))
+    return Invalidate();
+
+  return false;
+}
+
+AnalysisKey EscapeAnalysis::Key;
+
+EscapeAnalysis::Result EscapeAnalysis::run(Function &F,
+                                           FunctionAnalysisManager &FAM) {
+  EscapeAnalysisInfo EAI(F, FAM);
+  return EAI;
+}
+
+//===----------------------------------------------------------------------===//
+// Printing Pass for Verification
+//===----------------------------------------------------------------------===//
+
+PreservedAnalyses
+EscapeAnalysisPrinterPass::run(Function &F,
+                               FunctionAnalysisManager &FAM) const {
+  if (F.isDeclaration())
+    return PreservedAnalyses::all();
+  OS << "Printing analysis 'Escape Analysis' for function '" << F.getName()
+     << "':\n";
+  FAM.getResult<EscapeAnalysis>(F).print(OS);
+  return PreservedAnalyses::all();
+}
\ No newline at end of file
diff --git a/llvm/lib/Passes/PassBuilder.cpp b/llvm/lib/Passes/PassBuilder.cpp
index f7db63ef8bf74..125a27206f7a4 100644
--- a/llvm/lib/Passes/PassBuilder.cpp
+++ b/llvm/lib/Passes/PassBuilder.cpp
@@ -39,6 +39,7 @@
 #include "llvm/Analysis/DomPrinter.h"
 #include "llvm/Analysis/DominanceFrontier.h"
 #include "llvm/Analysis/EphemeralValuesCache.h"
+#include "llvm/Analysis/EscapeAnalysis.h"
 #include "llvm/Analysis/FunctionPropertiesAnalysis.h"
 #include "llvm/Analysis/GlobalsModRef.h"
 #include "llvm/Analysis/HashRecognize.h"
diff --git a/llvm/lib/Passes/PassRegistry.def b/llvm/lib/Passes/PassRegistry.def
index 8f70c7cefa408..181aa2b1e97dc 100644
--- a/llvm/lib/Passes/PassRegistry.def
+++ b/llvm/lib/Passes/PassRegistry.def
@@ -367,6 +367,7 @@ FUNCTION_ANALYSIS("demanded-bits", DemandedBitsAnalysis())
 FUNCTION_ANALYSIS("domfrontier", DominanceFrontierAnalysis())
 FUNCTION_ANALYSIS("domtree", DominatorTreeAnalysis())
 FUNCTION_ANALYSIS("ephemerals", EphemeralValuesAnalysis())
+FUNCTION_ANALYSIS("escape-analysis", EscapeAnalysis())
 FUNCTION_ANALYSIS("func-properties", FunctionPropertiesAnalysis())
 FUNCTION_ANALYSIS("machine-function-info", MachineFunctionAnalysis(*TM))
 FUNCTION_ANALYSIS("gc-function", GCFunctionAnalysis())
@@ -521,6 +522,7 @@ FUNCTION_PASS("print<delinearization>", DelinearizationPrinterPass(errs()))
 FUNCTION_PASS("print<demanded-bits>", DemandedBitsPrinterPass(errs()))
 FUNCTION_PASS("print<domfrontier>", DominanceFrontierPrinterPass(errs()))
 FUNCTION_PASS("print<domtree>", DominatorTreePrinterPass(errs()))
+FUNCTION_PASS("print<escape-analysis>", EscapeAnalysisPrinterPass(errs()))
 FUNCTION_PASS("print<func-properties>", FunctionPropertiesPrinterPass(errs()))
 FUNCTION_PASS("print<inline-cost>", InlineCostAnnotationPrinterPass(errs()))
 FUNCTION_PASS("print<lazy-value-info>", LazyValueInfoPrinterPass(errs()))
diff --git a/llvm/test/Analysis/EscapeAnalysis/escape-analysis.ll b/llvm/test/Analysis/EscapeAnalysis/escape-analysis.ll
new file mode 100644
index 0000000000000..14c956770b84f
--- /dev/null
+++ b/llvm/test/Analysis/EscapeAnalysis/escape-analysis.ll
@@ -0,0 +1,1320 @@
+; RUN: opt -passes='print<escape-analysis>' -disable-output %s 2>&1 | FileCheck %s
+
+; NOTE:
+; - The printer emits:
+;   "Printing analysis 'Escape Analysis' for function '<func>':"
+;   "<alloc-name> escapes: yes|no" per allocation site (alloca/malloc-like).
+;   "none" if no allocations in the function.
+; - Names are taken from SSA. We avoid relying on "unnamed#N" in tests.
+
+target datalayout = "e-p:64:64:64-i1:8:8-i8:8:8-i16:16:16-i32:32:32-i64:64:64-f32:32:32-f64:64:64-v64:64:64-v128:128:128-a0:0:64-s0:64:64-f80:128:128-n8:16:32:64-S128"
+
+ at GPtr = dso_local global ptr null, align 8
+ at GPtrPtr = dso_local global ptr null, align 8
+ at GPtrPtrPtr = dso_local global ptr null, align 8
+ at GAlias = alias ptr, ptr @GPtr
+
+%S = type { ptr, ptr }
+ at GS = dso_local global %S zeroinitializer, align 8
+
+declare noalias ptr @malloc(i64) allockind("alloc,uninitialized") allocsize(0) "alloc-family"="malloc"
+declare noalias ptr @calloc(i64, i64) allockind("alloc,zeroed") allocsize(0,1) "alloc-family"="malloc"
+declare noalias ptr @external(ptr)
+declare void @might_throw()
+
+; ============================================================================ ;
+; Basics and locals
+; ============================================================================ ;
+
+; No allocations
+define void @no_allocs() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'no_allocs':
+; CHECK-NEXT: none
+  ret void
+}
+
+; Using pointer in icmp -> no escape
+define void @icmp_no_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'icmp_no_escape':
+; CHECK: a escapes: no
+  %a = alloca i8, align 1
+  %cmp = icmp eq ptr %a, null
+  ret void
+}
+
+; Passthrough via phi/select-like -> no escape
+define void @passthrough_phi_no_escape(i1 %c) {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'passthrough_phi_no_escape':
+; CHECK: a escapes: no
+entry:
+  %a = alloca i8, align 1
+  br i1 %c, label %t, label %f
+t:
+  br label %merge
+f:
+  br label %merge
+merge:
+  %p = phi ptr [ %a, %t ], [ %a, %f ]
+  ret void
+}
+
+; Safe store to local memory -> no escape.
+define void @store_to_local_ok() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'store_to_local_ok':
+; CHECK: a escapes: no
+; CHECK: p escapes: no
+  %a = alloca i8, align 1
+  %p = alloca ptr, align 8
+  store ptr %a, ptr %p
+  ret void
+}
+
+; Chain through local pointer (double indirection) remains local:
+; %x is stored in %p, %p in %pp -> no escape.
+define void @double_ptr_local_ok() sanitize_thread {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'double_ptr_local_ok':
+; CHECK:  x escapes: no
+; CHECK:  p escapes: no
+; CHECK:  pp escapes: no
+  %x  = alloca i32, align 4
+  %p  = alloca ptr, align 8
+  %pp = alloca ptr, align 8
+  store ptr %x, ptr %p
+  store ptr %p, ptr %pp
+  store i32 1, ptr %x
+  %lv = load i32, ptr %x
+  ret void
+}
+
+; Purely local pointer cycles do not escape on their own.
+define void @local_self_cycle_no_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'local_self_cycle_no_escape':
+; CHECK: a escapes: no
+entry:
+  %a = alloca ptr, align 8
+  store ptr %a, ptr %a, align 8
+  ret void
+}
+
+define void @local_mutual_cycle_no_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'local_mutual_cycle_no_escape':
+; CHECK: a escapes: no
+; CHECK: b escapes: no
+entry:
+  %a = alloca ptr, align 8
+  %b = alloca ptr, align 8
+  store ptr %a, ptr %b, align 8
+  store ptr %b, ptr %a, align 8
+  ret void
+}
+
+; ============================================================================ ;
+; Returns and heap allocations
+; ============================================================================ ;
+
+; Returning alloca pointer -> escape
+define ptr @return_alloca_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'return_alloca_escape':
+; CHECK: a escapes: yes
+  %a = alloca i8, align 1
+  ret ptr %a
+}
+
+; Calloc (another heap allocation family): not returned -> no escape; returned -> escape
+define void @calloc_no_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'calloc_no_escape':
+; CHECK: p escapes: no
+  %p = call ptr @calloc(i64 4, i64 16)
+  ret void
+}
+
+define ptr @calloc_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'calloc_escape':
+; CHECK: p escapes: yes
+  %p = call ptr @calloc(i64 4, i64 16)
+  ret ptr %p
+}
+
+; Malloc-like allocations
+define ptr @malloc_local_no_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'malloc_local_no_escape':
+; CHECK: m1 escapes: no
+; CHECK: m2 escapes: yes
+  %m1 = call ptr @malloc(i64 16)
+  %m2 = call ptr @malloc(i64 32)
+  ret ptr %m2
+}
+
+; Escape of malloc calls
+define dso_local void @malloc_escape() #0 {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'malloc_escape':
+; CHECK:   p escapes: no
+; CHECK:   call escapes: yes
+; CHECK:   call1 escapes: yes
+entry:
+  %p = alloca ptr, align 8
+  %call = call noalias ptr @malloc(i64 noundef 4) #2
+  store ptr %call, ptr @GPtr, align 8
+  %call1 = call noalias ptr @malloc(i64 noundef 4) #2
+  store ptr %call1, ptr %p, align 8
+  %0 = load ptr, ptr %p, align 8
+  store ptr %0, ptr @GPtr, align 8
+  ret void
+}
+
+; Store to malloc'ed array element, which escapes -> local escapes
+define dso_local void @escape_through_malloc() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'escape_through_malloc':
+; CHECK:   a escapes: yes
+; CHECK:   x escapes: yes
+; CHECK:   b escapes: no
+; CHECK:   y escapes: yes
+; CHECK:   call escapes: yes
+; CHECK:   call1 escapes: yes
+entry:
+  %a = alloca ptr, align 8
+  %x = alloca i32, align 4
+  %b = alloca ptr, align 8
+  %y = alloca i32, align 4
+  %call = call noalias ptr @malloc(i64 noundef 80)
+  store ptr %call, ptr %a, align 8
+  store ptr %a, ptr @GPtrPtrPtr, align 8
+  %0 = load ptr, ptr %a, align 8
+  %arrayidx = getelementptr inbounds ptr, ptr %0, i64 5
+  store ptr %x, ptr %arrayidx, align 8
+  %call1 = call noalias ptr @malloc(i64 noundef 80)
+  store ptr %call1, ptr %b, align 8
+  %1 = load ptr, ptr %b, align 8
+  store ptr %1, ptr @GPtrPtr, align 8
+  %2 = load ptr, ptr %b, align 8
+  %arrayidx2 = getelementptr inbounds ptr, ptr %2, i64 5
+  store ptr %y, ptr %arrayidx2, align 8
+  ret void
+}
+
+; Store to malloc'ed array element, which does not escape -> no escape
+define dso_local void @no_escape_through_malloc() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'no_escape_through_malloc':
+; CHECK:   a escapes: no
+; CHECK:   x escapes: no
+; CHECK:   b escapes: no
+; CHECK:   y escapes: no
+; CHECK:   call escapes: no
+; CHECK:   call1 escapes: no
+entry:
+  %a = alloca ptr, align 8
+  %x = alloca i32, align 4
+  %b = alloca ptr, align 8
+  %y = alloca i32, align 4
+  %call = call noalias ptr @malloc(i64 noundef 80)
+  store ptr %call, ptr %a, align 8
+  %0 = load ptr, ptr %a, align 8
+  %arrayidx = getelementptr inbounds ptr, ptr %0, i64 5
+  store ptr %x, ptr %arrayidx, align 8
+  %call1 = call noalias ptr @malloc(i64 noundef 80)
+  store ptr %call1, ptr %b, align 8
+  %1 = load ptr, ptr %b, align 8
+  %arrayidx2 = getelementptr inbounds ptr, ptr %1, i64 5
+  store ptr %y, ptr %arrayidx2, align 8
+  ret void
+}
+
+; ============================================================================ ;
+; Globals, arguments, and mixed destinations
+; ============================================================================ ;
+
+; Store to global, global alias and global structure field -> escape
+define void @store_to_global_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'store_to_global_escape':
+; CHECK: a escapes: yes
+; CHECK: b escapes: yes
+; CHECK: c escapes: yes
+  %a = alloca i8, align 1
+  %b = alloca i8, align 1
+  %c = alloca i8, align 1
+  store ptr %a, ptr @GPtr
+  store ptr %b, ptr @GAlias
+  %f0 = getelementptr inbounds %S, ptr @GS, i64 0, i32 0
+  store ptr %c, ptr %f0, align 8
+  ret void
+}
+
+; Store to argument -> escape
+define void @store_to_arg_escape(ptr %out) {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'store_to_arg_escape':
+; CHECK: a escapes: yes
+  %a = alloca i8, align 1
+  store ptr %a, ptr %out
+  ret void
+}
+
+; Store to the pointer returned by an external function -> escape
+define void @store_to_unknown_ret_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'store_to_unknown_ret_escape':
+; CHECK: a escapes: yes
+; CHECK: b escapes: yes
+  %a = alloca i8, align 1
+  %b = alloca i8, align 1
+  %p = call ptr @external(ptr %b)
+  store ptr %a, ptr %p
+  ret void
+}
+
+; Cyclic dependency between local allocas, one stored to global -> both escape
+define void @cycle_allocas_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'cycle_allocas_escape':
+; CHECK: a escapes: yes
+; CHECK: b escapes: yes
+  %a = alloca ptr, align 8
+  %b = alloca ptr, align 8
+  store ptr %a, ptr %b
+  store ptr %b, ptr %a
+  store ptr %a, ptr @GPtr
+  ret void
+}
+
+; Destination via phi mixing local and global -> escape
+define void @phi_mixed_dest_escape(i1 %c) {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'phi_mixed_dest_escape':
+; CHECK: a escapes: yes
+; CHECK: p escapes: no
+  %a = alloca i8, align 1
+  %p = alloca ptr, align 8
+  br i1 %c, label %t, label %f
+t:
+  br label %m
+f:
+  br label %m
+m:
+  %dst = phi ptr [ %p, %t ], [ @GPtr, %f ]
+  store ptr %a, ptr %dst, align 8
+  ret void
+}
+
+; Store to local pointer and then store from global pointer -> no escape
+define dso_local void @store_ptr_store_from_global_no_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'store_ptr_store_from_global_no_escape':
+; CHECK: x escapes: no
+; CHECK: p escapes: no
+  %x = alloca i32, align 4
+  %p = alloca ptr, align 8
+  store ptr %x, ptr %p, align 8
+  %1 = load ptr, ptr @GPtr, align 8
+  store ptr %1, ptr %p, align 8
+  ret void
+}
+
+declare void @varargs_func(ptr, ...)
+
+; Passing pointer to varargs -> escape
+define void @varargs_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'varargs_escape':
+; CHECK: a escapes: yes
+  %a = alloca i32, align 4
+  call void (ptr, ...) @varargs_func(ptr null, ptr %a)
+  ret void
+}
+
+; ============================================================================ ;
+; Loaded destination patterns
+; ============================================================================ ;
+
+; Store through pointer loaded from argument (LiveOnEntry) -> escape
+define void @store_through_loaded_arg_escape(ptr %out) {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'store_through_loaded_arg_escape':
+; CHECK: a escapes: yes
+  %a = alloca i8, align 1
+  %l = load ptr, ptr %out, align 8
+  store ptr %a, ptr %l, align 8
+  ret void
+}
+
+; Multiple stores to same location - last one escapes
+define void @multiple_stores_last_escapes(i1 %c) {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'multiple_stores_last_escapes':
+; CHECK: x1 escapes: no
+; CHECK: x2 escapes: yes
+; CHECK: p escapes: no
+  %x1 = alloca i32, align 4
+  %x2 = alloca i32, align 4
+  %p = alloca ptr, align 8
+
+  store ptr %x1, ptr %p, align 8
+  store ptr %x2, ptr %p, align 8
+
+  %loaded = load ptr, ptr %p, align 8
+  store ptr %loaded, ptr @GPtr, align 8
+  ret void
+}
+
+; Loaded destination with MemoryPhi (two stores) -> no escape
+define void @loaded_dest_memphi_local_ok(i1 %c) {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'loaded_dest_memphi_local_ok':
+; CHECK: x escapes: no
+; CHECK: p escapes: no
+; CHECK: s1 escapes: no
+; CHECK: s2 escapes: no
+entry:
+  %x = alloca i32, align 4
+  %p = alloca ptr, align 8
+  %s1 = alloca ptr, align 8
+  %s2 = alloca ptr, align 8
+  br i1 %c, label %t, label %f
+t:
+  store ptr %s1, ptr %p, align 8
+  br label %m
+f:
+  store ptr %s2, ptr %p, align 8
+  br label %m
+m:
+  %l = load ptr, ptr %p, align 8
+  store ptr %x, ptr %l, align 8
+  ret void
+}
+
+; ============================================================================ ;
+; Arrays and GEP
+; ============================================================================ ;
+
+; Store to local array element via GEP -> no escape
+define void @store_to_gep_local_ok() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'store_to_gep_local_ok':
+; CHECK: a escapes: no
+; CHECK: arr escapes: no
+  %a = alloca i8, align 1
+  %arr = alloca [2 x ptr], align 8
+  %elem = getelementptr inbounds [2 x ptr], ptr %arr, i64 0, i64 1
+  store ptr %a, ptr %elem, align 8
+  ret void
+}
+
+; Escape through heap array element: store pointer to local into malloc'ed array
+; element, then read back and store to global -> local escapes; the malloc escapes.
+define dso_local void @escape_through_heap_array_element() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'escape_through_heap_array_element':
+; CHECK:  x escapes: yes
+; CHECK:  p escapes: no
+; CHECK:  call escapes: yes
+entry:
+  %x = alloca i32, align 4
+  %p = alloca ptr, align 8
+  %call = call noalias ptr @malloc(i64 noundef 800) #2
+  store ptr %call, ptr %p, align 8
+  %0 = load ptr, ptr %p, align 8
+  %arrayidx = getelementptr inbounds ptr, ptr %0, i64 33
+  store ptr %x, ptr %arrayidx, align 8
+  %1 = load ptr, ptr %p, align 8
+  %arrayidx1 = getelementptr inbounds ptr, ptr %1, i64 11
+  %2 = load ptr, ptr %arrayidx1, align 8
+  store ptr %2, ptr @GPtr, align 8
+  ret void
+}
+
+; Escape through stack array element read from another index: local escapes,
+; array remains local (element copied out to global).
+define dso_local void @escape_through_array_element() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'escape_through_array_element':
+; CHECK:   x escapes: yes
+; CHECK:   p escapes: no
+entry:
+  %x = alloca i32, align 4
+  %p = alloca [100 x ptr], align 16
+  %arrayidx = getelementptr inbounds [100 x ptr], ptr %p, i64 0, i64 33
+  store ptr %x, ptr %arrayidx, align 8
+  %arrayidx1 = getelementptr inbounds [100 x ptr], ptr %p, i64 0, i64 0
+  %0 = load ptr, ptr %arrayidx1, align 16
+  store ptr %0, ptr @GPtr, align 8
+  ret void
+}
+
+; May-aliasing array slots as a store destination. The destination is loaded
+; from arr[%k], which may-alias arr[%i] (holding the address of a global) and
+; arr[%j] (holding a local). Storing %x through that destination may write into
+; the global slot, so %x escapes. The backward base-finding walk must not stop
+; at the first may-aliasing store (arr[%j]=%loc): it has to keep walking up to
+; see arr[%i]=@GPtr, otherwise it would unsoundly report %x as non-escaping.
+define void @mayalias_dest_escape(i64 %i, i64 %j, i64 %k) {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'mayalias_dest_escape':
+; CHECK: arr escapes: no
+; CHECK: x escapes: yes
+; CHECK: loc escapes: no
+  %arr = alloca [4 x ptr], align 8
+  %x = alloca i8, align 1
+  %loc = alloca i32, align 4
+  %gi = getelementptr inbounds [4 x ptr], ptr %arr, i64 0, i64 %i
+  store ptr @GPtr, ptr %gi, align 8
+  %gj = getelementptr inbounds [4 x ptr], ptr %arr, i64 0, i64 %j
+  store ptr %loc, ptr %gj, align 8
+  %gk = getelementptr inbounds [4 x ptr], ptr %arr, i64 0, i64 %k
+  %dst = load ptr, ptr %gk, align 8
+  store ptr %x, ptr %dst, align 8
+  ret void
+}
+
+; Whole array (stack): leak address of an element itself -> the array escapes
+define dso_local void @escape_whole_array() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'escape_whole_array':
+; CHECK:   a1 escapes: yes
+entry:
+  %a1 = alloca [100 x i32], align 16
+  %arrayidx = getelementptr inbounds [100 x i32], ptr %a1, i64 0, i64 33
+  store ptr %arrayidx, ptr @GPtr, align 8
+  ret void
+}
+
+; Whole array (heap): leak address of an element; also keep a local alloca with
+; the malloc pointer to ensure both are reported.
+define dso_local void @escape_whole_array_heap() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'escape_whole_array_heap':
+; CHECK:   a2 escapes: yes
+; CHECK:   call escapes: yes
+entry:
+  %a2 = alloca ptr, align 8
+  %call = call noalias ptr @malloc(i64 noundef 400) #2
+  store ptr %call, ptr %a2, align 8
+  store ptr %a2, ptr @GPtrPtr, align 8
+  ret void
+}
+
+; Struct (stack): leak address of a field -> the struct itself escapes
+define void @struct_stack_self_escape_via_field_addr() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'struct_stack_self_escape_via_field_addr':
+; CHECK: s escapes: yes
+  %s = alloca %S, align 8
+  %f0 = getelementptr inbounds %S, ptr %s, i64 0, i32 0
+  store ptr %f0, ptr @GPtr, align 8
+  ret void
+}
+
+; Struct (heap-esque via malloc): leak address of a field -> the heap object escapes
+define void @struct_heap_self_escape_via_field_addr() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'struct_heap_self_escape_via_field_addr':
+; CHECK: m escapes: yes
+  %m = call ptr @malloc(i64 16)
+  %f0 = getelementptr inbounds %S, ptr %m, i64 0, i32 0
+  store ptr %f0, ptr @GPtr, align 8
+  ret void
+}
+
+; Two-dimensional heap array pointer-contained structures:
+; store local into an element, then array escapes -> local escapes; mallocs escape.
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'two_dimensional_array_escape':
+; CHECK:   x escapes: yes
+; CHECK:   arr escapes: no
+; CHECK:   i escapes: no
+; CHECK:   call escapes: yes
+; CHECK:   call1 escapes: yes
+define dso_local void @two_dimensional_array_escape() {
+entry:
+  %x = alloca i32, align 4
+  %arr = alloca ptr, align 8
+  %i = alloca i32, align 4
+  %call = call noalias ptr @malloc(i64 noundef 80)
+  store ptr %call, ptr %arr, align 8
+  store i32 0, ptr %i, align 4
+  br label %for.cond
+
+for.cond:                                         ; preds = %for.inc, %entry
+  %0 = load i32, ptr %i, align 4
+  %cmp = icmp slt i32 %0, 10
+  br i1 %cmp, label %for.body, label %for.end
+
+for.body:                                         ; preds = %for.cond
+  %call1 = call noalias ptr @malloc(i64 noundef 160)
+  %1 = load ptr, ptr %arr, align 8
+  %2 = load i32, ptr %i, align 4
+  %idxprom = sext i32 %2 to i64
+  %arrayidx = getelementptr inbounds ptr, ptr %1, i64 %idxprom
+  store ptr %call1, ptr %arrayidx, align 8
+  br label %for.inc
+
+for.inc:                                          ; preds = %for.body
+  %3 = load i32, ptr %i, align 4
+  %inc = add nsw i32 %3, 1
+  store i32 %inc, ptr %i, align 4
+  br label %for.cond
+
+for.end:                                          ; preds = %for.cond
+  %4 = load ptr, ptr %arr, align 8
+  %arrayidx2 = getelementptr inbounds ptr, ptr %4, i64 5
+  %5 = load ptr, ptr %arrayidx2, align 8
+  %arrayidx3 = getelementptr inbounds %S, ptr %5, i64 5
+  %f1 = getelementptr inbounds nuw %S, ptr %arrayidx3, i32 0, i32 0
+  store ptr %x, ptr %f1, align 8
+  %6 = load ptr, ptr %arr, align 8
+  %arrayidx4 = getelementptr inbounds ptr, ptr %6, i64 0
+  %7 = load ptr, ptr %arrayidx4, align 8
+  %arrayidx5 = getelementptr inbounds %S, ptr %7, i64 0
+  store ptr %arrayidx5, ptr @GPtr, align 8
+  ret void
+}
+
+; ============================================================================ ;
+; Structs and struct fields
+; ============================================================================ ;
+
+; Store into field of a local struct -> no escape
+define void @struct_field_local_ok() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'struct_field_local_ok':
+; CHECK: x escapes: no
+; CHECK: s escapes: no
+  %x = alloca i8, align 1
+  %s = alloca %S, align 8
+  %f0 = getelementptr inbounds %S, ptr %s, i64 0, i32 0
+  store ptr %x, ptr %f0, align 8
+  ret void
+}
+
+; Loaded-dest via field of a local struct -> no escape
+define void @loaded_dest_struct_local_ok() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'loaded_dest_struct_local_ok':
+; CHECK: x escapes: no
+; CHECK: q escapes: no
+; CHECK: s escapes: no
+  %x = alloca i8, align 1
+  %q = alloca ptr, align 8
+  %s = alloca %S, align 8
+  %f1 = getelementptr inbounds %S, ptr %s, i64 0, i32 1
+  store ptr %q, ptr %f1, align 8
+  %l = load ptr, ptr %f1, align 8
+  store ptr %x, ptr %l, align 8
+  ret void
+}
+
+; Local struct holds pointer to local; then the pointer is stored to global
+; -> local escapes, struct remains local.
+define void @struct_field_local_escape_via_global() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'struct_field_local_escape_via_global':
+; CHECK: x escapes: yes
+; CHECK: s escapes: no
+  %x = alloca i8, align 1
+  %s = alloca %S, align 8
+  %f0 = getelementptr inbounds %S, ptr %s, i64 0, i32 0
+  store ptr %x, ptr %f0, align 8
+  %loaded = load ptr, ptr %f0, align 8
+  store ptr %loaded, ptr @GPtr, align 8
+  ret void
+}
+
+; Flat alloca slot: pointer stored into plain alloca ptr, loaded back, escaped to global.
+; Exercises doesStoredPointerEscapeViaLoads on the simple (non-GEP) path.
+define void @store_slot_load_global_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'store_slot_load_global_escape':
+; CHECK: a escapes: yes
+; CHECK: slot escapes: no
+  %a = alloca i8, align 1
+  %slot = alloca ptr, align 8
+  store ptr %a, ptr %slot
+  %p = load ptr, ptr %slot
+  store ptr %p, ptr @GPtr
+  ret void
+}
+
+; Flat alloca slot: loaded pointer stored back into same local slot — no escape.
+define void @store_slot_load_local_no_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'store_slot_load_local_no_escape':
+; CHECK: a escapes: no
+; CHECK: slot escapes: no
+  %a = alloca i8, align 1
+  %slot = alloca ptr, align 8
+  store ptr %a, ptr %slot
+  %p = load ptr, ptr %slot
+  store ptr %p, ptr %slot
+  ret void
+}
+
+; Global struct stores the address of a local slot ->
+; x escapes via escaping slot; q escapes
+define void @loaded_dest_struct_global_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'loaded_dest_struct_global_escape':
+; CHECK: x escapes: yes
+; CHECK: q escapes: yes
+  %x = alloca i8, align 1
+  %q = alloca ptr, align 8
+  store ptr %q, ptr getelementptr inbounds (%S, ptr @GS, i32 0, i32 1), align 8
+  %l = load ptr, ptr %q, align 8
+  store ptr %x, ptr %l, align 8
+  ret void
+}
+
+; Select between local and global struct as container -> escape
+define void @select_struct_dest_mixed_escape(i1 %c) {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'select_struct_dest_mixed_escape':
+; CHECK: x escapes: yes
+; CHECK: s escapes: no
+  %x = alloca i8, align 1
+  %s = alloca %S, align 8
+  %dst = select i1 %c, ptr %s, ptr @GS
+  %f0 = getelementptr inbounds %S, ptr %dst, i64 0, i32 0
+  store ptr %x, ptr %f0, align 8
+  ret void
+}
+
+; Return a struct containing a pointer to a local -> escape
+define %S @return_struct_containing_ptr_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'return_struct_containing_ptr_escape':
+; CHECK: x escapes: yes
+  %x = alloca i8, align 1
+  %u = insertvalue %S poison, ptr %x, 0
+  %u2 = insertvalue %S %u, ptr null, 1
+  ret %S %u2
+}
+
+; Deep hierarchy of nested structures
+%struct.Parent = type { i32, %struct.Child }
+%struct.Child = type { i32, i32, ptr }
+%struct.GrandParent = type { i32, %struct.Parent }
+define dso_local void @escape_through_nested_struct() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'escape_through_nested_struct':
+; CHECK:  sp escapes: yes
+; CHECK:  sg escapes: yes
+; CHECK:  x escapes: yes
+entry:
+  %sp = alloca %struct.Parent, align 8
+  %sg = alloca %struct.GrandParent, align 8
+  %x = alloca i32, align 4
+  %s = getelementptr inbounds nuw %struct.Parent, ptr %sp, i32 0, i32 1
+  %a = getelementptr inbounds nuw %struct.Child, ptr %s, i32 0, i32 0
+  store ptr %a, ptr @GPtr, align 8
+  %sp1 = getelementptr inbounds nuw %struct.GrandParent, ptr %sg, i32 0, i32 1
+  %s2 = getelementptr inbounds nuw %struct.Parent, ptr %sp1, i32 0, i32 1
+  %b = getelementptr inbounds nuw %struct.Child, ptr %s2, i32 0, i32 1
+  store ptr %b, ptr @GPtr, align 8
+  %sp3 = getelementptr inbounds nuw %struct.GrandParent, ptr %sg, i32 0, i32 1
+  %s4 = getelementptr inbounds nuw %struct.Parent, ptr %sp3, i32 0, i32 1
+  %p = getelementptr inbounds nuw %struct.Child, ptr %s4, i32 0, i32 2
+  store ptr %x, ptr %p, align 8
+  ret void
+}
+
+; ============================================================================ ;
+; Atomics and volatile
+; ============================================================================ ;
+
+; Non-simple (atomic or volatile) store of pointer -> treated as escape because
+; !Store->isSimple() short-circuits the destination check. Both atomic and
+; volatile stores hit this same branch; one representative case is sufficient.
+define void @atomic_store_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'atomic_store_escape':
+; CHECK: a escapes: yes
+; CHECK: p escapes: no
+  %a = alloca i8, align 1
+  %p = alloca ptr, align 8
+  store atomic ptr %a, ptr %p seq_cst, align 8
+  ret void
+}
+
+; Volatile store of pointer -> treated as escape
+define void @volatile_store_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'volatile_store_escape':
+; CHECK: a escapes: yes
+; CHECK: p escapes: no
+  %a = alloca i8, align 1
+  %p = alloca ptr, align 8
+  store volatile ptr %a, ptr %p
+  ret void
+}
+
+; ============================================================================ ;
+; freeze instruction
+; ============================================================================ ;
+
+; freeze result unused -> no escape (freeze is semantically passthrough)
+define void @freeze_unused_no_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'freeze_unused_no_escape':
+; CHECK: a escapes: no
+  %a = alloca i8, align 1
+  %frozen = freeze ptr %a
+  ret void
+}
+
+; freeze result used only in icmp -> no escape
+define void @freeze_icmp_no_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'freeze_icmp_no_escape':
+; CHECK: a escapes: no
+  %a = alloca i8, align 1
+  %frozen = freeze ptr %a
+  %cmp = icmp eq ptr %frozen, null
+  ret void
+}
+
+; freeze result stored to global -> escape (the frozen copy is exposed)
+define void @freeze_global_store_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'freeze_global_store_escape':
+; CHECK: a escapes: yes
+  %a = alloca i8, align 1
+  %frozen = freeze ptr %a
+  store ptr %frozen, ptr @GPtr
+  ret void
+}
+
+; ============================================================================ ;
+; Casts
+; ============================================================================ ;
+
+; PtrToInt cast -> escape
+define void @ptrtoint_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'ptrtoint_escape':
+; CHECK: a escapes: yes
+  %a = alloca i8, align 1
+  %c1 = icmp ne ptr %a, null
+  %c2 = icmp eq ptr %a, null
+  %sel = select i1 %c1, ptr %a, ptr %a
+  %use = ptrtoint ptr %sel to i64
+  ret void
+}
+
+; addrspacecast: result unused -> no escape (addrspacecast is passthrough)
+define void @addrspacecast_unused_no_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'addrspacecast_unused_no_escape':
+; CHECK: a escapes: no
+  %a = alloca i32, align 4
+  %c = addrspacecast ptr %a to ptr addrspace(1)
+  ret void
+}
+
+; addrspacecast: chain back to addrspace(0), result stored to global -> escape
+define void @addrspacecast_chain_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'addrspacecast_chain_escape':
+; CHECK: a escapes: yes
+  %a = alloca i32, align 4
+  %c1 = addrspacecast ptr %a to ptr addrspace(1)
+  %c0 = addrspacecast ptr addrspace(1) %c1 to ptr
+  store ptr %c0, ptr @GPtr, align 8
+  ret void
+}
+
+; ============================================================================ ;
+; Escape through double pointers
+; ============================================================================ ;
+
+; Store pp to global triple pointer -> all three allocations escape
+define dso_local void @esc_thorugh_double_ptr1() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'esc_thorugh_double_ptr1':
+; CHECK:  x escapes: yes
+; CHECK:  p escapes: yes
+; CHECK:  pp escapes: yes
+entry:
+  %x = alloca i32, align 4
+  %p = alloca ptr, align 8
+  %pp = alloca ptr, align 8
+  store ptr %x, ptr %p, align 8
+  store ptr %p, ptr %pp, align 8
+  store ptr %pp, ptr @GPtrPtrPtr, align 8
+  ret void
+}
+
+; Store p (loaded from pp) to global double pointer
+; -> x and p escape, pp stays local
+define dso_local void @esc_thorugh_double_ptr2() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'esc_thorugh_double_ptr2':
+; CHECK:  x escapes: yes
+; CHECK:  p escapes: yes
+; CHECK:  pp escapes: no
+entry:
+  %x = alloca i32, align 4
+  %p = alloca ptr, align 8
+  %pp = alloca ptr, align 8
+  store ptr %x, ptr %p, align 8
+  store ptr %p, ptr %pp, align 8
+  %0 = load ptr, ptr %pp, align 8
+  store ptr %0, ptr @GPtrPtr, align 8
+  ret void
+}
+
+; ============================================================================ ;
+; Loops
+; ============================================================================ ;
+
+; Store pointer in a loop -> escape detection through loop iterations
+define void @store_in_loop_escape(i32 %n) {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'store_in_loop_escape':
+; CHECK: x escapes: yes
+; CHECK: arr escapes: no
+entry:
+  %x = alloca i32, align 4
+  %arr = alloca [10 x ptr], align 8
+  br label %loop
+
+loop:
+  %i = phi i32 [ 0, %entry ], [ %next, %loop ]
+  %gep = getelementptr inbounds [10 x ptr], ptr %arr, i64 0, i32 %i
+  store ptr %x, ptr %gep, align 8
+  %next = add i32 %i, 1
+  %cond = icmp slt i32 %next, %n
+  br i1 %cond, label %loop, label %exit
+
+exit:
+  %load_gep = getelementptr inbounds [10 x ptr], ptr %arr, i64 0, i32 5
+  %loaded = load ptr, ptr %load_gep, align 8
+  store ptr %loaded, ptr @GPtr, align 8
+  ret void
+}
+
+; ============================================================================ ;
+; Calls with nocapture arguments (e.g. memory intrinsics)
+; ============================================================================ ;
+
+declare void @memintr_like_func(ptr noalias readonly captures(none))
+declare void @memintr_like_func_writeonly(ptr noalias readonly captures(none)) writeonly
+declare void @memintr_like_func_arg_writeonly(ptr noalias writeonly captures(none))
+declare void @memintr_like_func_readonly(ptr noalias readonly captures(none)) readonly
+
+; Object stored into array, which is passed to a nocapture argument -> escape
+define dso_local void @pass_nocapture_arg() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'pass_nocapture_arg':
+; Case 0: store followed by nocapture argument, but writeonly -> no escape
+; CHECK:  x01 escapes: no
+; CHECK:  arr01 escapes: no
+; CHECK:  x02 escapes: no
+; CHECK:  arr02 escapes: no
+; Case 1: stored pointer is read by a nocapture-like call -> escape
+; CHECK:  x1 escapes: yes
+; CHECK:  arr1 escapes: no
+; Case 2: another independent case -> escape
+; CHECK:  x2 escapes: yes
+; CHECK:  arr2 escapes: no
+; Overwrite case below: first store is overwritten before the call -> x3 does not escape, x4 escapes
+; CHECK:  x3 escapes: no
+; CHECK:  x4 escapes: yes
+; CHECK:  arr3 escapes: no
+entry:
+  ; Case 0-1: store followed by nocapture argument, but the function is writeonly -> no escape
+  %x01 = alloca i32, align 4
+  %arr01 = alloca [10 x ptr], align 16
+  %arrayidx0 = getelementptr inbounds [10 x ptr], ptr %arr01, i64 0, i64 5
+  store ptr %x01, ptr %arrayidx0, align 8
+  %arraydecay0 = getelementptr inbounds [10 x ptr], ptr %arr01, i64 0, i64 0
+  call void @memintr_like_func_writeonly(ptr align 16 %arraydecay0)
+
+  ; Case 0-2: store followed by nocapture argument,
+  ; but the function argument is writeonly -> no escape
+  %x02 = alloca i32, align 4
+  %arr02 = alloca [10 x ptr], align 16
+  %arrayidx02 = getelementptr inbounds [10 x ptr], ptr %arr02, i64 0, i64 5
+  store ptr %x02, ptr %arrayidx02, align 8
+  %arraydecay02 = getelementptr inbounds [10 x ptr], ptr %arr02, i64 0, i64 0
+  call void @memintr_like_func_arg_writeonly(ptr align 16 %arraydecay02)
+
+  ; Case 1: stored pointer is read by a nocapture-like call -> escape
+  %x1 = alloca i32, align 4
+  %arr1 = alloca [10 x ptr], align 16
+  %arrayidx1 = getelementptr inbounds [10 x ptr], ptr %arr1, i64 0, i64 5
+  store ptr %x1, ptr %arrayidx1, align 8
+  %arraydecay1 = getelementptr inbounds [10 x ptr], ptr %arr1, i64 0, i64 0
+  call void @memintr_like_func(ptr align 16 %arraydecay1)
+
+  ; Case 2: another independent case -> escape
+  %x2 = alloca i32, align 4
+  %arr2 = alloca [10 x ptr], align 16
+  %arrayidx2 = getelementptr inbounds [10 x ptr], ptr %arr2, i64 0, i64 5
+  store ptr %x2, ptr %arrayidx2, align 8
+  %arraydecay2 = getelementptr inbounds [10 x ptr], ptr %arr2, i64 0, i64 0
+  call void @memintr_like_func_readonly(ptr align 16 %arraydecay2)
+
+  ; Case 3: Overwrite before the nocapture call:
+  ; first store (%x3) is overwritten by second store (%x4) before the call.
+  ; Expected: x3 does not escape; x4 escapes; the array itself does not escape.
+  %x3 = alloca i32, align 4
+  %x4 = alloca i32, align 4
+  %arr3 = alloca [10 x ptr], align 16
+  %slot3 = getelementptr inbounds [10 x ptr], ptr %arr3, i64 0, i64 5
+  store ptr %x3, ptr %slot3, align 8
+  store ptr %x4, ptr %slot3, align 8
+  %decay3 = getelementptr inbounds [10 x ptr], ptr %arr3, i64 0, i64 0
+  call void @memintr_like_func(ptr align 16 %decay3)
+
+  ret void
+}
+
+; Calls with nocapture arguments (heap): passing a heap pointer itself to a nocapture
+; consumer does NOT make the heap allocation escape; however, objects stored inside
+; that heap memory may escape. Includes overwrite cases for both heap pointers and x's.
+define dso_local void @pass_nocapture_arg_heap() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'pass_nocapture_arg_heap':
+; Case 1: Store local x into heap memory, then nocapture reads -> x escapes
+; CHECK:  x1 escapes: yes
+; CHECK:  arr1 escapes: no
+; CHECK:  call1 escapes: no
+; Case 2: Readonly variant -> x still escapes
+; CHECK:  x2 escapes: yes
+; CHECK:  arr2 escapes: no
+; CHECK:  call2 escapes: no
+; Case 3: Overwrite scenario for x on heap:
+; CHECK:  x3 escapes: no
+; CHECK:  x4 escapes: yes
+; CHECK:  arr3 escapes: no
+; CHECK:  call3 escapes: no
+entry:
+  ; Case 1: Store local x into heap memory, then nocapture reads -> x escapes
+  %x1 = alloca i32, align 4
+  %arr1 = alloca ptr, align 8
+  %call1 = call noalias ptr @malloc(i64 noundef 80)
+  store ptr %call1, ptr %arr1, align 8
+  %hp1 = load ptr, ptr %arr1, align 8
+  %slot1 = getelementptr inbounds ptr, ptr %hp1, i64 5
+  store ptr %x1, ptr %slot1, align 8
+  %base1 = load ptr, ptr %arr1, align 8
+  call void @memintr_like_func(ptr align 8 %base1)
+
+  ; Case 2: Readonly variant -> x still escapes
+  %x2 = alloca i32, align 4
+  %arr2 = alloca ptr, align 8
+  %call2 = call noalias ptr @malloc(i64 noundef 80)
+  store ptr %call2, ptr %arr2, align 8
+  %hp2 = load ptr, ptr %arr2, align 8
+  %slot2 = getelementptr inbounds ptr, ptr %hp2, i64 5
+  store ptr %x2, ptr %slot2, align 8
+  %base2 = load ptr, ptr %arr2, align 8
+  call void @memintr_like_func_readonly(ptr align 8 %base2)
+
+  ; Case 3: Overwrite scenario for x on heap:
+  %x3 = alloca i32, align 4
+  %x4 = alloca i32, align 4
+  %arr3 = alloca ptr, align 8
+  %call3 = call noalias ptr @malloc(i64 noundef 80)
+  store ptr %call3, ptr %arr3, align 8
+  %hp3 = load ptr, ptr %arr3, align 8
+  %slot3 = getelementptr inbounds ptr, ptr %hp3, i64 5
+  store ptr %x3, ptr %slot3, align 8
+  store ptr %x4, ptr %slot3, align 8
+  %base3 = load ptr, ptr %arr3, align 8
+  call void @memintr_like_func(ptr align 8 %base3)
+
+  ret void
+}
+
+; ============================================================================ ;
+; Indirect calls and invoke
+; ============================================================================ ;
+
+; Passing alloca to an indirect call (unknown callee) -> escape
+define void @indirect_call_ptr_arg_escape(ptr %fn) {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'indirect_call_ptr_arg_escape':
+; CHECK: a escapes: yes
+  %a = alloca i32, align 4
+  call void %fn(ptr %a)
+  ret void
+}
+
+; invoke: alloca passed as argument -> escape
+define void @invoke_ptr_arg_escape(ptr %fn) personality ptr null {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'invoke_ptr_arg_escape':
+; CHECK: a escapes: yes
+entry:
+  %a = alloca i32, align 4
+  invoke void %fn(ptr %a)
+    to label %cont unwind label %lpad
+cont:
+  ret void
+lpad:
+  %lp = landingpad { ptr, i32 }
+    cleanup
+  ret void
+}
+
+; ============================================================================ ;
+; Atomic instructions
+; ============================================================================ ;
+
+; cmpxchg writing alloca address into global -> escape
+define void @cmpxchg_ptr_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'cmpxchg_ptr_escape':
+; CHECK: a escapes: yes
+  %a = alloca i32, align 4
+  %old = cmpxchg ptr @GPtr, ptr null, ptr %a seq_cst seq_cst
+  ret void
+}
+
+; atomicrmw: value operand (the pointer stored atomically) -> escape
+define void @atomicrmw_ptr_val_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'atomicrmw_ptr_val_escape':
+; CHECK: a escapes: yes
+  %a = alloca i32, align 4
+  %old = atomicrmw xchg ptr @GPtr, ptr %a seq_cst
+  ret void
+}
+
+; atomicrmw: pointer operand (the location being modified) -> no escape
+define void @atomicrmw_ptr_dest_no_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'atomicrmw_ptr_dest_no_escape':
+; CHECK: a escapes: no
+  %a = alloca ptr, align 8
+  %old = atomicrmw xchg ptr %a, ptr null seq_cst
+  ret void
+}
+
+; ============================================================================ ;
+; memcpy / memintrinsic: source address is nocapture -> no address escape
+; (Note: memcpy copies *bytes*, not pointer values from an alloca's memory;
+;  the analysis conservatively leaves byte-copy escape tracking to downstream
+;  passes that understand the memory model.)
+; ============================================================================ ;
+
+declare void @llvm.memcpy.p0.p0.i64(ptr noalias nocapture writeonly,
+                                    ptr noalias nocapture readonly, i64, i1)
+
+; memcpy from local src: src is passed as nocapture readonly -> no address escape.
+define void @memcpy_local_src_global_dst_no_addr_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'memcpy_local_src_global_dst_no_addr_escape':
+; CHECK: src escapes: no
+  %src = alloca [16 x i8], align 1
+  call void @llvm.memcpy.p0.p0.i64(ptr @GPtr, ptr %src, i64 16, i1 false)
+  ret void
+}
+
+; ============================================================================ ;
+; MemoryPhi paths – cases where MemorySSA's join nodes are the deciding factor
+; ============================================================================ ;
+
+; IR-level phi joins two allocas; phi result stored to global → both escape.
+; (No MemorySSA needed here; CaptureTracking handles phi passthrough.)
+define void @phi_two_allocas_escape(i1 %c) {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'phi_two_allocas_escape':
+; CHECK: a escapes: yes
+; CHECK: b escapes: yes
+entry:
+  %a = alloca i32, align 4
+  %b = alloca i32, align 4
+  br i1 %c, label %t, label %f
+t:
+  br label %merge
+f:
+  br label %merge
+merge:
+  %v = phi ptr [%a, %t], [%b, %f]
+  store ptr %v, ptr @GPtr, align 8
+  ret void
+}
+
+; IR-level phi: one arm is the alloca, other is null. Result stored to global → escape.
+define void @phi_alloca_null_escape(i1 %c) {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'phi_alloca_null_escape':
+; CHECK: a escapes: yes
+entry:
+  %a = alloca i32, align 4
+  br i1 %c, label %t, label %f
+t:
+  br label %merge
+f:
+  br label %merge
+merge:
+  %v = phi ptr [%a, %t], [null, %f]
+  store ptr %v, ptr @GPtr, align 8
+  ret void
+}
+
+; select between alloca and null: result stored to global -> escape
+; (select is passthrough in CaptureTracking, same as phi)
+define void @select_alloca_null_escape(i1 %c) {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'select_alloca_null_escape':
+; CHECK: a escapes: yes
+  %a = alloca i32, align 4
+  %v = select i1 %c, ptr %a, ptr null
+  store ptr %v, ptr @GPtr, align 8
+  ret void
+}
+
+; select between alloca and null: result unused -> no escape
+define void @select_alloca_null_no_escape(i1 %c) {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'select_alloca_null_no_escape':
+; CHECK: a escapes: no
+  %a = alloca i32, align 4
+  %v = select i1 %c, ptr %a, ptr null
+  ret void
+}
+
+; MemoryPhi in getUnderlyingObjectsThroughLoads (store-destination side):
+; t-branch stores a LOCAL pointer into slot %p;
+; f-branch stores the ADDRESS of a global (@GPtr) into slot %p.
+; The load from %p sees a MemoryPhi → underlying objects = {%q, @GPtr}.
+; Since @GPtr is external, storing %x through the loaded dest causes escape.
+define void @loaded_dest_memphi_one_global_escape(i1 %c) {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'loaded_dest_memphi_one_global_escape':
+; CHECK: x escapes: yes
+; CHECK: p escapes: no
+; CHECK: q escapes: no
+entry:
+  %x = alloca i8, align 1
+  %p = alloca ptr, align 8
+  %q = alloca ptr, align 8
+  br i1 %c, label %t, label %f
+t:
+  store ptr %q, ptr %p, align 8
+  br label %m
+f:
+  store ptr @GPtr, ptr %p, align 8
+  br label %m
+m:
+  %l = load ptr, ptr %p, align 8
+  store ptr %x, ptr %l, align 8
+  ret void
+}
+
+; MemoryPhi in findStoreReadersAndExports (forward walk, stored-value side):
+; Store %x into %p (startDef). One arm conditionally overwrites %p with null.
+; Exit load sees MemoryPhi({startDef, nullStore}) and the loaded value is stored
+; to global → %x escapes (it may be the value read by the exit load).
+define void @conditional_overwrite_store_escape(i1 %c) {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'conditional_overwrite_store_escape':
+; CHECK: x escapes: yes
+; CHECK: p escapes: no
+entry:
+  %x = alloca i32, align 4
+  %p = alloca ptr, align 8
+  store ptr %x, ptr %p, align 8
+  br i1 %c, label %t, label %exit
+t:
+  store ptr null, ptr %p, align 8
+  br label %exit
+exit:
+  %v = load ptr, ptr %p, align 8
+  store ptr %v, ptr @GPtr, align 8
+  ret void
+}
+
+; Same MemoryPhi structure, but loaded value is only used in a comparison.
+; No pointer escapes.
+define void @conditional_overwrite_store_no_escape(i1 %c) {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'conditional_overwrite_store_no_escape':
+; CHECK: x escapes: no
+; CHECK: p escapes: no
+entry:
+  %x = alloca i32, align 4
+  %p = alloca ptr, align 8
+  store ptr %x, ptr %p, align 8
+  br i1 %c, label %t, label %exit
+t:
+  store ptr null, ptr %p, align 8
+  br label %exit
+exit:
+  %v = load ptr, ptr %p, align 8
+  %cmp = icmp ne ptr %v, null
+  ret void
+}
+
+; stemsFromStartStore + LiveOnEntry path:
+; %slot is written on one CFG arm but NOT the other. The exit load's defining
+; access is a MemoryPhi with two incoming edges: startDef (from %t) and
+; LiveOnEntry (from entry, slot uninitialised). stemsFromStartStore walks
+; backward, hits LiveOnEntry on one arm → SkipSuccessors (our guard), finds
+; startDef on the other arm → returns true. %x escapes.
+define void @memphi_liveentry_arm_escape(i1 %c) {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'memphi_liveentry_arm_escape':
+; CHECK: x escapes: yes
+; CHECK: slot escapes: no
+entry:
+  %x = alloca i8, align 1
+  %slot = alloca ptr, align 8
+  br i1 %c, label %t, label %merge
+t:
+  store ptr %x, ptr %slot
+  br label %merge
+merge:
+  ; MemoryPhi: {startDef from %t, LiveOnEntry from entry}
+  %p = load ptr, ptr %slot
+  store ptr %p, ptr @GPtr
+  ret void
+}
+
+; Same structure, but loaded value not stored anywhere → %x does not escape.
+define void @memphi_liveentry_arm_no_escape(i1 %c) {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'memphi_liveentry_arm_no_escape':
+; CHECK: x escapes: no
+; CHECK: slot escapes: no
+entry:
+  %x = alloca i8, align 1
+  %slot = alloca ptr, align 8
+  br i1 %c, label %t, label %merge
+t:
+  store ptr %x, ptr %slot
+  br label %merge
+merge:
+  %p = load ptr, ptr %slot
+  ret void
+}
+
+; MemorySSA precision: loop always executes at least once and always overwrites
+; %p with null before exit. The exit-load's defining access is the null store
+; (not a MemoryPhi including startDef), so MemorySSA knows %x is never the
+; value loaded at exit → %x does NOT escape.
+define void @loop_always_overwrites_no_escape(i32 %n) {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'loop_always_overwrites_no_escape':
+; CHECK: x escapes: no
+; CHECK: p escapes: no
+entry:
+  %x = alloca i32, align 4
+  %p = alloca ptr, align 8
+  store ptr %x, ptr %p, align 8
+  br label %loop
+loop:
+  %i = phi i32 [0, %entry], [%next, %loop]
+  store ptr null, ptr %p, align 8
+  %next = add i32 %i, 1
+  %cond = icmp slt i32 %next, %n
+  br i1 %cond, label %loop, label %exit
+exit:
+  %v = load ptr, ptr %p, align 8
+  store ptr %v, ptr @GPtr, align 8
+  ret void
+}
+
+; ============================================================================ ;
+; Deallocation functions (free, realloc)
+; ============================================================================ ;
+
+declare void @free(ptr nocapture)
+declare noalias ptr @realloc(ptr, i64) allockind("realloc") allocsize(1) "alloc-family"="malloc"
+
+; free: argument is nocapture -> pointer does not escape
+define void @free_no_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'free_no_escape':
+; CHECK: p escapes: no
+  %p = call ptr @malloc(i64 16)
+  call void @free(ptr %p)
+  ret void
+}
+
+; realloc: the old pointer is passed as a regular (capturing) argument.
+; old pointer escapes; new pointer is not stored anywhere -> no escape.
+define void @realloc_old_ptr_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'realloc_old_ptr_escape':
+; CHECK: p escapes: yes
+; CHECK: p2 escapes: no
+  %p = call ptr @malloc(i64 16)
+  %p2 = call ptr @realloc(ptr %p, i64 32)
+  ret void
+}
+
+; ============================================================================ ;
+; Lifetime intrinsics
+; ============================================================================ ;
+
+declare void @llvm.lifetime.start.p0(i64, ptr nocapture)
+declare void @llvm.lifetime.end.p0(i64, ptr nocapture)
+
+; lifetime.start/end: both take nocapture ptr -> no escape
+define void @lifetime_no_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'lifetime_no_escape':
+; CHECK: a escapes: no
+  %a = alloca i32, align 4
+  call void @llvm.lifetime.start.p0(i64 4, ptr %a)
+  call void @llvm.lifetime.end.p0(i64 4, ptr %a)
+  ret void
+}
+
+; ============================================================================ ;
+; memset
+; ============================================================================ ;
+
+declare void @llvm.memset.p0.i64(ptr noalias nocapture writeonly, i8, i64, i1)
+
+; memset on local alloca: dest is nocapture writeonly -> no escape
+define void @memset_no_escape() {
+; CHECK-LABEL: Printing analysis 'Escape Analysis' for function 'memset_no_escape':
+; CHECK: a escapes: no
+  %a = alloca [16 x i8], align 1
+  call void @llvm.memset.p0.i64(ptr %a, i8 0, i64 16, i1 false)
+  ret void
+}
+
diff --git a/llvm/test/Analysis/EscapeAnalysis/invalidation.ll b/llvm/test/Analysis/EscapeAnalysis/invalidation.ll
new file mode 100644
index 0000000000000..c1cbd23bf35cb
--- /dev/null
+++ b/llvm/test/Analysis/EscapeAnalysis/invalidation.ll
@@ -0,0 +1,22 @@
+; RUN: opt -disable-output -debug-pass-manager \
+; RUN:   -passes='function(require<escape-analysis>,invalidate<escape-analysis>,require<escape-analysis>)' \
+; RUN:   %s 2>&1 | FileCheck %s
+
+; CHECK: Running pass: RequireAnalysisPass<llvm::EscapeAnalysis
+; CHECK: Running analysis: EscapeAnalysis on f
+; CHECK: Running pass: InvalidateAnalysisPass<llvm::EscapeAnalysis> on f
+; CHECK: Invalidating analysis: EscapeAnalysis on f
+; CHECK: Running pass: RequireAnalysisPass<llvm::EscapeAnalysis
+; CHECK: Running analysis: EscapeAnalysis on f
+
+; Verify that the re-run analysis also produces correct results by using
+; print<escape-analysis> which exercises isEscaping() on the result.
+; RUN: opt -passes='print<escape-analysis>' -disable-output %s 2>&1 | FileCheck %s --check-prefix=RESULT
+
+; RESULT-LABEL: Printing analysis 'Escape Analysis' for function 'f':
+; RESULT:         a escapes: no
+
+define void @f() {
+  %a = alloca i8, align 1
+  ret void
+}
diff --git a/llvm/test/Analysis/EscapeAnalysis/worklist-limit.ll b/llvm/test/Analysis/EscapeAnalysis/worklist-limit.ll
new file mode 100644
index 0000000000000..5fc74ff1322b1
--- /dev/null
+++ b/llvm/test/Analysis/EscapeAnalysis/worklist-limit.ll
@@ -0,0 +1,25 @@
+; RUN: opt -passes='print<escape-analysis>' -disable-output %s 2>&1 | FileCheck %s --check-prefix=CHECK-DEFAULT
+; RUN: opt -passes='print<escape-analysis>' -escape-analysis-worklist-limit=1 -disable-output %s 2>&1 | FileCheck %s --check-prefix=CHECK-LIMIT
+
+; With the default limit, the passthrough chain is fully explored and remains local.
+; With a tiny limit, CaptureTracking bails out conservatively and marks escape.
+define void @phi_limit_bailout(i1 %c) {
+; CHECK-DEFAULT-LABEL: Printing analysis 'Escape Analysis' for function 'phi_limit_bailout':
+; CHECK-DEFAULT: a escapes: no
+; CHECK-LIMIT-LABEL: Printing analysis 'Escape Analysis' for function 'phi_limit_bailout':
+; CHECK-LIMIT: a escapes: yes
+entry:
+  %a = alloca i8, align 1
+  br i1 %c, label %t, label %f
+
+t:
+  br label %merge
+
+f:
+  br label %merge
+
+merge:
+  %p = phi ptr [ %a, %t ], [ %a, %f ]
+  ret void
+}
+



More information about the llvm-commits mailing list