llvm / llvm/llvm-project

Regression from #219045: "cannot compile this tail call skipping over cleanups yet" for `[[clang::musttail]]` with C compound literals at -O1 and above

Open
#224,337 0 comments 0 reactions 0 assignees View on GitHub
clang
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

PR #219045 (commit `93b2629d1d1f`, merged 2026-09-11) emits lifetime markers for block-scoped compound literals in C. When the resulting cleanup is live at a `[[clang::musttail]]` call, clang now rejects the tail call:

```
error: cannot compile this tail call skipping over cleanups yet
```

The same code compiled before #219045. Only optimization levels that emit lifetime markers are affected, so `-O0` is fine and `-O1` and above are not.

Environment:

- clang 24.0.0git built from `cecd28d75cf5407287085e4042ef4fc7ed7feadd` (2026-09-15), which
contains #219045.
- Last known good: clang built from `8b690a085406337f7a02ab466df494bce5f75f41` (2026-07-23),
which predates it.
- Seen on `aarch64-unknown-linux-gnu`. The rejection is in target-independent CodeGen, so it
should not be target specific.
- Not a configuration artifact: plain `-c` with `--no-default-config` still fails.

Reproducer:

CPython 3.14 at `072c3a1405ccef6e9125011ed44cf50568e364b1`:

```bash
git clone https://github.com/python/cpython && cd cpython && git checkout 072c3a1405
mkdir b && cd b
../configure --with-computed-gotos --with-tail-call-interp --enable-shared >/dev/null
clang --no-default-config -O2 -std=gnu23 -c ../Python/ceval.c \
-I../Include/internal -I../Include/internal/mimalloc -IObjects -IInclude -IPython -I. -I../Include \
-fPIC -DPy_BUILD_CORE -DNDEBUG
```

The errors land in `Python/generated_cases.c.h`, reached through `Python/ceval_macros.h:98`:

```c
# define DISPATCH_GOTO() \
do { \
Py_MUSTTAIL return (INSTRUCTION_TABLE[opcode])(TAIL_CALL_ARGS); \
} while (0)
```

One failing block is `TARGET(CHECK_EXC_MATCH)`, which assigns `b = res ? PyStackRef_True : PyStackRef_False;` (`generated_cases.c.h:4662`) and then dispatches. Both operands are compound literals in the default GIL build (`Include/internal/pycore_stackref.h:473-474`).

Expected behavior:

The tail call compiles. A `lifetime.end` that would run after a musttail call is a no-op, since the tail call replaces the caller's frame and the alloca stops existing either way. Clang already takes this position for ordinary locals, skipping their lifetime cleanups at a musttail.

Actual behavior:

At `-O1` and above the compile fails with the error above, once per affected dispatch site. At `-O0` the same source compiles, because no lifetime markers are emitted.

Root cause:

`CodeGenFunction::EmitCall` walks the `EHStack` between the call and the function scope when `IsMustTail` is set, and rejects anything that is neither a fake use nor redundant before return (`CGCall.cpp:6622-6648` at `cecd28d75c`):

```cpp
EHCleanupScope *Cleanup = dyn_cast(&*it);
if (Cleanup && Cleanup->isFakeUse()) { /* emit before the call */ }
else if (!(Cleanup && Cleanup->getCleanup()->isRedundantBeforeReturn())) {
CGM.ErrorUnsupported(MustTailCall, "tail call skipping over cleanups");
}
```

The obvious objection is that `CallLifetimeEnd::isRedundantBeforeReturn()` returns `true` unconditionally (`CodeGenFunction.h:725`), so a bare one is already exempt. That is why an ordinary local across a musttail compiles today: `CGDecl.cpp:1748` pushes it with `EHStack.pushCleanup`.

The difference is the wrapper. #219045 pushes the compound literal's cleanup from `EmitCompoundLiteralLValue` (`CGExpr.cpp:6023`) with
`pushCleanupAfterFullExpr`. Inside a conditional branch that does not queue a `CallLifetimeEnd`; it queues `EHScopeStack::ConditionalCleanup` (`CodeGenFunction.h:946-958`), which never overrides `isRedundantBeforeReturn()` and so inherits the base default of `false` (`EHScopeStack.h:167`). The scope's `isLifetimeMarker()` bit is still
set (`CGCleanup.cpp:160`), but the loop consults only the virtual function, never the bit.

Each cite above is readable at `cecd28d75c`. The causal chain between them is our reading of the source, not something we confirmed in a debugger.

FIWW #109255 (merged 2024) documented that lifetimes must end before a musttail call rather than before the return, and added diagnostics for some cases. On that reading #219045 just gave compound literals the storage duration C already specifies, and the diagnostic is doing its job.

Clang's own treatment of locals is what argues otherwise. If lifetime markers really had to be honored across a musttail, `isRedundantBeforeReturn()` returning `true` for `CallLifetimeEnd` would itself be a bug and every musttail function with a local would be miscompiled. It's not a bug and the exemption is deliberate. So the question is only whether a compound literal should differ from a local here. Nothing about frame teardown distinguishes them, and #219045's stated goal of letting StackColoring merge slots has no stake in the musttail case.

Proposed fix:

Exempt lifetime-marker scopes in the loop:

```diff
EHCleanupScope *Cleanup = dyn_cast(&*it);
if (Cleanup && Cleanup->isFakeUse()) {
...
- } else if (!(Cleanup &&
- Cleanup->getCleanup()->isRedundantBeforeReturn())) {
+ } else if (!(Cleanup &&
+ (Cleanup->getCleanup()->isRedundantBeforeReturn() ||
+ Cleanup->isLifetimeMarker()))) {
CGM.ErrorUnsupported(MustTailCall, "tail call skipping over cleanups");
}
```

An alternative is to make `ConditionalCleanup` forward `isRedundantBeforeReturn()` to the cleanup it wraps. That is arguably more correct since the wrapper hides the property from every caller rather than just this one, but it has a wider blast radius.

Current workaround:

We carry the loop-side exemption above as a patch during our clang build. At `cecd28d75c`, against the same `ceval.c`: reverting #219045's `CGExpr.cpp` makes it compile, and keeping #219045 while adding the exemption also makes it compile. We took the second so the optimization stays on.

We have not run CPython's test suite against the patched compiler so we have shown only that it builds not that the generated code is correct.

Related:

- llvm/llvm-project#164312, an open issue with the same diagnostic for a `noexcept` caller, where
an `EHTerminateScope` blocks the tail call instead. The loop at `cecd28d75c` already skips
`EHTerminateScope` for a `nounwind` callee, so that report may be partly stale.
- llvm/llvm-project#109255, which documented the limitation but added no exemption mechanism.
- llvm/llvm-project#219045, the regressing PR. It fixes #68746.

Contributor guide

Open the contributing guide

Research direction

Start in CodeGenFunction::EmitCall in CGCall.cpp, then trace ConditionalCleanup and isLifetimeMarker through CodeGenFunction.h and EHScopeStack.h. Reproduce the failure with the provided CPython ceval.c command at -O2, and verify that the musttail dispatch compiles with a regression test covering compound literals and optimization levels without changing ordinary cleanup diagnostics.

Written by the indexing model from the issue text.

Assessment

Tech stack
c, cpp
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
52/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.