Skip to content

Commit 2e4261b

Browse files
committed
[LifetimeSafety] Propagate loans using dataflow analysis
1 parent e18ae65 commit 2e4261b

File tree

2 files changed

+440
-1
lines changed

2 files changed

+440
-1
lines changed

clang/lib/Analysis/LifetimeSafety.cpp

Lines changed: 254 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,247 @@ class FactGenerator : public ConstStmtVisitor<FactGenerator> {
491491
};
492492

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

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

0 commit comments

Comments
 (0)