Skip to content

Commit f6ca902

Browse files
committed
[LifetimeSafety] Implement dataflow analysis for loan propagation
1 parent 3076794 commit f6ca902

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"
@@ -493,7 +496,247 @@ class FactGenerator : public ConstStmtVisitor<FactGenerator> {
493496
};
494497

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

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

0 commit comments

Comments
 (0)