Repository navigation
Description
Summary
Starting with clang 23, a coroutine passed as an argument to a [[clang::coro_await_elidable_argument]] parameter is no longer elided, even though the outer call is directly co_awaited. This is exactly the documented when_all use case of the attribute
Maybe related to #214151
Reproducer
#include <coroutine>
#include <cstdio>
#include <cstdlib>
#include <new>
#include <utility>
static int allocations = 0;
template <typename T>
struct [[clang::coro_await_elidable]] Task {
struct promise_type {
T value{};
std::coroutine_handle<> continuation;
static void* operator new(std::size_t size) {
++allocations;
return ::operator new(size);
}
static void operator delete(void* ptr, std::size_t size) {
::operator delete(ptr, size);
}
Task get_return_object() {
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() noexcept {
return {};
}
auto final_suspend() noexcept {
struct awaiter {
bool await_ready() noexcept {
return false;
}
std::coroutine_handle<> await_suspend(std::coroutine_handle<promise_type> h) noexcept {
auto c = h.promise().continuation;
return c ? c : std::noop_coroutine();
}
void await_resume() noexcept {
}
};
return awaiter{};
}
void return_value(T v) {
value = v;
}
void unhandled_exception() {
std::abort();
}
};
std::coroutine_handle<promise_type> handle;
explicit Task(std::coroutine_handle<promise_type> h) : handle(h) {
}
Task(Task&& other) noexcept : handle(std::exchange(other.handle, nullptr)) {
}
~Task() {
if (handle)
handle.destroy();
}
auto operator co_await() && noexcept {
struct awaiter {
std::coroutine_handle<promise_type> h;
bool await_ready() noexcept {
return false;
}
std::coroutine_handle<> await_suspend(std::coroutine_handle<> caller) noexcept {
h.promise().continuation = caller;
return h;
}
T await_resume() noexcept {
return h.promise().value;
}
};
return awaiter{handle};
}
};
Task<int> leaf(int x) {
co_return x;
}
// `arg` is in safe elide context when `forward(...)` itself is co_awaited
Task<int> forward([[clang::coro_await_elidable_argument]] Task<int> arg) {
co_return co_await std::move(arg);
}
Task<int> direct() {
co_return co_await leaf(1); // safe elide context: immediate operand of co_await
}
Task<int> via_argument() {
co_return co_await forward(leaf(2)); // leaf(2) is in safe elide context via forward's parameter
}
template <typename F>
int count_callee_allocations(F f) {
Task<int> root = f(); // root frame itself is heap-allocated, do not count it
allocations = 0;
root.handle.resume();
if (!root.handle.done())
std::abort();
return allocations;
}
int main() {
std::printf("direct co_await leaf(): %d allocation(s) (expected 0)\n", count_callee_allocations(direct));
std::printf("co_await forward(leaf()): %d allocation(s) (expected 0)\n",
count_callee_allocations(via_argument));
}clang++ -std=c++20 -O2 repro.cpp -o repro && ./repro
Results (x86_64 Linux, official release binaries / Ubuntu packages)
| clang | co_await leaf() |
co_await forward(leaf()) |
|---|---|---|
| 20.1.2 | 0 | 0 |
| 21.1.8 | 0 | 0 |
| 22.1.8 | 0 | 0 |
| 23.1.5 | 0 | 1 |
The result on 23.1.5 is the same with -O1, -O2 and -O3.
With -mllvm -coro-elide-branch-ratio=0 on 23.1.5 both cases report 0 allocations.
Proposal: add kelcoro and its full test suite to llvm-test-suite
This regression was caught by the test suite of kelcoro, a C++20 coroutine library I maintain
Every clang release since 14 to 22 has failed on kelcoro with different ICE. clang 23 - this regression
kelcoro is small enough to build and run on every release candidate, so regressions like this one would be caught in rc1 instead of in user code.
LLVM code analysis from llm (looks correct):
The frontend still marks the call as `coro_elide_safe`, so the problem is in the middle end. `-mllvm -coro-elide-branch-ratio=0` restores the elision, so this is very likely the same root cause as #214151 (branch frequency changes from #194281). I am filing it separately because the effect here is stronger: no PGO, no unusual control flow, and the documented pattern for `coro_await_elidable_argument` stops working at every optimization level.
Activity
llvmorg-github-actions commentedon Nov 12, 2026
@llvm/issue-subscribers-coroutines
Author: kelbon
Details
SummaryStarting with clang 23, a coroutine passed as an argument to a [[clang::coro_await_elidable_argument]] parameter is no longer elided, even though the outer call is directly co_awaited. This is exactly the documented when_all use case of the attribute
Maybe related to #214151
Reproducer
Results (x86_64 Linux, official release binaries / Ubuntu packages)
co_await leaf()co_await forward(leaf())The result on 23.1.5 is the same with
-O1,-O2and-O3.With
-mllvm -coro-elide-branch-ratio=0on 23.1.5 both cases report 0 allocations.Proposal: add kelcoro and its full test suite to llvm-test-suite
This regression was caught by the test suite of kelcoro, a C++20 coroutine library I maintain
Every clang release since 14 to 22 has failed on kelcoro with different ICE. clang 23 - this regression
kelcoro is small enough to build and run on every release candidate, so regressions like this one would be caught in rc1 instead of in user code.
LLVM code analysis from llm (looks correct):
yuxuanchen1997 commentedon Nov 12, 2026
Thanks for the report. This appears to be a duplicate of #214151