[clang] [clang] Introduce [[clang::lifetime_capture_by(X)]] (PR #111499)
Utkarsh Saxena via cfe-commits
cfe-commits at lists.llvm.org
Tue Oct 8 14:10:01 PDT 2024
https://github.com/usx95 updated https://github.com/llvm/llvm-project/pull/111499
>From 4951a7b9b87f9800bc3629bd44f65141ba98c6b0 Mon Sep 17 00:00:00 2001
From: Utkarsh Saxena <usx at google.com>
Date: Tue, 8 Oct 2024 08:19:56 +0000
Subject: [PATCH 1/3] start working on lifetime capture
---
clang/include/clang/Basic/Attr.td | 40 +++++++
.../clang/Basic/DiagnosticSemaKinds.td | 16 +++
clang/include/clang/Sema/Sema.h | 7 ++
clang/lib/AST/TypePrinter.cpp | 21 ++++
clang/lib/Sema/CheckExprLifetime.cpp | 54 +++++++---
clang/lib/Sema/CheckExprLifetime.h | 26 +++--
clang/lib/Sema/SemaChecking.cpp | 27 +++++
clang/lib/Sema/SemaDecl.cpp | 1 +
clang/lib/Sema/SemaDeclAttr.cpp | 102 ++++++++++++++++++
clang/lib/Sema/SemaExpr.cpp | 4 +-
clang/lib/Sema/SemaInit.cpp | 2 +-
clang/lib/Sema/SemaOverload.cpp | 4 +-
clang/lib/Sema/SemaType.cpp | 13 +++
clang/test/SemaCXX/attr-lifetimebound.cpp | 94 ++++++++++++++++
14 files changed, 387 insertions(+), 24 deletions(-)
diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td
index 35b9716e13ff21..4dcb143b91f84f 100644
--- a/clang/include/clang/Basic/Attr.td
+++ b/clang/include/clang/Basic/Attr.td
@@ -1869,6 +1869,46 @@ def LifetimeBound : DeclOrTypeAttr {
let SimpleHandler = 1;
}
+def LifetimeCaptureBy : DeclOrTypeAttr {
+ let Spellings = [Clang<"lifetime_capture_by", 0>];
+ let Subjects = SubjectList<[ParmVar, ImplicitObjectParameter], ErrorDiag>;
+ let Args = [VariadicParamOrParamIdxArgument<"Params">];
+ let Documentation = [LifetimeBoundDocs];
+ let LangOpts = [CPlusPlus];
+
+ // let SimpleHandler = 1;
+ // let LateParsed = LateAttrParseStandard;
+ // let HasCustomParsing = 1;
+ // let ParseArgumentsAsUnevaluated = 1;
+
+ let AdditionalMembers = [{
+private:
+ SmallVector<IdentifierInfo*, 1> ArgIdents;
+ SmallVector<SourceLocation, 1> ArgLocs;
+
+public:
+ static const int INVALID = -2;
+ static const int UNKNOWN = -1;
+ static const int GLOBAL = -1;
+ static const int THIS = 0;
+
+ void setArgs(SmallVector<IdentifierInfo*, 1> Idents,
+ SmallVector<SourceLocation, 1> Locs) {
+ assert(Idents.size() == Locs.size());
+ assert(Idents.size() == params_Size);
+ ArgIdents = std::move(Idents);
+ ArgLocs = std::move(Locs);
+ }
+
+ const SmallVector<IdentifierInfo*, 1>& getArgIdents() const { return ArgIdents; }
+ const SmallVector<SourceLocation, 1>& getArgLocs() const { return ArgLocs; }
+ void setParamIdx(size_t Idx, int Val) {
+ assert(Idx < params_Size);
+ params_[Idx] = Val;
+ }
+}];
+}
+
def TrivialABI : InheritableAttr {
// This attribute does not have a C [[]] spelling because it requires the
// CPlusPlus language option.
diff --git a/clang/include/clang/Basic/DiagnosticSemaKinds.td b/clang/include/clang/Basic/DiagnosticSemaKinds.td
index e8b64f3c5a0187..ea034af77c3dbe 100644
--- a/clang/include/clang/Basic/DiagnosticSemaKinds.td
+++ b/clang/include/clang/Basic/DiagnosticSemaKinds.td
@@ -3382,6 +3382,18 @@ def err_callback_callee_is_variadic : Error<
"'callback' attribute callee may not be variadic">;
def err_callback_implicit_this_not_available : Error<
"'callback' argument at position %0 references unavailable implicit 'this'">;
+
+def err_capture_by_attribute_multiple : Error<
+ "multiple 'lifetime_capture' attributes specified">;
+def err_capture_by_attribute_no_entity : Error<
+ "'lifetime_capture_by' attribute specifies no capturing entity">;
+def err_capture_by_implicit_this_not_available : Error<
+ "'lifetime_capture_by' argument references unavailable implicit 'this'">;
+def err_capture_by_attribute_argument_unknown : Error<
+ "'lifetime_capture_by' attribute argument %0 is not a known function parameter"
+ ". Must be a function parameter of one of 'this', 'global' or 'unknown'">;
+def err_capture_by_references_itself : Error<"'lifetime_capture_by' argument references itself">;
+
def err_init_method_bad_return_type : Error<
"init methods must return an object pointer type, not %0">;
def err_attribute_invalid_size : Error<
@@ -10185,6 +10197,10 @@ def warn_dangling_pointer_assignment : Warning<
"object backing the pointer %0 "
"will be destroyed at the end of the full-expression">,
InGroup<DanglingAssignment>;
+def warn_dangling_reference_captured : Warning<
+ "object captured by the '%0' "
+ "will be destroyed at the end of the full-expression">,
+ InGroup<DanglingAssignment>;
// For non-floating point, expressions of the form x == x or x != x
// should result in a warning, since these always evaluate to a constant.
diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h
index 0809ac1b144ef6..a26b3fa8755161 100644
--- a/clang/include/clang/Sema/Sema.h
+++ b/clang/include/clang/Sema/Sema.h
@@ -1830,6 +1830,10 @@ class Sema final : public SemaBase {
/// Add [[gsl::Pointer]] attributes for std:: types.
void inferGslPointerAttribute(TypedefNameDecl *TD);
+ LifetimeCaptureByAttr *ParseLifetimeCaptureByAttr(const ParsedAttr &AL,
+ StringRef ParamName);
+ void LazyProcessLifetimeCaptureByParams(FunctionDecl *FD);
+
/// Add _Nullable attributes for std:: types.
void inferNullableClassAttribute(CXXRecordDecl *CRD);
@@ -2384,6 +2388,9 @@ class Sema final : public SemaBase {
bool BuiltinVectorMath(CallExpr *TheCall, QualType &Res);
bool BuiltinVectorToScalarMath(CallExpr *TheCall);
+ void checkLifetimeCaptureBy(FunctionDecl *FDecl, bool IsMemberFunction,
+ const Expr *ThisArg, ArrayRef<const Expr *> Args);
+
/// Handles the checks for format strings, non-POD arguments to vararg
/// functions, NULL arguments passed to non-NULL parameters, diagnose_if
/// attributes and AArch64 SME attributes.
diff --git a/clang/lib/AST/TypePrinter.cpp b/clang/lib/AST/TypePrinter.cpp
index ca75bb97c158e1..02bece7cce51d7 100644
--- a/clang/lib/AST/TypePrinter.cpp
+++ b/clang/lib/AST/TypePrinter.cpp
@@ -12,6 +12,7 @@
#include "clang/AST/ASTContext.h"
#include "clang/AST/Attr.h"
+#include "clang/AST/Attrs.inc"
#include "clang/AST/Decl.h"
#include "clang/AST/DeclBase.h"
#include "clang/AST/DeclCXX.h"
@@ -25,6 +26,7 @@
#include "clang/AST/TextNodeDumper.h"
#include "clang/AST/Type.h"
#include "clang/Basic/AddressSpaces.h"
+#include "clang/Basic/AttrKinds.h"
#include "clang/Basic/ExceptionSpecificationType.h"
#include "clang/Basic/IdentifierTable.h"
#include "clang/Basic/LLVM.h"
@@ -1907,6 +1909,24 @@ void TypePrinter::printAttributedAfter(const AttributedType *T,
OS << " [[clang::lifetimebound]]";
return;
}
+ if (T->getAttrKind() == attr::LifetimeCaptureBy) {
+ OS << " [[clang::lifetime_capture_by(...)";
+ // const LifetimeCaptureByAttr* A= T->getAs<LifetimeCaptureByAttr>();
+ // bool valid = true;
+ // for (int I : A->params())
+ // valid &= I != -2;
+ // if (valid) {
+ // OS << "invalid)";
+ // return;
+ // }
+ // for (size_t I = 0; I < A->params_size(); ++I) {
+ // OS << A->getArgIdents()[I]->getName()
+ // << "(idx: " << *(A->params_begin() + I) << ")";
+ // if (I != A->params_size() - 1)
+ // OS << ", ";
+ // }
+ return;
+ }
// The printing of the address_space attribute is handled by the qualifier
// since it is still stored in the qualifier. Return early to prevent printing
@@ -1966,6 +1986,7 @@ void TypePrinter::printAttributedAfter(const AttributedType *T,
case attr::SizedBy:
case attr::SizedByOrNull:
case attr::LifetimeBound:
+ case attr::LifetimeCaptureBy:
case attr::TypeNonNull:
case attr::TypeNullable:
case attr::TypeNullableResult:
diff --git a/clang/lib/Sema/CheckExprLifetime.cpp b/clang/lib/Sema/CheckExprLifetime.cpp
index c98fbca849faba..3e2e3565eb2337 100644
--- a/clang/lib/Sema/CheckExprLifetime.cpp
+++ b/clang/lib/Sema/CheckExprLifetime.cpp
@@ -41,10 +41,14 @@ enum LifetimeKind {
/// a default member initializer), the program is ill-formed.
LK_MemInitializer,
- /// The lifetime of a temporary bound to this entity probably ends too soon,
+ /// The lifetime of a temporary bound to this entity may end too soon,
/// because the entity is a pointer and we assign the address of a temporary
/// object to it.
LK_Assignment,
+
+ /// The lifetime of a temporary bound to this entity probably ends too soon,
+ /// because the entity may capture the reference to a temporary object.
+ LK_LifetimeCapture,
};
using LifetimeResult =
llvm::PointerIntPair<const InitializedEntity *, 3, LifetimeKind>;
@@ -189,6 +193,7 @@ struct IndirectLocalPathEntry {
VarInit,
LValToRVal,
LifetimeBoundCall,
+ LifetimeCapture,
TemporaryCopy,
LambdaCaptureInit,
GslReferenceInit,
@@ -898,6 +903,7 @@ static SourceRange nextPathEntryRange(const IndirectLocalPath &Path, unsigned I,
case IndirectLocalPathEntry::AddressOf:
case IndirectLocalPathEntry::LValToRVal:
case IndirectLocalPathEntry::LifetimeBoundCall:
+ case IndirectLocalPathEntry::LifetimeCapture:
case IndirectLocalPathEntry::TemporaryCopy:
case IndirectLocalPathEntry::GslReferenceInit:
case IndirectLocalPathEntry::GslPointerInit:
@@ -928,6 +934,7 @@ static bool pathOnlyHandlesGslPointer(IndirectLocalPath &Path) {
case IndirectLocalPathEntry::VarInit:
case IndirectLocalPathEntry::AddressOf:
case IndirectLocalPathEntry::LifetimeBoundCall:
+ case IndirectLocalPathEntry::LifetimeCapture:
continue;
case IndirectLocalPathEntry::GslPointerInit:
case IndirectLocalPathEntry::GslReferenceInit:
@@ -948,11 +955,11 @@ static bool isAssignmentOperatorLifetimeBound(CXXMethodDecl *CMD) {
}
static bool shouldRunGSLAssignmentAnalysis(const Sema &SemaRef,
- const AssignedEntity &Entity) {
+ const CapturingEntity &Entity) {
bool EnableGSLAssignmentWarnings = !SemaRef.getDiagnostics().isIgnored(
diag::warn_dangling_lifetime_pointer_assignment, SourceLocation());
return (EnableGSLAssignmentWarnings &&
- (isRecordWithAttr<PointerAttr>(Entity.LHS->getType()) ||
+ (isRecordWithAttr<PointerAttr>(Entity.Expression->getType()) ||
isAssignmentOperatorLifetimeBound(Entity.AssignmentOperator)));
}
@@ -960,9 +967,10 @@ static void checkExprLifetimeImpl(Sema &SemaRef,
const InitializedEntity *InitEntity,
const InitializedEntity *ExtendingEntity,
LifetimeKind LK,
- const AssignedEntity *AEntity, Expr *Init) {
- assert((AEntity && LK == LK_Assignment) ||
- (InitEntity && LK != LK_Assignment));
+ const CapturingEntity *CEntity, Expr *Init) {
+ assert(InitEntity || CEntity);
+ assert(!CEntity || LK == LK_Assignment || LK == LK_LifetimeCapture);
+ assert(!InitEntity || LK != LK_Assignment);
// If this entity doesn't have an interesting lifetime, don't bother looking
// for temporaries within its initializer.
if (LK == LK_FullExpression)
@@ -1046,6 +1054,17 @@ static void checkExprLifetimeImpl(Sema &SemaRef,
break;
}
+ case LK_LifetimeCapture: {
+ if (!MTE)
+ return false;
+ assert(shouldLifetimeExtendThroughPath(Path) ==
+ PathLifetimeKind::NoExtend &&
+ "No lifetime extension for in function calls");
+ SemaRef.Diag(DiagLoc, diag::warn_dangling_reference_captured)
+ << CEntity->Expression << DiagRange;
+ return false;
+ }
+
case LK_Assignment: {
if (!MTE || pathContainsInit(Path))
return false;
@@ -1056,7 +1075,7 @@ static void checkExprLifetimeImpl(Sema &SemaRef,
IsGslPtrValueFromGslTempOwner
? diag::warn_dangling_lifetime_pointer_assignment
: diag::warn_dangling_pointer_assignment)
- << AEntity->LHS << DiagRange;
+ << CEntity->Expression << DiagRange;
return false;
}
case LK_MemInitializer: {
@@ -1199,6 +1218,7 @@ static void checkExprLifetimeImpl(Sema &SemaRef,
break;
case IndirectLocalPathEntry::LifetimeBoundCall:
+ case IndirectLocalPathEntry::LifetimeCapture:
case IndirectLocalPathEntry::TemporaryCopy:
case IndirectLocalPathEntry::GslPointerInit:
case IndirectLocalPathEntry::GslReferenceInit:
@@ -1243,8 +1263,10 @@ static void checkExprLifetimeImpl(Sema &SemaRef,
};
llvm::SmallVector<IndirectLocalPathEntry, 8> Path;
- if (LK == LK_Assignment && shouldRunGSLAssignmentAnalysis(SemaRef, *AEntity))
+ if (LK == LK_Assignment && shouldRunGSLAssignmentAnalysis(SemaRef, *CEntity))
Path.push_back({IndirectLocalPathEntry::GslPointerAssignment, Init});
+ else if (LK == LK_LifetimeCapture)
+ Path.push_back({IndirectLocalPathEntry::LifetimeCapture, Init});
if (Init->isGLValue())
visitLocalsRetainedByReferenceBinding(Path, Init, RK_ReferenceBinding,
@@ -1256,7 +1278,7 @@ static void checkExprLifetimeImpl(Sema &SemaRef,
/*RevisitSubinits=*/!InitEntity);
}
-void checkExprLifetime(Sema &SemaRef, const InitializedEntity &Entity,
+void checkInitLifetime(Sema &SemaRef, const InitializedEntity &Entity,
Expr *Init) {
auto LTResult = getEntityLifetime(&Entity);
LifetimeKind LK = LTResult.getInt();
@@ -1265,12 +1287,12 @@ void checkExprLifetime(Sema &SemaRef, const InitializedEntity &Entity,
/*AEntity*/ nullptr, Init);
}
-void checkExprLifetime(Sema &SemaRef, const AssignedEntity &Entity,
- Expr *Init) {
+void checkAssignmentLifetime(Sema &SemaRef, const CapturingEntity &Entity,
+ Expr *RHS) {
bool EnableDanglingPointerAssignment = !SemaRef.getDiagnostics().isIgnored(
diag::warn_dangling_pointer_assignment, SourceLocation());
bool RunAnalysis = (EnableDanglingPointerAssignment &&
- Entity.LHS->getType()->isPointerType()) ||
+ Entity.Expression->getType()->isPointerType()) ||
shouldRunGSLAssignmentAnalysis(SemaRef, Entity);
if (!RunAnalysis)
@@ -1278,7 +1300,13 @@ void checkExprLifetime(Sema &SemaRef, const AssignedEntity &Entity,
checkExprLifetimeImpl(SemaRef, /*InitEntity=*/nullptr,
/*ExtendingEntity=*/nullptr, LK_Assignment, &Entity,
- Init);
+ RHS);
}
+void checkCaptureLifetime(Sema &SemaRef, const CapturingEntity &Entity,
+ Expr *Captured) {
+ checkExprLifetimeImpl(SemaRef, /*InitEntity=*/nullptr,
+ /*ExtendingEntity=*/nullptr, LK_LifetimeCapture,
+ &Entity, Captured);
+}
} // namespace clang::sema
diff --git a/clang/lib/Sema/CheckExprLifetime.h b/clang/lib/Sema/CheckExprLifetime.h
index 8c8d0806dee0a3..98562fba54b188 100644
--- a/clang/lib/Sema/CheckExprLifetime.h
+++ b/clang/lib/Sema/CheckExprLifetime.h
@@ -18,22 +18,36 @@
namespace clang::sema {
-/// Describes an entity that is being assigned.
-struct AssignedEntity {
- // The left-hand side expression of the assignment.
- Expr *LHS = nullptr;
+struct CapturingEntity {
+ // The expression of the entity which captures another entity.
+ // For example:
+ // 1. In an assignment, this would be the left-hand side expression.
+ // std::string_view sv;
+ // sv = std::string(); // Here 'sv' is the 'Entity'.
+ //
+ // 2. In an function call involving a lifetime capture, this would be the
+ // argument capturing the lifetime of another argument.
+ // void addToSet(std::string_view s [[clang::lifetime_capture_by(sv)]],
+ // set<std::string_view>& setsv);
+ // set<std::string_view> ssv;
+ // addToSet(std::string(), ssv); // Here 'ssv' is the 'Entity'.
+ Expr *Expression = nullptr;
CXXMethodDecl *AssignmentOperator = nullptr;
};
/// Check that the lifetime of the given expr (and its subobjects) is
/// sufficient for initializing the entity, and perform lifetime extension
/// (when permitted) if not.
-void checkExprLifetime(Sema &SemaRef, const InitializedEntity &Entity,
+void checkInitLifetime(Sema &SemaRef, const InitializedEntity &Entity,
Expr *Init);
/// Check that the lifetime of the given expr (and its subobjects) is
/// sufficient for assigning to the entity.
-void checkExprLifetime(Sema &SemaRef, const AssignedEntity &Entity, Expr *Init);
+void checkAssignmentLifetime(Sema &SemaRef, const CapturingEntity &Entity,
+ Expr *RHS);
+
+void checkCaptureLifetime(Sema &SemaRef, const CapturingEntity &Entity,
+ Expr *Captured);
} // namespace clang::sema
diff --git a/clang/lib/Sema/SemaChecking.cpp b/clang/lib/Sema/SemaChecking.cpp
index 99500daca295c9..b6d74650c3e0bc 100644
--- a/clang/lib/Sema/SemaChecking.cpp
+++ b/clang/lib/Sema/SemaChecking.cpp
@@ -11,10 +11,12 @@
//
//===----------------------------------------------------------------------===//
+#include "CheckExprLifetime.h"
#include "clang/AST/APValue.h"
#include "clang/AST/ASTContext.h"
#include "clang/AST/Attr.h"
#include "clang/AST/AttrIterator.h"
+#include "clang/AST/Attrs.inc"
#include "clang/AST/CharUnits.h"
#include "clang/AST/Decl.h"
#include "clang/AST/DeclBase.h"
@@ -3203,6 +3205,29 @@ void Sema::CheckArgAlignment(SourceLocation Loc, NamedDecl *FDecl,
<< ParamName << (FDecl != nullptr) << FDecl;
}
+void Sema::checkLifetimeCaptureBy(FunctionDecl *FD, bool IsMemberFunction,
+ const Expr *ThisArg,
+ ArrayRef<const Expr *> Args) {
+ auto GetArgAt = [&](int Idx) {
+ if (IsMemberFunction && Idx == 0)
+ return const_cast<Expr *>(ThisArg);
+ return const_cast<Expr *>(Args[Idx - int(IsMemberFunction)]);
+ };
+ for (unsigned I = 0; I < FD->getNumParams(); ++I) {
+ auto *CapturedByAttr =
+ FD->getParamDecl(I)->getAttr<LifetimeCaptureByAttr>();
+ if (!CapturedByAttr)
+ continue;
+ for (int CapturingParamIdx : CapturedByAttr->params()) {
+ Expr *Capturing = GetArgAt(CapturingParamIdx);
+ Expr *Captured = GetArgAt(I + IsMemberFunction);
+ CapturingEntity CE{Capturing};
+ // Ensure that 'Captured' lives atleast as long as the 'Capturing' entity.
+ checkCaptureLifetime(*this, CE, Captured);
+ }
+ }
+}
+
void Sema::checkCall(NamedDecl *FDecl, const FunctionProtoType *Proto,
const Expr *ThisArg, ArrayRef<const Expr *> Args,
bool IsMemberFunction, SourceLocation Loc,
@@ -3244,6 +3269,8 @@ void Sema::checkCall(NamedDecl *FDecl, const FunctionProtoType *Proto,
}
}
+ if (FD)
+ checkLifetimeCaptureBy(FD, IsMemberFunction, ThisArg, Args);
if (FDecl || Proto) {
CheckNonNullArguments(*this, FDecl, Proto, Args, Loc);
diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp
index 8557c25b93a8da..04e69f355d96df 100644
--- a/clang/lib/Sema/SemaDecl.cpp
+++ b/clang/lib/Sema/SemaDecl.cpp
@@ -16615,6 +16615,7 @@ void Sema::AddKnownFunctionAttributes(FunctionDecl *FD) {
}
inferLifetimeBoundAttribute(FD);
+ LazyProcessLifetimeCaptureByParams(FD);
AddKnownFunctionAttributesForReplaceableGlobalAllocationFunction(FD);
// If C++ exceptions are enabled but we are told extern "C" functions cannot
diff --git a/clang/lib/Sema/SemaDeclAttr.cpp b/clang/lib/Sema/SemaDeclAttr.cpp
index 14cc51cf89665a..ccf0b555538dfd 100644
--- a/clang/lib/Sema/SemaDeclAttr.cpp
+++ b/clang/lib/Sema/SemaDeclAttr.cpp
@@ -14,6 +14,7 @@
#include "clang/AST/ASTContext.h"
#include "clang/AST/ASTMutationListener.h"
#include "clang/AST/CXXInheritance.h"
+#include "clang/AST/Decl.h"
#include "clang/AST/DeclCXX.h"
#include "clang/AST/DeclObjC.h"
#include "clang/AST/DeclTemplate.h"
@@ -3834,6 +3835,104 @@ static void handleCallbackAttr(Sema &S, Decl *D, const ParsedAttr &AL) {
S.Context, AL, EncodingIndices.data(), EncodingIndices.size()));
}
+LifetimeCaptureByAttr *Sema::ParseLifetimeCaptureByAttr(const ParsedAttr &AL,
+ StringRef ParamName) {
+ // Atleast one capture by is required.
+ if (AL.getNumArgs() == 0) {
+ Diag(AL.getLoc(), diag::err_capture_by_attribute_no_entity)
+ << AL.getRange();
+ return nullptr;
+ }
+ SmallVector<IdentifierInfo *, 1> ParamIdents;
+ SmallVector<SourceLocation, 1> ParamLocs;
+ for (unsigned I = 0; I < AL.getNumArgs(); ++I) {
+ if (AL.isArgExpr(I)) {
+ Expr *E = AL.getArgAsExpr(I);
+ Diag(E->getExprLoc(), diag::err_capture_by_attribute_argument_unknown)
+ << E << E->getExprLoc();
+ continue;
+ }
+ assert(AL.isArgIdent(I));
+ IdentifierLoc *IdLoc = AL.getArgAsIdent(I);
+ if (IdLoc->Ident->getName() == ParamName) {
+ Diag(IdLoc->Loc, diag::err_capture_by_references_itself) << IdLoc->Loc;
+ continue;
+ }
+ ParamIdents.push_back(IdLoc->Ident);
+ ParamLocs.push_back(IdLoc->Loc);
+ }
+ SmallVector<int, 1> FakeParamIndices(ParamIdents.size(), -2);
+ LifetimeCaptureByAttr *CapturedBy = ::new (Context) LifetimeCaptureByAttr(
+ Context, AL, FakeParamIndices.data(), FakeParamIndices.size());
+ CapturedBy->setArgs(std::move(ParamIdents), std::move(ParamLocs));
+ return CapturedBy;
+}
+
+static void HandleLifetimeCaptureByAttr(Sema &S, Decl *D,
+ const ParsedAttr &AL) {
+ // Do not allow multiple attributes.
+ if (D->hasAttr<LifetimeCaptureByAttr>()) {
+ S.Diag(AL.getLoc(), diag::err_capture_by_attribute_multiple)
+ << AL.getRange();
+ return;
+ }
+ auto *PVD = dyn_cast<ParmVarDecl>(D);
+ assert(PVD);
+ auto *CaptureByAttr = S.ParseLifetimeCaptureByAttr(AL, PVD->getName());
+ if (CaptureByAttr)
+ D->addAttr(CaptureByAttr);
+}
+
+void Sema::LazyProcessLifetimeCaptureByParams(FunctionDecl *FD) {
+ bool HasImplicitThisParam = isInstanceMethod(FD);
+
+ llvm::StringMap<int> NameIdxMapping;
+ NameIdxMapping["global"] = -1;
+ NameIdxMapping["unknown"] = -1;
+ int Idx = 0;
+ if (HasImplicitThisParam) {
+ NameIdxMapping["this"] = 0;
+ Idx++;
+ }
+ for (const ParmVarDecl *PVD : FD->parameters())
+ NameIdxMapping[PVD->getName()] = Idx++;
+ auto HandleCaptureBy = [&](LifetimeCaptureByAttr *CapturedBy) {
+ if (!CapturedBy)
+ return;
+ const auto &Entities = CapturedBy->getArgIdents();
+ for (size_t I = 0; I < Entities.size(); ++I) {
+ StringRef Name = Entities[I]->getName();
+ auto It = NameIdxMapping.find(Name);
+ if (It == NameIdxMapping.end()) {
+ auto Loc = CapturedBy->getArgLocs()[I];
+ if (!HasImplicitThisParam && Name == "this")
+ Diag(Loc, diag::err_capture_by_implicit_this_not_available) << Loc;
+ else
+ Diag(Loc, diag::err_capture_by_attribute_argument_unknown)
+ << Entities[I] << Loc;
+ continue;
+ }
+ CapturedBy->setParamIdx(I, It->second);
+ }
+ };
+ for (ParmVarDecl *PVD : FD->parameters())
+ HandleCaptureBy(PVD->getAttr<LifetimeCaptureByAttr>());
+ if (!HasImplicitThisParam)
+ return;
+ TypeSourceInfo *TSI = FD->getTypeSourceInfo();
+ if (!TSI)
+ return;
+ AttributedTypeLoc ATL;
+ for (TypeLoc TL = TSI->getTypeLoc();
+ (ATL = TL.getAsAdjusted<AttributedTypeLoc>());
+ TL = ATL.getModifiedLoc()) {
+ auto *A = ATL.getAttrAs<LifetimeCaptureByAttr>();
+ if (!A)
+ continue;
+ HandleCaptureBy(const_cast<LifetimeCaptureByAttr *>(A));
+ }
+}
+
static bool isFunctionLike(const Type &T) {
// Check for explicit function types.
// 'called_once' is only supported in Objective-C and it has
@@ -6618,6 +6717,9 @@ ProcessDeclAttribute(Sema &S, Scope *scope, Decl *D, const ParsedAttr &AL,
case ParsedAttr::AT_Callback:
handleCallbackAttr(S, D, AL);
break;
+ case ParsedAttr::AT_LifetimeCaptureBy:
+ HandleLifetimeCaptureByAttr(S, D, AL);
+ break;
case ParsedAttr::AT_CalledOnce:
handleCalledOnceAttr(S, D, AL);
break;
diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp
index 80c252c79e4d7a..55b0f574170f74 100644
--- a/clang/lib/Sema/SemaExpr.cpp
+++ b/clang/lib/Sema/SemaExpr.cpp
@@ -13707,8 +13707,8 @@ QualType Sema::CheckAssignmentOperands(Expr *LHSExpr, ExprResult &RHS,
CheckForNullPointerDereference(*this, LHSExpr);
- AssignedEntity AE{LHSExpr};
- checkExprLifetime(*this, AE, RHS.get());
+ CapturingEntity AE{LHSExpr};
+ checkAssignmentLifetime(*this, AE, RHS.get());
if (getLangOpts().CPlusPlus20 && LHSType.isVolatileQualified()) {
if (CompoundType.isNull()) {
diff --git a/clang/lib/Sema/SemaInit.cpp b/clang/lib/Sema/SemaInit.cpp
index 4d11f2a43fcc6b..026a38e21103b2 100644
--- a/clang/lib/Sema/SemaInit.cpp
+++ b/clang/lib/Sema/SemaInit.cpp
@@ -7397,7 +7397,7 @@ PerformConstructorInitialization(Sema &S,
void Sema::checkInitializerLifetime(const InitializedEntity &Entity,
Expr *Init) {
- return sema::checkExprLifetime(*this, Entity, Init);
+ return sema::checkInitLifetime(*this, Entity, Init);
}
static void DiagnoseNarrowingInInitList(Sema &S,
diff --git a/clang/lib/Sema/SemaOverload.cpp b/clang/lib/Sema/SemaOverload.cpp
index d304f322aced64..1c96ba890256c1 100644
--- a/clang/lib/Sema/SemaOverload.cpp
+++ b/clang/lib/Sema/SemaOverload.cpp
@@ -14800,8 +14800,8 @@ ExprResult Sema::CreateOverloadedBinOp(SourceLocation OpLoc,
// Check for a self move.
DiagnoseSelfMove(Args[0], Args[1], OpLoc);
// lifetime check.
- checkExprLifetime(
- *this, AssignedEntity{Args[0], dyn_cast<CXXMethodDecl>(FnDecl)},
+ checkAssignmentLifetime(
+ *this, CapturingEntity{Args[0], dyn_cast<CXXMethodDecl>(FnDecl)},
Args[1]);
}
if (ImplicitThis) {
diff --git a/clang/lib/Sema/SemaType.cpp b/clang/lib/Sema/SemaType.cpp
index 950bd6db0359d1..edf99a25088686 100644
--- a/clang/lib/Sema/SemaType.cpp
+++ b/clang/lib/Sema/SemaType.cpp
@@ -8548,6 +8548,15 @@ static void HandleLifetimeBoundAttr(TypeProcessingState &State,
}
}
+static void HandleLifetimeCaptureByAttr(TypeProcessingState &State,
+ QualType &CurType, ParsedAttr &PA) {
+ if (State.getDeclarator().isDeclarationOfFunction()) {
+ auto *Attr = State.getSema().ParseLifetimeCaptureByAttr(PA, "this");
+ if (Attr)
+ CurType = State.getAttributedType(Attr, CurType, CurType);
+ }
+}
+
static void HandleHLSLParamModifierAttr(TypeProcessingState &State,
QualType &CurType,
const ParsedAttr &Attr, Sema &S) {
@@ -8709,6 +8718,10 @@ static void processTypeAttrs(TypeProcessingState &state, QualType &type,
if (TAL == TAL_DeclChunk)
HandleLifetimeBoundAttr(state, type, attr);
break;
+ case ParsedAttr::AT_LifetimeCaptureBy:
+ if (TAL == TAL_DeclChunk)
+ HandleLifetimeCaptureByAttr(state, type, attr);
+ break;
case ParsedAttr::AT_NoDeref: {
// FIXME: `noderef` currently doesn't work correctly in [[]] syntax.
diff --git a/clang/test/SemaCXX/attr-lifetimebound.cpp b/clang/test/SemaCXX/attr-lifetimebound.cpp
index 0fb997a5671085..7a0215fa3c576a 100644
--- a/clang/test/SemaCXX/attr-lifetimebound.cpp
+++ b/clang/test/SemaCXX/attr-lifetimebound.cpp
@@ -307,3 +307,97 @@ void test(StatusOr<FooView> foo1, StatusOr<NonAnnotatedFooView> foo2) {
foo2 = NonAnnotatedFoo();
}
} // namespace GH106372
+
+namespace lifetime_capture_by {
+
+struct S {
+ const int *x;
+ void captureInt(const int&x [[clang::lifetime_capture_by(this)]]) { this->x = &x; }
+ void captureSV(std::string_view sv [[clang::lifetime_capture_by(this)]]);
+};
+
+///////////////////////////
+// Test for valid usages.
+///////////////////////////
+[[clang::lifetime_capture_by(unknown)]] // expected-error {{'lifetime_capture_by' attribute only applies to parameters and implicit object parameters}}
+void nonMember(
+ const int &x1 [[clang::lifetime_capture_by(s, t)]],
+ S &s,
+ S &t,
+ const int &x2 [[clang::lifetime_capture_by(12345 + 12)]], // expected-error {{'lifetime_capture_by' attribute argument 12345 + 12 is not a known function parameter. Must be a function parameter of one of 'this', 'global' or 'unknown'}}
+ const int &x3 [[clang::lifetime_capture_by(abcdefgh)]], // expected-error {{'lifetime_capture_by' attribute argument 'abcdefgh' is not a known function parameter. Must be a function parameter of one of 'this', 'global' or 'unknown'}}
+ const int &x4 [[clang::lifetime_capture_by("abcdefgh")]], // expected-error {{'lifetime_capture_by' attribute argument "abcdefgh" is not a known function parameter. Must be a function parameter of one of 'this', 'global' or 'unknown'}}
+ const int &x5 [[clang::lifetime_capture_by(this)]], // expected-error {{'lifetime_capture_by' argument references unavailable implicit 'this'}}
+ const int &x6 [[clang::lifetime_capture_by()]], // expected-error {{'lifetime_capture_by' attribute specifies no capturing entity}}
+ const int& x7 [[clang::lifetime_capture_by(u,
+ x7)]], // expected-error {{'lifetime_capture_by' argument references itself}}
+ const S& u
+ )
+{
+ s.captureInt(x1);
+}
+
+struct T {
+ void member(
+ const int &x [[clang::lifetime_capture_by(s)]],
+ S &s,
+ S &t,
+ const int &y [[clang::lifetime_capture_by(s)]],
+ const int &z [[clang::lifetime_capture_by(this, x, y)]],
+ const int &u [[clang::lifetime_capture_by(global, x, s)]])
+ {
+ s.captureInt(x);
+ }
+};
+
+struct ThisIsCaptured {
+ void foo(S& s) [[clang::lifetime_capture_by(s)]];
+ void bar(S& s) [[clang::lifetime_capture_by(abcd)]]; // expected-error {{'lifetime_capture_by' attribute argument 'abcd' is not a known function parameter}}
+ void baz(S& s) [[clang::lifetime_capture_by(this)]]; // expected-error {{'lifetime_capture_by' argument references itself}}
+};
+
+///////////////////////////
+// Detect dangling cases.
+///////////////////////////
+void captureInt(const int&x [[clang::lifetime_capture_by(s)]], S&s);
+void noCaptureInt(int x [[clang::lifetime_capture_by(s)]], S&s);
+
+std::string_view substr(const std::string& s [[clang::lifetimebound]]);
+std::string_view strcopy(const std::string& s);
+
+void captureSV(std::string_view x [[clang::lifetime_capture_by(s)]], S&s);
+void noCaptureSV(std::string_view x, S&s);
+
+void use() {
+ S s;
+ int local;
+ captureInt(1, // expected-warning {{object captured by the 's' will be destroyed at the end of the full-expression}}
+ s);
+ captureInt(local, s);
+
+ noCaptureInt(1, s);
+ noCaptureInt(local, s);
+
+ std::string_view local_sv;
+ captureSV(local_sv, s);
+ captureSV(local_sv + local_sv, s);
+ captureSV(std::string(), // expected-warning {{object captured by the 's'}}
+ s);
+ captureSV(substr(
+ std::string() // expected-warning {{object captured by the 's'}}
+ ), s);
+ captureSV(strcopy(std::string()), s);
+
+ noCaptureSV(local_sv, s);
+ noCaptureSV(std::string(), s);
+ noCaptureSV(substr(std::string()), s);
+
+ s.captureInt(1); // expected-warning {{object captured by the 's' will be destroyed at the end of the full-expression}}
+ s.captureSV(std::string()); // expected-warning {{object captured by the 's'}}
+ s.captureSV(substr(std::string())); // expected-warning {{object captured by the 's'}}
+ s.captureSV(strcopy(std::string()));
+}
+} // namespace lifetime_capture_by_usage
+
+// Test for templated code.
+// 2 nested function calls foo(sv, bar(sv, setsv));
\ No newline at end of file
>From 51d7dc80690b4416ee972ed1aef31f78b323923d Mon Sep 17 00:00:00 2001
From: Utkarsh Saxena <usx at google.com>
Date: Tue, 8 Oct 2024 08:19:56 +0000
Subject: [PATCH 2/3] start working on lifetime capture
---
clang/lib/AST/TypePrinter.cpp | 16 ++--------------
clang/lib/Sema/CheckExprLifetime.cpp | 11 ++---------
clang/lib/Sema/CheckExprLifetime.h | 6 ------
clang/test/SemaCXX/attr-lifetimebound.cpp | 1 -
4 files changed, 4 insertions(+), 30 deletions(-)
diff --git a/clang/lib/AST/TypePrinter.cpp b/clang/lib/AST/TypePrinter.cpp
index 02bece7cce51d7..52bf59a9717ecb 100644
--- a/clang/lib/AST/TypePrinter.cpp
+++ b/clang/lib/AST/TypePrinter.cpp
@@ -1910,21 +1910,9 @@ void TypePrinter::printAttributedAfter(const AttributedType *T,
return;
}
if (T->getAttrKind() == attr::LifetimeCaptureBy) {
+ // FIXME: Print the attribute arguments once we have a way to retrieve these
+ // here.
OS << " [[clang::lifetime_capture_by(...)";
- // const LifetimeCaptureByAttr* A= T->getAs<LifetimeCaptureByAttr>();
- // bool valid = true;
- // for (int I : A->params())
- // valid &= I != -2;
- // if (valid) {
- // OS << "invalid)";
- // return;
- // }
- // for (size_t I = 0; I < A->params_size(); ++I) {
- // OS << A->getArgIdents()[I]->getName()
- // << "(idx: " << *(A->params_begin() + I) << ")";
- // if (I != A->params_size() - 1)
- // OS << ", ";
- // }
return;
}
diff --git a/clang/lib/Sema/CheckExprLifetime.cpp b/clang/lib/Sema/CheckExprLifetime.cpp
index c0180add9a1c41..9e58961dfa4226 100644
--- a/clang/lib/Sema/CheckExprLifetime.cpp
+++ b/clang/lib/Sema/CheckExprLifetime.cpp
@@ -50,7 +50,7 @@ enum LifetimeKind {
/// object to it.
LK_Assignment,
- /// The lifetime of a temporary bound to this entity probably ends too soon,
+ /// The lifetime of a temporary bound to this entity may end too soon,
/// because the entity may capture the reference to a temporary object.
LK_LifetimeCapture,
};
@@ -1202,7 +1202,7 @@ static void checkExprLifetimeImpl(Sema &SemaRef,
return false;
assert(shouldLifetimeExtendThroughPath(Path) ==
PathLifetimeKind::NoExtend &&
- "No lifetime extension for in function calls");
+ "No lifetime extension in function calls");
SemaRef.Diag(DiagLoc, diag::warn_dangling_reference_captured)
<< CEntity->Expression << DiagRange;
return false;
@@ -1435,13 +1435,6 @@ void checkInitLifetime(Sema &SemaRef, const InitializedEntity &Entity,
/*AEntity*/ nullptr, Init);
}
-void checkExprLifetimeMustTailArg(Sema &SemaRef,
- const InitializedEntity &Entity, Expr *Init) {
- checkExprLifetimeImpl(SemaRef, &Entity, nullptr, LK_MustTail,
- /*AEntity*/ nullptr, Init);
-}
-
-
void checkAssignmentLifetime(Sema &SemaRef, const CapturingEntity &Entity,
Expr *RHS) {
bool EnableDanglingPointerAssignment = !SemaRef.getDiagnostics().isIgnored(
diff --git a/clang/lib/Sema/CheckExprLifetime.h b/clang/lib/Sema/CheckExprLifetime.h
index 9c5241e9b5d806..98562fba54b188 100644
--- a/clang/lib/Sema/CheckExprLifetime.h
+++ b/clang/lib/Sema/CheckExprLifetime.h
@@ -49,12 +49,6 @@ void checkAssignmentLifetime(Sema &SemaRef, const CapturingEntity &Entity,
void checkCaptureLifetime(Sema &SemaRef, const CapturingEntity &Entity,
Expr *Captured);
-/// Check that the lifetime of the given expr (and its subobjects) is
-/// sufficient, assuming that it is passed as an argument to a musttail
-/// function.
-void checkExprLifetimeMustTailArg(Sema &SemaRef,
- const InitializedEntity &Entity, Expr *Init);
-
} // namespace clang::sema
#endif // LLVM_CLANG_SEMA_CHECK_EXPR_LIFETIME_H
diff --git a/clang/test/SemaCXX/attr-lifetimebound.cpp b/clang/test/SemaCXX/attr-lifetimebound.cpp
index 7a0215fa3c576a..a7f15896e97a88 100644
--- a/clang/test/SemaCXX/attr-lifetimebound.cpp
+++ b/clang/test/SemaCXX/attr-lifetimebound.cpp
@@ -380,7 +380,6 @@ void use() {
std::string_view local_sv;
captureSV(local_sv, s);
- captureSV(local_sv + local_sv, s);
captureSV(std::string(), // expected-warning {{object captured by the 's'}}
s);
captureSV(substr(
>From 37aca98638bee739aa43a7046bf6e83aa61c9d6b Mon Sep 17 00:00:00 2001
From: Utkarsh Saxena <usx at google.com>
Date: Tue, 8 Oct 2024 18:34:53 +0000
Subject: [PATCH 3/3] implement: 'this' is captured.
---
clang/lib/Sema/CheckExprLifetime.cpp | 6 ++++++
clang/lib/Sema/CheckExprLifetime.h | 5 +++++
clang/lib/Sema/SemaChecking.cpp | 19 +++++++++++++++++++
clang/test/SemaCXX/attr-lifetimebound.cpp | 20 +++++++++++++++++---
4 files changed, 47 insertions(+), 3 deletions(-)
diff --git a/clang/lib/Sema/CheckExprLifetime.cpp b/clang/lib/Sema/CheckExprLifetime.cpp
index 9e58961dfa4226..851d4cdb6575f5 100644
--- a/clang/lib/Sema/CheckExprLifetime.cpp
+++ b/clang/lib/Sema/CheckExprLifetime.cpp
@@ -1435,6 +1435,12 @@ void checkInitLifetime(Sema &SemaRef, const InitializedEntity &Entity,
/*AEntity*/ nullptr, Init);
}
+void checkExprLifetimeMustTailArg(Sema &SemaRef,
+ const InitializedEntity &Entity, Expr *Init) {
+ checkExprLifetimeImpl(SemaRef, &Entity, nullptr, LK_MustTail,
+ /*AEntity*/ nullptr, Init);
+}
+
void checkAssignmentLifetime(Sema &SemaRef, const CapturingEntity &Entity,
Expr *RHS) {
bool EnableDanglingPointerAssignment = !SemaRef.getDiagnostics().isIgnored(
diff --git a/clang/lib/Sema/CheckExprLifetime.h b/clang/lib/Sema/CheckExprLifetime.h
index 98562fba54b188..976260d40e46bf 100644
--- a/clang/lib/Sema/CheckExprLifetime.h
+++ b/clang/lib/Sema/CheckExprLifetime.h
@@ -49,6 +49,11 @@ void checkAssignmentLifetime(Sema &SemaRef, const CapturingEntity &Entity,
void checkCaptureLifetime(Sema &SemaRef, const CapturingEntity &Entity,
Expr *Captured);
+/// Check that the lifetime of the given expr (and its subobjects) is
+/// sufficient, assuming that it is passed as an argument to a musttail
+/// function.
+void checkExprLifetimeMustTailArg(Sema &SemaRef,
+ const InitializedEntity &Entity, Expr *Init);
} // namespace clang::sema
#endif // LLVM_CLANG_SEMA_CHECK_EXPR_LIFETIME_H
diff --git a/clang/lib/Sema/SemaChecking.cpp b/clang/lib/Sema/SemaChecking.cpp
index 3f0452875e0e8f..89214afde06a7d 100644
--- a/clang/lib/Sema/SemaChecking.cpp
+++ b/clang/lib/Sema/SemaChecking.cpp
@@ -3246,6 +3246,25 @@ void Sema::checkLifetimeCaptureBy(FunctionDecl *FD, bool IsMemberFunction,
checkCaptureLifetime(*this, CE, Captured);
}
}
+ if (IsMemberFunction) {
+ TypeSourceInfo *TSI = FD->getTypeSourceInfo();
+ if (!TSI)
+ return;
+ AttributedTypeLoc ATL;
+ for (TypeLoc TL = TSI->getTypeLoc();
+ (ATL = TL.getAsAdjusted<AttributedTypeLoc>());
+ TL = ATL.getModifiedLoc()) {
+ auto *CapturedByAttr = ATL.getAttrAs<LifetimeCaptureByAttr>();
+ if (!CapturedByAttr)
+ continue;
+ Expr *Captured = GetArgAt(0);
+ for (int CapturingParamIdx : CapturedByAttr->params()) {
+ Expr *Capturing = GetArgAt(CapturingParamIdx);
+ CapturingEntity CE{Capturing};
+ checkCaptureLifetime(*this, CE, Captured);
+ }
+ }
+ }
}
void Sema::checkCall(NamedDecl *FDecl, const FunctionProtoType *Proto,
diff --git a/clang/test/SemaCXX/attr-lifetimebound.cpp b/clang/test/SemaCXX/attr-lifetimebound.cpp
index a7f15896e97a88..8cd1d4e643b640 100644
--- a/clang/test/SemaCXX/attr-lifetimebound.cpp
+++ b/clang/test/SemaCXX/attr-lifetimebound.cpp
@@ -351,7 +351,7 @@ struct T {
};
struct ThisIsCaptured {
- void foo(S& s) [[clang::lifetime_capture_by(s)]];
+ void capture(S& s) [[clang::lifetime_capture_by(s)]];
void bar(S& s) [[clang::lifetime_capture_by(abcd)]]; // expected-error {{'lifetime_capture_by' attribute argument 'abcd' is not a known function parameter}}
void baz(S& s) [[clang::lifetime_capture_by(this)]]; // expected-error {{'lifetime_capture_by' argument references itself}}
};
@@ -366,9 +366,12 @@ std::string_view substr(const std::string& s [[clang::lifetimebound]]);
std::string_view strcopy(const std::string& s);
void captureSV(std::string_view x [[clang::lifetime_capture_by(s)]], S&s);
+void captureS(const std::string& x [[clang::lifetime_capture_by(s)]], S&s);
void noCaptureSV(std::string_view x, S&s);
void use() {
+ std::string_view local_sv;
+ std::string local_s;
S s;
int local;
captureInt(1, // expected-warning {{object captured by the 's' will be destroyed at the end of the full-expression}}
@@ -378,23 +381,34 @@ void use() {
noCaptureInt(1, s);
noCaptureInt(local, s);
- std::string_view local_sv;
+ // Capture using std::string_view.
captureSV(local_sv, s);
captureSV(std::string(), // expected-warning {{object captured by the 's'}}
s);
captureSV(substr(
std::string() // expected-warning {{object captured by the 's'}}
), s);
+ captureSV(substr(local_s), s);
captureSV(strcopy(std::string()), s);
-
+
noCaptureSV(local_sv, s);
noCaptureSV(std::string(), s);
noCaptureSV(substr(std::string()), s);
+ // Capture using std::string.
+ captureS(std::string(), s); // expected-warning {{object captured by the 's'}}
+ captureS(local_s, s);
+
+ // Member functions.
s.captureInt(1); // expected-warning {{object captured by the 's' will be destroyed at the end of the full-expression}}
s.captureSV(std::string()); // expected-warning {{object captured by the 's'}}
s.captureSV(substr(std::string())); // expected-warning {{object captured by the 's'}}
s.captureSV(strcopy(std::string()));
+
+ // This is captured.
+ ThisIsCaptured{}.capture(s); //expected-warning {{object captured by the 's'}}
+ ThisIsCaptured TIS;
+ TIS.capture(s);
}
} // namespace lifetime_capture_by_usage
More information about the cfe-commits
mailing list