Skip to content

[clang] Introduce [[clang::lifetime_capture_by(X)]] #111499

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
Nov 11, 2024
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
3 changes: 3 additions & 0 deletions clang/docs/ReleaseNotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,9 @@ Attribute Changes in Clang
- Fix a bug where clang doesn't automatically apply the ``[[gsl::Owner]]`` or
``[[gsl::Pointer]]`` to STL explicit template specialization decls. (#GH109442)

- Clang now supports ``[[clang::lifetime_capture_by(X)]]``. Similar to lifetimebound, this can be
used to specify when a reference to a function parameter is captured by another capturing entity ``X``.

Improvements to Clang's diagnostics
-----------------------------------

Expand Down
33 changes: 33 additions & 0 deletions clang/include/clang/Basic/Attr.td
Original file line number Diff line number Diff line change
Expand Up @@ -1889,6 +1889,39 @@ 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 = [LifetimeCaptureByDocs];
let AdditionalMembers = [{
private:
SmallVector<IdentifierInfo*, 1> ArgIdents;
SmallVector<SourceLocation, 1> ArgLocs;

public:
static constexpr int THIS = 0;
Copy link
Collaborator

Choose a reason for hiding this comment

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

I wish we had one universal way of indexing parameters in Clang. I think probably most attributes start from 1, but there are APINotes that start from 0. Not really an actionable comment here, more like a rant.

static constexpr int INVALID = -1;
static constexpr int UNKNOWN = -2;
static constexpr int GLOBAL = -3;

void setArgs(SmallVector<IdentifierInfo*>&& Idents,
SmallVector<SourceLocation>&& Locs) {
assert(Idents.size() == Locs.size());
assert(Idents.size() == params_Size);
ArgIdents = std::move(Idents);
ArgLocs = std::move(Locs);
}

ArrayRef<IdentifierInfo*> getArgIdents() const { return ArgIdents; }
ArrayRef<SourceLocation> 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.
Expand Down
69 changes: 69 additions & 0 deletions clang/include/clang/Basic/AttrDocs.td
Original file line number Diff line number Diff line change
Expand Up @@ -3918,6 +3918,75 @@ have their lifetimes extended.
}];
}

def LifetimeCaptureByDocs : Documentation {
let Category = DocCatFunction;
let Content = [{
Similar to `lifetimebound`_, the ``lifetime_capture_by(X)`` attribute on a function
parameter or implicit object parameter indicates that that objects that are referred to
by that parameter may also be referred to by the capturing entity ``X``.

By default, a reference is considered to refer to its referenced object, a
pointer is considered to refer to its pointee, a ``std::initializer_list<T>``
is considered to refer to its underlying array, and aggregates (arrays and
simple ``struct``\s) are considered to refer to all objects that their
transitive subobjects refer to.

The capturing entity ``X`` can be one of the following:
- Another (named) function parameter.

.. code-block:: c++

void addToSet(std::string_view a [[clang::lifetime_capture_by(s)]], std::set<std::string_view>& s) {
s.insert(a);
}

- ``this`` (in case of member functions).

.. code-block:: c++

class S {
void addToSet(std::string_view a [[clang::lifetime_capture_by(this)]]) {
s.insert(a);
}
std::set<std::string_view> s;
};

- 'global', 'unknown' (without quotes).

.. code-block:: c++

std::set<std::string_view> s;
void addToSet(std::string_view a [[clang::lifetime_capture_by(global)]]) {
s.insert(a);
}
void addSomewhere(std::string_view a [[clang::lifetime_capture_by(unknown)]]);

The attribute can be applied to the implicit ``this`` parameter of a member
function by writing the attribute after the function type:

.. code-block:: c++

struct S {
const char *data(std::set<S*>& s) [[clang::lifetime_capture_by(s)]] {
s.insert(this);
}
};

The attribute supports specifying more than one capturing entities:

.. code-block:: c++

void addToSets(std::string_view a [[clang::lifetime_capture_by(s1, s2)]],
std::set<std::string_view>& s1,
std::set<std::string_view>& s2) {
s1.insert(a);
s2.insert(a);
}

.. _`lifetimebound`: https://clang.llvm.org/docs/AttributeReference.html#lifetimebound
}];
}

def TrivialABIDocs : Documentation {
let Category = DocCatDecl;
let Content = [{
Expand Down
14 changes: 14 additions & 0 deletions clang/include/clang/Basic/DiagnosticSemaKinds.td
Original file line number Diff line number Diff line change
Expand Up @@ -3383,6 +3383,20 @@ 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, 'this', 'global' or 'unknown'">;
def err_capture_by_references_itself : Error<"'lifetime_capture_by' argument references itself">;
def err_capture_by_param_uses_reserved_name : Error<
"parameter cannot be named '%select{global|unknown}0' while using 'lifetime_capture_by(%select{global|unknown}0)'">;

def err_init_method_bad_return_type : Error<
"init methods must return an object pointer type, not %0">;
def err_attribute_invalid_size : Error<
Expand Down
8 changes: 8 additions & 0 deletions clang/include/clang/Sema/Sema.h
Original file line number Diff line number Diff line change
Expand Up @@ -1760,6 +1760,14 @@ class Sema final : public SemaBase {
/// Add [[gsl::Pointer]] attributes for std:: types.
void inferGslPointerAttribute(TypedefNameDecl *TD);

LifetimeCaptureByAttr *ParseLifetimeCaptureByAttr(const ParsedAttr &AL,
StringRef ParamName);
// Processes the argument 'X' in [[clang::lifetime_capture_by(X)]]. Since 'X'
// can be the name of a function parameter, we need to parse the function
// declaration and rest of the parameters before processesing 'X'. Therefore
// do this lazily instead of processing while parsing the annotation itself.
void LazyProcessLifetimeCaptureByParams(FunctionDecl *FD);

/// Add _Nullable attributes for std:: types.
void inferNullableClassAttribute(CXXRecordDecl *CRD);

Expand Down
15 changes: 15 additions & 0 deletions clang/lib/AST/TypePrinter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,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"
Expand Down Expand Up @@ -1909,6 +1910,19 @@ void TypePrinter::printAttributedAfter(const AttributedType *T,
OS << " [[clang::lifetimebound]]";
return;
}
if (T->getAttrKind() == attr::LifetimeCaptureBy) {
OS << " [[clang::lifetime_capture_by(";
if (auto *attr = dyn_cast_or_null<LifetimeCaptureByAttr>(T->getAttr())) {
auto Idents = attr->getArgIdents();
for (unsigned I = 0; I < Idents.size(); ++I) {
OS << Idents[I]->getName();
if (I != Idents.size() - 1)
OS << ", ";
}
}
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
Expand Down Expand Up @@ -1976,6 +1990,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:
Expand Down
1 change: 1 addition & 0 deletions clang/lib/Sema/SemaDecl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16687,6 +16687,7 @@ void Sema::AddKnownFunctionAttributes(FunctionDecl *FD) {
}
}

LazyProcessLifetimeCaptureByParams(FD);
inferLifetimeBoundAttribute(FD);
AddKnownFunctionAttributesForReplaceableGlobalAllocationFunction(FD);

Expand Down
111 changes: 111 additions & 0 deletions clang/lib/Sema/SemaDeclAttr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -3867,6 +3868,113 @@ 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(),
LifetimeCaptureByAttr::INVALID);
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"] = LifetimeCaptureByAttr::GLOBAL;
NameIdxMapping["unknown"] = LifetimeCaptureByAttr::UNKNOWN;
int Idx = 0;
if (HasImplicitThisParam) {
NameIdxMapping["this"] = 0;
Idx++;
}
for (const ParmVarDecl *PVD : FD->parameters())
NameIdxMapping[PVD->getName()] = Idx++;
auto DisallowReservedParams = [&](StringRef Reserved) {
for (const ParmVarDecl *PVD : FD->parameters())
if (PVD->getName() == Reserved)
Diag(PVD->getLocation(), diag::err_capture_by_param_uses_reserved_name)
<< (PVD->getName() == "unknown");
};
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;
}
if (Name == "unknown" || Name == "global")
DisallowReservedParams(Name);
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();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not necessarily for this PR, but I see this kind of loops many times in Sema code. I wonder if this is an indication that TSI should have a more convenient API to get a specific AttributedTypeLoc.

(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
Expand Down Expand Up @@ -6644,6 +6752,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;
Expand Down
13 changes: 13 additions & 0 deletions clang/lib/Sema/SemaType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8609,6 +8609,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) {
Expand Down Expand Up @@ -8770,6 +8779,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.
Expand Down
9 changes: 9 additions & 0 deletions clang/test/AST/attr-lifetime-capture-by.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// RUN: %clang_cc1 %s -ast-dump | FileCheck %s

// Verify that we print the [[clang::lifetime_capture_by(X)]] attribute.

struct S {
void foo(int &a, int &b) [[clang::lifetime_capture_by(a, b, global)]];
};

// CHECK: CXXMethodDecl {{.*}}clang::lifetime_capture_by(a, b, global)
Loading
Loading