https://github.com/usx95 updated https://github.com/llvm/llvm-project/pull/149158
>From 1b945d05ac984b6a62dd07f64eaafd8f364fbbfb Mon Sep 17 00:00:00 2001 From: Utkarsh Saxena <u...@google.com> Date: Wed, 16 Jul 2025 18:22:39 +0000 Subject: [PATCH] [LifetimeSafety] Revamp test suite using unittests --- .../clang/Analysis/Analyses/LifetimeSafety.h | 83 +++- clang/lib/Analysis/LifetimeSafety.cpp | 173 +++++-- clang/lib/Sema/AnalysisBasedWarnings.cpp | 4 +- .../Sema/warn-lifetime-safety-dataflow.cpp | 87 +--- clang/unittests/Analysis/CMakeLists.txt | 1 + .../unittests/Analysis/LifetimeSafetyTest.cpp | 431 ++++++++++++++++++ 6 files changed, 640 insertions(+), 139 deletions(-) create mode 100644 clang/unittests/Analysis/LifetimeSafetyTest.cpp diff --git a/clang/include/clang/Analysis/Analyses/LifetimeSafety.h b/clang/include/clang/Analysis/Analyses/LifetimeSafety.h index 9998702a41cab..87627d4388d79 100644 --- a/clang/include/clang/Analysis/Analyses/LifetimeSafety.h +++ b/clang/include/clang/Analysis/Analyses/LifetimeSafety.h @@ -17,14 +17,87 @@ //===----------------------------------------------------------------------===// #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 internal { +// Forward declarations of internal types. +class Fact; +class FactManager; +class LoanPropagationAnalysis; +struct LifetimeFactory; -} // namespace clang +/// 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; + + 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 + +/// The main entry point for the analysis. +void runLifetimeSafetyAnalysis(AnalysisDeclContext &AC); + +} // namespace clang::lifetimes #endif // LLVM_CLANG_ANALYSIS_ANALYSES_LIFETIMESAFETY_H diff --git a/clang/lib/Analysis/LifetimeSafety.cpp b/clang/lib/Analysis/LifetimeSafety.cpp index a95db6d8013bd..3f56c20820e08 100644 --- a/clang/lib/Analysis/LifetimeSafety.cpp +++ b/clang/lib/Analysis/LifetimeSafety.cpp @@ -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. @@ -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 { @@ -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: @@ -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 { + std::string Annotation; + +public: + static bool classof(const Fact *F) { return F->getKind() == Kind::TestPoint; } + + explicit TestPointFact(std::string Annotation) + : Fact(Kind::TestPoint), Annotation(std::move(Annotation)) {} + + const std::string &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 { @@ -363,6 +363,7 @@ class FactManager { }; class FactGenerator : public ConstStmtVisitor<FactGenerator> { + using Base = ConstStmtVisitor<FactGenerator>; public: FactGenerator(FactManager &FactMgr, AnalysisDeclContext &AC) @@ -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(); } @@ -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)) { + std::string Annotation = LiteralValue.drop_front(Prefix.length()).str(); + CurrentBlockFacts.push_back( + FactMgr.createFact<TestPointFact>(Annotation)); + return true; + } + } + return false; + } + FactManager &FactMgr; AnalysisDeclContext &AC; llvm::SmallVector<Fact *> CurrentBlockFacts; @@ -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"); } @@ -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) @@ -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()) @@ -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 @@ -807,17 +838,27 @@ 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 +// ========================================================================= // + +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. @@ -828,9 +869,49 @@ 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>()) + AnnotationToPointMap[TPF->getAnnotation()] = F; + return AnnotationToPointMap; +} +} // namespace internal + +void runLifetimeSafetyAnalysis(AnalysisDeclContext &AC) { + internal::LifetimeSafetyAnalysis Analysis(AC); + Analysis.run(); } -} // namespace clang +} // namespace clang::lifetimes diff --git a/clang/lib/Sema/AnalysisBasedWarnings.cpp b/clang/lib/Sema/AnalysisBasedWarnings.cpp index 5eba024e83634..89c5a3596f584 100644 --- a/clang/lib/Sema/AnalysisBasedWarnings.cpp +++ b/clang/lib/Sema/AnalysisBasedWarnings.cpp @@ -3030,8 +3030,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 && diff --git a/clang/test/Sema/warn-lifetime-safety-dataflow.cpp b/clang/test/Sema/warn-lifetime-safety-dataflow.cpp index 0e98904ade86a..0eb3bda918f82 100644 --- a/clang/test/Sema/warn-lifetime-safety-dataflow.cpp +++ b/clang/test/Sema/warn-lifetime-safety-dataflow.cpp @@ -1,4 +1,4 @@ -// RUN: %clang_cc1 -mllvm -debug-only=LifetimeFacts,LifetimeLoanPropagation -Wexperimental-lifetime-safety %s 2>&1 | FileCheck %s +// RUN: %clang_cc1 -mllvm -debug-only=LifetimeFacts -Wexperimental-lifetime-safety %s 2>&1 | FileCheck %s // REQUIRES: asserts struct MyObj { @@ -19,10 +19,6 @@ MyObj* return_local_addr() { // CHECK: ReturnOfOrigin (OriginID: [[O_RET_VAL]]) // CHECK: Expire (LoanID: [[L_X]]) } -// CHECK: LoanPropagation results: -// CHECK-DAG: Origin [[O_ADDR_X]] contains Loan [[L_X]] -// CHECK-DAG: Origin [[O_P]] contains Loan [[L_X]] -// CHECK-DAG: Origin [[O_RET_VAL]] contains Loan [[L_X]] // Pointer Assignment and Return @@ -47,15 +43,6 @@ MyObj* assign_and_return_local_addr() { // CHECK: ReturnOfOrigin (OriginID: [[O_PTR2_RVAL_2]]) // CHECK: Expire (LoanID: [[L_Y]]) } -// CHECK: LoanPropagation results: -// CHECK-DAG: Origin [[O_ADDR_Y]] contains Loan [[L_Y]] -// CHECK-DAG: Origin [[O_PTR1]] contains Loan [[L_Y]] -// CHECK-DAG: Origin [[O_PTR2]] contains Loan [[L_Y]] -// CHECK-DAG: Origin [[O_PTR1_RVAL]] contains Loan [[L_Y]] -// CHECK-DAG: Origin [[O_PTR1_RVAL_2]] contains Loan [[L_Y]] -// CHECK-DAG: Origin [[O_PTR2_RVAL]] contains Loan [[L_Y]] -// CHECK-DAG: Origin [[O_PTR2_RVAL_2]] contains Loan [[L_Y]] - // Return of Non-Pointer Type // CHECK-LABEL: Function: return_int_val @@ -65,8 +52,6 @@ int return_int_val() { return x; } // CHECK-NEXT: End of Block -// CHECK: LoanPropagation results: -// CHECK: <empty> // Loan Expiration (Automatic Variable, C++) @@ -79,9 +64,6 @@ void loan_expires_cpp() { // CHECK: AssignOrigin (DestID: [[O_POBJ:[0-9]+]], SrcID: [[O_ADDR_OBJ]]) // CHECK: Expire (LoanID: [[L_OBJ]]) } -// CHECK: LoanPropagation results: -// CHECK-DAG: Origin [[O_ADDR_OBJ]] contains Loan [[L_OBJ]] -// CHECK-DAG: Origin [[O_POBJ]] contains Loan [[L_OBJ]] // FIXME: No expire for Trivial Destructors @@ -96,10 +78,6 @@ void loan_expires_trivial() { // CHECK-NEXT: End of Block // FIXME: Add check for Expire once trivial destructors are handled for expiration. } -// CHECK: LoanPropagation results: -// CHECK-DAG: Origin [[O_ADDR_TRIVIAL_OBJ]] contains Loan [[L_TRIVIAL_OBJ]] -// CHECK-DAG: Origin [[O_PTOBJ]] contains Loan [[L_TRIVIAL_OBJ]] - // CHECK-LABEL: Function: conditional void conditional(bool condition) { @@ -119,13 +97,6 @@ void conditional(bool condition) { // CHECK: AssignOrigin (DestID: [[O_P_RVAL:[0-9]+]], SrcID: [[O_P]]) // CHECK: AssignOrigin (DestID: [[O_Q:[0-9]+]], SrcID: [[O_P_RVAL]]) } -// CHECK: LoanPropagation results: -// CHECK-DAG: Origin [[O_ADDR_A]] contains Loan [[L_A]] -// CHECK-DAG: Origin [[O_ADDR_B]] contains Loan [[L_B]] -// CHECK-DAG: Origin [[O_P]] contains Loan [[L_A]] -// CHECK-DAG: Origin [[O_P]] contains Loan [[L_B]] -// CHECK-DAG: Origin [[O_Q]] contains Loan [[L_A]] -// CHECK-DAG: Origin [[O_Q]] contains Loan [[L_B]] // CHECK-LABEL: Function: pointers_in_a_cycle @@ -161,25 +132,6 @@ void pointers_in_a_cycle(bool condition) { // CHECK: AssignOrigin (DestID: [[O_P3]], SrcID: [[O_TEMP_RVAL]]) } } -// At the end of the analysis, the origins for the pointers involved in the cycle -// (p1, p2, p3, temp) should all contain the loans from v1, v2, and v3 at the fixed point. -// CHECK: LoanPropagation results: -// CHECK-DAG: Origin [[O_P1]] contains Loan [[L_V1]] -// CHECK-DAG: Origin [[O_P1]] contains Loan [[L_V2]] -// CHECK-DAG: Origin [[O_P1]] contains Loan [[L_V3]] -// CHECK-DAG: Origin [[O_P2]] contains Loan [[L_V1]] -// CHECK-DAG: Origin [[O_P2]] contains Loan [[L_V2]] -// CHECK-DAG: Origin [[O_P2]] contains Loan [[L_V3]] -// CHECK-DAG: Origin [[O_P3]] contains Loan [[L_V1]] -// CHECK-DAG: Origin [[O_P3]] contains Loan [[L_V2]] -// CHECK-DAG: Origin [[O_P3]] contains Loan [[L_V3]] -// CHECK-DAG: Origin [[O_TEMP]] contains Loan [[L_V1]] -// CHECK-DAG: Origin [[O_TEMP]] contains Loan [[L_V2]] -// CHECK-DAG: Origin [[O_TEMP]] contains Loan [[L_V3]] -// CHECK-DAG: Origin [[O_ADDR_V1]] contains Loan [[L_V1]] -// CHECK-DAG: Origin [[O_ADDR_V2]] contains Loan [[L_V2]] -// CHECK-DAG: Origin [[O_ADDR_V3]] contains Loan [[L_V3]] - // CHECK-LABEL: Function: overwrite_origin void overwrite_origin() { @@ -195,10 +147,6 @@ void overwrite_origin() { // CHECK: Expire (LoanID: [[L_S2]]) // CHECK: Expire (LoanID: [[L_S1]]) } -// CHECK: LoanPropagation results: -// CHECK: Origin [[O_P]] contains Loan [[L_S2]] -// CHECK-NOT: Origin [[O_P]] contains Loan [[L_S1]] - // CHECK-LABEL: Function: reassign_to_null void reassign_to_null() { @@ -213,8 +161,6 @@ void reassign_to_null() { } // FIXME: Have a better representation for nullptr than just an empty origin. // It should be a separate loan and origin kind. -// CHECK: LoanPropagation results: -// CHECK: Origin [[O_P]] contains no loans // CHECK-LABEL: Function: reassign_in_if @@ -235,11 +181,6 @@ void reassign_in_if(bool condition) { // CHECK: Expire (LoanID: [[L_S2]]) // CHECK: Expire (LoanID: [[L_S1]]) } -// CHECK: LoanPropagation results: -// CHECK-DAG: Origin [[O_P]] contains Loan [[L_S1]] -// CHECK-DAG: Origin [[O_P]] contains Loan [[L_S2]] -// CHECK-DAG: Origin [[O_ADDR_S1]] contains Loan [[L_S1]] -// CHECK-DAG: Origin [[O_ADDR_S2]] contains Loan [[L_S2]] // CHECK-LABEL: Function: assign_in_switch @@ -276,14 +217,6 @@ void assign_in_switch(int mode) { // CHECK-DAG: Expire (LoanID: [[L_S2]]) // CHECK-DAG: Expire (LoanID: [[L_S1]]) } -// CHECK: LoanPropagation results: -// CHECK-DAG: Origin [[O_P]] contains Loan [[L_S1]] -// CHECK-DAG: Origin [[O_P]] contains Loan [[L_S2]] -// CHECK-DAG: Origin [[O_P]] contains Loan [[L_S3]] -// CHECK-DAG: Origin [[O_ADDR_S1]] contains Loan [[L_S1]] -// CHECK-DAG: Origin [[O_ADDR_S2]] contains Loan [[L_S2]] -// CHECK-DAG: Origin [[O_ADDR_S3]] contains Loan [[L_S3]] - // CHECK-LABEL: Function: loan_in_loop void loan_in_loop(bool condition) { @@ -299,10 +232,6 @@ void loan_in_loop(bool condition) { // CHECK: Expire (LoanID: [[L_INNER]]) } } -// CHECK: LoanPropagation results: -// CHECK-DAG: Origin [[O_P]] contains Loan [[L_INNER]] -// CHECK-DAG: Origin [[O_ADDR_INNER]] contains Loan [[L_INNER]] - // CHECK-LABEL: Function: loop_with_break void loop_with_break(int count) { @@ -326,13 +255,6 @@ void loop_with_break(int count) { // CHECK: Expire (LoanID: [[L_S1]]) } -// CHECK-LABEL: LoanPropagation results: -// CHECK-DAG: Origin [[O_P]] contains Loan [[L_S1]] -// CHECK-DAG: Origin [[O_P]] contains Loan [[L_S2]] -// CHECK-DAG: Origin [[O_ADDR_S1]] contains Loan [[L_S1]] -// CHECK-DAG: Origin [[O_ADDR_S2]] contains Loan [[L_S2]] - - // CHECK-LABEL: Function: nested_scopes void nested_scopes() { MyObj* p = nullptr; @@ -355,13 +277,6 @@ void nested_scopes() { // CHECK: Expire (LoanID: [[L_OUTER]]) } -// CHECK-LABEL: LoanPropagation results: -// CHECK-DAG: Origin [[O_P]] contains Loan [[L_INNER]] -// CHECK-DAG: Origin [[O_ADDR_INNER]] contains Loan [[L_INNER]] -// CHECK-DAG: Origin [[O_ADDR_OUTER]] contains Loan [[L_OUTER]] -// CHECK-NOT: Origin [[O_P]] contains Loan [[L_OUTER]] - - // CHECK-LABEL: Function: pointer_indirection void pointer_indirection() { int a; diff --git a/clang/unittests/Analysis/CMakeLists.txt b/clang/unittests/Analysis/CMakeLists.txt index 059a74843155c..52e7d2854633d 100644 --- a/clang/unittests/Analysis/CMakeLists.txt +++ b/clang/unittests/Analysis/CMakeLists.txt @@ -4,6 +4,7 @@ add_clang_unittest(ClangAnalysisTests CloneDetectionTest.cpp ExprMutationAnalyzerTest.cpp IntervalPartitionTest.cpp + LifetimeSafetyTest.cpp MacroExpansionContextTest.cpp UnsafeBufferUsageTest.cpp CLANG_LIBS diff --git a/clang/unittests/Analysis/LifetimeSafetyTest.cpp b/clang/unittests/Analysis/LifetimeSafetyTest.cpp new file mode 100644 index 0000000000000..c945379c263d4 --- /dev/null +++ b/clang/unittests/Analysis/LifetimeSafetyTest.cpp @@ -0,0 +1,431 @@ +//===- LifetimeSafetyTest.cpp - Lifetime Safety Tests -*---------- C++-*-===// +// +// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. +// See https://llvm.org/LICENSE.txt for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +//===----------------------------------------------------------------------===// + +#include "clang/Analysis/Analyses/LifetimeSafety.h" +#include "clang/ASTMatchers/ASTMatchFinder.h" +#include "clang/ASTMatchers/ASTMatchers.h" +#include "clang/Testing/TestAST.h" +#include "llvm/ADT/StringMap.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include <optional> +#include <vector> + +namespace clang::lifetimes::internal { +namespace { + +using namespace ast_matchers; +using ::testing::UnorderedElementsAreArray; + +// A helper class to run the full lifetime analysis on a piece of code +// and provide an interface for querying the results. +class LifetimeTestRunner { +public: + LifetimeTestRunner(llvm::StringRef Code) { + std::string FullCode = R"( + #define POINT(name) void("__lifetime_test_point_" #name) + struct MyObj { ~MyObj() {} int i; }; + )"; + FullCode += Code.str(); + + TestAST = std::make_unique<clang::TestAST>(FullCode); + ASTCtx = &TestAST->context(); + + // Find the target function using AST matchers. + auto MatchResult = + match(functionDecl(hasName("target")).bind("target"), *ASTCtx); + auto *FD = selectFirst<FunctionDecl>("target", MatchResult); + if (!FD) { + ADD_FAILURE() << "Test case must have a function named 'target'"; + return; + } + AnalysisCtx = std::make_unique<AnalysisDeclContext>(nullptr, FD); + AnalysisCtx->getCFGBuildOptions().setAllAlwaysAdd(); + + // Run the main analysis. + Analysis = std::make_unique<LifetimeSafetyAnalysis>(*AnalysisCtx); + Analysis->run(); + + AnnotationToPointMap = Analysis->getTestPoints(); + } + + LifetimeSafetyAnalysis &getAnalysis() { return *Analysis; } + ASTContext &getASTContext() { return *ASTCtx; } + + ProgramPoint getProgramPoint(llvm::StringRef Annotation) { + auto It = AnnotationToPointMap.find(Annotation); + if (It == AnnotationToPointMap.end()) { + ADD_FAILURE() << "Annotation '" << Annotation << "' not found."; + return nullptr; + } + return It->second; + } + +private: + std::unique_ptr<TestAST> TestAST; + ASTContext *ASTCtx = nullptr; + std::unique_ptr<AnalysisDeclContext> AnalysisCtx; + std::unique_ptr<LifetimeSafetyAnalysis> Analysis; + llvm::StringMap<ProgramPoint> AnnotationToPointMap; +}; + +// A convenience wrapper that uses the LifetimeSafetyAnalysis public API. +class LifetimeTestHelper { +public: + LifetimeTestHelper(LifetimeTestRunner &Runner) + : Runner(Runner), Analysis(Runner.getAnalysis()) {} + + std::optional<OriginID> getOriginForDecl(llvm::StringRef VarName) { + auto *VD = findDecl<ValueDecl>(VarName); + if (!VD) + return std::nullopt; + auto OID = Analysis.getOriginIDForDecl(VD); + if (!OID) + ADD_FAILURE() << "Origin for '" << VarName << "' not found."; + return OID; + } + + std::optional<LoanID> getLoanForVar(llvm::StringRef VarName) { + auto *VD = findDecl<VarDecl>(VarName); + if (!VD) + return std::nullopt; + std::vector<LoanID> LID = Analysis.getLoanIDForVar(VD); + if (LID.empty()) { + ADD_FAILURE() << "Loan for '" << VarName << "' not found."; + return std::nullopt; + } + // TODO: Support retrieving more than one loans to a var. + if (LID.size() > 1) { + ADD_FAILURE() << "More than 1 loans found for '" << VarName; + return std::nullopt; + } + return LID[0]; + } + + std::optional<LoanSet> getLoansAtPoint(OriginID OID, + llvm::StringRef Annotation) { + ProgramPoint PP = Runner.getProgramPoint(Annotation); + if (!PP) + return std::nullopt; + return Analysis.getLoansAtPoint(OID, PP); + } + +private: + template <typename DeclT> DeclT *findDecl(llvm::StringRef Name) { + auto &Ctx = Runner.getASTContext(); + auto Results = match(valueDecl(hasName(Name)).bind("v"), Ctx); + if (Results.empty()) { + ADD_FAILURE() << "Declaration '" << Name << "' not found in AST."; + return nullptr; + } + return const_cast<DeclT *>(selectFirst<DeclT>("v", Results)); + } + + LifetimeTestRunner &Runner; + LifetimeSafetyAnalysis &Analysis; +}; + +// ========================================================================= // +// GTest Matchers & Fixture +// ========================================================================= // + +// It holds the name of the origin variable and a reference to the helper. +class OriginInfo { +public: + OriginInfo(llvm::StringRef OriginVar, LifetimeTestHelper &Helper) + : OriginVar(OriginVar), Helper(Helper) {} + llvm::StringRef OriginVar; + LifetimeTestHelper &Helper; +}; + +// The implementation of the matcher. It takes a vector of strings. +MATCHER_P2(HasLoansToImpl, LoanVars, Annotation, "") { + const OriginInfo &Info = arg; + std::optional<OriginID> OIDOpt = Info.Helper.getOriginForDecl(Info.OriginVar); + if (!OIDOpt) { + *result_listener << "could not find origin for '" << Info.OriginVar.str() + << "'"; + return false; + } + + std::optional<LoanSet> ActualLoansSetOpt = + Info.Helper.getLoansAtPoint(*OIDOpt, Annotation); + if (!ActualLoansSetOpt) { + *result_listener << "could not get a valid loan set at point '" + << Annotation << "'"; + return false; + } + std::vector<LoanID> ActualLoans(ActualLoansSetOpt->begin(), + ActualLoansSetOpt->end()); + + std::vector<LoanID> ExpectedLoans; + for (const auto &LoanVar : LoanVars) { + std::optional<LoanID> ExpectedLIDOpt = Info.Helper.getLoanForVar(LoanVar); + if (!ExpectedLIDOpt) { + *result_listener << "could not find loan for var '" << LoanVar << "'"; + return false; + } + ExpectedLoans.push_back(*ExpectedLIDOpt); + } + + return ExplainMatchResult(UnorderedElementsAreArray(ExpectedLoans), + ActualLoans, result_listener); +} + +// Base test fixture to manage the runner and helper. +class LifetimeAnalysisTest : public ::testing::Test { +protected: + void SetupTest(llvm::StringRef Code) { + Runner = std::make_unique<LifetimeTestRunner>(Code); + Helper = std::make_unique<LifetimeTestHelper>(*Runner); + } + + OriginInfo Origin(llvm::StringRef OriginVar) { + return OriginInfo(OriginVar, *Helper); + } + + // Factory function that hides the std::vector creation. + auto HasLoansTo(std::initializer_list<std::string> LoanVars, + const char *Annotation) { + return HasLoansToImpl(std::vector<std::string>(LoanVars), Annotation); + } + + std::unique_ptr<LifetimeTestRunner> Runner; + std::unique_ptr<LifetimeTestHelper> Helper; +}; + +// ========================================================================= // +// TESTS +// ========================================================================= // + +TEST_F(LifetimeAnalysisTest, SimpleLoanAndOrigin) { + SetupTest(R"( + void target() { + int x; + int* p = &x; + POINT(p1); + } + )"); + EXPECT_THAT(Origin("p"), HasLoansTo({"x"}, "p1")); +} + +TEST_F(LifetimeAnalysisTest, OverwriteOrigin) { + SetupTest(R"( + void target() { + MyObj s1, s2; + + MyObj* p = &s1; + POINT(after_s1); + + p = &s2; + POINT(after_s2); + } + )"); + EXPECT_THAT(Origin("p"), HasLoansTo({"s1"}, "after_s1")); + EXPECT_THAT(Origin("p"), HasLoansTo({"s2"}, "after_s2")); +} + +TEST_F(LifetimeAnalysisTest, ConditionalLoan) { + SetupTest(R"( + void target(bool cond) { + int a, b; + int *p = nullptr; + if (cond) { + p = &a; + POINT(after_then); + } else { + p = &b; + POINT(after_else); + } + POINT(after_if); + } + )"); + EXPECT_THAT(Origin("p"), HasLoansTo({"a"}, "after_then")); + EXPECT_THAT(Origin("p"), HasLoansTo({"b"}, "after_else")); + EXPECT_THAT(Origin("p"), HasLoansTo({"a", "b"}, "after_if")); +} + +TEST_F(LifetimeAnalysisTest, PointerChain) { + SetupTest(R"( + void target() { + MyObj y; + MyObj* ptr1 = &y; + POINT(p1); + + MyObj* ptr2 = ptr1; + POINT(p2); + + ptr2 = ptr1; + POINT(p3); + + ptr2 = ptr2; // Self assignment + POINT(p4); + } + )"); + EXPECT_THAT(Origin("ptr1"), HasLoansTo({"y"}, "p1")); + EXPECT_THAT(Origin("ptr2"), HasLoansTo({"y"}, "p2")); + EXPECT_THAT(Origin("ptr2"), HasLoansTo({"y"}, "p3")); + EXPECT_THAT(Origin("ptr2"), HasLoansTo({"y"}, "p4")); +} + +TEST_F(LifetimeAnalysisTest, ReassignToNull) { + SetupTest(R"( + void target() { + MyObj s1; + MyObj* p = &s1; + POINT(before_null); + p = nullptr; + POINT(after_null); + } + )"); + EXPECT_THAT(Origin("p"), HasLoansTo({"s1"}, "before_null")); + // After assigning to null, the origin for `p` should have no loans. + EXPECT_THAT(Origin("p"), HasLoansTo({}, "after_null")); +} + +TEST_F(LifetimeAnalysisTest, ReassignInIf) { + SetupTest(R"( + void target(bool condition) { + MyObj s1, s2; + MyObj* p = &s1; + POINT(before_if); + if (condition) { + p = &s2; + POINT(after_reassign); + } + POINT(after_if); + } + )"); + EXPECT_THAT(Origin("p"), HasLoansTo({"s1"}, "before_if")); + EXPECT_THAT(Origin("p"), HasLoansTo({"s2"}, "after_reassign")); + EXPECT_THAT(Origin("p"), HasLoansTo({"s1", "s2"}, "after_if")); +} + +TEST_F(LifetimeAnalysisTest, AssignInSwitch) { + SetupTest(R"( + void target(int mode) { + MyObj s1, s2, s3; + MyObj* p = nullptr; + switch (mode) { + case 1: + p = &s1; + POINT(case1); + break; + case 2: + p = &s2; + POINT(case2); + break; + default: + p = &s3; + POINT(case3); + break; + } + POINT(after_switch); + } + )"); + EXPECT_THAT(Origin("p"), HasLoansTo({"s1"}, "case1")); + EXPECT_THAT(Origin("p"), HasLoansTo({"s2"}, "case2")); + EXPECT_THAT(Origin("p"), HasLoansTo({"s3"}, "case3")); + EXPECT_THAT(Origin("p"), HasLoansTo({"s1", "s2", "s3"}, "after_switch")); +} + +TEST_F(LifetimeAnalysisTest, LoanInLoop) { + SetupTest(R"( + void target(bool condition) { + MyObj* p = nullptr; + while (condition) { + MyObj inner; + p = &inner; + POINT(in_loop); + } + POINT(after_loop); + } + )"); + EXPECT_THAT(Origin("p"), HasLoansTo({"inner"}, "in_loop")); + EXPECT_THAT(Origin("p"), HasLoansTo({"inner"}, "after_loop")); +} + +TEST_F(LifetimeAnalysisTest, LoopWithBreak) { + SetupTest(R"( + void target(int count) { + MyObj s1; + MyObj s2; + MyObj* p = &s1; + POINT(before_loop); + for (int i = 0; i < count; ++i) { + if (i == 5) { + p = &s2; + POINT(inside_if); + break; + } + POINT(after_if); + } + POINT(after_loop); + } + )"); + EXPECT_THAT(Origin("p"), HasLoansTo({"s1"}, "before_loop")); + EXPECT_THAT(Origin("p"), HasLoansTo({"s2"}, "inside_if")); + // At the join point after if, s2 cannot make it to p without the if. + EXPECT_THAT(Origin("p"), HasLoansTo({"s1"}, "after_if")); + // At the join point after the loop, p could hold a loan to s1 (if the loop + // completed normally) or to s2 (if the loop was broken). + EXPECT_THAT(Origin("p"), HasLoansTo({"s1", "s2"}, "after_loop")); +} + +TEST_F(LifetimeAnalysisTest, PointersInACycle) { + SetupTest(R"( + void target(bool condition) { + MyObj v1, v2, v3; + MyObj *p1 = &v1, *p2 = &v2, *p3 = &v3; + + POINT(before_while); + while (condition) { + MyObj* temp = p1; + p1 = p2; + p2 = p3; + p3 = temp; + } + POINT(after_loop); + } + )"); + EXPECT_THAT(Origin("p1"), HasLoansTo({"v1"}, "before_while")); + EXPECT_THAT(Origin("p2"), HasLoansTo({"v2"}, "before_while")); + EXPECT_THAT(Origin("p3"), HasLoansTo({"v3"}, "before_while")); + + // At the fixed point after the loop, all pointers could point to any of + // the three variables. + EXPECT_THAT(Origin("p1"), HasLoansTo({"v1", "v2", "v3"}, "after_loop")); + EXPECT_THAT(Origin("p2"), HasLoansTo({"v1", "v2", "v3"}, "after_loop")); + EXPECT_THAT(Origin("p3"), HasLoansTo({"v1", "v2", "v3"}, "after_loop")); + EXPECT_THAT(Origin("temp"), HasLoansTo({"v1", "v2", "v3"}, "after_loop")); +} + +TEST_F(LifetimeAnalysisTest, NestedScopes) { + SetupTest(R"( + void target() { + MyObj* p = nullptr; + { + MyObj outer; + p = &outer; + POINT(before_inner_scope); + { + MyObj inner; + p = &inner; + POINT(inside_inner_scope); + } // inner expires + POINT(after_inner_scope); + } // outer expires + } + )"); + EXPECT_THAT(Origin("p"), HasLoansTo({"outer"}, "before_inner_scope")); + EXPECT_THAT(Origin("p"), HasLoansTo({"inner"}, "inside_inner_scope")); + EXPECT_THAT(Origin("p"), HasLoansTo({"inner"}, "after_inner_scope")); +} + +} // anonymous namespace +} // namespace clang::lifetimes::internal _______________________________________________ llvm-branch-commits mailing list llvm-branch-commits@lists.llvm.org https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-branch-commits