================
@@ -493,7 +496,247 @@ class FactGenerator : public 
ConstStmtVisitor<FactGenerator> {
 };
 
 // ========================================================================= //
-//  TODO: Run dataflow analysis to propagate loans, analyse and error 
reporting.
+//                              The Dataflow Lattice
+// ========================================================================= //
+
+// Using LLVM's immutable collections is efficient for dataflow analysis
+// as it avoids deep copies during state transitions.
+// TODO(opt): Consider using a bitset to represent the set of loans.
+using LoanSet = llvm::ImmutableSet<LoanID>;
+using OriginLoanMap = llvm::ImmutableMap<OriginID, LoanSet>;
+
+/// An object to hold the factories for immutable collections, ensuring
+/// that all created states share the same underlying memory management.
+struct LifetimeFactory {
+  OriginLoanMap::Factory OriginMapFact;
+  LoanSet::Factory LoanSetFact;
+
+  LoanSet createLoanSet(LoanID LID) {
+    return LoanSetFact.add(LoanSetFact.getEmptySet(), LID);
+  }
+};
+
+/// LifetimeLattice 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 {
+  /// The map from an origin to the set of loans it contains.
+  /// TODO(opt): To reduce the lattice size, propagate origins of declarations,
+  /// not expressions, because expressions are not visible across blocks.
+  OriginLoanMap Origins = OriginLoanMap(nullptr);
+
+  explicit LifetimeLattice(const OriginLoanMap &S) : Origins(S) {}
+  LifetimeLattice() = default;
+
+  bool operator==(const LifetimeLattice &Other) const {
+    return Origins == Other.Origins;
+  }
+  bool operator!=(const LifetimeLattice &Other) const {
+    return !(*this == Other);
+  }
+
+  LoanSet getLoans(OriginID OID, LifetimeFactory &Factory) const {
+    if (auto *Loans = Origins.lookup(OID))
+      return *Loans;
+    return Factory.LoanSetFact.getEmptySet();
+  }
+
+  /// 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.
+  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.OriginMapFact.add(
+          JoinedState, OID,
+          join(getLoans(OID, Factory), 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())
+      OS << "  <empty>\n";
+    for (const auto &Entry : Origins) {
+      if (Entry.second.isEmpty())
+        OS << "  Origin " << Entry.first << " contains no loans\n";
+      for (const LoanID &LID : Entry.second)
+        OS << "  Origin " << Entry.first << " contains Loan " << LID << "\n";
+    }
+  }
+};
+
+// ========================================================================= //
+//                              The Transfer Function
+// ========================================================================= //
+class Transferer {
+  FactManager &AllFacts;
+  LifetimeFactory &Factory;
+
+public:
+  explicit Transferer(FactManager &F, LifetimeFactory &Factory)
+      : AllFacts(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);
+
+    for (const Fact *F : Facts) {
+      BlockState = transferFact(BlockState, F);
+    }
+    return BlockState;
+  }
+
+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");
+  }
+
+  /// A new loan is issued to the origin. Old loans are erased.
+  LifetimeLattice transfer(LifetimeLattice In, const IssueFact &F) {
+    OriginID OID = F.getOriginID();
+    LoanID LID = F.getLoanID();
+    return LifetimeLattice(
+        Factory.OriginMapFact.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) 
{
+    OriginID DestOID = F.getDestOriginID();
+    OriginID SrcOID = F.getSrcOriginID();
+    LoanSet SrcLoans = InState.getLoans(SrcOID, Factory);
+    return LifetimeLattice(
+        Factory.OriginMapFact.add(InState.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;
----------------
usx95 wrote:

At this point, this is primarily for demonstration purposes. FWIW, I think even 
this is not enough. We would need more granular information, e.g., 
`map<CFGPoint, LifetimeLattice>` to answer queries of the form 
`DoesContain(Origin O, Loan L, CFGPoint P)`.
If the concern is around the size of these maps, note that this is bound by the 
#facts (LifetimeLattice are merely pointers to underlying immutable storage).

If the concern is around the eager computation of the in-states, then I agree 
that there are a couple of options:
1. (current) Process block, eagerly compute in-states of successors, if 
new-state -> enqueue the successor.
2. For the current block: compute in-state by merging out-states of all 
predecessors, process the block, if we have a new exit state of current block 
-> enqueue all successor blocks, 

Cost: Assuming `join` is the most expensive operation, **1** is strictly more 
efficient. The join operation is performed incrementally, once per CFG edge 
traversal. The `BlockEntryStates` map effectively caches the merged state from 
all predecessors visited so far.
**2**  requires re-computing the full entry state merge every time a block is 
processed. For a block with multiple predecessors that is visited multiple 
times, this involves many (O(in-degree)) join operations per visit. 
**1** also ensures no redundant visits to a block.


https://github.com/llvm/llvm-project/pull/148065
_______________________________________________
cfe-commits mailing list
cfe-commits@lists.llvm.org
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to