Skip to content

Commit e870b04

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

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"
@@ -482,7 +485,247 @@ class FactGenerator : public ConstStmtVisitor<FactGenerator> {
482485
};
483486

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

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

0 commit comments

Comments
 (0)