Skip to content

Commit

Permalink
[coroutines] Introduce [[clang::coro_lifetimebound]] (#72851)
Browse files Browse the repository at this point in the history
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]]``.
  • Loading branch information
usx95 committed Nov 22, 2023
1 parent de7fbfe commit fbfd2c9
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 3 deletions.
4 changes: 4 additions & 0 deletions clang/docs/ReleaseNotes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,10 @@ Attribute Changes in Clang
[[clang::code_align(A)]] for(;;) { }
}

- Clang now introduced ``[[clang::coro_lifetimebound]]`` attribute.
All parameters of a function are considered to be lifetime bound if the function
returns a type annotated with ``[[clang::coro_lifetimebound]]`` and ``[[clang::coro_return_type]]``.

Improvements to Clang's diagnostics
-----------------------------------
- Clang constexpr evaluator now prints template arguments when displaying
Expand Down
8 changes: 8 additions & 0 deletions clang/include/clang/Basic/Attr.td
Original file line number Diff line number Diff line change
Expand Up @@ -1110,6 +1110,14 @@ def CoroWrapper : InheritableAttr {
let SimpleHandler = 1;
}

def CoroLifetimeBound : InheritableAttr {
let Spellings = [Clang<"coro_lifetimebound">];
let Subjects = SubjectList<[CXXRecord]>;
let LangOpts = [CPlusPlus];
let Documentation = [CoroLifetimeBoundDoc];
let SimpleHandler = 1;
}

// OSObject-based attributes.
def OSConsumed : InheritableParamAttr {
let Spellings = [Clang<"os_consumed">];
Expand Down
58 changes: 57 additions & 1 deletion clang/include/clang/Basic/AttrDocs.td
Original file line number Diff line number Diff line change
Expand Up @@ -7483,7 +7483,6 @@ generation of the other destruction cases, optimizing the above `foo.destroy` to
}];
}


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

}];
}

def CoroLifetimeBoundDoc : Documentation {
let Category = DocCatDecl;
let Content = [{
The ``[[clang::coro_lifetimebound]]`` is a class attribute which can be applied
to a `coroutine return type (CRT) <https://clang.llvm.org/docs/AttributeReference.html#coro-return-type>` _ (i.e.
it should also be annotated with ``[[clang::coro_return_type]]``).

All parameters of a function are considered to be lifetime bound. See documentation
of ``[[clang::lifetimebound]]`` for more `details <https://clang.llvm.org/docs/AttributeReference.html#lifetimebound> _`.
if the function returns a coroutine return type (CRT) annotated with ``[[clang::coro_lifetimebound]]``.

Reference parameters of a coroutine are susceptible to capturing references to temporaries or local variables.

For example,

.. code-block:: c++

task<int> coro(const int& a) { co_return a + 1; }
task<int> dangling_refs(int a) {
// `coro` captures reference to a temporary. `foo` would now contain a dangling reference to `a`.
auto foo = coro(1);
// `coro` captures reference to local variable `a` which is destroyed after the return.
return coro(a);
}

`Lifetime bound <https://clang.llvm.org/docs/AttributeReference.html#lifetimebound> _` static analysis
can be used to detect such instances when coroutines capture references which may die earlier than the
coroutine frame itself. In the above example, if the CRT `task` is annotated with
``[[clang::coro_lifetimebound]]``, then lifetime bound analysis would detect capturing reference to
temporaries or return address of a local variable.

Both coroutines and coroutine wrappers are part of this analysis.

.. code-block:: c++

template <typename T> struct [[clang::coro_return_type, clang::coro_lifetimebound]] Task {
using promise_type = some_promise_type;
};

Task<int> coro(const int& a) { co_return a + 1; }
Task<int> [[clang::coro_wrapper]] coro_wrapper(const int& a, const int& b) {
return a > b ? coro(a) : coro(b);
}
Task<int> temporary_reference() {
auto foo = coro(1); // warning: capturing reference to a temporary which would die after the expression.

int a = 1;
auto bar = coro_wrapper(a, 0); // warning: `b` captures reference to a temporary.

co_return co_await coro(1); // fine.
}
[[clang::coro_wrapper]] Task<int> stack_reference(int a) {
return coro(a); // warning: returning address of stack variable `a`.
}
}];
}
7 changes: 6 additions & 1 deletion clang/lib/Sema/SemaInit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7580,10 +7580,15 @@ static void visitLifetimeBoundArguments(IndirectLocalPath &Path, Expr *Call,
if (ObjectArg && implicitObjectParamIsLifetimeBound(Callee))
VisitLifetimeBoundArg(Callee, ObjectArg);

bool CheckCoroCall = false;
if (const auto *RD = Callee->getReturnType()->getAsRecordDecl()) {
CheckCoroCall = RD->hasAttr<CoroLifetimeBoundAttr>() &&
RD->hasAttr<CoroReturnTypeAttr>();
}
for (unsigned I = 0,
N = std::min<unsigned>(Callee->getNumParams(), Args.size());
I != N; ++I) {
if (Callee->getParamDecl(I)->hasAttr<LifetimeBoundAttr>())
if (CheckCoroCall || Callee->getParamDecl(I)->hasAttr<LifetimeBoundAttr>())
VisitLifetimeBoundArg(Callee->getParamDecl(I), Args[I]);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,10 @@
// CHECK-NEXT: ConsumableAutoCast (SubjectMatchRule_record)
// CHECK-NEXT: ConsumableSetOnRead (SubjectMatchRule_record)
// CHECK-NEXT: Convergent (SubjectMatchRule_function)
// CHECK-NEXT: CoroLifetimeBound (SubjectMatchRule_record)
// CHECK-NEXT: CoroOnlyDestroyWhenComplete (SubjectMatchRule_record)
// CHECK-NEXT: CoroReturnType (SubjectMatchRule_record)
// CHECK-NEXT: CoroWrapper
// CHECK-NEXT: CoroWrapper (SubjectMatchRule_function)
// CHECK-NEXT: CountedBy (SubjectMatchRule_field)
// CHECK-NEXT: DLLExport (SubjectMatchRule_function, SubjectMatchRule_variable, SubjectMatchRule_record, SubjectMatchRule_objc_interface)
// CHECK-NEXT: DLLImport (SubjectMatchRule_function, SubjectMatchRule_variable, SubjectMatchRule_record, SubjectMatchRule_objc_interface)
Expand Down
123 changes: 123 additions & 0 deletions clang/test/SemaCXX/coro-lifetimebound.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// RUN: %clang_cc1 -triple x86_64-apple-darwin9 %s -std=c++20 -fsyntax-only -verify -Wall -Wextra -Wno-error=unreachable-code -Wno-unused

#include "Inputs/std-coroutine.h"

using std::suspend_always;
using std::suspend_never;

template <typename T> struct [[clang::coro_lifetimebound, clang::coro_return_type]] Co {
struct promise_type {
Co<T> get_return_object() {
return {};
}
suspend_always initial_suspend();
suspend_always final_suspend() noexcept;
void unhandled_exception();
void return_value(const T &t);

template <typename U>
auto await_transform(const Co<U> &) {
struct awaitable {
bool await_ready() noexcept { return false; }
void await_suspend(std::coroutine_handle<>) noexcept {}
U await_resume() noexcept { return {}; }
};
return awaitable{};
}
};
};

Co<int> foo_coro(const int& b) {
if (b > 0)
co_return 1;
co_return 2;
}

int getInt() { return 0; }

Co<int> bar_coro(const int &b, int c) {
int x = co_await foo_coro(b);
int y = co_await foo_coro(1);
int z = co_await foo_coro(getInt());
auto unsafe1 = foo_coro(1); // expected-warning {{temporary whose address is used as value of local variable}}
auto unsafe2 = foo_coro(getInt()); // expected-warning {{temporary whose address is used as value of local variable}}
auto safe1 = foo_coro(b);
auto safe2 = foo_coro(c);
co_return co_await foo_coro(co_await foo_coro(1));
}

[[clang::coro_wrapper]] Co<int> plain_return_co(int b) {
return foo_coro(b); // expected-warning {{address of stack memory associated with parameter}}
}

[[clang::coro_wrapper]] Co<int> safe_forwarding(const int& b) {
return foo_coro(b);
}

[[clang::coro_wrapper]] Co<int> unsafe_wrapper(int b) {
return safe_forwarding(b); // expected-warning {{address of stack memory associated with parameter}}
}

[[clang::coro_wrapper]] Co<int> complex_plain_return(int b) {
return b > 0
? foo_coro(1) // expected-warning {{returning address of local temporary object}}
: bar_coro(0, 1); // expected-warning {{returning address of local temporary object}}
}

#define CORO_WRAPPER \
_Pragma("clang diagnostic push") \
_Pragma("clang diagnostic ignored \"-Wc++23-extensions\"") \
[[clang::coro_wrapper]] \
_Pragma("clang diagnostic pop")

void lambdas() {
auto unsafe_lambda = [] CORO_WRAPPER (int b) {
return foo_coro(b); // expected-warning {{address of stack memory associated with parameter}}
};
auto coro_lambda = [] (const int&) -> Co<int> {
co_return 0;
};
auto unsafe_coro_lambda = [&] (const int& b) -> Co<int> {
int x = co_await coro_lambda(b);
auto safe = coro_lambda(b);
auto unsafe1 = coro_lambda(1); // expected-warning {{temporary whose address is used as value of local variable}}
auto unsafe2 = coro_lambda(getInt()); // expected-warning {{temporary whose address is used as value of local variable}}
auto unsafe3 = coro_lambda(co_await coro_lambda(b)); // expected-warning {{temporary whose address is used as value of local variable}}
co_return co_await safe;
};
auto safe_lambda = [](int b) -> Co<int> {
int x = co_await foo_coro(1);
co_return x + co_await foo_coro(b);
};
}
// =============================================================================
// Safe usage when parameters are value
// =============================================================================
namespace by_value {
Co<int> value_coro(int b) { co_return co_await foo_coro(b); }
[[clang::coro_wrapper]] Co<int> wrapper1(int b) { return value_coro(b); }
[[clang::coro_wrapper]] Co<int> wrapper2(const int& b) { return value_coro(b); }
}

// =============================================================================
// Lifetime bound but not a Coroutine Return Type: No analysis.
// =============================================================================
namespace not_a_crt {
template <typename T> struct [[clang::coro_lifetimebound]] CoNoCRT {
struct promise_type {
CoNoCRT<T> get_return_object() {
return {};
}
suspend_always initial_suspend();
suspend_always final_suspend() noexcept;
void unhandled_exception();
void return_value(const T &t);
};
};

CoNoCRT<int> foo_coro(const int& a) { co_return a; }
CoNoCRT<int> bar(int a) {
auto x = foo_coro(a);
co_return 1;
}
} // namespace not_a_crt

0 comments on commit fbfd2c9

Please sign in to comment.