Skip to content

Commit 688ea04

Browse files
authored
[LifetimeSafety] Revamp test suite using unittests (#149158)
Refactor the Lifetime Safety Analysis infrastructure to support unit testing. - Created a public API class `LifetimeSafetyAnalysis` that encapsulates the analysis functionality - Added support for test points via a special `TestPointFact` that can be used to mark specific program points - Added unit tests that verify loan propagation in various code patterns
1 parent c14c0a1 commit 688ea04

File tree

6 files changed

+665
-139
lines changed

6 files changed

+665
-139
lines changed

clang/include/clang/Analysis/Analyses/LifetimeSafety.h

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,96 @@
1717
//===----------------------------------------------------------------------===//
1818
#ifndef LLVM_CLANG_ANALYSIS_ANALYSES_LIFETIMESAFETY_H
1919
#define LLVM_CLANG_ANALYSIS_ANALYSES_LIFETIMESAFETY_H
20-
#include "clang/AST/DeclBase.h"
2120
#include "clang/Analysis/AnalysisDeclContext.h"
2221
#include "clang/Analysis/CFG.h"
23-
namespace clang {
22+
#include "llvm/ADT/ImmutableSet.h"
23+
#include "llvm/ADT/StringMap.h"
24+
#include <memory>
2425

25-
void runLifetimeSafetyAnalysis(const DeclContext &DC, const CFG &Cfg,
26-
AnalysisDeclContext &AC);
26+
namespace clang::lifetimes {
2727

28-
} // namespace clang
28+
/// The main entry point for the analysis.
29+
void runLifetimeSafetyAnalysis(AnalysisDeclContext &AC);
30+
31+
namespace internal {
32+
// Forward declarations of internal types.
33+
class Fact;
34+
class FactManager;
35+
class LoanPropagationAnalysis;
36+
struct LifetimeFactory;
37+
38+
/// A generic, type-safe wrapper for an ID, distinguished by its `Tag` type.
39+
/// Used for giving ID to loans and origins.
40+
template <typename Tag> struct ID {
41+
uint32_t Value = 0;
42+
43+
bool operator==(const ID<Tag> &Other) const { return Value == Other.Value; }
44+
bool operator!=(const ID<Tag> &Other) const { return !(*this == Other); }
45+
bool operator<(const ID<Tag> &Other) const { return Value < Other.Value; }
46+
ID<Tag> operator++(int) {
47+
ID<Tag> Tmp = *this;
48+
++Value;
49+
return Tmp;
50+
}
51+
void Profile(llvm::FoldingSetNodeID &IDBuilder) const {
52+
IDBuilder.AddInteger(Value);
53+
}
54+
};
55+
56+
using LoanID = ID<struct LoanTag>;
57+
using OriginID = ID<struct OriginTag>;
58+
59+
// Using LLVM's immutable collections is efficient for dataflow analysis
60+
// as it avoids deep copies during state transitions.
61+
// TODO(opt): Consider using a bitset to represent the set of loans.
62+
using LoanSet = llvm::ImmutableSet<LoanID>;
63+
using OriginSet = llvm::ImmutableSet<OriginID>;
64+
65+
/// A `ProgramPoint` identifies a location in the CFG by pointing to a specific
66+
/// `Fact`. identified by a lifetime-related event (`Fact`).
67+
///
68+
/// A `ProgramPoint` has "after" semantics: it represents the location
69+
/// immediately after its corresponding `Fact`.
70+
using ProgramPoint = const Fact *;
71+
72+
/// Running the lifetime safety analysis and querying its results. It
73+
/// encapsulates the various dataflow analyses.
74+
class LifetimeSafetyAnalysis {
75+
public:
76+
LifetimeSafetyAnalysis(AnalysisDeclContext &AC);
77+
~LifetimeSafetyAnalysis();
78+
79+
void run();
80+
81+
/// Returns the set of loans an origin holds at a specific program point.
82+
LoanSet getLoansAtPoint(OriginID OID, ProgramPoint PP) const;
83+
84+
/// Finds the OriginID for a given declaration.
85+
/// Returns a null optional if not found.
86+
std::optional<OriginID> getOriginIDForDecl(const ValueDecl *D) const;
87+
88+
/// Finds the LoanID's for the loan created with the specific variable as
89+
/// their Path.
90+
std::vector<LoanID> getLoanIDForVar(const VarDecl *VD) const;
91+
92+
/// Retrieves program points that were specially marked in the source code
93+
/// for testing.
94+
///
95+
/// The analysis recognizes special function calls of the form
96+
/// `void("__lifetime_test_point_<name>")` as test points. This method returns
97+
/// a map from the annotation string (<name>) to the corresponding
98+
/// `ProgramPoint`. This allows test harnesses to query the analysis state at
99+
/// user-defined locations in the code.
100+
/// \note This is intended for testing only.
101+
llvm::StringMap<ProgramPoint> getTestPoints() const;
102+
103+
private:
104+
AnalysisDeclContext &AC;
105+
std::unique_ptr<LifetimeFactory> Factory;
106+
std::unique_ptr<FactManager> FactMgr;
107+
std::unique_ptr<LoanPropagationAnalysis> LoanPropagation;
108+
};
109+
} // namespace internal
110+
} // namespace clang::lifetimes
29111

30112
#endif // LLVM_CLANG_ANALYSIS_ANALYSES_LIFETIMESAFETY_H

clang/lib/Analysis/LifetimeSafety.cpp

Lines changed: 135 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,14 @@
2424
#include "llvm/Support/TimeProfiler.h"
2525
#include <cstdint>
2626

27-
namespace clang {
27+
namespace clang::lifetimes {
28+
namespace internal {
2829
namespace {
30+
template <typename Tag>
31+
inline llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, ID<Tag> ID) {
32+
return OS << ID.Value;
33+
}
34+
} // namespace
2935

3036
/// Represents the storage location being borrowed, e.g., a specific stack
3137
/// variable.
@@ -36,32 +42,6 @@ struct AccessPath {
3642
AccessPath(const clang::ValueDecl *D) : D(D) {}
3743
};
3844

39-
/// A generic, type-safe wrapper for an ID, distinguished by its `Tag` type.
40-
/// Used for giving ID to loans and origins.
41-
template <typename Tag> struct ID {
42-
uint32_t Value = 0;
43-
44-
bool operator==(const ID<Tag> &Other) const { return Value == Other.Value; }
45-
bool operator!=(const ID<Tag> &Other) const { return !(*this == Other); }
46-
bool operator<(const ID<Tag> &Other) const { return Value < Other.Value; }
47-
ID<Tag> operator++(int) {
48-
ID<Tag> Tmp = *this;
49-
++Value;
50-
return Tmp;
51-
}
52-
void Profile(llvm::FoldingSetNodeID &IDBuilder) const {
53-
IDBuilder.AddInteger(Value);
54-
}
55-
};
56-
57-
template <typename Tag>
58-
inline llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, ID<Tag> ID) {
59-
return OS << ID.Value;
60-
}
61-
62-
using LoanID = ID<struct LoanTag>;
63-
using OriginID = ID<struct OriginTag>;
64-
6545
/// Information about a single borrow, or "Loan". A loan is created when a
6646
/// reference or pointer is created.
6747
struct Loan {
@@ -223,7 +203,9 @@ class Fact {
223203
/// An origin is propagated from a source to a destination (e.g., p = q).
224204
AssignOrigin,
225205
/// An origin escapes the function by flowing into the return value.
226-
ReturnOfOrigin
206+
ReturnOfOrigin,
207+
/// A marker for a specific point in the code, for testing.
208+
TestPoint,
227209
};
228210

229211
private:
@@ -310,6 +292,24 @@ class ReturnOfOriginFact : public Fact {
310292
}
311293
};
312294

295+
/// A dummy-fact used to mark a specific point in the code for testing.
296+
/// It is generated by recognizing a `void("__lifetime_test_point_...")` cast.
297+
class TestPointFact : public Fact {
298+
StringRef Annotation;
299+
300+
public:
301+
static bool classof(const Fact *F) { return F->getKind() == Kind::TestPoint; }
302+
303+
explicit TestPointFact(StringRef Annotation)
304+
: Fact(Kind::TestPoint), Annotation(Annotation) {}
305+
306+
StringRef getAnnotation() const { return Annotation; }
307+
308+
void dump(llvm::raw_ostream &OS) const override {
309+
OS << "TestPoint (Annotation: \"" << getAnnotation() << "\")\n";
310+
}
311+
};
312+
313313
class FactManager {
314314
public:
315315
llvm::ArrayRef<const Fact *> getFacts(const CFGBlock *B) const {
@@ -363,6 +363,7 @@ class FactManager {
363363
};
364364

365365
class FactGenerator : public ConstStmtVisitor<FactGenerator> {
366+
using Base = ConstStmtVisitor<FactGenerator>;
366367

367368
public:
368369
FactGenerator(FactManager &FactMgr, AnalysisDeclContext &AC)
@@ -458,6 +459,15 @@ class FactGenerator : public ConstStmtVisitor<FactGenerator> {
458459
}
459460
}
460461

462+
void VisitCXXFunctionalCastExpr(const CXXFunctionalCastExpr *FCE) {
463+
// Check if this is a test point marker. If so, we are done with this
464+
// expression.
465+
if (VisitTestPoint(FCE))
466+
return;
467+
// Visit as normal otherwise.
468+
Base::VisitCXXFunctionalCastExpr(FCE);
469+
}
470+
461471
private:
462472
// Check if a type has an origin.
463473
bool hasOrigin(QualType QT) { return QT->isPointerOrReferenceType(); }
@@ -491,6 +501,27 @@ class FactGenerator : public ConstStmtVisitor<FactGenerator> {
491501
}
492502
}
493503

504+
/// Checks if the expression is a `void("__lifetime_test_point_...")` cast.
505+
/// If so, creates a `TestPointFact` and returns true.
506+
bool VisitTestPoint(const CXXFunctionalCastExpr *FCE) {
507+
if (!FCE->getType()->isVoidType())
508+
return false;
509+
510+
const auto *SubExpr = FCE->getSubExpr()->IgnoreParenImpCasts();
511+
if (const auto *SL = dyn_cast<StringLiteral>(SubExpr)) {
512+
llvm::StringRef LiteralValue = SL->getString();
513+
const std::string Prefix = "__lifetime_test_point_";
514+
515+
if (LiteralValue.starts_with(Prefix)) {
516+
StringRef Annotation = LiteralValue.drop_front(Prefix.length());
517+
CurrentBlockFacts.push_back(
518+
FactMgr.createFact<TestPointFact>(Annotation));
519+
return true;
520+
}
521+
}
522+
return false;
523+
}
524+
494525
FactManager &FactMgr;
495526
AnalysisDeclContext &AC;
496527
llvm::SmallVector<Fact *> CurrentBlockFacts;
@@ -637,6 +668,8 @@ class DataflowAnalysis {
637668
return D->transfer(In, *F->getAs<AssignOriginFact>());
638669
case Fact::Kind::ReturnOfOrigin:
639670
return D->transfer(In, *F->getAs<ReturnOfOriginFact>());
671+
case Fact::Kind::TestPoint:
672+
return D->transfer(In, *F->getAs<TestPointFact>());
640673
}
641674
llvm_unreachable("Unknown fact kind");
642675
}
@@ -646,14 +679,16 @@ class DataflowAnalysis {
646679
Lattice transfer(Lattice In, const ExpireFact &) { return In; }
647680
Lattice transfer(Lattice In, const AssignOriginFact &) { return In; }
648681
Lattice transfer(Lattice In, const ReturnOfOriginFact &) { return In; }
682+
Lattice transfer(Lattice In, const TestPointFact &) { return In; }
649683
};
650684

651685
namespace utils {
652686

653687
/// Computes the union of two ImmutableSets.
654688
template <typename T>
655-
llvm::ImmutableSet<T> join(llvm::ImmutableSet<T> A, llvm::ImmutableSet<T> B,
656-
typename llvm::ImmutableSet<T>::Factory &F) {
689+
static llvm::ImmutableSet<T> join(llvm::ImmutableSet<T> A,
690+
llvm::ImmutableSet<T> B,
691+
typename llvm::ImmutableSet<T>::Factory &F) {
657692
if (A.getHeight() < B.getHeight())
658693
std::swap(A, B);
659694
for (const T &E : B)
@@ -666,7 +701,7 @@ llvm::ImmutableSet<T> join(llvm::ImmutableSet<T> A, llvm::ImmutableSet<T> B,
666701
// efficient merge could be implemented using a Patricia Trie or HAMT
667702
// instead of the current AVL-tree-based ImmutableMap.
668703
template <typename K, typename V, typename Joiner>
669-
llvm::ImmutableMap<K, V>
704+
static llvm::ImmutableMap<K, V>
670705
join(llvm::ImmutableMap<K, V> A, llvm::ImmutableMap<K, V> B,
671706
typename llvm::ImmutableMap<K, V>::Factory &F, Joiner joinValues) {
672707
if (A.getHeight() < B.getHeight())
@@ -690,10 +725,6 @@ join(llvm::ImmutableMap<K, V> A, llvm::ImmutableMap<K, V> B,
690725
// Loan Propagation Analysis
691726
// ========================================================================= //
692727

693-
// Using LLVM's immutable collections is efficient for dataflow analysis
694-
// as it avoids deep copies during state transitions.
695-
// TODO(opt): Consider using a bitset to represent the set of loans.
696-
using LoanSet = llvm::ImmutableSet<LoanID>;
697728
using OriginLoanMap = llvm::ImmutableMap<OriginID, LoanSet>;
698729

699730
/// An object to hold the factories for immutable collections, ensuring
@@ -807,17 +838,28 @@ class LoanPropagationAnalysis
807838
// - Modify origin liveness analysis to answer `bool isLive(Origin O, Point P)`
808839
// - Using the above three to perform the final error reporting.
809840
// ========================================================================= //
810-
} // anonymous namespace
811841

812-
void runLifetimeSafetyAnalysis(const DeclContext &DC, const CFG &Cfg,
813-
AnalysisDeclContext &AC) {
842+
// ========================================================================= //
843+
// LifetimeSafetyAnalysis Class Implementation
844+
// ========================================================================= //
845+
846+
// We need this here for unique_ptr with forward declared class.
847+
LifetimeSafetyAnalysis::~LifetimeSafetyAnalysis() = default;
848+
849+
LifetimeSafetyAnalysis::LifetimeSafetyAnalysis(AnalysisDeclContext &AC)
850+
: AC(AC), Factory(std::make_unique<LifetimeFactory>()),
851+
FactMgr(std::make_unique<FactManager>()) {}
852+
853+
void LifetimeSafetyAnalysis::run() {
814854
llvm::TimeTraceScope TimeProfile("LifetimeSafetyAnalysis");
855+
856+
const CFG &Cfg = *AC.getCFG();
815857
DEBUG_WITH_TYPE("PrintCFG", Cfg.dump(AC.getASTContext().getLangOpts(),
816858
/*ShowColors=*/true));
817-
FactManager FactMgr;
818-
FactGenerator FactGen(FactMgr, AC);
859+
860+
FactGenerator FactGen(*FactMgr, AC);
819861
FactGen.run();
820-
DEBUG_WITH_TYPE("LifetimeFacts", FactMgr.dump(Cfg, AC));
862+
DEBUG_WITH_TYPE("LifetimeFacts", FactMgr->dump(Cfg, AC));
821863

822864
/// TODO(opt): Consider optimizing individual blocks before running the
823865
/// dataflow analysis.
@@ -828,9 +870,56 @@ void runLifetimeSafetyAnalysis(const DeclContext &DC, const CFG &Cfg,
828870
/// blocks; only Decls are visible. Therefore, loans in a block that
829871
/// never reach an Origin associated with a Decl can be safely dropped by
830872
/// the analysis.
831-
LifetimeFactory Factory;
832-
LoanPropagationAnalysis LoanPropagation(Cfg, AC, FactMgr, Factory);
833-
LoanPropagation.run();
834-
DEBUG_WITH_TYPE("LifetimeLoanPropagation", LoanPropagation.dump());
873+
LoanPropagation =
874+
std::make_unique<LoanPropagationAnalysis>(Cfg, AC, *FactMgr, *Factory);
875+
LoanPropagation->run();
876+
}
877+
878+
LoanSet LifetimeSafetyAnalysis::getLoansAtPoint(OriginID OID,
879+
ProgramPoint PP) const {
880+
assert(LoanPropagation && "Analysis has not been run.");
881+
return LoanPropagation->getLoans(OID, PP);
882+
}
883+
884+
std::optional<OriginID>
885+
LifetimeSafetyAnalysis::getOriginIDForDecl(const ValueDecl *D) const {
886+
assert(FactMgr && "FactManager not initialized");
887+
// This assumes the OriginManager's `get` can find an existing origin.
888+
// We might need a `find` method on OriginManager to avoid `getOrCreate` logic
889+
// in a const-query context if that becomes an issue.
890+
return FactMgr->getOriginMgr().get(*D);
891+
}
892+
893+
std::vector<LoanID>
894+
LifetimeSafetyAnalysis::getLoanIDForVar(const VarDecl *VD) const {
895+
assert(FactMgr && "FactManager not initialized");
896+
std::vector<LoanID> Result;
897+
for (const Loan &L : FactMgr->getLoanMgr().getLoans())
898+
if (L.Path.D == VD)
899+
Result.push_back(L.ID);
900+
return Result;
901+
}
902+
903+
llvm::StringMap<ProgramPoint> LifetimeSafetyAnalysis::getTestPoints() const {
904+
assert(FactMgr && "FactManager not initialized");
905+
llvm::StringMap<ProgramPoint> AnnotationToPointMap;
906+
for (const CFGBlock *Block : *AC.getCFG()) {
907+
for (const Fact *F : FactMgr->getFacts(Block)) {
908+
if (const auto *TPF = F->getAs<TestPointFact>()) {
909+
StringRef PointName = TPF->getAnnotation();
910+
assert(AnnotationToPointMap.find(PointName) ==
911+
AnnotationToPointMap.end() &&
912+
"more than one test points with the same name");
913+
AnnotationToPointMap[PointName] = F;
914+
}
915+
}
916+
}
917+
return AnnotationToPointMap;
918+
}
919+
} // namespace internal
920+
921+
void runLifetimeSafetyAnalysis(AnalysisDeclContext &AC) {
922+
internal::LifetimeSafetyAnalysis Analysis(AC);
923+
Analysis.run();
835924
}
836-
} // namespace clang
925+
} // namespace clang::lifetimes

clang/lib/Sema/AnalysisBasedWarnings.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3029,8 +3029,8 @@ void clang::sema::AnalysisBasedWarnings::IssueWarnings(
30293029
// TODO: Enable lifetime safety analysis for other languages once it is
30303030
// stable.
30313031
if (EnableLifetimeSafetyAnalysis && S.getLangOpts().CPlusPlus) {
3032-
if (CFG *cfg = AC.getCFG())
3033-
runLifetimeSafetyAnalysis(*cast<DeclContext>(D), *cfg, AC);
3032+
if (AC.getCFG())
3033+
lifetimes::runLifetimeSafetyAnalysis(AC);
30343034
}
30353035
// Check for violations of "called once" parameter properties.
30363036
if (S.getLangOpts().ObjC && !S.getLangOpts().CPlusPlus &&

0 commit comments

Comments
 (0)