[clang] [clang][ThreadSafety] Enable late parse for all capabilities, under the experimental flag (PR #212615)
Jameson Nash via cfe-commits
cfe-commits at lists.llvm.org
Thu Sep 24 08:48:03 PDT 2026
https://github.com/vtjnash updated https://github.com/llvm/llvm-project/pull/212615
>From df2e601645b9b1342458abe05da42b5a0ca31266 Mon Sep 17 00:00:00 2001
From: Jameson Nash <vtjnash at gmail.com>
Date: Tue, 28 Jul 2026 15:34:53 +0000
Subject: [PATCH 1/3] [clang][ThreadSafety] Late parse capability attributes
under the experimental extension
guarded_by, pt_guarded_by, acquired_after and acquired_before are marked
LateAttrParseExperimentalExt, but requires_capability, acquire_capability,
release_capability, assert_capability, try_acquire_capability, locks_excluded
and lock_returned are only LateAttrParseStandard. So within one feature
-fexperimental-late-parse-attributes extends some attributes and not others,
and a requirement cannot name a member declared later in the same record --
including the shape ThreadSafetyAnalysis.rst itself shows:
struct Cache {
Mutex mu;
void (*read)(void) REQUIRES(mu);
};
Mark the remaining seven LateAttrParseExperimentalExt so the family agrees.
Late parsing alone would regress the opposite case. An attribute on a function
pointer declarator routinely names a parameter of the pointee type,
void (*unlock)(struct BDev *bdev) UNLOCK_FUNCTION(bdev->lock);
which the eager path resolved because the prototype's scope was still open. Late
parsing runs at the end of the record, after that scope is popped, so 'bdev' no
longer resolves; Sema/warn-thread-safety-analysis.c covers this. It is not a
hypothetical shape either: the Linux kernel writes it today in blkdev.h,
libata.h, arm_vgic.h and landlock, and it is the only form that stays qualified
per instance.
So keep the prototype's parameters on the LateParsedAttribute and make them
visible again while the arguments are parsed. Two details matter:
- The parameters go into the current scope, not a nested one. A sibling member
of the enclosing record is found only while the record's scope is innermost,
and a callback field's requirement may name either a sibling member or a
pointee parameter; entering a scope for the parameters loses the former.
- Only for a declarator that is not itself a function. A real function keeps
its parameters in scope for its body, so taking them out after the attribute
would break the body -- and ActOnReenterFunctionContext already covers that
case.
Both forms now work, including together in one record.
---
clang/docs/ReleaseNotes.md | 18 ++++++
clang/include/clang/Basic/Attr.td | 14 ++---
clang/include/clang/Parse/Parser.h | 5 ++
clang/include/clang/Sema/Sema.h | 9 +++
clang/lib/Parse/ParseCXXInlineMethods.cpp | 14 +++++
clang/lib/Parse/ParseDecl.cpp | 16 ++++++
clang/lib/Sema/SemaDecl.cpp | 28 +++++++---
clang/test/Sema/thread-safety-late-parse.c | 65 ++++++++++++++++++++++
8 files changed, 154 insertions(+), 15 deletions(-)
create mode 100644 clang/test/Sema/thread-safety-late-parse.c
diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index e5da258b9950a..787971ad4a9b2 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -282,6 +282,24 @@ features cannot lower the translation-unit ABI level;
- Clang now recognizes the `[[gnu::flag_enum]]` attribute and treats it equivalent to `[[clang::flag_enum]]`
+- Under `-fexperimental-late-parse-attributes`, the thread safety capability
+ attributes (`requires_capability`, `acquire_capability`,
+ `release_capability`, `assert_capability`, `try_acquire_capability`,
+ `locks_excluded` and `lock_returned`) are now late parsed, as `guarded_by`
+ and `pt_guarded_by` already were, so a requirement may name a member declared
+ later in the same record:
+
+ ```c++
+ struct Cache {
+ void (*read)(void) REQUIRES(mu); // 'mu' declared below
+ Mutex mu;
+ };
+ ```
+
+ A requirement naming a parameter of the pointee, such as
+ `void (*unlock)(struct BDev *bdev) UNLOCK_FUNCTION(bdev->lock)`, keeps
+ working. Without the flag the forward reference remains an error.
+
### Improvements to Clang's diagnostics
- `-Wfortify-source` now diagnoses when `strlcat`, `__builtin_strlcat`, `strlcpy`, or
diff --git a/clang/include/clang/Basic/Attr.td b/clang/include/clang/Basic/Attr.td
index b9eb41654a81b..8604256e5038b 100644
--- a/clang/include/clang/Basic/Attr.td
+++ b/clang/include/clang/Basic/Attr.td
@@ -4166,7 +4166,7 @@ def AssertCapability : InheritableAttr {
GNU<"assert_exclusive_lock">,
GNU<"assert_shared_lock">];
let Subjects = SubjectList<[Function, Var, Field]>;
- let LateParsed = LateAttrParseStandard;
+ let LateParsed = LateAttrParseExperimentalExt;
let TemplateDependent = 1;
let ParseArgumentsAsUnevaluated = 1;
let InheritEvenIfAlreadyPresent = 1;
@@ -4185,7 +4185,7 @@ def AcquireCapability : InheritableAttr {
GNU<"exclusive_lock_function">,
GNU<"shared_lock_function">];
let Subjects = SubjectList<[Function, Var, Field]>;
- let LateParsed = LateAttrParseStandard;
+ let LateParsed = LateAttrParseExperimentalExt;
let TemplateDependent = 1;
let ParseArgumentsAsUnevaluated = 1;
let InheritEvenIfAlreadyPresent = 1;
@@ -4204,7 +4204,7 @@ def TryAcquireCapability : InheritableAttr {
GNU<"exclusive_trylock_function">,
GNU<"shared_trylock_function">];
let Subjects = SubjectList<[Function, Var, Field]>;
- let LateParsed = LateAttrParseStandard;
+ let LateParsed = LateAttrParseExperimentalExt;
let TemplateDependent = 1;
let ParseArgumentsAsUnevaluated = 1;
let InheritEvenIfAlreadyPresent = 1;
@@ -4223,7 +4223,7 @@ def ReleaseCapability : InheritableAttr {
Clang<"release_generic_capability", 0>,
Clang<"unlock_function", 0>];
let Subjects = SubjectList<[Function, Var, Field]>;
- let LateParsed = LateAttrParseStandard;
+ let LateParsed = LateAttrParseExperimentalExt;
let TemplateDependent = 1;
let ParseArgumentsAsUnevaluated = 1;
let InheritEvenIfAlreadyPresent = 1;
@@ -4245,7 +4245,7 @@ def RequiresCapability : InheritableAttr {
Clang<"shared_locks_required", 0>];
let Args = [VariadicExprArgument<"Args">];
let AcceptsExprPack = 1;
- let LateParsed = LateAttrParseStandard;
+ let LateParsed = LateAttrParseExperimentalExt;
let TemplateDependent = 1;
let ParseArgumentsAsUnevaluated = 1;
let InheritEvenIfAlreadyPresent = 1;
@@ -4312,7 +4312,7 @@ def AcquiredBefore : InheritableAttr {
def LockReturned : InheritableAttr {
let Spellings = [GNU<"lock_returned">];
let Args = [ExprArgument<"Arg">];
- let LateParsed = LateAttrParseStandard;
+ let LateParsed = LateAttrParseExperimentalExt;
let TemplateDependent = 1;
let ParseArgumentsAsUnevaluated = 1;
let Subjects = SubjectList<[Function]>;
@@ -4323,7 +4323,7 @@ def LocksExcluded : InheritableAttr {
let Spellings = [GNU<"locks_excluded">];
let Args = [VariadicExprArgument<"Args">];
let AcceptsExprPack = 1;
- let LateParsed = LateAttrParseStandard;
+ let LateParsed = LateAttrParseExperimentalExt;
let TemplateDependent = 1;
let ParseArgumentsAsUnevaluated = 1;
let InheritEvenIfAlreadyPresent = 1;
diff --git a/clang/include/clang/Parse/Parser.h b/clang/include/clang/Parse/Parser.h
index 6de876b151097..24f1b23992958 100644
--- a/clang/include/clang/Parse/Parser.h
+++ b/clang/include/clang/Parse/Parser.h
@@ -203,6 +203,11 @@ struct LateParsedAttribute : public LateParsedDeclaration {
SourceLocation AttrNameLoc;
SmallVector<Decl *, 2> Decls;
+ /// Parameters of the prototype the attribute was written on, kept because
+ /// late parsing runs after their scope is popped and the arguments may name
+ /// one: 'void (*unlock)(struct BDev *bdev) UNLOCK_FUNCTION(bdev->lock)'.
+ SmallVector<ParmVarDecl *, 4> ProtoParams;
+
private:
Kind K;
diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h
index 5f24adbf9e9cf..7eaf7bb7974cc 100644
--- a/clang/include/clang/Sema/Sema.h
+++ b/clang/include/clang/Sema/Sema.h
@@ -4508,6 +4508,15 @@ class Sema final : public SemaBase {
/// Push the parameters of D, which must be a function, into scope.
void ActOnReenterFunctionContext(Scope *S, Decl *D);
+
+ /// Add \p Params to scope, so a late-parsed attribute can name them.
+ void ActOnReenterFunctionPrototypeParams(Scope *S,
+ ArrayRef<ParmVarDecl *> Params);
+ /// Undo the above. Needed because these go into a scope that outlives the
+ /// attribute, so unlike ActOnReenterFunctionContext no scope pop removes
+ /// them.
+ void ActOnExitFunctionPrototypeParams(Scope *S,
+ ArrayRef<ParmVarDecl *> Params);
void ActOnExitFunctionContext();
/// Add this decl to the scope shadowed decl chains.
diff --git a/clang/lib/Parse/ParseCXXInlineMethods.cpp b/clang/lib/Parse/ParseCXXInlineMethods.cpp
index 35dbec8dfb2f7..73a13f5a179db 100644
--- a/clang/lib/Parse/ParseCXXInlineMethods.cpp
+++ b/clang/lib/Parse/ParseCXXInlineMethods.cpp
@@ -744,9 +744,23 @@ void Parser::ParseLexedAttribute(LateParsedAttribute &LPA, bool EnterScope,
Actions.ActOnReenterFunctionContext(Actions.CurScope, D);
}
+ // For a function pointer field or variable, the arguments may name a
+ // parameter of the pointee. Add them to the current scope rather than a
+ // nested one: while late parsing a record's attributes, a member of that
+ // record resolves only if the record's scope is innermost, and the
+ // attribute could name either a member or a parameter.
+ bool HasProtoParams = !HasFuncScope && !LPA.ProtoParams.empty();
+ if (HasProtoParams)
+ Actions.ActOnReenterFunctionPrototypeParams(Actions.getCurScope(),
+ LPA.ProtoParams);
+
ParsedAttributes Parsed = ParseLexedAttributeTokens(LPA);
Attrs.takeAllAppendingFrom(Parsed);
+ if (HasProtoParams)
+ Actions.ActOnExitFunctionPrototypeParams(Actions.getCurScope(),
+ LPA.ProtoParams);
+
if (HasFuncScope)
Actions.ActOnExitFunctionContext();
} else {
diff --git a/clang/lib/Parse/ParseDecl.cpp b/clang/lib/Parse/ParseDecl.cpp
index 5976f5a7ccdea..b4089a197d33e 100644
--- a/clang/lib/Parse/ParseDecl.cpp
+++ b/clang/lib/Parse/ParseDecl.cpp
@@ -195,6 +195,22 @@ bool Parser::ParseSingleGNUAttribute(ParsedAttributes &Attrs,
// Handle attributes with arguments that require late parsing.
LateParsedAttribute *LA =
new LateParsedAttribute(this, *AttrName, AttrNameLoc);
+
+ // Keep the innermost prototype's parameters available in case they are needed
+ // by late-parsing attributes.
+ if (D && !D->isFunctionDeclarator()) {
+ for (unsigned I = 0, E = D->getNumTypeObjects(); I != E; ++I) {
+ const DeclaratorChunk &Chunk = D->getTypeObject(I);
+ if (Chunk.Kind != DeclaratorChunk::Function)
+ continue;
+ const DeclaratorChunk::FunctionTypeInfo &FTI = Chunk.Fun;
+ for (unsigned P = 0, NumParams = FTI.NumParams; P != NumParams; ++P)
+ if (auto *Param = dyn_cast_or_null<ParmVarDecl>(FTI.Params[P].Param))
+ LA->ProtoParams.push_back(Param);
+ break;
+ }
+ }
+
LateAttrs->push_back(LA);
// Attributes in a class are parsed at the end of the class, along
diff --git a/clang/lib/Sema/SemaDecl.cpp b/clang/lib/Sema/SemaDecl.cpp
index db5e66cb96c3e..457d34bccee2b 100644
--- a/clang/lib/Sema/SemaDecl.cpp
+++ b/clang/lib/Sema/SemaDecl.cpp
@@ -1505,6 +1505,25 @@ void Sema::EnterTemplatedContext(Scope *S, DeclContext *DC) {
}
}
+void Sema::ActOnReenterFunctionPrototypeParams(Scope *S,
+ ArrayRef<ParmVarDecl *> Params) {
+ for (ParmVarDecl *Param : Params)
+ if (Param->getIdentifier()) {
+ S->AddDecl(Param);
+ IdResolver.AddDecl(Param);
+ }
+}
+
+void Sema::ActOnExitFunctionPrototypeParams(Scope *S,
+ ArrayRef<ParmVarDecl *> Params) {
+ // Remove in reverse so each name is the most recent one when it is removed.
+ for (ParmVarDecl *Param : llvm::reverse(Params))
+ if (Param->getIdentifier()) {
+ IdResolver.RemoveDecl(Param);
+ S->RemoveDecl(Param);
+ }
+}
+
void Sema::ActOnReenterFunctionContext(Scope* S, Decl *D) {
// We assume that the caller has already called
// ActOnReenterTemplateScope so getTemplatedDecl() works.
@@ -1519,14 +1538,7 @@ void Sema::ActOnReenterFunctionContext(Scope* S, Decl *D) {
CurContext = FD;
S->setEntity(CurContext);
- for (unsigned P = 0, NumParams = FD->getNumParams(); P < NumParams; ++P) {
- ParmVarDecl *Param = FD->getParamDecl(P);
- // If the parameter has an identifier, then add it to the scope
- if (Param->getIdentifier()) {
- S->AddDecl(Param);
- IdResolver.AddDecl(Param);
- }
- }
+ ActOnReenterFunctionPrototypeParams(S, FD->parameters());
}
void Sema::ActOnExitFunctionContext() {
diff --git a/clang/test/Sema/thread-safety-late-parse.c b/clang/test/Sema/thread-safety-late-parse.c
new file mode 100644
index 0000000000000..54df3c6e45caa
--- /dev/null
+++ b/clang/test/Sema/thread-safety-late-parse.c
@@ -0,0 +1,65 @@
+// Capability attributes are late parsed under -fexperimental-late-parse-attributes,
+// like guarded_by and pt_guarded_by already were, so they can name a member
+// declared later in the same struct.
+//
+// RUN: %clang_cc1 -fexperimental-late-parse-attributes -fsyntax-only -verify=late %s
+// RUN: %clang_cc1 -fsyntax-only -verify=early %s
+
+// late-no-diagnostics
+
+#define REQUIRES(...) __attribute__((requires_capability(__VA_ARGS__)))
+#define ACQUIRE(...) __attribute__((acquire_capability(__VA_ARGS__)))
+#define RELEASE(...) __attribute__((release_capability(__VA_ARGS__)))
+#define ASSERT_CAP(...) __attribute__((assert_capability(__VA_ARGS__)))
+#define TRY_ACQUIRE(...) __attribute__((try_acquire_capability(__VA_ARGS__)))
+#define EXCLUDES(...) __attribute__((locks_excluded(__VA_ARGS__)))
+#define RETURN_CAP(x) __attribute__((lock_returned(x)))
+#define GUARDED_BY(x) __attribute__((guarded_by(x)))
+
+struct __attribute__((capability("mutex"))) Mutex {
+ int dummy;
+};
+
+struct Requires {
+ void (*cb)(void) REQUIRES(mu); // early-error{{use of undeclared identifier 'mu'}}
+ struct Mutex mu;
+};
+
+struct Acquire {
+ void (*cb)(void) ACQUIRE(mu); // early-error{{use of undeclared identifier 'mu'}}
+ struct Mutex mu;
+};
+
+struct Release {
+ void (*cb)(void) RELEASE(mu); // early-error{{use of undeclared identifier 'mu'}}
+ struct Mutex mu;
+};
+
+struct Assert {
+ void (*cb)(void) ASSERT_CAP(mu); // early-error{{use of undeclared identifier 'mu'}}
+ struct Mutex mu;
+};
+
+struct TryAcquire {
+ int (*cb)(void) TRY_ACQUIRE(1, mu); // early-error{{use of undeclared identifier 'mu'}}
+ struct Mutex mu;
+};
+
+struct Excludes {
+ void (*cb)(void) EXCLUDES(mu); // early-error{{use of undeclared identifier 'mu'}}
+ struct Mutex mu;
+};
+
+// guarded_by was already late parsed; it is here to show the family now agrees.
+struct Guarded {
+ int data GUARDED_BY(mu); // early-error{{use of undeclared identifier 'mu'}}
+ struct Mutex mu;
+};
+
+// An attribute after a complete parameter list already sees those parameters
+// without late parsing; this must keep working in both modes.
+struct WithGetter {
+ struct Mutex mu;
+};
+struct Mutex *get_mu(struct WithGetter *w) RETURN_CAP(w->mu);
+void use_getter(struct WithGetter *w) REQUIRES(get_mu(w));
>From 66c962875af276302690ab27d91ef572b4c5b1f5 Mon Sep 17 00:00:00 2001
From: Jameson Nash <vtjnash at gmail.com>
Date: Tue, 28 Jul 2026 16:10:09 +0000
Subject: [PATCH 2/3] [clang][ThreadSafety] Late parse a parameter's attribute
to the end of the prototype
A capability attribute on a parameter routinely names another parameter of the
same prototype, and that parameter may be declared later:
int kref_put_lock(struct kref *kref,
void (*release)(struct kref *) RELEASE(lock),
spinlock_t *lock);
Parameter attributes are parsed eagerly, so the forward reference is a hard
error -- 'use of undeclared identifier lock' -- and the contract Linux's
include/linux/kref.h states in prose ("The @release function will release the
lock") cannot be written down at all. Naming an *earlier* parameter already
works, so the restriction is purely one of parse order.
Give ParseParameterDeclarationClause a LateParsedAttrList and run it after the
parameter loop. Unlike the record case, no scope has to be re-entered: the
prototype scope is still open there, so every parameter is already visible. Each
deferred attribute is bound to its own parameter by DistributeCLateParsedAttrs as
that parameter is created, the same way fields do it. The list is
experimental-ext-only, so this is gated on
-fexperimental-late-parse-attributes and the error is unchanged without it.
The requirement is then honored at the indirect call inside the callee, so a
release through such a callback transfers the lock state and a second release is
reported.
---
clang/docs/ReleaseNotes.md | 11 +++++++++-
clang/lib/Parse/ParseDecl.cpp | 22 ++++++++++++++++++-
clang/test/Sema/thread-safety-late-parse.c | 18 +++++++++++++++
clang/test/Sema/warn-thread-safety-analysis.c | 19 ++++++++++++++++
4 files changed, 68 insertions(+), 2 deletions(-)
diff --git a/clang/docs/ReleaseNotes.md b/clang/docs/ReleaseNotes.md
index 787971ad4a9b2..d6641469feffe 100644
--- a/clang/docs/ReleaseNotes.md
+++ b/clang/docs/ReleaseNotes.md
@@ -296,9 +296,18 @@ features cannot lower the translation-unit ABI level;
};
```
+ and a requirement on a parameter may name another parameter of the same
+ prototype, declared later:
+
+ ```c++
+ int kref_put_lock(struct kref *kref,
+ void (*release)(struct kref *) RELEASE(lock),
+ spinlock_t *lock);
+ ```
+
A requirement naming a parameter of the pointee, such as
`void (*unlock)(struct BDev *bdev) UNLOCK_FUNCTION(bdev->lock)`, keeps
- working. Without the flag the forward reference remains an error.
+ working. Without the flag both forward references remain errors.
### Improvements to Clang's diagnostics
diff --git a/clang/lib/Parse/ParseDecl.cpp b/clang/lib/Parse/ParseDecl.cpp
index b4089a197d33e..bec5e30d620b5 100644
--- a/clang/lib/Parse/ParseDecl.cpp
+++ b/clang/lib/Parse/ParseDecl.cpp
@@ -7604,6 +7604,18 @@ void Parser::ParseParameterDeclarationClause(
AllowImplicitTypename = ImplicitTypenameContext::Yes;
}
+ // A capability attribute on a parameter may name another parameter of the
+ // same prototype, declared later:
+ //
+ // int kref_put_lock(struct kref *kref,
+ // void (*release)(struct kref *) RELEASE(lock),
+ // spinlock_t *lock);
+ //
+ // Defer those to the end of the clause, where every parameter is declared and
+ // the prototype scope is still open, so no scope need be re-entered.
+ LateParsedAttrList LateParamAttrs(/*PSoon=*/true,
+ /*LateAttrParseExperimentalExtOnly=*/true);
+
do {
// FIXME: Issue a diagnostic if we parsed an attribute-specifier-seq
// before deciding this was a parameter-declaration-clause.
@@ -7661,7 +7673,7 @@ void Parser::ParseParameterDeclarationClause(
ParmDeclarator.SetRangeBegin(ThisLoc);
// Parse GNU attributes, if present.
- MaybeParseGNUAttributes(ParmDeclarator);
+ MaybeParseGNUAttributes(ParmDeclarator, &LateParamAttrs);
if (getLangOpts().HLSL)
MaybeParseHLSLAnnotations(DS.getAttributes());
@@ -7741,6 +7753,8 @@ void Parser::ParseParameterDeclarationClause(
// added to the current scope.
Decl *Param =
Actions.ActOnParamDeclarator(getCurScope(), ParmDeclarator, ThisLoc);
+ // Claim deferred attributes now, before the next parameter can.
+ DistributeCLateParsedAttrs(Param, &LateParamAttrs);
// Parse the default argument, if any. We parse the default
// arguments in all dialects; the semantic analysis in
// ActOnParamDefaultArgument will reject the default argument in
@@ -7853,6 +7867,12 @@ void Parser::ParseParameterDeclarationClause(
// If the next token is a comma, consume it and keep reading arguments.
} while (TryConsumeToken(tok::comma));
+
+ // Every parameter is declared and still in scope, so a deferred attribute can
+ // name any of them.
+ if (!LateParamAttrs.empty())
+ ParseLexedAttributeList(LateParamAttrs, /*D=*/nullptr, /*EnterScope=*/false,
+ /*OnDefinition=*/false);
}
void Parser::ParseBracketDeclarator(Declarator &D) {
diff --git a/clang/test/Sema/thread-safety-late-parse.c b/clang/test/Sema/thread-safety-late-parse.c
index 54df3c6e45caa..2af02fa7efdc0 100644
--- a/clang/test/Sema/thread-safety-late-parse.c
+++ b/clang/test/Sema/thread-safety-late-parse.c
@@ -63,3 +63,21 @@ struct WithGetter {
};
struct Mutex *get_mu(struct WithGetter *w) RETURN_CAP(w->mu);
void use_getter(struct WithGetter *w) REQUIRES(get_mu(w));
+
+// An attribute on a parameter may name another parameter of the same prototype,
+// including one declared later -- the kref_put_lock() shape.
+void put_later(void (*release)(int) RELEASE(mu), // early-error{{use of undeclared identifier 'mu'}}
+ struct Mutex *mu);
+
+// Naming an earlier parameter needs no late parsing; it works in both modes.
+void put_earlier(struct Mutex *mu, void (*release)(int) RELEASE(mu));
+
+// The requirement may also name a member reached through a later parameter.
+struct Holder {
+ struct Mutex mu;
+};
+void put_member(void (*release)(int) RELEASE(&h->mu), // early-error{{use of undeclared identifier 'h'}}
+ struct Holder *h);
+
+// A parameter of the pointee type is still resolved, in both modes.
+void pointee_param(void (*release)(struct Holder *inner) RELEASE(&inner->mu));
diff --git a/clang/test/Sema/warn-thread-safety-analysis.c b/clang/test/Sema/warn-thread-safety-analysis.c
index c61152d59a7dd..d88c736b5f08f 100644
--- a/clang/test/Sema/warn-thread-safety-analysis.c
+++ b/clang/test/Sema/warn-thread-safety-analysis.c
@@ -376,6 +376,25 @@ void test_bdev_ops_fail(struct BDevOps *ops, struct BDev *bdev) {
ops->unlock(bdev); // expected-warning {{releasing mutex 'bdev->lock' that was not held}}
}
+#ifdef LATE_PARSING
+// A requirement on a parameter may name another parameter declared later. The
+// deferred attribute has to end up on the parameter it was written on and stay
+// live, so check that the call through it is really honored rather than merely
+// accepted: the release consumes the lock, making the second one unheld.
+void late_param_cb(void (*release)(struct Mutex *) UNLOCK_FUNCTION(mu),
+ struct Mutex *mu) EXCLUSIVE_LOCKS_REQUIRED(mu) { // expected-note {{mutex acquired here}}
+ release(mu); // expected-note {{mutex released here}}
+ mutex_exclusive_unlock(mu); // expected-warning {{releasing mutex 'mu' that was not held}}
+} // expected-warning {{expecting mutex 'mu' to be held at the end of function}}
+
+// It names 'mu', not the neighbouring parameter: only 'mu' is consumed.
+void late_param_other(void (*release)(struct Mutex *) UNLOCK_FUNCTION(mu),
+ struct Mutex *other, struct Mutex *mu)
+ EXCLUSIVE_LOCKS_REQUIRED(mu, other) { // expected-note {{mutex acquired here}}
+ release(mu);
+} // expected-warning {{expecting mutex 'mu' to be held at the end of function}}
+#endif
+
// Test unusual trylock patterns
void do_some_work(void);
int work_data GUARDED_BY(mu1);
>From b397b7fe4977b37eb76fe6e6f01af05103776c61 Mon Sep 17 00:00:00 2001
From: Jameson Nash <vtjnash at gmail.com>
Date: Wed, 23 Sep 2026 15:54:44 +0000
Subject: [PATCH 3/3] [clang][ThreadSafety] Address review comments
- Rename the test to warn-thread-safety-late-parsing.c, reword its header so
it describes the feature rather than the change, and add the suggested cases:
a parameter of function type, one attribute naming both a pointee parameter
and a later sibling member, and nested function declarators.
- Add SemaCXX/warn-thread-safety-late-parsing.cpp, covering functions,
lambdas, member functions, templates and class members.
- Describe the parameter-clause deferral in ParseDecl.cpp without quoting
kernel code.
The new tests found bugs in re-entering a pointee's parameters, each checked
against a compiler built without this PR:
- In C++, the parameters were added to the class's own scope, where lookup
took them for members of the class and the access check asserted. A class
field's attributes are late parsed in C++ even without the flag, so
'void (*cb)(Holder *h) REQUIRES(h->mu);' in a class crashed. Re-enter them
in a nested prototype scope instead, as there was when the attribute was
written. The same applies to a parameter clause, where adding a pointee's
parameter to the clause's own scope made it ambiguous with a later
parameter of the same name.
- A C struct still needs them in the struct's scope, since ActOnIdExpression
finds its members only while that scope is innermost. There, member lookup
took a member over a pointee parameter of the same name, silently binding
the attribute to the member; a parameter in that scope now shadows the
member, as when the attribute was written.
- In a C++ class, a pointee's parameters were never in scope before, so a
name that now binds to one used to bind to a member or a global of that
name. Reject that as ambiguous rather than silently change its meaning.
- Template instantiation cannot map a pointee's parameters (an existing
problem for eagerly parsed attributes in a function template too), nor a
parameter declared after the one the attribute is on, which is instantiated
after that parameter's attributes. Both asserted on instantiation. Names
bind the same way in a template as elsewhere, but an attribute naming such
a parameter is now rejected with an error.
- A parameter declared with a function type ('void release(struct H *h)
RELEASE(&h->mu)') is a function declarator, so its parameters were not kept.
It is adjusted to a pointer, so keep them in a prototype context.
Co-Authored-By: Claude Opus 5.5 <noreply at anthropic.com>
---
.../clang/Basic/DiagnosticParseKinds.td | 3 +
clang/include/clang/Parse/Parser.h | 6 ++
clang/include/clang/Sema/Sema.h | 5 +-
clang/lib/Parse/ParseCXXInlineMethods.cpp | 99 +++++++++++++++++--
clang/lib/Parse/ParseDecl.cpp | 23 +++--
clang/lib/Sema/SemaExpr.cpp | 9 +-
clang/test/Sema/warn-thread-safety-analysis.c | 18 ++++
...se.c => warn-thread-safety-late-parsing.c} | 63 ++++++++++--
.../warn-thread-safety-late-parsing.cpp | 89 +++++++++++++++++
9 files changed, 284 insertions(+), 31 deletions(-)
rename clang/test/Sema/{thread-safety-late-parse.c => warn-thread-safety-late-parsing.c} (51%)
create mode 100644 clang/test/SemaCXX/warn-thread-safety-late-parsing.cpp
diff --git a/clang/include/clang/Basic/DiagnosticParseKinds.td b/clang/include/clang/Basic/DiagnosticParseKinds.td
index 594d158d25e92..7665688686b5d 100644
--- a/clang/include/clang/Basic/DiagnosticParseKinds.td
+++ b/clang/include/clang/Basic/DiagnosticParseKinds.td
@@ -324,6 +324,9 @@ def warn_gcc_variable_decl_in_for_loop : Warning<
def warn_attribute_no_decl : Warning<
"attribute %0 ignored, because it is not attached to a declaration">,
InGroup<IgnoredAttributes>;
+def err_late_attribute_param_in_template : Error<
+ "%0 attribute in a template cannot name "
+ "%select{pointee function parameter|later parameter}1 %2">;
def err_ms_attributes_not_enabled : Error<
"'__declspec' attributes are not enabled; use '-fdeclspec' or "
"'-fms-extensions' to enable support for __declspec attributes">;
diff --git a/clang/include/clang/Parse/Parser.h b/clang/include/clang/Parse/Parser.h
index 24f1b23992958..0eebe1baefb1e 100644
--- a/clang/include/clang/Parse/Parser.h
+++ b/clang/include/clang/Parse/Parser.h
@@ -1533,6 +1533,12 @@ class Parser : public CodeCompletionHandler {
/// attributes. Shared implementation used by both ParseLexedAttribute and
/// ParseLexedTypeAttribute.
ParsedAttributes ParseLexedAttributeTokens(LateParsedAttribute &LPA);
+ /// Diagnose a late-parsed attribute naming a parameter that it cannot, and
+ /// return true if it must be dropped.
+ bool checkLateAttributeParamRefs(const LateParsedAttribute &LPA,
+ const Decl *D,
+ ArrayRef<const DeclRefExpr *> ParamRefs,
+ bool ReenteredProtoParams);
/// Helper function to move LateParsedTypeAttribute pointers from one list
/// to another. Filters type attributes from \p From and appends them to \p
diff --git a/clang/include/clang/Sema/Sema.h b/clang/include/clang/Sema/Sema.h
index 7eaf7bb7974cc..1dd6f475ae13b 100644
--- a/clang/include/clang/Sema/Sema.h
+++ b/clang/include/clang/Sema/Sema.h
@@ -4512,9 +4512,8 @@ class Sema final : public SemaBase {
/// Add \p Params to scope, so a late-parsed attribute can name them.
void ActOnReenterFunctionPrototypeParams(Scope *S,
ArrayRef<ParmVarDecl *> Params);
- /// Undo the above. Needed because these go into a scope that outlives the
- /// attribute, so unlike ActOnReenterFunctionContext no scope pop removes
- /// them.
+ /// Undo the above, for when \p S outlives the attribute so no scope pop
+ /// removes them.
void ActOnExitFunctionPrototypeParams(Scope *S,
ArrayRef<ParmVarDecl *> Params);
void ActOnExitFunctionContext();
diff --git a/clang/lib/Parse/ParseCXXInlineMethods.cpp b/clang/lib/Parse/ParseCXXInlineMethods.cpp
index 73a13f5a179db..2941c59960833 100644
--- a/clang/lib/Parse/ParseCXXInlineMethods.cpp
+++ b/clang/lib/Parse/ParseCXXInlineMethods.cpp
@@ -12,10 +12,12 @@
#include "clang/AST/DeclTemplate.h"
#include "clang/Basic/DiagnosticParse.h"
+#include "clang/Basic/DiagnosticSema.h"
#include "clang/Parse/Parser.h"
#include "clang/Parse/RAIIObjectsForParser.h"
#include "clang/Sema/DeclSpec.h"
#include "clang/Sema/EnterExpressionEvaluationContext.h"
+#include "clang/Sema/Lookup.h"
#include "clang/Sema/Scope.h"
#include "llvm/ADT/ScopeExit.h"
@@ -715,6 +717,79 @@ void Parser::ParseLexedAttributeList(LateParsedAttrList &LAs, Decl *D,
LAs.clear();
}
+/// Collect the references to parameters in \p Attrs' arguments.
+static void collectParamRefs(const ParsedAttributes &Attrs,
+ SmallVectorImpl<const DeclRefExpr *> &Refs) {
+ SmallVector<const Stmt *, 8> Worklist;
+ for (const ParsedAttr &AL : Attrs)
+ for (unsigned I = 0, E = AL.getNumArgs(); I != E; ++I)
+ if (AL.isArgExpr(I))
+ Worklist.push_back(AL.getArgAsExpr(I));
+ while (!Worklist.empty()) {
+ const Stmt *S = Worklist.pop_back_val();
+ if (!S)
+ continue;
+ if (const auto *DRE = dyn_cast<DeclRefExpr>(S);
+ DRE && isa<ParmVarDecl>(DRE->getDecl()))
+ Refs.push_back(DRE);
+ llvm::append_range(Worklist, S->children());
+ }
+}
+
+bool Parser::checkLateAttributeParamRefs(
+ const LateParsedAttribute &LPA, const Decl *D,
+ ArrayRef<const DeclRefExpr *> ParamRefs, bool ReenteredProtoParams) {
+ auto IsPointeeParam = [&](const ParmVarDecl *PVD) {
+ return ReenteredProtoParams && llvm::is_contained(LPA.ProtoParams, PVD);
+ };
+
+ // In a C++ class, attributes were always late parsed and a pointee's
+ // parameters were not in scope, so a name that now binds to one used to bind
+ // to whatever else it names outside the prototype. Rather than silently
+ // change its meaning, reject the attribute as ambiguous.
+ if (getLangOpts().CPlusPlus && getCurScope()->isClassScope()) {
+ for (const DeclRefExpr *DRE : ParamRefs) {
+ const auto *PVD = cast<ParmVarDecl>(DRE->getDecl());
+ if (!IsPointeeParam(PVD))
+ continue;
+ LookupResult R(Actions, PVD->getDeclName(), DRE->getLocation(),
+ Sema::LookupOrdinaryName);
+ if (!Actions.LookupName(R, getCurScope()) || R.isAmbiguous())
+ continue;
+ Diag(DRE->getLocation(), diag::err_ambiguous_reference)
+ << PVD->getDeclName();
+ Diag(PVD->getLocation(), diag::note_ambiguous_candidate) << PVD;
+ for (const NamedDecl *Other : R)
+ Diag(Other->getLocation(), diag::note_ambiguous_candidate) << Other;
+ return true;
+ }
+ }
+
+ // A name binds the same way in a template, but instantiation cannot yet map a
+ // pointee's parameters, which are instantiated with the declaration's type
+ // and not kept, nor a parameter declared after the one the attribute is on,
+ // which is instantiated after that parameter's attributes. Reject the
+ // attribute rather than instantiate it wrongly.
+ // FIXME: Map these parameters during template instantiation.
+ if (Actions.CurContext->isDependentContext() ||
+ getCurScope()->getTemplateParamParent() ||
+ Actions.getCurGenericLambda()) {
+ const SourceManager &SM = PP.getSourceManager();
+ for (const DeclRefExpr *DRE : ParamRefs) {
+ const auto *PVD = cast<ParmVarDecl>(DRE->getDecl());
+ bool Pointee = IsPointeeParam(PVD);
+ if (!Pointee &&
+ !(isa<ParmVarDecl>(D) &&
+ SM.isBeforeInTranslationUnit(D->getLocation(), PVD->getLocation())))
+ continue;
+ Diag(DRE->getLocation(), diag::err_late_attribute_param_in_template)
+ << &LPA.AttrName << !Pointee << PVD;
+ return true;
+ }
+ }
+ return false;
+}
+
void Parser::ParseLexedAttribute(LateParsedAttribute &LPA, bool EnterScope,
bool OnDefinition,
ParsedAttributes *OutAttrs) {
@@ -745,21 +820,33 @@ void Parser::ParseLexedAttribute(LateParsedAttribute &LPA, bool EnterScope,
}
// For a function pointer field or variable, the arguments may name a
- // parameter of the pointee. Add them to the current scope rather than a
- // nested one: while late parsing a record's attributes, a member of that
- // record resolves only if the record's scope is innermost, and the
- // attribute could name either a member or a parameter.
+ // parameter of the pointee. Re-enter them in a prototype scope nested as it
+ // was when the attribute was written, so they shadow any outer declaration
+ // of the same name. In a C struct, ActOnIdExpression finds the struct's
+ // members only while its scope is innermost, so add the parameters to that
+ // scope instead; it lets them shadow a member of the same name.
bool HasProtoParams = !HasFuncScope && !LPA.ProtoParams.empty();
+ bool InCStruct = !IsCPlusPlus && getCurScope()->isClassScope();
+ ParseScope ProtoScope(this,
+ Scope::FunctionPrototypeScope | Scope::DeclScope,
+ HasProtoParams && !InCStruct);
if (HasProtoParams)
Actions.ActOnReenterFunctionPrototypeParams(Actions.getCurScope(),
LPA.ProtoParams);
ParsedAttributes Parsed = ParseLexedAttributeTokens(LPA);
- Attrs.takeAllAppendingFrom(Parsed);
- if (HasProtoParams)
+ if (HasProtoParams && InCStruct)
Actions.ActOnExitFunctionPrototypeParams(Actions.getCurScope(),
LPA.ProtoParams);
+ ProtoScope.Exit();
+
+ SmallVector<const DeclRefExpr *, 4> ParamRefs;
+ collectParamRefs(Parsed, ParamRefs);
+ if (!ParamRefs.empty() &&
+ checkLateAttributeParamRefs(LPA, D, ParamRefs, HasProtoParams))
+ Parsed.clear();
+ Attrs.takeAllAppendingFrom(Parsed);
if (HasFuncScope)
Actions.ActOnExitFunctionContext();
diff --git a/clang/lib/Parse/ParseDecl.cpp b/clang/lib/Parse/ParseDecl.cpp
index bec5e30d620b5..0c97dfdb064dc 100644
--- a/clang/lib/Parse/ParseDecl.cpp
+++ b/clang/lib/Parse/ParseDecl.cpp
@@ -197,15 +197,18 @@ bool Parser::ParseSingleGNUAttribute(ParsedAttributes &Attrs,
new LateParsedAttribute(this, *AttrName, AttrNameLoc);
// Keep the innermost prototype's parameters available in case they are needed
- // by late-parsing attributes.
- if (D && !D->isFunctionDeclarator()) {
+ // by late-parsing attributes. A function keeps its own parameters in scope,
+ // so skip it; a parameter of function type is adjusted to a pointer, so keep
+ // it.
+ if (D && (!D->isFunctionDeclarator() || D->isPrototypeContext())) {
for (unsigned I = 0, E = D->getNumTypeObjects(); I != E; ++I) {
const DeclaratorChunk &Chunk = D->getTypeObject(I);
if (Chunk.Kind != DeclaratorChunk::Function)
continue;
const DeclaratorChunk::FunctionTypeInfo &FTI = Chunk.Fun;
for (unsigned P = 0, NumParams = FTI.NumParams; P != NumParams; ++P)
- if (auto *Param = dyn_cast_or_null<ParmVarDecl>(FTI.Params[P].Param))
+ if (auto *Param = dyn_cast_or_null<ParmVarDecl>(FTI.Params[P].Param);
+ Param && Param->getIdentifier())
LA->ProtoParams.push_back(Param);
break;
}
@@ -7604,15 +7607,11 @@ void Parser::ParseParameterDeclarationClause(
AllowImplicitTypename = ImplicitTypenameContext::Yes;
}
- // A capability attribute on a parameter may name another parameter of the
- // same prototype, declared later:
- //
- // int kref_put_lock(struct kref *kref,
- // void (*release)(struct kref *) RELEASE(lock),
- // spinlock_t *lock);
- //
- // Defer those to the end of the clause, where every parameter is declared and
- // the prototype scope is still open, so no scope need be re-entered.
+ // An attribute on a parameter may name another parameter of the same
+ // prototype that is declared later, such as a callback parameter whose
+ // attribute names a lock passed after it. Defer those to the end of the
+ // clause, where every parameter is declared and the prototype scope is still
+ // open, so no scope need be re-entered.
LateParsedAttrList LateParamAttrs(/*PSoon=*/true,
/*LateAttrParseExperimentalExtOnly=*/true);
diff --git a/clang/lib/Sema/SemaExpr.cpp b/clang/lib/Sema/SemaExpr.cpp
index be1dc9f85d4f7..9e0c260b5d342 100644
--- a/clang/lib/Sema/SemaExpr.cpp
+++ b/clang/lib/Sema/SemaExpr.cpp
@@ -2916,7 +2916,14 @@ ExprResult Sema::ActOnIdExpression(Scope *S, CXXScopeSpec &SS,
// This specially handles arguments of attributes appertains to a type of C
// struct field such that the name lookup within a struct finds the member
// name, which is not the case for other contexts in C.
- if (isAttrContext() && !getLangOpts().CPlusPlus && S->isClassScope()) {
+ // A late-parsed attribute on a function pointer field has the pointee's
+ // parameters in the struct's scope too; as when the attribute was written, a
+ // parameter shadows a field of the same name.
+ if (isAttrContext() && !getLangOpts().CPlusPlus && S->isClassScope() &&
+ llvm::none_of(S->decls(), [&](Decl *D) {
+ return isa<ParmVarDecl>(D) &&
+ cast<ParmVarDecl>(D)->getDeclName() == NameInfo.getName();
+ })) {
// See if this is reference to a field of struct.
LookupResult R(*this, NameInfo, LookupMemberName);
// LookupName handles a name lookup from within anonymous struct.
diff --git a/clang/test/Sema/warn-thread-safety-analysis.c b/clang/test/Sema/warn-thread-safety-analysis.c
index d88c736b5f08f..2715478a35f13 100644
--- a/clang/test/Sema/warn-thread-safety-analysis.c
+++ b/clang/test/Sema/warn-thread-safety-analysis.c
@@ -376,6 +376,24 @@ void test_bdev_ops_fail(struct BDevOps *ops, struct BDev *bdev) {
ops->unlock(bdev); // expected-warning {{releasing mutex 'bdev->lock' that was not held}}
}
+// A pointee parameter shadows a member of the same name, in both modes: the
+// release is of the argument, not of 'ops->mu'.
+struct ShadowOps {
+ void (*unlock)(struct Mutex *mu) UNLOCK_FUNCTION(mu);
+ struct Mutex mu;
+};
+
+void test_shadow_ops(struct ShadowOps *ops, struct Mutex *m) {
+ ops->unlock(m); // expected-warning {{releasing mutex 'm' that was not held}}
+}
+
+// Likewise a later parameter of the same name: 'release' releases its own
+// argument, leaving the enclosing function's 'mu' held.
+void shadow_param(void (*release)(struct Mutex *mu) UNLOCK_FUNCTION(mu),
+ struct Mutex *mu) EXCLUSIVE_LOCKS_REQUIRED(mu) {
+ release(&mu1); // expected-warning {{releasing mutex 'mu1' that was not held}}
+}
+
#ifdef LATE_PARSING
// A requirement on a parameter may name another parameter declared later. The
// deferred attribute has to end up on the parameter it was written on and stay
diff --git a/clang/test/Sema/thread-safety-late-parse.c b/clang/test/Sema/warn-thread-safety-late-parsing.c
similarity index 51%
rename from clang/test/Sema/thread-safety-late-parse.c
rename to clang/test/Sema/warn-thread-safety-late-parsing.c
index 2af02fa7efdc0..83cc666b38f4a 100644
--- a/clang/test/Sema/thread-safety-late-parse.c
+++ b/clang/test/Sema/warn-thread-safety-late-parsing.c
@@ -1,11 +1,10 @@
-// Capability attributes are late parsed under -fexperimental-late-parse-attributes,
-// like guarded_by and pt_guarded_by already were, so they can name a member
-// declared later in the same struct.
+// Under -fexperimental-late-parse-attributes, capability attributes are late
+// parsed, so they can name a member declared later in the same struct or a
+// parameter declared later in the same prototype. Without it, those forward
+// references are errors.
//
-// RUN: %clang_cc1 -fexperimental-late-parse-attributes -fsyntax-only -verify=late %s
-// RUN: %clang_cc1 -fsyntax-only -verify=early %s
-
-// late-no-diagnostics
+// RUN: %clang_cc1 -fexperimental-late-parse-attributes -fsyntax-only -verify=both %s
+// RUN: %clang_cc1 -fsyntax-only -verify=both,early %s
#define REQUIRES(...) __attribute__((requires_capability(__VA_ARGS__)))
#define ACQUIRE(...) __attribute__((acquire_capability(__VA_ARGS__)))
@@ -50,7 +49,7 @@ struct Excludes {
struct Mutex mu;
};
-// guarded_by was already late parsed; it is here to show the family now agrees.
+// guarded_by behaves the same as the rest of the family.
struct Guarded {
int data GUARDED_BY(mu); // early-error{{use of undeclared identifier 'mu'}}
struct Mutex mu;
@@ -65,7 +64,7 @@ struct Mutex *get_mu(struct WithGetter *w) RETURN_CAP(w->mu);
void use_getter(struct WithGetter *w) REQUIRES(get_mu(w));
// An attribute on a parameter may name another parameter of the same prototype,
-// including one declared later -- the kref_put_lock() shape.
+// including one declared later.
void put_later(void (*release)(int) RELEASE(mu), // early-error{{use of undeclared identifier 'mu'}}
struct Mutex *mu);
@@ -81,3 +80,49 @@ void put_member(void (*release)(int) RELEASE(&h->mu), // early-error{{use of und
// A parameter of the pointee type is still resolved, in both modes.
void pointee_param(void (*release)(struct Holder *inner) RELEASE(&inner->mu));
+
+// A parameter declared with a function type is adjusted to a function pointer;
+// both a later parameter and a pointee parameter resolve the same way.
+void put_later_decayed(void release(int) RELEASE(mu), // early-error{{use of undeclared identifier 'mu'}}
+ struct Mutex *mu);
+void pointee_decayed(void release(struct Holder *inner) RELEASE(&inner->mu));
+
+// One attribute may name both a pointee parameter and a later sibling member.
+struct Both {
+ void (*cb)(struct Mutex *pm) REQUIRES(pm, sm); // early-error{{use of undeclared identifier 'sm'}}
+ struct Mutex sm;
+};
+
+// For nested function declarators, the parameters of the innermost prototype
+// are the ones in scope, in both modes.
+void nested(void (*(*f)(struct Mutex *m))(int) RELEASE(m));
+
+// A pointee parameter shadows a later member or parameter of the same name, and
+// may be named together with a later parameter. Which declaration each name
+// binds to is checked in warn-thread-safety-analysis.c.
+struct ShadowMember {
+ void (*cb)(struct Holder *mu) REQUIRES(&mu->mu);
+ struct Mutex mu;
+};
+void shadow_param(void (*release)(struct Holder *mu) RELEASE(&mu->mu),
+ struct Mutex *mu);
+void pointee_and_later(void (*release)(struct Holder *h) RELEASE(&h->mu, mu), // early-error{{use of undeclared identifier 'mu'}}
+ struct Mutex *mu);
+
+// A pointee's parameters are in scope only for its own attributes.
+struct Leak {
+ void (*cb)(struct Mutex *p) REQUIRES(p);
+ int x GUARDED_BY(p); // both-error{{use of undeclared identifier 'p'}}
+};
+void leak(void (*r)(struct Mutex *p) RELEASE(p),
+ void (*s)(int) RELEASE(p)); // both-error{{use of undeclared identifier 'p'}}
+struct Multi {
+ void (*a)(struct Mutex *p) REQUIRES(p),
+ (*b)(struct Mutex *q) REQUIRES(q, p); // both-error{{use of undeclared identifier 'p'}}
+};
+
+// Outside a struct, and with unnamed parameters alongside.
+void (*global_cb)(struct Mutex *p) REQUIRES(p);
+struct Unnamed {
+ void (*cb)(int, struct Mutex *p, int) REQUIRES(p);
+};
diff --git a/clang/test/SemaCXX/warn-thread-safety-late-parsing.cpp b/clang/test/SemaCXX/warn-thread-safety-late-parsing.cpp
new file mode 100644
index 0000000000000..72785faa96129
--- /dev/null
+++ b/clang/test/SemaCXX/warn-thread-safety-late-parsing.cpp
@@ -0,0 +1,89 @@
+// Under -fexperimental-late-parse-attributes, an attribute on a parameter may
+// name a parameter declared later in the same prototype. Without it, that
+// forward reference is an error.
+//
+// RUN: %clang_cc1 -fexperimental-late-parse-attributes -fsyntax-only -verify=both,late -Wthread-safety -std=c++20 %s
+// RUN: %clang_cc1 -fsyntax-only -verify=both,early -Wthread-safety -std=c++20 %s
+
+#define RELEASE(...) __attribute__((release_capability(__VA_ARGS__)))
+#define REQUIRES(...) __attribute__((requires_capability(__VA_ARGS__)))
+
+class __attribute__((capability("mutex"))) Mutex {};
+
+struct Holder {
+ Mutex lock;
+};
+
+void put_later(void (*release)(int) RELEASE(mu), // early-error{{use of undeclared identifier 'mu'}}
+ Mutex *mu);
+
+// A lambda's parameter clause is parsed the same way.
+auto lambda = [](void (*release)(int) RELEASE(mu), // early-error{{use of undeclared identifier 'mu'}}
+ Mutex *mu) {};
+
+struct Methods {
+ void put_later(void (*release)(int) RELEASE(mu), // early-error{{use of undeclared identifier 'mu'}}
+ Mutex *mu);
+ // A class's attributes are late parsed in C++ regardless of the flag; one may
+ // name a pointee parameter and a member of the class together.
+ void (*cb)(Holder *h) REQUIRES(h->lock, own);
+ Mutex own;
+};
+
+
+void default_arg(void (*release)(int) RELEASE(mu), // early-error{{use of undeclared identifier 'mu'}}
+ Mutex *mu = nullptr);
+
+// A class's attributes were always late parsed, when a pointee's parameters
+// were not in scope, so a name that would now bind to one instead of to a
+// member or a global is ambiguous rather than silently rebound.
+Mutex global_mu; // both-note{{candidate found by name lookup is 'global_mu'}}
+struct Scoping {
+ void (*shadow)(Holder *own) REQUIRES(own->lock); // both-error{{reference to 'own' is ambiguous}} \
+ // both-note{{candidate found by name lookup is 'own'}}
+ Holder *own; // both-note{{candidate found by name lookup is 'Scoping::own'}}
+ void (*shadow_global)(Mutex *global_mu) REQUIRES(global_mu); // both-error{{reference to 'global_mu' is ambiguous}} \
+ // both-note{{candidate found by name lookup is 'global_mu'}}
+ // It is in scope only for its own attributes.
+ void (*cb)(Holder *p) REQUIRES(p->lock);
+ int x __attribute__((guarded_by(p->lock))); // both-error{{use of undeclared identifier 'p'}}
+};
+
+// A nested class's attribute may name a pointee parameter and a member of the
+// enclosing class.
+struct Outer {
+ struct Inner {
+ void (*cb)(Holder *h) REQUIRES(h->lock, m);
+ };
+ static Mutex m;
+};
+
+// Template instantiation cannot yet map a pointee's parameters, or a parameter
+// declared after the one an attribute is on. A name binds the same way in a
+// template as elsewhere, but an attribute naming one of those is rejected.
+template <typename T>
+struct InTemplate {
+ void (*cb)(T *h) REQUIRES(h->lock); // both-error{{'requires_capability' attribute in a template cannot name pointee function parameter 'h'}}
+ // Ambiguous, as outside a template.
+ void (*shadow)(T *own) REQUIRES(own->lock); // both-error{{reference to 'own' is ambiguous}} \
+ // both-note{{candidate found by name lookup is 'own'}}
+ T *own; // both-note{{candidate found by name lookup is 'InTemplate::own'}}
+ void method(void (*release)(T) RELEASE(mu), // early-error{{use of undeclared identifier 'mu'}} \
+ // late-error{{'release_capability' attribute in a template cannot name later parameter 'mu'}}
+ Mutex *mu);
+};
+InTemplate<Holder> in_template;
+
+template <typename T>
+void put_later_template(void (*release)(T) RELEASE(mu), // early-error{{use of undeclared identifier 'mu'}} \
+ // late-error{{'release_capability' attribute in a template cannot name later parameter 'mu'}}
+ Mutex *mu);
+
+auto generic_lambda = [](auto x, void (*release)(int) RELEASE(mu), // early-error{{use of undeclared identifier 'mu'}} \
+ // late-error{{'release_capability' attribute in a template cannot name later parameter 'mu'}}
+ Mutex *mu) {};
+
+// An earlier parameter is instantiated first, so it can be named.
+template <typename T>
+void put_earlier_template(Mutex *mu, void (*release)(T) RELEASE(mu)) {}
+void use_put_earlier_template(Mutex *mu) { put_earlier_template<int>(mu, nullptr); }
More information about the cfe-commits
mailing list