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

Reply via email to