Skip to content

[LifetimeSafety] Make the dataflow analysis generic #148222

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jul 16, 2025

Conversation

usx95
Copy link
Contributor

@usx95 usx95 commented Jul 11, 2025

Refactored the lifetime safety analysis to use a generic dataflow framework with a policy-based design.

Changes

  • Introduced a generic DataflowAnalysis template class that can be specialized for different analyses
  • Renamed LifetimeLattice to LoanPropagationLattice to better reflect its purpose
  • Created a LoanPropagationAnalysis class that inherits from the generic framework
  • Moved transfer functions from the standalone Transferer class into the analysis class
  • Restructured the code to separate the dataflow engine from the specific analysis logic
  • Updated debug output and test expectations to use the new class names

Motivation

In order to add more analyses, e.g. loan expiry and origin liveness, the previous implementation would have separate, nearly identical dataflow runners for each analysis. This change creates a single, reusable component, which will make it much simpler to add subsequent analyses without repeating boilerplate code.

This is quite close to the existing dataflow framework!

Copy link

github-actions bot commented Jul 11, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

@usx95 usx95 force-pushed the users/usx95/lifetime-safety-liveness branch from 14a9c8b to 4742560 Compare July 14, 2025 15:51
@usx95 usx95 changed the base branch from users/usx95/lifetime-safety-add-dataflow to users/usx95/lifetime-safety-benchmarking July 14, 2025 16:44
@usx95 usx95 force-pushed the users/usx95/lifetime-safety-benchmarking branch 2 times, most recently from 5fbfcd7 to a558fe4 Compare July 14, 2025 18:21
Base automatically changed from users/usx95/lifetime-safety-benchmarking to main July 14, 2025 18:23
@usx95 usx95 force-pushed the users/usx95/lifetime-safety-liveness branch 5 times, most recently from e20c098 to 0c787ef Compare July 14, 2025 19:34
@usx95 usx95 changed the title [LifetimeSafety] Add expired loans analysis [LifetimeSafety] Implement generic dataflow framework Jul 14, 2025
@usx95 usx95 force-pushed the users/usx95/lifetime-safety-liveness branch from 0c787ef to c217d0d Compare July 14, 2025 19:38
@usx95 usx95 changed the title [LifetimeSafety] Implement generic dataflow framework [LifetimeSafety] Make the dataflow analysis generic Jul 14, 2025
@usx95 usx95 force-pushed the users/usx95/lifetime-safety-liveness branch 2 times, most recently from 702d255 to 7e098b0 Compare July 14, 2025 19:52
@usx95 usx95 self-assigned this Jul 14, 2025
@usx95 usx95 marked this pull request as ready for review July 14, 2025 20:00
@llvmbot llvmbot added clang Clang issues not falling into any other category clang:analysis labels Jul 14, 2025
@llvmbot
Copy link
Member

llvmbot commented Jul 14, 2025

@llvm/pr-subscribers-clang

Author: Utkarsh Saxena (usx95)

Changes

Refactored the lifetime safety analysis to use a generic dataflow framework with a policy-based design.

Changes

  • Introduced a generic DataflowAnalysis template class that can be specialized for different analyses
  • Renamed LifetimeLattice to LoanPropagationLattice to better reflect its purpose
  • Created a LoanPropagationAnalysis class that inherits from the generic framework
  • Moved transfer functions from the standalone Transferer class into the analysis class
  • Restructured the code to separate the dataflow engine from the specific analysis logic
  • Updated debug output and test expectations to use the new class names

Motivation

In order to add more analyses, e.g. loan expiry and origin liveness, the previous implementation would have separate, nearly identical dataflow runners for each analysis. This change creates a single, reusable component, which will make it much simpler to add subsequent analyses without repeating boilerplate code.

This is quite close to the existing dataflow framework!


Patch is 22.61 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/148222.diff

2 Files Affected:

  • (modified) clang/lib/Analysis/LifetimeSafety.cpp (+188-167)
  • (modified) clang/test/Sema/warn-lifetime-safety-dataflow.cpp (+15-15)
diff --git a/clang/lib/Analysis/LifetimeSafety.cpp b/clang/lib/Analysis/LifetimeSafety.cpp
index bf67bea6c9933..99bd7bc36faf5 100644
--- a/clang/lib/Analysis/LifetimeSafety.cpp
+++ b/clang/lib/Analysis/LifetimeSafety.cpp
@@ -496,7 +496,130 @@ class FactGenerator : public ConstStmtVisitor<FactGenerator> {
 };
 
 // ========================================================================= //
-//                              The Dataflow Lattice
+//                         Generic Dataflow Analysis
+// ========================================================================= //
+/// A generic, policy-based driver for forward dataflow analyses. It combines
+/// the dataflow runner and the transferer logic into a single class hierarchy.
+///
+/// The derived class is expected to provide:
+/// - A `Lattice` type.
+/// - `const char *getAnalysisName() const`
+/// - `Lattice getInitialState();` The initial state at the function entry.
+/// - `Lattice join(Lattice, Lattice);` Merges states from multiple CFG paths.
+/// - `Lattice transfer(Lattice, const FactType&);` Defines how a single
+///   lifetime-relevant `Fact` transforms the lattice state. Only overloads
+///   for facts relevant to the analysis need to be implemented.
+///
+/// \tparam Derived The CRTP derived class that implements the specific
+/// analysis.
+/// \tparam LatticeType The lattice type used by the analysis.
+/// TODO: Maybe use the dataflow framework! The framework might need changes
+/// to support the current comparison done at block-entry.
+template <typename Derived, typename LatticeType> class DataflowAnalysis {
+public:
+  using Lattice = LatticeType;
+
+private:
+  const CFG &Cfg;
+  AnalysisDeclContext &AC;
+
+  llvm::DenseMap<const CFGBlock *, Lattice> BlockEntryStates;
+  llvm::DenseMap<const CFGBlock *, Lattice> BlockExitStates;
+
+protected:
+  FactManager &AllFacts;
+
+  explicit DataflowAnalysis(const CFG &C, AnalysisDeclContext &AC,
+                            FactManager &F)
+      : Cfg(C), AC(AC), AllFacts(F) {}
+
+public:
+  void run() {
+    Derived &D = static_cast<Derived &>(*this);
+    llvm::TimeTraceScope Time(D.getAnalysisName());
+
+    ForwardDataflowWorklist Worklist(Cfg, AC);
+    const CFGBlock *Entry = &Cfg.getEntry();
+    BlockEntryStates[Entry] = D.getInitialState();
+    Worklist.enqueueBlock(Entry);
+
+    while (const CFGBlock *B = Worklist.dequeue()) {
+      Lattice EntryState = getEntryState(B);
+      Lattice ExitState = transferBlock(B, EntryState);
+      BlockExitStates[B] = ExitState;
+
+      for (const CFGBlock *Successor : B->succs()) {
+        auto SuccIt = BlockEntryStates.find(Successor);
+        Lattice OldSuccEntryState = (SuccIt != BlockEntryStates.end())
+                                        ? SuccIt->second
+                                        : D.getInitialState();
+        Lattice NewSuccEntryState = D.join(OldSuccEntryState, ExitState);
+        // Enqueue the successor if its entry state has changed.
+        // TODO(opt): Consider changing 'join' to report a change if !=
+        // comparison is found expensive.
+        if (SuccIt == BlockEntryStates.end() ||
+            NewSuccEntryState != OldSuccEntryState) {
+          BlockEntryStates[Successor] = NewSuccEntryState;
+          Worklist.enqueueBlock(Successor);
+        }
+      }
+    }
+  }
+
+  Lattice getEntryState(const CFGBlock *B) const {
+    return BlockEntryStates.lookup(B);
+  }
+
+  Lattice getExitState(const CFGBlock *B) const {
+    return BlockExitStates.lookup(B);
+  }
+
+  void dump() const {
+    const Derived *D = static_cast<const Derived *>(this);
+    llvm::dbgs() << "==========================================\n";
+    llvm::dbgs() << "   " << D->getAnalysisName() << " results:\n";
+    llvm::dbgs() << "==========================================\n";
+    const CFGBlock &B = Cfg.getExit();
+    getExitState(&B).dump(llvm::dbgs());
+  }
+
+private:
+  /// Computes the exit state of a block by applying all its facts sequentially
+  /// to a given entry state.
+  /// TODO: We might need to store intermediate states per-fact in the block for
+  /// later analysis.
+  Lattice transferBlock(const CFGBlock *Block, Lattice EntryState) {
+    Lattice BlockState = EntryState;
+    for (const Fact *F : AllFacts.getFacts(Block)) {
+      BlockState = transferFact(BlockState, F);
+    }
+    return BlockState;
+  }
+
+  Lattice transferFact(Lattice In, const Fact *F) {
+    Derived *d = static_cast<Derived *>(this);
+    switch (F->getKind()) {
+    case Fact::Kind::Issue:
+      return d->transfer(In, *F->getAs<IssueFact>());
+    case Fact::Kind::Expire:
+      return d->transfer(In, *F->getAs<ExpireFact>());
+    case Fact::Kind::AssignOrigin:
+      return d->transfer(In, *F->getAs<AssignOriginFact>());
+    case Fact::Kind::ReturnOfOrigin:
+      return d->transfer(In, *F->getAs<ReturnOfOriginFact>());
+    }
+    llvm_unreachable("Unknown fact kind");
+  }
+
+public:
+  Lattice transfer(Lattice In, const IssueFact &) { return In; }
+  Lattice transfer(Lattice In, const ExpireFact &) { return In; }
+  Lattice transfer(Lattice In, const AssignOriginFact &) { return In; }
+  Lattice transfer(Lattice In, const ReturnOfOriginFact &) { return In; }
+};
+
+// ========================================================================= //
+//                          Loan Propagation Analysis
 // ========================================================================= //
 
 // Using LLVM's immutable collections is efficient for dataflow analysis
@@ -517,10 +640,10 @@ struct LifetimeFactory {
   }
 };
 
-/// LifetimeLattice represents the state of our analysis at a given program
-/// point. It is an immutable object, and all operations produce a new
+/// LoanPropagationLattice represents the state of our analysis at a given
+/// program point. It is an immutable object, and all operations produce a new
 /// instance rather than modifying the existing one.
-struct LifetimeLattice {
+struct LoanPropagationLattice {
   /// The map from an origin to the set of loans it contains.
   /// The lattice has a finite height: An origin's loan set is bounded by the
   /// total number of loans in the function.
@@ -528,61 +651,16 @@ struct LifetimeLattice {
   /// not expressions, because expressions are not visible across blocks.
   OriginLoanMap Origins = OriginLoanMap(nullptr);
 
-  explicit LifetimeLattice(const OriginLoanMap &S) : Origins(S) {}
-  LifetimeLattice() = default;
+  explicit LoanPropagationLattice(const OriginLoanMap &S) : Origins(S) {}
+  LoanPropagationLattice() = default;
 
-  bool operator==(const LifetimeLattice &Other) const {
+  bool operator==(const LoanPropagationLattice &Other) const {
     return Origins == Other.Origins;
   }
-  bool operator!=(const LifetimeLattice &Other) const {
+  bool operator!=(const LoanPropagationLattice &Other) const {
     return !(*this == Other);
   }
 
-  LoanSet getLoans(OriginID OID) const {
-    if (auto *Loans = Origins.lookup(OID))
-      return *Loans;
-    return LoanSet(nullptr);
-  }
-
-  /// Computes the union of two lattices by performing a key-wise join of
-  /// their OriginLoanMaps.
-  // TODO(opt): This key-wise join is a performance bottleneck. A more
-  // efficient merge could be implemented using a Patricia Trie or HAMT
-  // instead of the current AVL-tree-based ImmutableMap.
-  // TODO(opt): Keep the state small by removing origins which become dead.
-  LifetimeLattice join(const LifetimeLattice &Other,
-                       LifetimeFactory &Factory) const {
-    /// Merge the smaller map into the larger one ensuring we iterate over the
-    /// smaller map.
-    if (Origins.getHeight() < Other.Origins.getHeight())
-      return Other.join(*this, Factory);
-
-    OriginLoanMap JoinedState = Origins;
-    // For each origin in the other map, union its loan set with ours.
-    for (const auto &Entry : Other.Origins) {
-      OriginID OID = Entry.first;
-      LoanSet OtherLoanSet = Entry.second;
-      JoinedState = Factory.OriginMapFactory.add(
-          JoinedState, OID, join(getLoans(OID), OtherLoanSet, Factory));
-    }
-    return LifetimeLattice(JoinedState);
-  }
-
-  LoanSet join(LoanSet a, LoanSet b, LifetimeFactory &Factory) const {
-    /// Merge the smaller set into the larger one ensuring we iterate over the
-    /// smaller set.
-    if (a.getHeight() < b.getHeight())
-      std::swap(a, b);
-    LoanSet Result = a;
-    for (LoanID LID : b) {
-      /// TODO(opt): Profiling shows that this loop is a major performance
-      /// bottleneck. Investigate using a BitVector to represent the set of
-      /// loans for improved join performance.
-      Result = Factory.LoanSetFact.add(Result, LID);
-    }
-    return Result;
-  }
-
   void dump(llvm::raw_ostream &OS) const {
     OS << "LifetimeLattice State:\n";
     if (Origins.isEmpty())
@@ -596,143 +674,85 @@ struct LifetimeLattice {
   }
 };
 
-// ========================================================================= //
-//                              The Transfer Function
-// ========================================================================= //
-class Transferer {
-  FactManager &AllFacts;
+class LoanPropagationAnalysis
+    : public DataflowAnalysis<LoanPropagationAnalysis, LoanPropagationLattice> {
+
   LifetimeFactory &Factory;
 
 public:
-  explicit Transferer(FactManager &F, LifetimeFactory &Factory)
-      : AllFacts(F), Factory(Factory) {}
+  LoanPropagationAnalysis(const CFG &C, AnalysisDeclContext &AC, FactManager &F,
+                          LifetimeFactory &Factory)
+      : DataflowAnalysis(C, AC, F), Factory(Factory) {}
 
-  /// Computes the exit state of a block by applying all its facts sequentially
-  /// to a given entry state.
-  /// TODO: We might need to store intermediate states per-fact in the block for
-  /// later analysis.
-  LifetimeLattice transferBlock(const CFGBlock *Block,
-                                LifetimeLattice EntryState) {
-    LifetimeLattice BlockState = EntryState;
-    llvm::ArrayRef<const Fact *> Facts = AllFacts.getFacts(Block);
+  using DataflowAnalysis<LoanPropagationAnalysis, Lattice>::transfer;
 
-    for (const Fact *F : Facts) {
-      BlockState = transferFact(BlockState, F);
+  const char *getAnalysisName() const { return "LoanPropagation"; }
+
+  Lattice getInitialState() { return Lattice{}; }
+
+  /// Computes the union of two lattices by performing a key-wise join of
+  /// their OriginLoanMaps.
+  // TODO(opt): This key-wise join is a performance bottleneck. A more
+  // efficient merge could be implemented using a Patricia Trie or HAMT
+  // instead of the current AVL-tree-based ImmutableMap.
+  // TODO(opt): Keep the state small by removing origins which become dead.
+  Lattice join(Lattice A, Lattice B) {
+    /// Merge the smaller map into the larger one ensuring we iterate over the
+    /// smaller map.
+    if (A.Origins.getHeight() < B.Origins.getHeight())
+      std::swap(A, B);
+
+    OriginLoanMap JoinedState = A.Origins;
+    // For each origin in the other map, union its loan set with ours.
+    for (const auto &Entry : B.Origins) {
+      OriginID OID = Entry.first;
+      LoanSet OtherLoanSet = Entry.second;
+      JoinedState = Factory.OriginMapFactory.add(
+          JoinedState, OID, join(getLoans(A, OID), OtherLoanSet));
     }
-    return BlockState;
+    return Lattice(JoinedState);
   }
 
-private:
-  LifetimeLattice transferFact(LifetimeLattice In, const Fact *F) {
-    switch (F->getKind()) {
-    case Fact::Kind::Issue:
-      return transfer(In, *F->getAs<IssueFact>());
-    case Fact::Kind::AssignOrigin:
-      return transfer(In, *F->getAs<AssignOriginFact>());
-    // Expire and ReturnOfOrigin facts don't modify the Origins and the State.
-    case Fact::Kind::Expire:
-    case Fact::Kind::ReturnOfOrigin:
-      return In;
-    }
-    llvm_unreachable("Unknown fact kind");
+  LoanSet join(LoanSet A, LoanSet B) {
+    if (A.getHeight() < B.getHeight())
+      std::swap(A, B);
+    for (LoanID L : B)
+      A = Factory.LoanSetFact.add(A, L);
+    return A;
   }
 
   /// A new loan is issued to the origin. Old loans are erased.
-  LifetimeLattice transfer(LifetimeLattice In, const IssueFact &F) {
+  Lattice transfer(Lattice In, const IssueFact &F) {
     OriginID OID = F.getOriginID();
     LoanID LID = F.getLoanID();
-    return LifetimeLattice(Factory.OriginMapFactory.add(
+    return LoanPropagationLattice(Factory.OriginMapFactory.add(
         In.Origins, OID, Factory.createLoanSet(LID)));
   }
 
   /// The destination origin's loan set is replaced by the source's.
   /// This implicitly "resets" the old loans of the destination.
-  LifetimeLattice transfer(LifetimeLattice InState, const AssignOriginFact &F) {
+  Lattice transfer(Lattice In, const AssignOriginFact &F) {
     OriginID DestOID = F.getDestOriginID();
     OriginID SrcOID = F.getSrcOriginID();
-    LoanSet SrcLoans = InState.getLoans(SrcOID);
-    return LifetimeLattice(
-        Factory.OriginMapFactory.add(InState.Origins, DestOID, SrcLoans));
+    LoanSet SrcLoans = getLoans(In, SrcOID);
+    return LoanPropagationLattice(
+        Factory.OriginMapFactory.add(In.Origins, DestOID, SrcLoans));
   }
-};
 
-// ========================================================================= //
-//                              Dataflow analysis
-// ========================================================================= //
-
-/// Drives the intra-procedural dataflow analysis.
-///
-/// Orchestrates the analysis by iterating over the CFG using a worklist
-/// algorithm. It computes a fixed point by propagating the LifetimeLattice
-/// state through each block until the state no longer changes.
-/// TODO: Maybe use the dataflow framework! The framework might need changes
-/// to support the current comparison done at block-entry.
-class LifetimeDataflow {
-  const CFG &Cfg;
-  AnalysisDeclContext &AC;
-  LifetimeFactory LifetimeFact;
-
-  Transferer Xfer;
-
-  /// Stores the merged analysis state at the entry of each CFG block.
-  llvm::DenseMap<const CFGBlock *, LifetimeLattice> BlockEntryStates;
-  /// Stores the analysis state at the exit of each CFG block, after the
-  /// transfer function has been applied.
-  llvm::DenseMap<const CFGBlock *, LifetimeLattice> BlockExitStates;
-
-public:
-  LifetimeDataflow(const CFG &C, FactManager &FS, AnalysisDeclContext &AC)
-      : Cfg(C), AC(AC), Xfer(FS, LifetimeFact) {}
-
-  void run() {
-    llvm::TimeTraceScope TimeProfile("Lifetime Dataflow");
-    ForwardDataflowWorklist Worklist(Cfg, AC);
-    const CFGBlock *Entry = &Cfg.getEntry();
-    BlockEntryStates[Entry] = LifetimeLattice{};
-    Worklist.enqueueBlock(Entry);
-    while (const CFGBlock *B = Worklist.dequeue()) {
-      LifetimeLattice EntryState = getEntryState(B);
-      LifetimeLattice ExitState = Xfer.transferBlock(B, EntryState);
-      BlockExitStates[B] = ExitState;
-
-      for (const CFGBlock *Successor : B->succs()) {
-        auto SuccIt = BlockEntryStates.find(Successor);
-        LifetimeLattice OldSuccEntryState = (SuccIt != BlockEntryStates.end())
-                                                ? SuccIt->second
-                                                : LifetimeLattice{};
-        LifetimeLattice NewSuccEntryState =
-            OldSuccEntryState.join(ExitState, LifetimeFact);
-        // Enqueue the successor if its entry state has changed.
-        // TODO(opt): Consider changing 'join' to report a change if !=
-        // comparison is found expensive.
-        if (SuccIt == BlockEntryStates.end() ||
-            NewSuccEntryState != OldSuccEntryState) {
-          BlockEntryStates[Successor] = NewSuccEntryState;
-          Worklist.enqueueBlock(Successor);
-        }
-      }
-    }
-  }
-
-  void dump() const {
-    llvm::dbgs() << "==========================================\n";
-    llvm::dbgs() << "       Dataflow results:\n";
-    llvm::dbgs() << "==========================================\n";
-    const CFGBlock &B = Cfg.getExit();
-    getExitState(&B).dump(llvm::dbgs());
-  }
-
-  LifetimeLattice getEntryState(const CFGBlock *B) const {
-    return BlockEntryStates.lookup(B);
-  }
-
-  LifetimeLattice getExitState(const CFGBlock *B) const {
-    return BlockExitStates.lookup(B);
+private:
+  LoanSet getLoans(Lattice L, OriginID OID) {
+    if (auto *Loans = L.Origins.lookup(OID))
+      return *Loans;
+    return Factory.LoanSetFact.getEmptySet();
   }
 };
 
 // ========================================================================= //
-//  TODO: Analysing dataflow results and error reporting.
+//  TODO: 
+// - Modifying loan propagation to answer `LoanSet getLoans(Origin O, Point P)`
+// - Adding loan expiry analysis to answer `bool isExpired(Loan L, Point P)`
+// - Adding origin liveness analysis to answer `bool isLive(Origin O, Point P)`
+// - Using the above three to perform the final error reporting.
 // ========================================================================= //
 } // anonymous namespace
 
@@ -755,8 +775,9 @@ void runLifetimeSafetyAnalysis(const DeclContext &DC, const CFG &Cfg,
   ///    blocks; only Decls are visible.  Therefore, loans in a block that
   ///    never reach an Origin associated with a Decl can be safely dropped by
   ///    the analysis.
-  LifetimeDataflow Dataflow(Cfg, FactMgr, AC);
-  Dataflow.run();
-  DEBUG_WITH_TYPE("LifetimeDataflow", Dataflow.dump());
+  LifetimeFactory LifetimeFact;
+  LoanPropagationAnalysis LoanPropagation(Cfg, AC, FactMgr, LifetimeFact);
+  LoanPropagation.run();
+  DEBUG_WITH_TYPE("LifetimeLoanPropagation", LoanPropagation.dump());
 }
 } // namespace clang
diff --git a/clang/test/Sema/warn-lifetime-safety-dataflow.cpp b/clang/test/Sema/warn-lifetime-safety-dataflow.cpp
index 38dfdb98f08fc..0e98904ade86a 100644
--- a/clang/test/Sema/warn-lifetime-safety-dataflow.cpp
+++ b/clang/test/Sema/warn-lifetime-safety-dataflow.cpp
@@ -1,4 +1,4 @@
-// RUN: %clang_cc1 -mllvm -debug-only=LifetimeFacts,LifetimeDataflow -Wexperimental-lifetime-safety %s 2>&1 | FileCheck %s
+// RUN: %clang_cc1 -mllvm -debug-only=LifetimeFacts,LifetimeLoanPropagation -Wexperimental-lifetime-safety %s 2>&1 | FileCheck %s
 // REQUIRES: asserts
 
 struct MyObj {
@@ -19,7 +19,7 @@ MyObj* return_local_addr() {
 // CHECK:   ReturnOfOrigin (OriginID: [[O_RET_VAL]])
 // CHECK:   Expire (LoanID: [[L_X]])
 }
-// CHECK: Dataflow results:
+// CHECK: LoanPropagation results:
 // CHECK-DAG: Origin [[O_ADDR_X]] contains Loan [[L_X]]
 // CHECK-DAG: Origin [[O_P]] contains Loan [[L_X]]
 // CHECK-DAG: Origin [[O_RET_VAL]] contains Loan [[L_X]]
@@ -47,7 +47,7 @@ MyObj* assign_and_return_local_addr() {
 // CHECK: ReturnOfOrigin (OriginID: [[O_PTR2_RVAL_2]])
 // CHECK: Expire (LoanID: [[L_Y]])
 }
-// CHECK: Dataflow results:
+// CHECK: LoanPropagation results:
 // CHECK-DAG: Origin [[O_ADDR_Y]] contains Loan [[L_Y]]
 // CHECK-DAG: Origin [[O_PTR1]] contains Loan [[L_Y]]
 // CHECK-DAG: Origin [[O_PTR2]] contains Loan [[L_Y]]
@@ -65,7 +65,7 @@ int return_int_val() {
   return x;
 }
 // CHECK-NEXT: End of Block
-// CHECK: Dataflow results:
+// CHECK: LoanPropagation results:
 // CHECK:  <empty>
 
 
@@ -79,7 +79,7 @@ void loan_expires_cpp() {
 // CHECK: AssignOrigin (DestID: [[O_POBJ:[0-9]+]], SrcID: [[O_ADDR_OBJ]])
 // CHECK: Expire (LoanID: [[L_OBJ]])
 }
-// CHECK: Dataflow results:
+// CHECK: LoanPropagation results:
 // CHECK-DAG: Origin [[O_ADDR_OBJ]] contains Loan [[L_OBJ]]
 // CHECK-DAG: Origin [[O_POBJ]] contains Loan [[L_OBJ]]
 
@@ -96,7 +96,7 @@ void loan_expires_trivial() {
 // CHECK-NEXT: End of Block
   // FIXME: Add check for Expire once trivial destructors are handled for expiration.
 }
-// CHECK: Dataflow results:
+// CHECK: LoanPropagation results:
 // CHECK-DAG: Origin [[O_ADDR_TRIVIAL_OBJ]] contains Loan [[L_TRIVIAL_OBJ]]
 // CHECK-DAG: Origin [[O_PTOBJ]] contains Loan [[L_TRIVIAL_OBJ]]
 
@@ -119,7 +119,7 @@ void conditional(bool condition) {
   // CHECK: AssignOrigin (DestID: [[O_P_RVAL:[0-9]+]], SrcID: [[O_P]])
   // CHECK: AssignOrigin (DestID: [[O_Q:[0-9]+]], SrcID: [[O_P_RVAL]])
 }
-// CHECK: Dataflow results:
+// CHECK: LoanPropagation results:
 // CHECK-DAG: Origin [[O_ADDR_A]] contains Loan [[L_A]]
 // CHECK-DAG: Origin [[O_ADDR_B]] contains Loan [[L_B]]
 // CHECK-DAG: Origin [[O_P]] contains Loan [[L_A]]
@@ -163,7 +163,7 @@ void pointers_in_a_cycle(bool condition) {
 }
 // At the end of the analysis, the origins for the po...
[truncated]

@llvmbot
Copy link
Member

llvmbot commented Jul 14, 2025

@llvm/pr-subscribers-clang-analysis

Author: Utkarsh Saxena (usx95)

Changes

Refactored the lifetime safety analysis to use a generic dataflow framework with a policy-based design.

Changes

  • Introduced a generic DataflowAnalysis template class that can be specialized for different analyses
  • Renamed LifetimeLattice to LoanPropagationLattice to better reflect its purpose
  • Created a LoanPropagationAnalysis class that inherits from the generic framework
  • Moved transfer functions from the standalone Transferer class into the analysis class
  • Restructured the code to separate the dataflow engine from the specific analysis logic
  • Updated debug output and test expectations to use the new class names

Motivation

In order to add more analyses, e.g. loan expiry and origin liveness, the previous implementation would have separate, nearly identical dataflow runners for each analysis. This change creates a single, reusable component, which will make it much simpler to add subsequent analyses without repeating boilerplate code.

This is quite close to the existing dataflow framework!


Patch is 22.61 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/148222.diff

2 Files Affected:

  • (modified) clang/lib/Analysis/LifetimeSafety.cpp (+188-167)
  • (modified) clang/test/Sema/warn-lifetime-safety-dataflow.cpp (+15-15)
diff --git a/clang/lib/Analysis/LifetimeSafety.cpp b/clang/lib/Analysis/LifetimeSafety.cpp
index bf67bea6c9933..99bd7bc36faf5 100644
--- a/clang/lib/Analysis/LifetimeSafety.cpp
+++ b/clang/lib/Analysis/LifetimeSafety.cpp
@@ -496,7 +496,130 @@ class FactGenerator : public ConstStmtVisitor<FactGenerator> {
 };
 
 // ========================================================================= //
-//                              The Dataflow Lattice
+//                         Generic Dataflow Analysis
+// ========================================================================= //
+/// A generic, policy-based driver for forward dataflow analyses. It combines
+/// the dataflow runner and the transferer logic into a single class hierarchy.
+///
+/// The derived class is expected to provide:
+/// - A `Lattice` type.
+/// - `const char *getAnalysisName() const`
+/// - `Lattice getInitialState();` The initial state at the function entry.
+/// - `Lattice join(Lattice, Lattice);` Merges states from multiple CFG paths.
+/// - `Lattice transfer(Lattice, const FactType&);` Defines how a single
+///   lifetime-relevant `Fact` transforms the lattice state. Only overloads
+///   for facts relevant to the analysis need to be implemented.
+///
+/// \tparam Derived The CRTP derived class that implements the specific
+/// analysis.
+/// \tparam LatticeType The lattice type used by the analysis.
+/// TODO: Maybe use the dataflow framework! The framework might need changes
+/// to support the current comparison done at block-entry.
+template <typename Derived, typename LatticeType> class DataflowAnalysis {
+public:
+  using Lattice = LatticeType;
+
+private:
+  const CFG &Cfg;
+  AnalysisDeclContext &AC;
+
+  llvm::DenseMap<const CFGBlock *, Lattice> BlockEntryStates;
+  llvm::DenseMap<const CFGBlock *, Lattice> BlockExitStates;
+
+protected:
+  FactManager &AllFacts;
+
+  explicit DataflowAnalysis(const CFG &C, AnalysisDeclContext &AC,
+                            FactManager &F)
+      : Cfg(C), AC(AC), AllFacts(F) {}
+
+public:
+  void run() {
+    Derived &D = static_cast<Derived &>(*this);
+    llvm::TimeTraceScope Time(D.getAnalysisName());
+
+    ForwardDataflowWorklist Worklist(Cfg, AC);
+    const CFGBlock *Entry = &Cfg.getEntry();
+    BlockEntryStates[Entry] = D.getInitialState();
+    Worklist.enqueueBlock(Entry);
+
+    while (const CFGBlock *B = Worklist.dequeue()) {
+      Lattice EntryState = getEntryState(B);
+      Lattice ExitState = transferBlock(B, EntryState);
+      BlockExitStates[B] = ExitState;
+
+      for (const CFGBlock *Successor : B->succs()) {
+        auto SuccIt = BlockEntryStates.find(Successor);
+        Lattice OldSuccEntryState = (SuccIt != BlockEntryStates.end())
+                                        ? SuccIt->second
+                                        : D.getInitialState();
+        Lattice NewSuccEntryState = D.join(OldSuccEntryState, ExitState);
+        // Enqueue the successor if its entry state has changed.
+        // TODO(opt): Consider changing 'join' to report a change if !=
+        // comparison is found expensive.
+        if (SuccIt == BlockEntryStates.end() ||
+            NewSuccEntryState != OldSuccEntryState) {
+          BlockEntryStates[Successor] = NewSuccEntryState;
+          Worklist.enqueueBlock(Successor);
+        }
+      }
+    }
+  }
+
+  Lattice getEntryState(const CFGBlock *B) const {
+    return BlockEntryStates.lookup(B);
+  }
+
+  Lattice getExitState(const CFGBlock *B) const {
+    return BlockExitStates.lookup(B);
+  }
+
+  void dump() const {
+    const Derived *D = static_cast<const Derived *>(this);
+    llvm::dbgs() << "==========================================\n";
+    llvm::dbgs() << "   " << D->getAnalysisName() << " results:\n";
+    llvm::dbgs() << "==========================================\n";
+    const CFGBlock &B = Cfg.getExit();
+    getExitState(&B).dump(llvm::dbgs());
+  }
+
+private:
+  /// Computes the exit state of a block by applying all its facts sequentially
+  /// to a given entry state.
+  /// TODO: We might need to store intermediate states per-fact in the block for
+  /// later analysis.
+  Lattice transferBlock(const CFGBlock *Block, Lattice EntryState) {
+    Lattice BlockState = EntryState;
+    for (const Fact *F : AllFacts.getFacts(Block)) {
+      BlockState = transferFact(BlockState, F);
+    }
+    return BlockState;
+  }
+
+  Lattice transferFact(Lattice In, const Fact *F) {
+    Derived *d = static_cast<Derived *>(this);
+    switch (F->getKind()) {
+    case Fact::Kind::Issue:
+      return d->transfer(In, *F->getAs<IssueFact>());
+    case Fact::Kind::Expire:
+      return d->transfer(In, *F->getAs<ExpireFact>());
+    case Fact::Kind::AssignOrigin:
+      return d->transfer(In, *F->getAs<AssignOriginFact>());
+    case Fact::Kind::ReturnOfOrigin:
+      return d->transfer(In, *F->getAs<ReturnOfOriginFact>());
+    }
+    llvm_unreachable("Unknown fact kind");
+  }
+
+public:
+  Lattice transfer(Lattice In, const IssueFact &) { return In; }
+  Lattice transfer(Lattice In, const ExpireFact &) { return In; }
+  Lattice transfer(Lattice In, const AssignOriginFact &) { return In; }
+  Lattice transfer(Lattice In, const ReturnOfOriginFact &) { return In; }
+};
+
+// ========================================================================= //
+//                          Loan Propagation Analysis
 // ========================================================================= //
 
 // Using LLVM's immutable collections is efficient for dataflow analysis
@@ -517,10 +640,10 @@ struct LifetimeFactory {
   }
 };
 
-/// LifetimeLattice represents the state of our analysis at a given program
-/// point. It is an immutable object, and all operations produce a new
+/// LoanPropagationLattice represents the state of our analysis at a given
+/// program point. It is an immutable object, and all operations produce a new
 /// instance rather than modifying the existing one.
-struct LifetimeLattice {
+struct LoanPropagationLattice {
   /// The map from an origin to the set of loans it contains.
   /// The lattice has a finite height: An origin's loan set is bounded by the
   /// total number of loans in the function.
@@ -528,61 +651,16 @@ struct LifetimeLattice {
   /// not expressions, because expressions are not visible across blocks.
   OriginLoanMap Origins = OriginLoanMap(nullptr);
 
-  explicit LifetimeLattice(const OriginLoanMap &S) : Origins(S) {}
-  LifetimeLattice() = default;
+  explicit LoanPropagationLattice(const OriginLoanMap &S) : Origins(S) {}
+  LoanPropagationLattice() = default;
 
-  bool operator==(const LifetimeLattice &Other) const {
+  bool operator==(const LoanPropagationLattice &Other) const {
     return Origins == Other.Origins;
   }
-  bool operator!=(const LifetimeLattice &Other) const {
+  bool operator!=(const LoanPropagationLattice &Other) const {
     return !(*this == Other);
   }
 
-  LoanSet getLoans(OriginID OID) const {
-    if (auto *Loans = Origins.lookup(OID))
-      return *Loans;
-    return LoanSet(nullptr);
-  }
-
-  /// Computes the union of two lattices by performing a key-wise join of
-  /// their OriginLoanMaps.
-  // TODO(opt): This key-wise join is a performance bottleneck. A more
-  // efficient merge could be implemented using a Patricia Trie or HAMT
-  // instead of the current AVL-tree-based ImmutableMap.
-  // TODO(opt): Keep the state small by removing origins which become dead.
-  LifetimeLattice join(const LifetimeLattice &Other,
-                       LifetimeFactory &Factory) const {
-    /// Merge the smaller map into the larger one ensuring we iterate over the
-    /// smaller map.
-    if (Origins.getHeight() < Other.Origins.getHeight())
-      return Other.join(*this, Factory);
-
-    OriginLoanMap JoinedState = Origins;
-    // For each origin in the other map, union its loan set with ours.
-    for (const auto &Entry : Other.Origins) {
-      OriginID OID = Entry.first;
-      LoanSet OtherLoanSet = Entry.second;
-      JoinedState = Factory.OriginMapFactory.add(
-          JoinedState, OID, join(getLoans(OID), OtherLoanSet, Factory));
-    }
-    return LifetimeLattice(JoinedState);
-  }
-
-  LoanSet join(LoanSet a, LoanSet b, LifetimeFactory &Factory) const {
-    /// Merge the smaller set into the larger one ensuring we iterate over the
-    /// smaller set.
-    if (a.getHeight() < b.getHeight())
-      std::swap(a, b);
-    LoanSet Result = a;
-    for (LoanID LID : b) {
-      /// TODO(opt): Profiling shows that this loop is a major performance
-      /// bottleneck. Investigate using a BitVector to represent the set of
-      /// loans for improved join performance.
-      Result = Factory.LoanSetFact.add(Result, LID);
-    }
-    return Result;
-  }
-
   void dump(llvm::raw_ostream &OS) const {
     OS << "LifetimeLattice State:\n";
     if (Origins.isEmpty())
@@ -596,143 +674,85 @@ struct LifetimeLattice {
   }
 };
 
-// ========================================================================= //
-//                              The Transfer Function
-// ========================================================================= //
-class Transferer {
-  FactManager &AllFacts;
+class LoanPropagationAnalysis
+    : public DataflowAnalysis<LoanPropagationAnalysis, LoanPropagationLattice> {
+
   LifetimeFactory &Factory;
 
 public:
-  explicit Transferer(FactManager &F, LifetimeFactory &Factory)
-      : AllFacts(F), Factory(Factory) {}
+  LoanPropagationAnalysis(const CFG &C, AnalysisDeclContext &AC, FactManager &F,
+                          LifetimeFactory &Factory)
+      : DataflowAnalysis(C, AC, F), Factory(Factory) {}
 
-  /// Computes the exit state of a block by applying all its facts sequentially
-  /// to a given entry state.
-  /// TODO: We might need to store intermediate states per-fact in the block for
-  /// later analysis.
-  LifetimeLattice transferBlock(const CFGBlock *Block,
-                                LifetimeLattice EntryState) {
-    LifetimeLattice BlockState = EntryState;
-    llvm::ArrayRef<const Fact *> Facts = AllFacts.getFacts(Block);
+  using DataflowAnalysis<LoanPropagationAnalysis, Lattice>::transfer;
 
-    for (const Fact *F : Facts) {
-      BlockState = transferFact(BlockState, F);
+  const char *getAnalysisName() const { return "LoanPropagation"; }
+
+  Lattice getInitialState() { return Lattice{}; }
+
+  /// Computes the union of two lattices by performing a key-wise join of
+  /// their OriginLoanMaps.
+  // TODO(opt): This key-wise join is a performance bottleneck. A more
+  // efficient merge could be implemented using a Patricia Trie or HAMT
+  // instead of the current AVL-tree-based ImmutableMap.
+  // TODO(opt): Keep the state small by removing origins which become dead.
+  Lattice join(Lattice A, Lattice B) {
+    /// Merge the smaller map into the larger one ensuring we iterate over the
+    /// smaller map.
+    if (A.Origins.getHeight() < B.Origins.getHeight())
+      std::swap(A, B);
+
+    OriginLoanMap JoinedState = A.Origins;
+    // For each origin in the other map, union its loan set with ours.
+    for (const auto &Entry : B.Origins) {
+      OriginID OID = Entry.first;
+      LoanSet OtherLoanSet = Entry.second;
+      JoinedState = Factory.OriginMapFactory.add(
+          JoinedState, OID, join(getLoans(A, OID), OtherLoanSet));
     }
-    return BlockState;
+    return Lattice(JoinedState);
   }
 
-private:
-  LifetimeLattice transferFact(LifetimeLattice In, const Fact *F) {
-    switch (F->getKind()) {
-    case Fact::Kind::Issue:
-      return transfer(In, *F->getAs<IssueFact>());
-    case Fact::Kind::AssignOrigin:
-      return transfer(In, *F->getAs<AssignOriginFact>());
-    // Expire and ReturnOfOrigin facts don't modify the Origins and the State.
-    case Fact::Kind::Expire:
-    case Fact::Kind::ReturnOfOrigin:
-      return In;
-    }
-    llvm_unreachable("Unknown fact kind");
+  LoanSet join(LoanSet A, LoanSet B) {
+    if (A.getHeight() < B.getHeight())
+      std::swap(A, B);
+    for (LoanID L : B)
+      A = Factory.LoanSetFact.add(A, L);
+    return A;
   }
 
   /// A new loan is issued to the origin. Old loans are erased.
-  LifetimeLattice transfer(LifetimeLattice In, const IssueFact &F) {
+  Lattice transfer(Lattice In, const IssueFact &F) {
     OriginID OID = F.getOriginID();
     LoanID LID = F.getLoanID();
-    return LifetimeLattice(Factory.OriginMapFactory.add(
+    return LoanPropagationLattice(Factory.OriginMapFactory.add(
         In.Origins, OID, Factory.createLoanSet(LID)));
   }
 
   /// The destination origin's loan set is replaced by the source's.
   /// This implicitly "resets" the old loans of the destination.
-  LifetimeLattice transfer(LifetimeLattice InState, const AssignOriginFact &F) {
+  Lattice transfer(Lattice In, const AssignOriginFact &F) {
     OriginID DestOID = F.getDestOriginID();
     OriginID SrcOID = F.getSrcOriginID();
-    LoanSet SrcLoans = InState.getLoans(SrcOID);
-    return LifetimeLattice(
-        Factory.OriginMapFactory.add(InState.Origins, DestOID, SrcLoans));
+    LoanSet SrcLoans = getLoans(In, SrcOID);
+    return LoanPropagationLattice(
+        Factory.OriginMapFactory.add(In.Origins, DestOID, SrcLoans));
   }
-};
 
-// ========================================================================= //
-//                              Dataflow analysis
-// ========================================================================= //
-
-/// Drives the intra-procedural dataflow analysis.
-///
-/// Orchestrates the analysis by iterating over the CFG using a worklist
-/// algorithm. It computes a fixed point by propagating the LifetimeLattice
-/// state through each block until the state no longer changes.
-/// TODO: Maybe use the dataflow framework! The framework might need changes
-/// to support the current comparison done at block-entry.
-class LifetimeDataflow {
-  const CFG &Cfg;
-  AnalysisDeclContext &AC;
-  LifetimeFactory LifetimeFact;
-
-  Transferer Xfer;
-
-  /// Stores the merged analysis state at the entry of each CFG block.
-  llvm::DenseMap<const CFGBlock *, LifetimeLattice> BlockEntryStates;
-  /// Stores the analysis state at the exit of each CFG block, after the
-  /// transfer function has been applied.
-  llvm::DenseMap<const CFGBlock *, LifetimeLattice> BlockExitStates;
-
-public:
-  LifetimeDataflow(const CFG &C, FactManager &FS, AnalysisDeclContext &AC)
-      : Cfg(C), AC(AC), Xfer(FS, LifetimeFact) {}
-
-  void run() {
-    llvm::TimeTraceScope TimeProfile("Lifetime Dataflow");
-    ForwardDataflowWorklist Worklist(Cfg, AC);
-    const CFGBlock *Entry = &Cfg.getEntry();
-    BlockEntryStates[Entry] = LifetimeLattice{};
-    Worklist.enqueueBlock(Entry);
-    while (const CFGBlock *B = Worklist.dequeue()) {
-      LifetimeLattice EntryState = getEntryState(B);
-      LifetimeLattice ExitState = Xfer.transferBlock(B, EntryState);
-      BlockExitStates[B] = ExitState;
-
-      for (const CFGBlock *Successor : B->succs()) {
-        auto SuccIt = BlockEntryStates.find(Successor);
-        LifetimeLattice OldSuccEntryState = (SuccIt != BlockEntryStates.end())
-                                                ? SuccIt->second
-                                                : LifetimeLattice{};
-        LifetimeLattice NewSuccEntryState =
-            OldSuccEntryState.join(ExitState, LifetimeFact);
-        // Enqueue the successor if its entry state has changed.
-        // TODO(opt): Consider changing 'join' to report a change if !=
-        // comparison is found expensive.
-        if (SuccIt == BlockEntryStates.end() ||
-            NewSuccEntryState != OldSuccEntryState) {
-          BlockEntryStates[Successor] = NewSuccEntryState;
-          Worklist.enqueueBlock(Successor);
-        }
-      }
-    }
-  }
-
-  void dump() const {
-    llvm::dbgs() << "==========================================\n";
-    llvm::dbgs() << "       Dataflow results:\n";
-    llvm::dbgs() << "==========================================\n";
-    const CFGBlock &B = Cfg.getExit();
-    getExitState(&B).dump(llvm::dbgs());
-  }
-
-  LifetimeLattice getEntryState(const CFGBlock *B) const {
-    return BlockEntryStates.lookup(B);
-  }
-
-  LifetimeLattice getExitState(const CFGBlock *B) const {
-    return BlockExitStates.lookup(B);
+private:
+  LoanSet getLoans(Lattice L, OriginID OID) {
+    if (auto *Loans = L.Origins.lookup(OID))
+      return *Loans;
+    return Factory.LoanSetFact.getEmptySet();
   }
 };
 
 // ========================================================================= //
-//  TODO: Analysing dataflow results and error reporting.
+//  TODO: 
+// - Modifying loan propagation to answer `LoanSet getLoans(Origin O, Point P)`
+// - Adding loan expiry analysis to answer `bool isExpired(Loan L, Point P)`
+// - Adding origin liveness analysis to answer `bool isLive(Origin O, Point P)`
+// - Using the above three to perform the final error reporting.
 // ========================================================================= //
 } // anonymous namespace
 
@@ -755,8 +775,9 @@ void runLifetimeSafetyAnalysis(const DeclContext &DC, const CFG &Cfg,
   ///    blocks; only Decls are visible.  Therefore, loans in a block that
   ///    never reach an Origin associated with a Decl can be safely dropped by
   ///    the analysis.
-  LifetimeDataflow Dataflow(Cfg, FactMgr, AC);
-  Dataflow.run();
-  DEBUG_WITH_TYPE("LifetimeDataflow", Dataflow.dump());
+  LifetimeFactory LifetimeFact;
+  LoanPropagationAnalysis LoanPropagation(Cfg, AC, FactMgr, LifetimeFact);
+  LoanPropagation.run();
+  DEBUG_WITH_TYPE("LifetimeLoanPropagation", LoanPropagation.dump());
 }
 } // namespace clang
diff --git a/clang/test/Sema/warn-lifetime-safety-dataflow.cpp b/clang/test/Sema/warn-lifetime-safety-dataflow.cpp
index 38dfdb98f08fc..0e98904ade86a 100644
--- a/clang/test/Sema/warn-lifetime-safety-dataflow.cpp
+++ b/clang/test/Sema/warn-lifetime-safety-dataflow.cpp
@@ -1,4 +1,4 @@
-// RUN: %clang_cc1 -mllvm -debug-only=LifetimeFacts,LifetimeDataflow -Wexperimental-lifetime-safety %s 2>&1 | FileCheck %s
+// RUN: %clang_cc1 -mllvm -debug-only=LifetimeFacts,LifetimeLoanPropagation -Wexperimental-lifetime-safety %s 2>&1 | FileCheck %s
 // REQUIRES: asserts
 
 struct MyObj {
@@ -19,7 +19,7 @@ MyObj* return_local_addr() {
 // CHECK:   ReturnOfOrigin (OriginID: [[O_RET_VAL]])
 // CHECK:   Expire (LoanID: [[L_X]])
 }
-// CHECK: Dataflow results:
+// CHECK: LoanPropagation results:
 // CHECK-DAG: Origin [[O_ADDR_X]] contains Loan [[L_X]]
 // CHECK-DAG: Origin [[O_P]] contains Loan [[L_X]]
 // CHECK-DAG: Origin [[O_RET_VAL]] contains Loan [[L_X]]
@@ -47,7 +47,7 @@ MyObj* assign_and_return_local_addr() {
 // CHECK: ReturnOfOrigin (OriginID: [[O_PTR2_RVAL_2]])
 // CHECK: Expire (LoanID: [[L_Y]])
 }
-// CHECK: Dataflow results:
+// CHECK: LoanPropagation results:
 // CHECK-DAG: Origin [[O_ADDR_Y]] contains Loan [[L_Y]]
 // CHECK-DAG: Origin [[O_PTR1]] contains Loan [[L_Y]]
 // CHECK-DAG: Origin [[O_PTR2]] contains Loan [[L_Y]]
@@ -65,7 +65,7 @@ int return_int_val() {
   return x;
 }
 // CHECK-NEXT: End of Block
-// CHECK: Dataflow results:
+// CHECK: LoanPropagation results:
 // CHECK:  <empty>
 
 
@@ -79,7 +79,7 @@ void loan_expires_cpp() {
 // CHECK: AssignOrigin (DestID: [[O_POBJ:[0-9]+]], SrcID: [[O_ADDR_OBJ]])
 // CHECK: Expire (LoanID: [[L_OBJ]])
 }
-// CHECK: Dataflow results:
+// CHECK: LoanPropagation results:
 // CHECK-DAG: Origin [[O_ADDR_OBJ]] contains Loan [[L_OBJ]]
 // CHECK-DAG: Origin [[O_POBJ]] contains Loan [[L_OBJ]]
 
@@ -96,7 +96,7 @@ void loan_expires_trivial() {
 // CHECK-NEXT: End of Block
   // FIXME: Add check for Expire once trivial destructors are handled for expiration.
 }
-// CHECK: Dataflow results:
+// CHECK: LoanPropagation results:
 // CHECK-DAG: Origin [[O_ADDR_TRIVIAL_OBJ]] contains Loan [[L_TRIVIAL_OBJ]]
 // CHECK-DAG: Origin [[O_PTOBJ]] contains Loan [[L_TRIVIAL_OBJ]]
 
@@ -119,7 +119,7 @@ void conditional(bool condition) {
   // CHECK: AssignOrigin (DestID: [[O_P_RVAL:[0-9]+]], SrcID: [[O_P]])
   // CHECK: AssignOrigin (DestID: [[O_Q:[0-9]+]], SrcID: [[O_P_RVAL]])
 }
-// CHECK: Dataflow results:
+// CHECK: LoanPropagation results:
 // CHECK-DAG: Origin [[O_ADDR_A]] contains Loan [[L_A]]
 // CHECK-DAG: Origin [[O_ADDR_B]] contains Loan [[L_B]]
 // CHECK-DAG: Origin [[O_P]] contains Loan [[L_A]]
@@ -163,7 +163,7 @@ void pointers_in_a_cycle(bool condition) {
 }
 // At the end of the analysis, the origins for the po...
[truncated]

@usx95 usx95 force-pushed the users/usx95/lifetime-safety-liveness branch from 7e098b0 to 793d58e Compare July 14, 2025 20:02
@usx95 usx95 requested review from Xazax-hun and jvoung July 14, 2025 20:05
@usx95 usx95 requested a review from ymand July 14, 2025 20:05
@usx95 usx95 moved this to In Progress in Lifetime Safety in Clang Jul 14, 2025
@usx95 usx95 force-pushed the users/usx95/lifetime-safety-liveness branch from 793d58e to 2bff132 Compare July 15, 2025 09:09
Copy link
Collaborator

@Xazax-hun Xazax-hun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall looks good to me. One question for the future: sometimes it might be better to run certain analyses side by side in the same worklist rather than running them sequentially after each other. Do you think there are some analysis steps that will be beneficial to be combined in that way?

@usx95 usx95 force-pushed the users/usx95/lifetime-safety-liveness branch 3 times, most recently from 51afc3e to 7b26a72 Compare July 15, 2025 17:53
@usx95 usx95 force-pushed the users/usx95/lifetime-safety-liveness branch from 7b26a72 to 839eb25 Compare July 15, 2025 22:20
Copy link
Contributor Author

usx95 commented Jul 16, 2025

One question for the future: sometimes it might be better to run certain analyses side by side in the same worklist rather than running them sequentially after each other. Do you think there are some analysis steps that will be beneficial to be combined in that way?

That is true to a certain extent. The analyses with similar # of fixed-point-iterations can be grouped together to save on multiple fixed-point iterations. At this point, I expect the LoanPropagation to significantly outweigh the rest of analyses to have any meaningful wins from a single worklist.

As you said, this can be addressed in the future when we have some evidence through profiling and benchmarking on real code bases.

Copy link
Contributor Author

usx95 commented Jul 16, 2025

Merge activity

  • Jul 16, 2:12 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Jul 16, 2:15 PM UTC: Graphite rebased this pull request as part of a merge.
  • Jul 16, 2:17 PM UTC: Graphite rebased this pull request as part of a merge.
  • Jul 16, 2:19 PM UTC: @usx95 merged this pull request with Graphite.

@usx95 usx95 force-pushed the users/usx95/lifetime-safety-liveness branch from 839eb25 to 954ba85 Compare July 16, 2025 14:14
@usx95 usx95 force-pushed the users/usx95/lifetime-safety-liveness branch from 954ba85 to a8d93c9 Compare July 16, 2025 14:17
@usx95 usx95 merged commit 752e31c into main Jul 16, 2025
7 of 9 checks passed
@github-project-automation github-project-automation bot moved this from In Progress to Done in Lifetime Safety in Clang Jul 16, 2025
@usx95 usx95 deleted the users/usx95/lifetime-safety-liveness branch July 16, 2025 14:19
@sjoerdmeijer
Copy link
Collaborator

This is a heads up, I have bisected a compiler crash to this commit.

The stack trace:

 #0 0x0000aaaab380d358 llvm::sys::PrintStackTrace(llvm::raw_ostream&, int) /llvm-project/llvm/lib/Support/Unix/Signals.inc:834:11
 #1 0x0000aaaab380d878 PrintStackTraceSignalHandler(void*) /llvm-project/llvm/lib/Support/Unix/Signals.inc:918:1
 #2 0x0000aaaab380b918 llvm::sys::RunSignalHandlers() /llvm-project/llvm/lib/Support/Signals.cpp:104:5
 #3 0x0000aaaab380cba0 llvm::sys::CleanupOnSignal(unsigned long) /llvm-project/llvm/lib/Support/Unix/Signals.inc:374:1
 #4 0x0000aaaab372eaf0 (anonymous namespace)::CrashRecoveryContextImpl::HandleCrash(int, unsigned long) /llvm-project/llvm/lib/Support/CrashRecoveryContext.cpp:71:7
 #5 0x0000aaaab372ef24 CrashRecoverySignalHandler(int) /llvm-project/llvm/lib/Support/CrashRecoveryContext.cpp:390:5
 #6 0x0000fffff7fb09d0 (linux-vdso.so.1+0x9d0)
 #7 0x0000aaaab6c7c1c4 clang::CFGBlock::getBlockID() const /llvm-project/clang/include/clang/Analysis/CFG.h:1111:40
 #8 0x0000aaaab8407cd8 clang::(anonymous namespace)::DataflowAnalysis<clang::(anonymous namespace)::LoanPropagationAnalysis, clang::(anonymous namespace)::LoanPropagationLattice>::run() /llvm-project/clang/lib/Analysis/LifetimeSafety.cpp:561:38
 #9 0x0000aaaab84074ec clang::runLifetimeSafetyAnalysis(clang::DeclContext const&, clang::CFG const&, clang::AnalysisDeclContext&) /llvm-project/clang/lib/Analysis/LifetimeSafety.cpp:801:3
#10 0x0000aaaab758a9a8 clang::sema::AnalysisBasedWarnings::IssueWarnings(clang::sema::AnalysisBasedWarnings::Policy, clang::sema::FunctionScopeInfo*, clang::Decl const*, clang::QualType) /llvm-project/clang/lib/Sema/AnalysisBasedWarnings.cpp:3034:7
#11 0x0000aaaab7548ed8 clang::Sema::PopFunctionScopeInfo(clang::sema::AnalysisBasedWarnings::Policy const*, clang::Decl const*, clang::QualType) /llvm-project/clang/lib/Sema/Sema.cpp:2458:3
#12 0x0000aaaab7816e80 clang::Sema::ActOnFinishFunctionBody(clang::Decl*, clang::Stmt*, bool) /llvm-project/clang/lib/Sema/SemaDecl.cpp:16679:3
#13 0x0000aaaab7814d9c clang::Sema::ActOnFinishFunctionBody(clang::Decl*, clang::Stmt*) /llvm-project/clang/lib/Sema/SemaDecl.cpp:16148:3
#14 0x0000aaaab744bf24 clang::Parser::ParseFunctionStatementBody(clang::Decl*, clang::Parser::ParseScope&) /llvm-project/clang/lib/Parse/ParseStmt.cpp:2402:18
#15 0x0000aaaab73900ac clang::Parser::ParseFunctionDefinition(clang::ParsingDeclarator&, clang::Parser::ParsedTemplateInfo const&, clang::Parser::LateParsedAttrList*) /llvm-project/clang/lib/Parse/Parser.cpp:1449:3
#16 0x0000aaaab741d97c clang::Parser::ParseDeclGroup(clang::ParsingDeclSpec&, clang::DeclaratorContext, clang::ParsedAttributes&, clang::Parser::ParsedTemplateInfo&, clang::SourceLocation*, clang::Parser::ForRangeInit*) /llvm-project/clang/lib/Parse/ParseDecl.cpp:2262:21
#17 0x0000aaaab738f1d0 clang::Parser::ParseDeclOrFunctionDefInternal(clang::ParsedAttributes&, clang::ParsedAttributes&, clang::ParsingDeclSpec&, clang::AccessSpecifier) /llvm-project/clang/lib/Parse/Parser.cpp:1187:10
#18 0x0000aaaab738e800 clang::Parser::ParseDeclarationOrFunctionDefinition(clang::ParsedAttributes&, clang::ParsedAttributes&, clang::ParsingDeclSpec*, clang::AccessSpecifier) /llvm-project/clang/lib/Parse/Parser.cpp:1209:12
#19 0x0000aaaab738e17c clang::Parser::ParseExternalDeclaration(clang::ParsedAttributes&, clang::ParsedAttributes&, clang::ParsingDeclSpec*) /llvm-project/clang/lib/Parse/Parser.cpp:1032:14
#20 0x0000aaaab73f4164 clang::Parser::ParseInnerNamespace(llvm::SmallVector<clang::Parser::InnerNamespaceInfo, 4u> const&, unsigned int, clang::SourceLocation&, clang::ParsedAttributes&, clang::BalancedDelimiterTracker&) /llvm-project/clang/lib/Parse/ParseDeclCXX.cpp:236:7
#21 0x0000aaaab73f3a74 clang::Parser::ParseNamespace(clang::DeclaratorContext, clang::SourceLocation&, clang::SourceLocation) /llvm-project/clang/lib/Parse/ParseDeclCXX.cpp:214:3
#22 0x0000aaaab741c748 clang::Parser::ParseDeclaration(clang::DeclaratorContext, clang::SourceLocation&, clang::ParsedAttributes&, clang::ParsedAttributes&, clang::SourceLocation*) /llvm-project/clang/lib/Parse/ParseDecl.cpp:1909:12
#23 0x0000aaaab738dc88 clang::Parser::ParseExternalDeclaration(clang::ParsedAttributes&, clang::ParsedAttributes&, clang::ParsingDeclSpec*) /llvm-project/clang/lib/Parse/Parser.cpp:946:14
#24 0x0000aaaab738c2ec clang::Parser::ParseTopLevelDecl(clang::OpaquePtr<clang::DeclGroupRef>&, clang::Sema::ModuleImportState&) /llvm-project/clang/lib/Parse/Parser.cpp:745:12
#25 0x0000aaaab738733c clang::ParseAST(clang::Sema&, bool, bool) /llvm-project/clang/lib/Parse/ParseAST.cpp:170:16
#26 0x0000aaaab4fa47f0 clang::ASTFrontendAction::ExecuteAction() /llvm-project/clang/lib/Frontend/FrontendAction.cpp:1344:1

I don't have a reproducer yet, I will work on that on Monday.
But maybe this location LifetimeSafety.cpp:561:38 helps, @usx95 ?

@usx95
Copy link
Contributor Author

usx95 commented Jul 18, 2025

@sjoerdmeijer How are you running this ? This analysis is currently experimental, hidden behind -Wexperimental-lifetime-safety.

@sjoerdmeijer
Copy link
Collaborator

Hi @usx95 , thanks for the quick reply, I have narrowed it down to the usage of -Weverything.
Is that intended behaviour, does -Wexperimental-lifetime-safety gets enabled by that?

@usx95
Copy link
Contributor Author

usx95 commented Jul 18, 2025

I have narrowed it down to the usage of -Weverything.
Is that intended behaviour, does -Wexperimental-lifetime-safety gets enabled by that?

Yes. I am trying to have a different option to enable this analysis instead of the warning -Wexperimental-lifetime-safety in #149592 to not break -Weverything users.

@dhruvachak
Copy link
Contributor

@usx95 Build of https://github.com/ROCm/composable_kernel is failing with the same crash. -Weverything is used there as well.

Can you please fix the problem or revert?

@usx95
Copy link
Contributor Author

usx95 commented Jul 22, 2025

This should be fixed with 0d04789

Filed #150095 to track this separately for future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:analysis clang Clang issues not falling into any other category
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

6 participants