| Issue |
178256
|
| Summary |
Parens break coroutine heap safe-elide context / HALO ( with LLM-proposed fix)
|
| Labels |
new issue
|
| Assignees |
|
| Reporter |
snarkmaster
|
Clang's `coro_await_elidable` and `coro_await_elidable_argument` can enable Heap eLision Optimization (HALO) in scenarios where coroutines are composed via arguments, or are wrapped in behavior-modifying awaitable types.
In the example below (run here: https://godbolt.org/z/cGz1q73T4), `fn` and `leaf()` should be HALO-able into the outer coro frame. However, adding parentheses around `co_await`'s argument breaks that.
Due to `co_await` binding tightly, parens are usually required in real-world code involving coroutine operators, so this is not a purely cosmetic issue.
```cpp
#include <coroutine>
#include <cstdio>
int allocs = 0;
struct [[clang::coro_await_elidable]] Task {
struct promise_type {
Task get_return_object() {
return {std::coroutine_handle<promise_type>::from_promise(*this)};
}
std::suspend_always initial_suspend() { return {}; }
std::suspend_always final_suspend() noexcept { return {}; }
void return_void() {}
void unhandled_exception() {}
static void* operator new(std::size_t n) {
++allocs;
return ::operator new(n);
}
};
std::coroutine_handle<promise_type> h;
bool await_ready() { return false; }
std::coroutine_handle<> await_suspend(std::coroutine_handle<>) { return h; }
void await_resume() { h.destroy(); }
};
Task leaf() { co_return; }
Task fn([[clang::coro_await_elidable_argument]] Task u) { co_await u; }
Task operator|(int, [[clang::coro_await_elidable_argument]] Task u) {
co_await u;
}
// The key comparison -- parentheses (except folds) break HALO:
Task noParen() { co_await fn(leaf()); } // 1 alloc, HALO works
Task withParen() { co_await (fn(leaf())); } // 3 allocs, HALO broken!
Task viaOp() { co_await (0 | leaf()); } // 3 allocs, parens required
template <auto... F>
Task viaOpFold() {
co_await (0 | ... | F()); // 1 alloc, HALO works
}
void run(Task (*f)(), const char* name) {
allocs = 0;
f().h.resume();
std::printf("%s: %d allocs\n", name, allocs);
}
int main() {
run(noParen, "noParen");
run(withParen, "withParen");
run(viaOp, "viaOp");
run(viaOpFold<leaf>, "viaOpFold");
return 0;
}
```
## LLM-proposed root cause
I asked an LLM for a possible root cause, and the output looks very plausible. But, I didn't verify it yet, since setting up an LLVM dev environment is time-consuming. Including it here in case it's helpful.
---
In `clang/lib/Sema/SemaCoroutine.cpp`, the `applySafeElideContext` function uses `IgnoreImplicit()` which only strips implicit casts, not explicit parentheses:
```cpp
static void applySafeElideContext(Expr *Operand) {
auto *Call = dyn_cast<CallExpr>(Operand->IgnoreImplicit()); // BUG
if (!Call || !Call->isPRValue())
return;
// ...
}
```
When you write `co_await (fn(leaf()))`, the operand is `ParenExpr(CallExpr)`. `IgnoreImplicit()` doesn't strip `ParenExpr`, so `dyn_cast<CallExpr>` returns null and HALO is not applied.
**Fix**: Use `IgnoreParenImpCasts()` instead of `IgnoreImplicit()`:
```cpp
auto *Call = dyn_cast<CallExpr>(Operand->IgnoreParenImpCasts());
```
Fold expressions work because when expanded, the inner operator| calls become arguments to the outer call. Arguments are processed via the recursive `applySafeElideContext(Call->getArg(ParmIdx))` call, and those expressions aren't wrapped in `ParenExpr`.
_______________________________________________
llvm-bugs mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-bugs