Skip to content

Commit fbfd2c9

Browse files
authored
[coroutines] Introduce [[clang::coro_lifetimebound]] (#72851)
Adds attribute `[[clang::coro_lifetimebound]]`. All arguments to a function are considered to be **lifetime bound** if the function returns a type annotated with ``[[clang::coro_lifetimebound]]`` and ``[[clang::coro_return_type]]``.
1 parent de7fbfe commit fbfd2c9

File tree

6 files changed

+200
-3
lines changed

6 files changed

+200
-3
lines changed

clang/docs/ReleaseNotes.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,10 @@ Attribute Changes in Clang
334334
[[clang::code_align(A)]] for(;;) { }
335335
}
336336

337+
- Clang now introduced ``[[clang::coro_lifetimebound]]`` attribute.
338+
All parameters of a function are considered to be lifetime bound if the function
339+
returns a type annotated with ``[[clang::coro_lifetimebound]]`` and ``[[clang::coro_return_type]]``.
340+
337341
Improvements to Clang's diagnostics
338342
-----------------------------------
339343
- Clang constexpr evaluator now prints template arguments when displaying

clang/include/clang/Basic/Attr.td

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1110,6 +1110,14 @@ def CoroWrapper : InheritableAttr {
11101110
let SimpleHandler = 1;
11111111
}
11121112

1113+
def CoroLifetimeBound : InheritableAttr {
1114+
let Spellings = [Clang<"coro_lifetimebound">];
1115+
let Subjects = SubjectList<[CXXRecord]>;
1116+
let LangOpts = [CPlusPlus];
1117+
let Documentation = [CoroLifetimeBoundDoc];
1118+
let SimpleHandler = 1;
1119+
}
1120+
11131121
// OSObject-based attributes.
11141122
def OSConsumed : InheritableParamAttr {
11151123
let Spellings = [Clang<"os_consumed">];

clang/include/clang/Basic/AttrDocs.td

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7483,7 +7483,6 @@ generation of the other destruction cases, optimizing the above `foo.destroy` to
74837483
}];
74847484
}
74857485

7486-
74877486
def CoroReturnTypeAndWrapperDoc : Documentation {
74887487
let Category = DocCatDecl;
74897488
let Content = [{
@@ -7581,3 +7580,60 @@ alignment boundary. Its value must be a power of 2, between 1 and 4096
75817580

75827581
}];
75837582
}
7583+
7584+
def CoroLifetimeBoundDoc : Documentation {
7585+
let Category = DocCatDecl;
7586+
let Content = [{
7587+
The ``[[clang::coro_lifetimebound]]`` is a class attribute which can be applied
7588+
to a `coroutine return type (CRT) <https://clang.llvm.org/docs/AttributeReference.html#coro-return-type>` _ (i.e.
7589+
it should also be annotated with ``[[clang::coro_return_type]]``).
7590+
7591+
All parameters of a function are considered to be lifetime bound. See documentation
7592+
of ``[[clang::lifetimebound]]`` for more `details <https://clang.llvm.org/docs/AttributeReference.html#lifetimebound> _`.
7593+
if the function returns a coroutine return type (CRT) annotated with ``[[clang::coro_lifetimebound]]``.
7594+
7595+
Reference parameters of a coroutine are susceptible to capturing references to temporaries or local variables.
7596+
7597+
For example,
7598+
7599+
.. code-block:: c++
7600+
7601+
task<int> coro(const int& a) { co_return a + 1; }
7602+
task<int> dangling_refs(int a) {
7603+
// `coro` captures reference to a temporary. `foo` would now contain a dangling reference to `a`.
7604+
auto foo = coro(1);
7605+
// `coro` captures reference to local variable `a` which is destroyed after the return.
7606+
return coro(a);
7607+
}
7608+
7609+
`Lifetime bound <https://clang.llvm.org/docs/AttributeReference.html#lifetimebound> _` static analysis
7610+
can be used to detect such instances when coroutines capture references which may die earlier than the
7611+
coroutine frame itself. In the above example, if the CRT `task` is annotated with
7612+
``[[clang::coro_lifetimebound]]``, then lifetime bound analysis would detect capturing reference to
7613+
temporaries or return address of a local variable.
7614+
7615+
Both coroutines and coroutine wrappers are part of this analysis.
7616+
7617+
.. code-block:: c++
7618+
7619+
template <typename T> struct [[clang::coro_return_type, clang::coro_lifetimebound]] Task {
7620+
using promise_type = some_promise_type;
7621+
};
7622+
7623+
Task<int> coro(const int& a) { co_return a + 1; }
7624+
Task<int> [[clang::coro_wrapper]] coro_wrapper(const int& a, const int& b) {
7625+
return a > b ? coro(a) : coro(b);
7626+
}
7627+
Task<int> temporary_reference() {
7628+
auto foo = coro(1); // warning: capturing reference to a temporary which would die after the expression.
7629+
7630+
int a = 1;
7631+
auto bar = coro_wrapper(a, 0); // warning: `b` captures reference to a temporary.
7632+
7633+
co_return co_await coro(1); // fine.
7634+
}
7635+
[[clang::coro_wrapper]] Task<int> stack_reference(int a) {
7636+
return coro(a); // warning: returning address of stack variable `a`.
7637+
}
7638+
}];
7639+
}

clang/lib/Sema/SemaInit.cpp

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7580,10 +7580,15 @@ static void visitLifetimeBoundArguments(IndirectLocalPath &Path, Expr *Call,
75807580
if (ObjectArg && implicitObjectParamIsLifetimeBound(Callee))
75817581
VisitLifetimeBoundArg(Callee, ObjectArg);
75827582

7583+
bool CheckCoroCall = false;
7584+
if (const auto *RD = Callee->getReturnType()->getAsRecordDecl()) {
7585+
CheckCoroCall = RD->hasAttr<CoroLifetimeBoundAttr>() &&
7586+
RD->hasAttr<CoroReturnTypeAttr>();
7587+
}
75837588
for (unsigned I = 0,
75847589
N = std::min<unsigned>(Callee->getNumParams(), Args.size());
75857590
I != N; ++I) {
7586-
if (Callee->getParamDecl(I)->hasAttr<LifetimeBoundAttr>())
7591+
if (CheckCoroCall || Callee->getParamDecl(I)->hasAttr<LifetimeBoundAttr>())
75877592
VisitLifetimeBoundArg(Callee->getParamDecl(I), Args[I]);
75887593
}
75897594
}

clang/test/Misc/pragma-attribute-supported-attributes-list.test

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,10 @@
5656
// CHECK-NEXT: ConsumableAutoCast (SubjectMatchRule_record)
5757
// CHECK-NEXT: ConsumableSetOnRead (SubjectMatchRule_record)
5858
// CHECK-NEXT: Convergent (SubjectMatchRule_function)
59+
// CHECK-NEXT: CoroLifetimeBound (SubjectMatchRule_record)
5960
// CHECK-NEXT: CoroOnlyDestroyWhenComplete (SubjectMatchRule_record)
6061
// CHECK-NEXT: CoroReturnType (SubjectMatchRule_record)
61-
// CHECK-NEXT: CoroWrapper
62+
// CHECK-NEXT: CoroWrapper (SubjectMatchRule_function)
6263
// CHECK-NEXT: CountedBy (SubjectMatchRule_field)
6364
// CHECK-NEXT: DLLExport (SubjectMatchRule_function, SubjectMatchRule_variable, SubjectMatchRule_record, SubjectMatchRule_objc_interface)
6465
// CHECK-NEXT: DLLImport (SubjectMatchRule_function, SubjectMatchRule_variable, SubjectMatchRule_record, SubjectMatchRule_objc_interface)
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
// RUN: %clang_cc1 -triple x86_64-apple-darwin9 %s -std=c++20 -fsyntax-only -verify -Wall -Wextra -Wno-error=unreachable-code -Wno-unused
2+
3+
#include "Inputs/std-coroutine.h"
4+
5+
using std::suspend_always;
6+
using std::suspend_never;
7+
8+
template <typename T> struct [[clang::coro_lifetimebound, clang::coro_return_type]] Co {
9+
struct promise_type {
10+
Co<T> get_return_object() {
11+
return {};
12+
}
13+
suspend_always initial_suspend();
14+
suspend_always final_suspend() noexcept;
15+
void unhandled_exception();
16+
void return_value(const T &t);
17+
18+
template <typename U>
19+
auto await_transform(const Co<U> &) {
20+
struct awaitable {
21+
bool await_ready() noexcept { return false; }
22+
void await_suspend(std::coroutine_handle<>) noexcept {}
23+
U await_resume() noexcept { return {}; }
24+
};
25+
return awaitable{};
26+
}
27+
};
28+
};
29+
30+
Co<int> foo_coro(const int& b) {
31+
if (b > 0)
32+
co_return 1;
33+
co_return 2;
34+
}
35+
36+
int getInt() { return 0; }
37+
38+
Co<int> bar_coro(const int &b, int c) {
39+
int x = co_await foo_coro(b);
40+
int y = co_await foo_coro(1);
41+
int z = co_await foo_coro(getInt());
42+
auto unsafe1 = foo_coro(1); // expected-warning {{temporary whose address is used as value of local variable}}
43+
auto unsafe2 = foo_coro(getInt()); // expected-warning {{temporary whose address is used as value of local variable}}
44+
auto safe1 = foo_coro(b);
45+
auto safe2 = foo_coro(c);
46+
co_return co_await foo_coro(co_await foo_coro(1));
47+
}
48+
49+
[[clang::coro_wrapper]] Co<int> plain_return_co(int b) {
50+
return foo_coro(b); // expected-warning {{address of stack memory associated with parameter}}
51+
}
52+
53+
[[clang::coro_wrapper]] Co<int> safe_forwarding(const int& b) {
54+
return foo_coro(b);
55+
}
56+
57+
[[clang::coro_wrapper]] Co<int> unsafe_wrapper(int b) {
58+
return safe_forwarding(b); // expected-warning {{address of stack memory associated with parameter}}
59+
}
60+
61+
[[clang::coro_wrapper]] Co<int> complex_plain_return(int b) {
62+
return b > 0
63+
? foo_coro(1) // expected-warning {{returning address of local temporary object}}
64+
: bar_coro(0, 1); // expected-warning {{returning address of local temporary object}}
65+
}
66+
67+
#define CORO_WRAPPER \
68+
_Pragma("clang diagnostic push") \
69+
_Pragma("clang diagnostic ignored \"-Wc++23-extensions\"") \
70+
[[clang::coro_wrapper]] \
71+
_Pragma("clang diagnostic pop")
72+
73+
void lambdas() {
74+
auto unsafe_lambda = [] CORO_WRAPPER (int b) {
75+
return foo_coro(b); // expected-warning {{address of stack memory associated with parameter}}
76+
};
77+
auto coro_lambda = [] (const int&) -> Co<int> {
78+
co_return 0;
79+
};
80+
auto unsafe_coro_lambda = [&] (const int& b) -> Co<int> {
81+
int x = co_await coro_lambda(b);
82+
auto safe = coro_lambda(b);
83+
auto unsafe1 = coro_lambda(1); // expected-warning {{temporary whose address is used as value of local variable}}
84+
auto unsafe2 = coro_lambda(getInt()); // expected-warning {{temporary whose address is used as value of local variable}}
85+
auto unsafe3 = coro_lambda(co_await coro_lambda(b)); // expected-warning {{temporary whose address is used as value of local variable}}
86+
co_return co_await safe;
87+
};
88+
auto safe_lambda = [](int b) -> Co<int> {
89+
int x = co_await foo_coro(1);
90+
co_return x + co_await foo_coro(b);
91+
};
92+
}
93+
// =============================================================================
94+
// Safe usage when parameters are value
95+
// =============================================================================
96+
namespace by_value {
97+
Co<int> value_coro(int b) { co_return co_await foo_coro(b); }
98+
[[clang::coro_wrapper]] Co<int> wrapper1(int b) { return value_coro(b); }
99+
[[clang::coro_wrapper]] Co<int> wrapper2(const int& b) { return value_coro(b); }
100+
}
101+
102+
// =============================================================================
103+
// Lifetime bound but not a Coroutine Return Type: No analysis.
104+
// =============================================================================
105+
namespace not_a_crt {
106+
template <typename T> struct [[clang::coro_lifetimebound]] CoNoCRT {
107+
struct promise_type {
108+
CoNoCRT<T> get_return_object() {
109+
return {};
110+
}
111+
suspend_always initial_suspend();
112+
suspend_always final_suspend() noexcept;
113+
void unhandled_exception();
114+
void return_value(const T &t);
115+
};
116+
};
117+
118+
CoNoCRT<int> foo_coro(const int& a) { co_return a; }
119+
CoNoCRT<int> bar(int a) {
120+
auto x = foo_coro(a);
121+
co_return 1;
122+
}
123+
} // namespace not_a_crt

0 commit comments

Comments
 (0)