Skip to content

[LifetimeSafety] Add loan expiry analysis #148712

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

Open
wants to merge 1 commit into
base: users/usx95/07-16-lifetime-safety-add-unit-tests
Choose a base branch
from
Open
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
10 changes: 10 additions & 0 deletions clang/include/clang/Analysis/Analyses/LifetimeSafety.h
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ namespace internal {
class Fact;
class FactManager;
class LoanPropagationAnalysis;
class ExpiredLoansAnalysis;
struct LifetimeFactory;

/// A generic, type-safe wrapper for an ID, distinguished by its `Tag` type.
Expand All @@ -53,6 +54,11 @@ template <typename Tag> struct ID {
}
};

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>;

Expand Down Expand Up @@ -81,6 +87,9 @@ class LifetimeSafetyAnalysis {
/// Returns the set of loans an origin holds at a specific program point.
LoanSet getLoansAtPoint(OriginID OID, ProgramPoint PP) const;

/// Returns the set of loans that have expired at a specific program point.
LoanSet getExpiredLoansAtPoint(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;
Expand All @@ -96,6 +105,7 @@ class LifetimeSafetyAnalysis {
std::unique_ptr<LifetimeFactory> Factory;
std::unique_ptr<FactManager> FactMgr;
std::unique_ptr<LoanPropagationAnalysis> LoanPropagation;
std::unique_ptr<ExpiredLoansAnalysis> ExpiredLoans;
};
} // namespace internal
} // namespace clang::lifetimes
Expand Down
77 changes: 73 additions & 4 deletions clang/lib/Analysis/LifetimeSafety.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,15 @@
#include "llvm/Support/Debug.h"
#include "llvm/Support/TimeProfiler.h"
#include <cstdint>
#include <memory>

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;
}
// 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
Expand Down Expand Up @@ -832,6 +833,65 @@ class LoanPropagationAnalysis
}
};

// ========================================================================= //
// Expired Loans Analysis
// ========================================================================= //

/// The dataflow lattice for tracking the set of expired loans.
struct ExpiredLattice {
LoanSet Expired;

ExpiredLattice() : Expired(nullptr) {};
explicit ExpiredLattice(LoanSet S) : Expired(S) {}

bool operator==(const ExpiredLattice &Other) const {
return Expired == Other.Expired;
}
bool operator!=(const ExpiredLattice &Other) const {
return !(*this == Other);
}

void dump(llvm::raw_ostream &OS) const {
OS << "ExpiredLattice State:\n";
if (Expired.isEmpty())
OS << " <empty>\n";
for (const LoanID &LID : Expired)
OS << " Loan " << LID << " is expired\n";
}
};

/// The analysis that tracks which loans have expired.
class ExpiredLoansAnalysis
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we have the expectation that we have a tight bound on this analysis. I wonder if there is a way to somehow add an assert to verify that the reality matches our expectations. Not super important but if it is not too complicated it could be nice.

We can also defer this to a later PR since we want to be able to add strict bounds to the number of iterations in the future and that might be required for us to easily assert on this.

: public DataflowAnalysis<ExpiredLoansAnalysis, ExpiredLattice,
Direction::Forward> {

LoanSet::Factory &Factory;

public:
ExpiredLoansAnalysis(const CFG &C, AnalysisDeclContext &AC, FactManager &F,
LifetimeFactory &Factory)
: DataflowAnalysis(C, AC, F), Factory(Factory.LoanSetFactory) {}

using Base::transfer;

StringRef getAnalysisName() const { return "ExpiredLoans"; }

Lattice getInitialState() { return Lattice(Factory.getEmptySet()); }

/// Merges two lattices by taking the union of the expired loan sets.
Lattice join(Lattice L1, Lattice L2) const {
return Lattice(utils::join(L1.Expired, L2.Expired, Factory));
}

Lattice transfer(Lattice In, const ExpireFact &F) {
return Lattice(Factory.add(In.Expired, F.getLoanID()));
}

Lattice transfer(Lattice In, const IssueFact &F) {
return Lattice(Factory.remove(In.Expired, F.getLoanID()));
}
};

// ========================================================================= //
// TODO:
// - Modify loan expiry analysis to answer `bool isExpired(Loan L, Point P)`
Expand Down Expand Up @@ -873,6 +933,10 @@ void LifetimeSafetyAnalysis::run() {
LoanPropagation =
std::make_unique<LoanPropagationAnalysis>(Cfg, AC, *FactMgr, *Factory);
LoanPropagation->run();

ExpiredLoans =
std::make_unique<ExpiredLoansAnalysis>(Cfg, AC, *FactMgr, *Factory);
ExpiredLoans->run();
}

LoanSet LifetimeSafetyAnalysis::getLoansAtPoint(OriginID OID,
Expand All @@ -881,6 +945,11 @@ LoanSet LifetimeSafetyAnalysis::getLoansAtPoint(OriginID OID,
return LoanPropagation->getLoans(OID, PP);
}

LoanSet LifetimeSafetyAnalysis::getExpiredLoansAtPoint(ProgramPoint PP) const {
assert(ExpiredLoans && "ExpiredLoansAnalysis has not been run.");
return ExpiredLoans->getState(PP).Expired;
}

std::optional<OriginID>
LifetimeSafetyAnalysis::getOriginIDForDecl(const ValueDecl *D) const {
assert(FactMgr && "FactManager not initialized");
Expand Down
167 changes: 166 additions & 1 deletion clang/unittests/Analysis/LifetimeSafetyTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ namespace clang::lifetimes::internal {
namespace {

using namespace ast_matchers;
using ::testing::Not;
using ::testing::UnorderedElementsAreArray;

// A helper class to run the full lifetime analysis on a piece of code
Expand All @@ -45,7 +46,10 @@ class LifetimeTestRunner {
return;
}
AnalysisCtx = std::make_unique<AnalysisDeclContext>(nullptr, FD);
AnalysisCtx->getCFGBuildOptions().setAllAlwaysAdd();
CFG::BuildOptions &BuildOptions = AnalysisCtx->getCFGBuildOptions();
BuildOptions.setAllAlwaysAdd();
BuildOptions.AddImplicitDtors = true;
BuildOptions.AddTemporaryDtors = true;

// Run the main analysis.
Analysis = std::make_unique<LifetimeSafetyAnalysis>(*AnalysisCtx);
Expand Down Expand Up @@ -115,6 +119,13 @@ class LifetimeTestHelper {
return Analysis.getLoansAtPoint(OID, PP);
}

std::optional<LoanSet> getExpiredLoansAtPoint(llvm::StringRef Annotation) {
ProgramPoint PP = Runner.getProgramPoint(Annotation);
if (!PP)
return std::nullopt;
return Analysis.getExpiredLoansAtPoint(PP);
}

private:
template <typename DeclT> DeclT *findDecl(llvm::StringRef Name) {
auto &Ctx = Runner.getASTContext();
Expand All @@ -134,6 +145,15 @@ class LifetimeTestHelper {
// GTest Matchers & Fixture
// ========================================================================= //

// A helper class to represent the subject of a check, e.g., "the loan to 'x'".
class LoanInfo {
public:
LoanInfo(llvm::StringRef LoanVar, LifetimeTestHelper &Helper)
: LoanVar(LoanVar), Helper(Helper) {}
llvm::StringRef LoanVar;
LifetimeTestHelper &Helper;
};

// It holds the name of the origin variable and a reference to the helper.
class OriginInfo {
public:
Expand Down Expand Up @@ -185,6 +205,33 @@ MATCHER_P2(HasLoansToImpl, LoanVars, Annotation, "") {
ActualLoans, result_listener);
}

/// Matcher to verify if a loan to a specific variable has expired at a given
// program point.
MATCHER_P(IsExpiredAt, Annotation, "") {
const LoanInfo &Info = arg;
std::optional<LoanID> TargetLoanIDOpt =
Info.Helper.getLoanForVar(Info.LoanVar);
if (!TargetLoanIDOpt) {
*result_listener << "could not find a loan for variable '"
<< Info.LoanVar.str() << "'";
return false;
}

std::optional<LoanSet> ExpiredLoansSetOpt =
Info.Helper.getExpiredLoansAtPoint(Annotation);
if (!ExpiredLoansSetOpt) {
*result_listener << "could not get a valid expired loan set at point '"
<< Annotation << "'";
return false;
}

if (ExpiredLoansSetOpt->contains(*TargetLoanIDOpt))
return true;

*result_listener << "was expected to be expired, but was not";
return false;
}

// Base test fixture to manage the runner and helper.
class LifetimeAnalysisTest : public ::testing::Test {
protected:
Expand All @@ -197,6 +244,10 @@ class LifetimeAnalysisTest : public ::testing::Test {
return OriginInfo(OriginVar, *Helper);
}

LoanInfo LoanTo(llvm::StringRef LoanVar) {
return LoanInfo(LoanVar, *Helper);
}

// Factory function that hides the std::vector creation.
auto HasLoansTo(std::initializer_list<std::string> LoanVars,
const char *Annotation) {
Expand Down Expand Up @@ -435,5 +486,119 @@ TEST_F(LifetimeAnalysisTest, NestedScopes) {
EXPECT_THAT(Origin("p"), HasLoansTo({"inner"}, "after_inner_scope"));
}

// ========================================================================= //
// Loan Expiration Tests
// ========================================================================= //

TEST_F(LifetimeAnalysisTest, SimpleExpiry) {
SetupTest(R"(
void target() {
MyObj* p = nullptr;
{
MyObj s;
p = &s;
POINT(before_expiry);
} // s goes out of scope here
POINT(after_expiry);
}
)");
EXPECT_THAT(LoanTo("s"), Not(IsExpiredAt("before_expiry")));
EXPECT_THAT(LoanTo("s"), IsExpiredAt("after_expiry"));
}

TEST_F(LifetimeAnalysisTest, NestedExpiry) {
SetupTest(R"(
void target() {
MyObj s1;
MyObj* p = &s1;
POINT(before_inner);
{
MyObj s2;
p = &s2;
POINT(in_inner);
} // s2 expires
POINT(after_inner);
}
)");
EXPECT_THAT(LoanTo("s1"), Not(IsExpiredAt("before_inner")));
EXPECT_THAT(LoanTo("s2"), Not(IsExpiredAt("in_inner")));
EXPECT_THAT(LoanTo("s1"), Not(IsExpiredAt("after_inner")));
EXPECT_THAT(LoanTo("s2"), IsExpiredAt("after_inner"));
}

TEST_F(LifetimeAnalysisTest, ConditionalExpiry) {
SetupTest(R"(
void target(bool cond) {
MyObj s1;
MyObj* p = &s1;
POINT(before_if);
if (cond) {
MyObj s2;
p = &s2;
POINT(then_block);
} // s2 expires here
POINT(after_if);
}
)");
EXPECT_THAT(LoanTo("s1"), Not(IsExpiredAt("before_if")));
EXPECT_THAT(LoanTo("s2"), Not(IsExpiredAt("then_block")));
// After the if-statement, the loan to s2 (created in the 'then' branch)
// will have expired.
EXPECT_THAT(LoanTo("s2"), IsExpiredAt("after_if"));
EXPECT_THAT(LoanTo("s1"), Not(IsExpiredAt("after_if")));
}

TEST_F(LifetimeAnalysisTest, LoopExpiry) {
SetupTest(R"(
void target() {
MyObj *p = nullptr;
for (int i = 0; i < 2; ++i) {
MyObj s;
p = &s;
POINT(in_loop);
} // s expires here on each iteration
POINT(after_loop);
}
)");
// Inside the loop, before the scope of 's' ends, its loan is not expired.
EXPECT_THAT(LoanTo("s"), Not(IsExpiredAt("in_loop")));
// After the loop finishes, the loan to 's' from the last iteration has
// expired.
EXPECT_THAT(LoanTo("s"), IsExpiredAt("after_loop"));
}

TEST_F(LifetimeAnalysisTest, MultipleExpiredLoans) {
SetupTest(R"(
void target() {
MyObj *p1, *p2, *p3;
{
MyObj s1;
p1 = &s1;
POINT(p1);
} // s1 expires
POINT(p2);
{
MyObj s2;
p2 = &s2;
MyObj s3;
p3 = &s3;
POINT(p3);
} // s2, s3 expire
POINT(p4);
}
)");
EXPECT_THAT(LoanTo("s1"), Not(IsExpiredAt("p1")));

EXPECT_THAT(LoanTo("s1"), IsExpiredAt("p2"));

EXPECT_THAT(LoanTo("s1"), IsExpiredAt("p3"));
EXPECT_THAT(LoanTo("s2"), Not(IsExpiredAt("p3")));
EXPECT_THAT(LoanTo("s3"), Not(IsExpiredAt("p3")));

EXPECT_THAT(LoanTo("s1"), IsExpiredAt("p4"));
EXPECT_THAT(LoanTo("s2"), IsExpiredAt("p4"));
EXPECT_THAT(LoanTo("s3"), IsExpiredAt("p4"));
}

} // anonymous namespace
} // namespace clang::lifetimes::internal
Loading