Skip to content

Commit 8cc690f

Browse files
committed
[LifetimeSafety] Propagate loans using dataflow analysis
1 parent 0cdd9f8 commit 8cc690f

File tree

2 files changed

+443
-1
lines changed

2 files changed

+443
-1
lines changed

clang/lib/Analysis/LifetimeSafety.cpp

Lines changed: 257 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
#include "clang/Analysis/Analyses/PostOrderCFGView.h"
1414
#include "clang/Analysis/AnalysisDeclContext.h"
1515
#include "clang/Analysis/CFG.h"
16+
#include "clang/Analysis/FlowSensitive/DataflowWorklist.h"
1617
#include "llvm/ADT/FoldingSet.h"
18+
#include "llvm/ADT/ImmutableMap.h"
19+
#include "llvm/ADT/ImmutableSet.h"
1720
#include "llvm/ADT/PointerUnion.h"
1821
#include "llvm/ADT/SmallVector.h"
1922
#include "llvm/Support/Debug.h"
@@ -487,7 +490,247 @@ class FactGenerator : public ConstStmtVisitor<FactGenerator> {
487490
};
488491

489492
// ========================================================================= //
490-
// TODO: Run dataflow analysis to propagate loans, analyse and error reporting.
493+
// The Dataflow Lattice
494+
// ========================================================================= //
495+
496+
// Using LLVM's immutable collections is efficient for dataflow analysis
497+
// as it avoids deep copies during state transitions.
498+
// TODO(opt): Consider using a bitset to represent the set of loans.
499+
using LoanSet = llvm::ImmutableSet<LoanID>;
500+
using OriginLoanMap = llvm::ImmutableMap<OriginID, LoanSet>;
501+
502+
/// An object to hold the factories for immutable collections, ensuring
503+
/// that all created states share the same underlying memory management.
504+
struct LifetimeFactory {
505+
OriginLoanMap::Factory OriginMapFact;
506+
LoanSet::Factory LoanSetFact;
507+
508+
LoanSet createLoanSet(LoanID LID) {
509+
return LoanSetFact.add(LoanSetFact.getEmptySet(), LID);
510+
}
511+
};
512+
513+
/// LifetimeLattice represents the state of our analysis at a given program
514+
/// point. It is an immutable object, and all operations produce a new
515+
/// instance rather than modifying the existing one.
516+
struct LifetimeLattice {
517+
/// The map from an origin to the set of loans it contains.
518+
/// TODO(opt): To reduce the lattice size, propagate origins of declarations,
519+
/// not expressions, because expressions are not visible across blocks.
520+
OriginLoanMap Origins = OriginLoanMap(nullptr);
521+
522+
explicit LifetimeLattice(const OriginLoanMap &S) : Origins(S) {}
523+
LifetimeLattice() = default;
524+
525+
bool operator==(const LifetimeLattice &Other) const {
526+
return Origins == Other.Origins;
527+
}
528+
bool operator!=(const LifetimeLattice &Other) const {
529+
return !(*this == Other);
530+
}
531+
532+
LoanSet getLoans(OriginID OID, LifetimeFactory &Factory) const {
533+
if (auto *Loans = Origins.lookup(OID))
534+
return *Loans;
535+
return Factory.LoanSetFact.getEmptySet();
536+
}
537+
538+
/// Computes the union of two lattices by performing a key-wise join of
539+
/// their OriginLoanMaps.
540+
// TODO(opt): This key-wise join is a performance bottleneck. A more
541+
// efficient merge could be implemented using a Patricia Trie or HAMT
542+
// instead of the current AVL-tree-based ImmutableMap.
543+
LifetimeLattice join(const LifetimeLattice &Other,
544+
LifetimeFactory &Factory) const {
545+
/// Merge the smaller map into the larger one ensuring we iterate over the
546+
/// smaller map.
547+
if (Origins.getHeight() < Other.Origins.getHeight())
548+
return Other.join(*this, Factory);
549+
550+
OriginLoanMap JoinedState = Origins;
551+
// For each origin in the other map, union its loan set with ours.
552+
for (const auto &Entry : Other.Origins) {
553+
OriginID OID = Entry.first;
554+
LoanSet OtherLoanSet = Entry.second;
555+
JoinedState = Factory.OriginMapFact.add(
556+
JoinedState, OID,
557+
join(getLoans(OID, Factory), OtherLoanSet, Factory));
558+
}
559+
return LifetimeLattice(JoinedState);
560+
}
561+
562+
LoanSet join(LoanSet a, LoanSet b, LifetimeFactory &Factory) const {
563+
/// Merge the smaller set into the larger one ensuring we iterate over the
564+
/// smaller set.
565+
if (a.getHeight() < b.getHeight())
566+
std::swap(a, b);
567+
LoanSet Result = a;
568+
for (LoanID LID : b) {
569+
/// TODO(opt): Profiling shows that this loop is a major performance
570+
/// bottleneck. Investigate using a BitVector to represent the set of
571+
/// loans for improved join performance.
572+
Result = Factory.LoanSetFact.add(Result, LID);
573+
}
574+
return Result;
575+
}
576+
577+
void dump(llvm::raw_ostream &OS) const {
578+
OS << "LifetimeLattice State:\n";
579+
if (Origins.isEmpty())
580+
OS << " <empty>\n";
581+
for (const auto &Entry : Origins) {
582+
if (Entry.second.isEmpty())
583+
OS << " Origin " << Entry.first << " contains no loans\n";
584+
for (const LoanID &LID : Entry.second)
585+
OS << " Origin " << Entry.first << " contains Loan " << LID << "\n";
586+
}
587+
}
588+
};
589+
590+
// ========================================================================= //
591+
// The Transfer Function
592+
// ========================================================================= //
593+
class Transferer {
594+
FactManager &AllFacts;
595+
LifetimeFactory &Factory;
596+
597+
public:
598+
explicit Transferer(FactManager &F, LifetimeFactory &Factory)
599+
: AllFacts(F), Factory(Factory) {}
600+
601+
/// Computes the exit state of a block by applying all its facts sequentially
602+
/// to a given entry state.
603+
/// TODO: We might need to store intermediate states per-fact in the block for
604+
/// later analysis.
605+
LifetimeLattice transferBlock(const CFGBlock *Block,
606+
LifetimeLattice EntryState) {
607+
LifetimeLattice BlockState = EntryState;
608+
llvm::ArrayRef<const Fact *> Facts = AllFacts.getFacts(Block);
609+
610+
for (const Fact *F : Facts) {
611+
BlockState = transferFact(BlockState, F);
612+
}
613+
return BlockState;
614+
}
615+
616+
private:
617+
LifetimeLattice transferFact(LifetimeLattice In, const Fact *F) {
618+
switch (F->getKind()) {
619+
case Fact::Kind::Issue:
620+
return transfer(In, *F->getAs<IssueFact>());
621+
case Fact::Kind::AssignOrigin:
622+
return transfer(In, *F->getAs<AssignOriginFact>());
623+
// Expire and ReturnOfOrigin facts don't modify the Origins and the State.
624+
case Fact::Kind::Expire:
625+
case Fact::Kind::ReturnOfOrigin:
626+
return In;
627+
}
628+
llvm_unreachable("Unknown fact kind");
629+
}
630+
631+
/// A new loan is issued to the origin. Old loans are erased.
632+
LifetimeLattice transfer(LifetimeLattice In, const IssueFact &F) {
633+
OriginID OID = F.getOriginID();
634+
LoanID LID = F.getLoanID();
635+
return LifetimeLattice(
636+
Factory.OriginMapFact.add(In.Origins, OID, Factory.createLoanSet(LID)));
637+
}
638+
639+
/// The destination origin's loan set is replaced by the source's.
640+
/// This implicitly "resets" the old loans of the destination.
641+
LifetimeLattice transfer(LifetimeLattice InState, const AssignOriginFact &F) {
642+
OriginID DestOID = F.getDestOriginID();
643+
OriginID SrcOID = F.getSrcOriginID();
644+
LoanSet SrcLoans = InState.getLoans(SrcOID, Factory);
645+
return LifetimeLattice(
646+
Factory.OriginMapFact.add(InState.Origins, DestOID, SrcLoans));
647+
}
648+
};
649+
// ========================================================================= //
650+
// Dataflow analysis
651+
// ========================================================================= //
652+
653+
/// Drives the intra-procedural dataflow analysis.
654+
///
655+
/// Orchestrates the analysis by iterating over the CFG using a worklist
656+
/// algorithm. It computes a fixed point by propagating the LifetimeLattice
657+
/// state through each block until the state no longer changes.
658+
/// TODO: Maybe use the dataflow framework! The framework might need changes
659+
/// to support the current comparison done at block-entry.
660+
class LifetimeDataflow {
661+
const CFG &Cfg;
662+
AnalysisDeclContext &AC;
663+
LifetimeFactory LifetimeFact;
664+
665+
Transferer Xfer;
666+
667+
/// Stores the merged analysis state at the entry of each CFG block.
668+
llvm::DenseMap<const CFGBlock *, LifetimeLattice> BlockEntryStates;
669+
/// Stores the analysis state at the exit of each CFG block, after the
670+
/// transfer function has been applied.
671+
llvm::DenseMap<const CFGBlock *, LifetimeLattice> BlockExitStates;
672+
673+
public:
674+
LifetimeDataflow(const CFG &C, FactManager &FS, AnalysisDeclContext &AC)
675+
: Cfg(C), AC(AC), Xfer(FS, LifetimeFact) {}
676+
677+
void run() {
678+
llvm::TimeTraceScope TimeProfile("Lifetime Dataflow");
679+
ForwardDataflowWorklist Worklist(Cfg, AC);
680+
const CFGBlock *Entry = &Cfg.getEntry();
681+
BlockEntryStates[Entry] = LifetimeLattice{};
682+
Worklist.enqueueBlock(Entry);
683+
while (const CFGBlock *B = Worklist.dequeue()) {
684+
LifetimeLattice EntryState = getEntryState(B);
685+
LifetimeLattice ExitState = Xfer.transferBlock(B, EntryState);
686+
BlockExitStates[B] = ExitState;
687+
688+
for (const CFGBlock *Successor : B->succs()) {
689+
auto SuccIt = BlockEntryStates.find(Successor);
690+
LifetimeLattice OldSuccEntryState = (SuccIt != BlockEntryStates.end())
691+
? SuccIt->second
692+
: LifetimeLattice{};
693+
LifetimeLattice NewSuccEntryState =
694+
OldSuccEntryState.join(ExitState, LifetimeFact);
695+
// Enqueue the successor if its entry state has changed.
696+
// TODO(opt): Consider changing 'join' to report a change if !=
697+
// comparison is found expensive.
698+
if (SuccIt == BlockEntryStates.end() ||
699+
NewSuccEntryState != OldSuccEntryState) {
700+
BlockEntryStates[Successor] = NewSuccEntryState;
701+
Worklist.enqueueBlock(Successor);
702+
}
703+
}
704+
}
705+
}
706+
707+
void dump() const {
708+
llvm::dbgs() << "==========================================\n";
709+
llvm::dbgs() << " Dataflow results:\n";
710+
llvm::dbgs() << "==========================================\n";
711+
const CFGBlock &B = Cfg.getExit();
712+
getExitState(&B).dump(llvm::dbgs());
713+
}
714+
715+
LifetimeLattice getEntryState(const CFGBlock *B) const {
716+
auto It = BlockEntryStates.find(B);
717+
if (It != BlockEntryStates.end()) {
718+
return It->second;
719+
}
720+
return LifetimeLattice{};
721+
}
722+
723+
LifetimeLattice getExitState(const CFGBlock *B) const {
724+
auto It = BlockExitStates.find(B);
725+
if (It != BlockExitStates.end()) {
726+
return It->second;
727+
}
728+
return LifetimeLattice{};
729+
}
730+
};
731+
732+
// ========================================================================= //
733+
// TODO: Analysing dataflow results and error reporting.
491734
// ========================================================================= //
492735
} // anonymous namespace
493736

@@ -500,5 +743,18 @@ void runLifetimeSafetyAnalysis(const DeclContext &DC, const CFG &Cfg,
500743
FactGenerator FactGen(FactMgr, AC);
501744
FactGen.run();
502745
DEBUG_WITH_TYPE("LifetimeFacts", FactMgr.dump(Cfg, AC));
746+
747+
/// TODO(opt): Consider optimizing individual blocks before running the
748+
/// dataflow analysis.
749+
/// 1. Expression Origins: These are assigned once and read at most once,
750+
/// forming simple chains. These chains can be compressed into a single
751+
/// assignment.
752+
/// 2. Block-Local Loans: Origins of expressions are never read by other
753+
/// blocks; only Decls are visible. Therefore, loans in a block that
754+
/// never reach an Origin associated with a Decl can be safely dropped by
755+
/// the analysis.
756+
LifetimeDataflow Dataflow(Cfg, FactMgr, AC);
757+
Dataflow.run();
758+
DEBUG_WITH_TYPE("LifetimeDataflow", Dataflow.dump());
503759
}
504760
} // namespace clang

0 commit comments

Comments
 (0)