Skip to content

[LifetimeSafety] Revamp test suite using unittests #149158

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 87 additions & 5 deletions clang/include/clang/Analysis/Analyses/LifetimeSafety.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,96 @@
//===----------------------------------------------------------------------===//
#ifndef LLVM_CLANG_ANALYSIS_ANALYSES_LIFETIMESAFETY_H
#define LLVM_CLANG_ANALYSIS_ANALYSES_LIFETIMESAFETY_H
#include "clang/AST/DeclBase.h"
#include "clang/Analysis/AnalysisDeclContext.h"
#include "clang/Analysis/CFG.h"
namespace clang {
#include "llvm/ADT/ImmutableSet.h"
#include "llvm/ADT/StringMap.h"
#include <memory>

void runLifetimeSafetyAnalysis(const DeclContext &DC, const CFG &Cfg,
AnalysisDeclContext &AC);
namespace clang::lifetimes {

} // namespace clang
/// The main entry point for the analysis.
void runLifetimeSafetyAnalysis(AnalysisDeclContext &AC);

namespace internal {
// Forward declarations of internal types.
class Fact;
class FactManager;
class LoanPropagationAnalysis;
struct LifetimeFactory;

/// A generic, type-safe wrapper for an ID, distinguished by its `Tag` type.
/// Used for giving ID to loans and origins.
template <typename Tag> struct ID {
uint32_t Value = 0;

bool operator==(const ID<Tag> &Other) const { return Value == Other.Value; }
bool operator!=(const ID<Tag> &Other) const { return !(*this == Other); }
bool operator<(const ID<Tag> &Other) const { return Value < Other.Value; }
ID<Tag> operator++(int) {
ID<Tag> Tmp = *this;
++Value;
return Tmp;
}
void Profile(llvm::FoldingSetNodeID &IDBuilder) const {
IDBuilder.AddInteger(Value);
}
};

using LoanID = ID<struct LoanTag>;
using OriginID = ID<struct OriginTag>;

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

/// A `ProgramPoint` identifies a location in the CFG by pointing to a specific
/// `Fact`. identified by a lifetime-related event (`Fact`).
///
/// A `ProgramPoint` has "after" semantics: it represents the location
/// immediately after its corresponding `Fact`.
using ProgramPoint = const Fact *;

/// Running the lifetime safety analysis and querying its results. It
/// encapsulates the various dataflow analyses.
class LifetimeSafetyAnalysis {
public:
LifetimeSafetyAnalysis(AnalysisDeclContext &AC);
~LifetimeSafetyAnalysis();

void run();

/// Returns the set of loans an origin holds at a specific program point.
LoanSet getLoansAtPoint(OriginID OID, ProgramPoint PP) const;

/// Finds the OriginID for a given declaration.
/// Returns a null optional if not found.
std::optional<OriginID> getOriginIDForDecl(const ValueDecl *D) const;

/// Finds the LoanID's for the loan created with the specific variable as
/// their Path.
std::vector<LoanID> getLoanIDForVar(const VarDecl *VD) const;

/// Retrieves program points that were specially marked in the source code
/// for testing.
///
/// The analysis recognizes special function calls of the form
/// `void("__lifetime_test_point_<name>")` as test points. This method returns
/// a map from the annotation string (<name>) to the corresponding
/// `ProgramPoint`. This allows test harnesses to query the analysis state at
/// user-defined locations in the code.
/// \note This is intended for testing only.
llvm::StringMap<ProgramPoint> getTestPoints() const;

private:
AnalysisDeclContext &AC;
std::unique_ptr<LifetimeFactory> Factory;
std::unique_ptr<FactManager> FactMgr;
std::unique_ptr<LoanPropagationAnalysis> LoanPropagation;
};
} // namespace internal
} // namespace clang::lifetimes

#endif // LLVM_CLANG_ANALYSIS_ANALYSES_LIFETIMESAFETY_H
181 changes: 135 additions & 46 deletions clang/lib/Analysis/LifetimeSafety.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,14 @@
#include "llvm/Support/TimeProfiler.h"
#include <cstdint>

namespace clang {
namespace clang::lifetimes {
namespace internal {
namespace {
template <typename Tag>
inline llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, ID<Tag> ID) {
return OS << ID.Value;
}
} // namespace

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

/// A generic, type-safe wrapper for an ID, distinguished by its `Tag` type.
/// Used for giving ID to loans and origins.
template <typename Tag> struct ID {
uint32_t Value = 0;

bool operator==(const ID<Tag> &Other) const { return Value == Other.Value; }
bool operator!=(const ID<Tag> &Other) const { return !(*this == Other); }
bool operator<(const ID<Tag> &Other) const { return Value < Other.Value; }
ID<Tag> operator++(int) {
ID<Tag> Tmp = *this;
++Value;
return Tmp;
}
void Profile(llvm::FoldingSetNodeID &IDBuilder) const {
IDBuilder.AddInteger(Value);
}
};

template <typename Tag>
inline llvm::raw_ostream &operator<<(llvm::raw_ostream &OS, ID<Tag> ID) {
return OS << ID.Value;
}

using LoanID = ID<struct LoanTag>;
using OriginID = ID<struct OriginTag>;

/// Information about a single borrow, or "Loan". A loan is created when a
/// reference or pointer is created.
struct Loan {
Expand Down Expand Up @@ -223,7 +203,9 @@ class Fact {
/// An origin is propagated from a source to a destination (e.g., p = q).
AssignOrigin,
/// An origin escapes the function by flowing into the return value.
ReturnOfOrigin
ReturnOfOrigin,
/// A marker for a specific point in the code, for testing.
TestPoint,
};

private:
Expand Down Expand Up @@ -310,6 +292,24 @@ class ReturnOfOriginFact : public Fact {
}
};

/// A dummy-fact used to mark a specific point in the code for testing.
/// It is generated by recognizing a `void("__lifetime_test_point_...")` cast.
class TestPointFact : public Fact {
StringRef Annotation;

public:
static bool classof(const Fact *F) { return F->getKind() == Kind::TestPoint; }

explicit TestPointFact(StringRef Annotation)
: Fact(Kind::TestPoint), Annotation(Annotation) {}

StringRef getAnnotation() const { return Annotation; }

void dump(llvm::raw_ostream &OS) const override {
OS << "TestPoint (Annotation: \"" << getAnnotation() << "\")\n";
}
};

class FactManager {
public:
llvm::ArrayRef<const Fact *> getFacts(const CFGBlock *B) const {
Expand Down Expand Up @@ -363,6 +363,7 @@ class FactManager {
};

class FactGenerator : public ConstStmtVisitor<FactGenerator> {
using Base = ConstStmtVisitor<FactGenerator>;

public:
FactGenerator(FactManager &FactMgr, AnalysisDeclContext &AC)
Expand Down Expand Up @@ -458,6 +459,15 @@ class FactGenerator : public ConstStmtVisitor<FactGenerator> {
}
}

void VisitCXXFunctionalCastExpr(const CXXFunctionalCastExpr *FCE) {
// Check if this is a test point marker. If so, we are done with this
// expression.
if (VisitTestPoint(FCE))
return;
// Visit as normal otherwise.
Base::VisitCXXFunctionalCastExpr(FCE);
}

private:
// Check if a type has an origin.
bool hasOrigin(QualType QT) { return QT->isPointerOrReferenceType(); }
Expand Down Expand Up @@ -491,6 +501,27 @@ class FactGenerator : public ConstStmtVisitor<FactGenerator> {
}
}

/// Checks if the expression is a `void("__lifetime_test_point_...")` cast.
/// If so, creates a `TestPointFact` and returns true.
bool VisitTestPoint(const CXXFunctionalCastExpr *FCE) {
if (!FCE->getType()->isVoidType())
return false;

const auto *SubExpr = FCE->getSubExpr()->IgnoreParenImpCasts();
if (const auto *SL = dyn_cast<StringLiteral>(SubExpr)) {
llvm::StringRef LiteralValue = SL->getString();
const std::string Prefix = "__lifetime_test_point_";

if (LiteralValue.starts_with(Prefix)) {
StringRef Annotation = LiteralValue.drop_front(Prefix.length());
CurrentBlockFacts.push_back(
FactMgr.createFact<TestPointFact>(Annotation));
return true;
}
}
return false;
}

FactManager &FactMgr;
AnalysisDeclContext &AC;
llvm::SmallVector<Fact *> CurrentBlockFacts;
Expand Down Expand Up @@ -637,6 +668,8 @@ class DataflowAnalysis {
return D->transfer(In, *F->getAs<AssignOriginFact>());
case Fact::Kind::ReturnOfOrigin:
return D->transfer(In, *F->getAs<ReturnOfOriginFact>());
case Fact::Kind::TestPoint:
return D->transfer(In, *F->getAs<TestPointFact>());
}
llvm_unreachable("Unknown fact kind");
}
Expand All @@ -646,14 +679,16 @@ class DataflowAnalysis {
Lattice transfer(Lattice In, const ExpireFact &) { return In; }
Lattice transfer(Lattice In, const AssignOriginFact &) { return In; }
Lattice transfer(Lattice In, const ReturnOfOriginFact &) { return In; }
Lattice transfer(Lattice In, const TestPointFact &) { return In; }
};

namespace utils {

/// Computes the union of two ImmutableSets.
template <typename T>
llvm::ImmutableSet<T> join(llvm::ImmutableSet<T> A, llvm::ImmutableSet<T> B,
typename llvm::ImmutableSet<T>::Factory &F) {
static llvm::ImmutableSet<T> join(llvm::ImmutableSet<T> A,
llvm::ImmutableSet<T> B,
typename llvm::ImmutableSet<T>::Factory &F) {
if (A.getHeight() < B.getHeight())
std::swap(A, B);
for (const T &E : B)
Expand All @@ -666,7 +701,7 @@ llvm::ImmutableSet<T> join(llvm::ImmutableSet<T> A, llvm::ImmutableSet<T> B,
// efficient merge could be implemented using a Patricia Trie or HAMT
// instead of the current AVL-tree-based ImmutableMap.
template <typename K, typename V, typename Joiner>
llvm::ImmutableMap<K, V>
static llvm::ImmutableMap<K, V>
join(llvm::ImmutableMap<K, V> A, llvm::ImmutableMap<K, V> B,
typename llvm::ImmutableMap<K, V>::Factory &F, Joiner joinValues) {
if (A.getHeight() < B.getHeight())
Expand All @@ -690,10 +725,6 @@ join(llvm::ImmutableMap<K, V> A, llvm::ImmutableMap<K, V> B,
// Loan Propagation Analysis
// ========================================================================= //

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

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

void runLifetimeSafetyAnalysis(const DeclContext &DC, const CFG &Cfg,
AnalysisDeclContext &AC) {
// ========================================================================= //
// LifetimeSafetyAnalysis Class Implementation
// ========================================================================= //

// We need this here for unique_ptr with forward declared class.
LifetimeSafetyAnalysis::~LifetimeSafetyAnalysis() = default;

LifetimeSafetyAnalysis::LifetimeSafetyAnalysis(AnalysisDeclContext &AC)
: AC(AC), Factory(std::make_unique<LifetimeFactory>()),
FactMgr(std::make_unique<FactManager>()) {}

void LifetimeSafetyAnalysis::run() {
llvm::TimeTraceScope TimeProfile("LifetimeSafetyAnalysis");

const CFG &Cfg = *AC.getCFG();
DEBUG_WITH_TYPE("PrintCFG", Cfg.dump(AC.getASTContext().getLangOpts(),
/*ShowColors=*/true));
FactManager FactMgr;
FactGenerator FactGen(FactMgr, AC);

FactGenerator FactGen(*FactMgr, AC);
FactGen.run();
DEBUG_WITH_TYPE("LifetimeFacts", FactMgr.dump(Cfg, AC));
DEBUG_WITH_TYPE("LifetimeFacts", FactMgr->dump(Cfg, AC));

/// TODO(opt): Consider optimizing individual blocks before running the
/// dataflow analysis.
Expand All @@ -828,9 +870,56 @@ void runLifetimeSafetyAnalysis(const DeclContext &DC, const CFG &Cfg,
/// blocks; only Decls are visible. Therefore, loans in a block that
/// never reach an Origin associated with a Decl can be safely dropped by
/// the analysis.
LifetimeFactory Factory;
LoanPropagationAnalysis LoanPropagation(Cfg, AC, FactMgr, Factory);
LoanPropagation.run();
DEBUG_WITH_TYPE("LifetimeLoanPropagation", LoanPropagation.dump());
LoanPropagation =
std::make_unique<LoanPropagationAnalysis>(Cfg, AC, *FactMgr, *Factory);
LoanPropagation->run();
}

LoanSet LifetimeSafetyAnalysis::getLoansAtPoint(OriginID OID,
ProgramPoint PP) const {
assert(LoanPropagation && "Analysis has not been run.");
return LoanPropagation->getLoans(OID, PP);
}

std::optional<OriginID>
LifetimeSafetyAnalysis::getOriginIDForDecl(const ValueDecl *D) const {
assert(FactMgr && "FactManager not initialized");
// This assumes the OriginManager's `get` can find an existing origin.
// We might need a `find` method on OriginManager to avoid `getOrCreate` logic
// in a const-query context if that becomes an issue.
return FactMgr->getOriginMgr().get(*D);
}

std::vector<LoanID>
LifetimeSafetyAnalysis::getLoanIDForVar(const VarDecl *VD) const {
assert(FactMgr && "FactManager not initialized");
std::vector<LoanID> Result;
for (const Loan &L : FactMgr->getLoanMgr().getLoans())
if (L.Path.D == VD)
Result.push_back(L.ID);
return Result;
}

llvm::StringMap<ProgramPoint> LifetimeSafetyAnalysis::getTestPoints() const {
assert(FactMgr && "FactManager not initialized");
llvm::StringMap<ProgramPoint> AnnotationToPointMap;
for (const CFGBlock *Block : *AC.getCFG()) {
for (const Fact *F : FactMgr->getFacts(Block)) {
if (const auto *TPF = F->getAs<TestPointFact>()) {
StringRef PointName = TPF->getAnnotation();
assert(AnnotationToPointMap.find(PointName) ==
AnnotationToPointMap.end() &&
"more than one test points with the same name");
AnnotationToPointMap[PointName] = F;
}
}
}
return AnnotationToPointMap;
}
} // namespace internal

void runLifetimeSafetyAnalysis(AnalysisDeclContext &AC) {
internal::LifetimeSafetyAnalysis Analysis(AC);
Analysis.run();
}
} // namespace clang
} // namespace clang::lifetimes
4 changes: 2 additions & 2 deletions clang/lib/Sema/AnalysisBasedWarnings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3029,8 +3029,8 @@ void clang::sema::AnalysisBasedWarnings::IssueWarnings(
// TODO: Enable lifetime safety analysis for other languages once it is
// stable.
if (EnableLifetimeSafetyAnalysis && S.getLangOpts().CPlusPlus) {
if (CFG *cfg = AC.getCFG())
runLifetimeSafetyAnalysis(*cast<DeclContext>(D), *cfg, AC);
if (AC.getCFG())
lifetimes::runLifetimeSafetyAnalysis(AC);
}
// Check for violations of "called once" parameter properties.
if (S.getLangOpts().ObjC && !S.getLangOpts().CPlusPlus &&
Expand Down
Loading
Loading