Skip to content

Commit dd2dd83

Browse files
committed
[LifetimeSafety] Propagate loans using dataflow analysis
1 parent 5f672e7 commit dd2dd83

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
@@ -482,7 +482,247 @@ class FactGenerator : public ConstStmtVisitor<FactGenerator> {
482482
};
483483

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

@@ -495,5 +735,18 @@ void runLifetimeSafetyAnalysis(const DeclContext &DC, const CFG &Cfg,
495735
FactGenerator FactGen(FactMgr, AC);
496736
FactGen.run();
497737
DEBUG_WITH_TYPE("LifetimeFacts", FactMgr.dump(Cfg, AC));
738+
739+
/// TODO(opt): Consider optimizing individual blocks before running the
740+
/// dataflow analysis.
741+
/// 1. Expression Origins: These are assigned once and read at most once,
742+
/// forming simple chains. These chains can be compressed into a single
743+
/// assignment.
744+
/// 2. Block-Local Loans: Origins of expressions are never read by other
745+
/// blocks; only Decls are visible. Therefore, loans in a block that
746+
/// never reach an Origin associated with a Decl can be safely dropped by
747+
/// the analysis.
748+
LifetimeDataflow Dataflow(Cfg, FactMgr, AC);
749+
Dataflow.run();
750+
DEBUG_WITH_TYPE("LifetimeDataflow", Dataflow.dump());
498751
}
499752
} // namespace clang

0 commit comments

Comments
 (0)