| Issue |
208385
|
| Summary |
[CIR] cir.condition error for loop condition variables with a non-trivial cleanup (while/for)
|
| Labels |
ClangIR
|
| Assignees |
|
| Reporter |
E00N777
|
### Summary
When I tried to emit `lifetime` marker in `clangir` in https://github.com/llvm/llvm-project/pull/206695 , I found a pre-existing issue:
`while/for` with a condition variable like :
```
// tmp.cpp
struct Guard {
int v;
Guard();
~Guard(); // non-trivial destructor
explicit operator bool() const;
};
int side();
void f() {
while (Guard g = Guard()) {
side();
}
}
```
that needs a cleanup produces invalid CIR;verification aborts with 'cir.condition' op condition must be within a conditional region. Explicitly framed as a pre-existing structural defect, independent of lifetime markers (main still errorNYIs marker emission, so the repro fails purely on the destructor cleanup).
### Reproduce the failure (ClangIR path) -- fails at O0 and O2
```
~/llvm-project$ build/bin/clang -cc1 -triple x86_64-unknown-linux-gnu -O0 -fclangir -emit-cir tmp.cpp -o /dev/null; echo "exit: $?"
loc("tmp.cpp":11:16): error: 'cir.condition' op condition must be within a conditional region
fatal error: CIR module verification error before running CIR-to-CIR passes
exit: 70
```
```
~/llvm-project$ build/bin/clang -cc1 -triple x86_64-unknown-linux-gnu -O2 -fclangir -emit-cir tmp.cpp -
o /dev/null; echo "exit: $?"
loc("tmp.cpp":11:16): error: 'cir.condition' op condition must be within a conditional region
fatal error: CIR module verification error before running CIR-to-CIR passes
exit: 70
```
Replacing the while in tmp.cpp with a for that has the same condition variable fails identically:
```
~/llvm-project$ # tmp.cpp with the loop replaced by: for (; Guard g = Guard();) { side(); }
~/llvm-project$ build/bin/clang -cc1 -triple x86_64-unknown-linux-gnu -O0 -fclangir -emit-cir tmp.cpp -o /dev/null; echo "exit: $?"
loc("tmp.cpp":11:16): error: 'cir.condition' op condition must be within a conditional region
fatal error: CIR module verification error before running CIR-to-CIR passes
exit: 70
```
But the same condition variable in an if compiles fine:
```
~/llvm-project$ # tmp.cpp with the loop replaced by: if (Guard g = Guard()) { side(); }
~/llvm-project$ build/bin/clang -cc1 -triple x86_64-unknown-linux-gnu -O0 -fclangir -emit-cir tmp.cpp -o /dev/null; echo "exit: $?"
exit: 0
```
And classic (non-ClangIR) CodeGen compiles the original tmp.cpp without complaint:
```
~/llvm-project$ build/bin/clang -cc1 -triple x86_64-unknown-linux-gnu -O0 -emit-llvm tmp.cpp -o /dev/null; echo "exit: $?"
exit: 0
```
| Statement | `-fclangir` result |
|---|---|
| `while (Guard g = Guard())` | ❌ `cir.condition` verifier error |
| `for (; Guard g = Guard();)` | ❌ `cir.condition` verifier error |
| `if (Guard g = Guard())` | ✅ ok |
| `do { } while (...)` | ✅ ok (can't declare a condition variable) |
| classic CodeGen (no `-fclangir`) | ✅ ok |
### Analysis
In `emitWhileStmt` the `cond` region is filled by a lambda that emits the condition variable and the `cir.condition` terminator back-to-back, with no cleanup scope spanning the cond+body pair (`clang/lib/CIR/CodeGen/CIRGenStmt.cpp`, `emitWhileStmt` ~L1096–1112):
```cpp
whileOp = builder.createWhile(
getLoc(s.getSourceRange()),
/*condBuilder=*/
[&](mlir::OpBuilder &b, mlir::Location loc) {
mlir::Value condVal;
if (s.getConditionVariable())
emitDecl(*s.getConditionVariable()); // (1) pushes the ~Guard cleanup here
condVal = evaluateExprAsBool(s.getCond());
builder.createCondition(condVal); // (3) terminator emitted here
},
/*bodyBuilder=*/ ... );
```
1. `emitDecl(*s.getConditionVariable())` reaches `emitAutoVarDecl`, which — because `Guard` has a non-trivial destructor — calls `ehStack.pushCleanup(...)`. The current insertion point is **inside the loop's `cond` region**.
2. `EHScopeStack::pushCleanup` (`clang/lib/CIR/CodeGen/CIRGenCleanup.cpp` ~L337) sees a `NormalCleanup`, which cannot be skipped, so it **immediately** creates a `cir.cleanup.scope` (~L362) and moves the insertion point into that scope's body region:
```cpp
builder.setInsertionPointToEnd(&cleanupScope.getBodyRegion().back()); // ~L373
```
3. Back in the cond lambda, `evaluateExprAsBool` and `builder.createCondition` now emit **into the `cir.cleanup.scope` body**, not directly into the loop's `cond` region.
4. `cir::ConditionOp::verify()` (`clang/lib/CIR/Dialect/IR/CIRDialect.cpp:532`) requires `cir.condition` to sit directly in a loop condition region. Its parent is now `cir.cleanup.scope`, so verification fails.
Expected vs. actual shape:
```mlir
// expected (valid) // actual (invalid)
cir.while { cir.while {
%g = cir.alloca ... %g = cir.alloca ...
... ctor / operator bool ... cir.cleanup.scope { // opened by pushCleanup
cir.condition(%c) // terminator ... ctor / operator bool ...
} do { ... } cir.condition(%c) // <-- trapped -> verifier error
} cleanup { ~Guard() }
} do { ... }
```
`emitForStmt`'s cond builder (`CIRGenStmt.cpp` ~L995–1004) is structurally identical, hence the same failure. `if` is immune because `emitIfStmt` emits the condition variable in the **enclosing** scope (`CIRGenStmt.cpp:544`) and `cir.if` has no `cir.condition` terminator, so the eagerly-created `cir.cleanup.scope` legally wraps the whole `cir.if`. Note `cir.cleanup.scope` is not part of the lifetime-marker work — it predates it (e.g. `a77010fc490f`, `a01dddd1e603`, `45bddca21c65`), which is why this repro fails on plain `main`.
**Why this is structural, not a local bug.** Per `[stmt.while]/p2` and `[stmt.for]`, the loop condition variable is **destroyed and re-constructed on every iteration**, so its cleanup must run on **two distinct edges**: the loop-exit edge (condition false) and the backedge (end of each body iteration). Classic CodeGen expresses this naturally in a flat CFG — it emits `~Guard` **once into a shared `cleanup` block reached from both edges**, with a cleanup-destination switch selecting the successor. From `-emit-llvm -O0` on `tmp.cpp`:
```llvm
while.cond: call @_ZN5GuardC1Ev(%g) ; ctor
%call = call i1 @_ZNK5GuardcvbEv(%g) ; operator bool
br i1 %call, label %while.body, label %while.exit
while.exit: br label %cleanup ; loop-exit edge ─┐
while.body: call @_Z4sidev() ; │
br label %cleanup ; backedge ─┐ │
cleanup: call @_ZN5GuardD1Ev(%g) ; ~Guard, shared by both edges
switch %cleanup.dest, ... [i32 0 -> while.cond (loop), i32 3 -> while.end (exit)]
```
In structured CIR, the `cir.while` `cond` and `body` are two **sibling** regions, and a `cir.cleanup.scope` can only wrap a contiguous span **within a single region**. "Run once on the false-exit edge of `cond` and once at the end of `body`" has **no legal placement** with today's ops — even if `pushCleanup` did not eagerly open a scope, there is nowhere to put a two-edge cleanup. The `cir.condition` verifier error is the surface symptom of this missing representation. Corroboration: the `emitWhileStmt` cond lambda has **no `RunCleanupsScope`**, whereas classic `EmitWhileStmt` wraps cond+body in a `ConditionScope` (`RunCleanupsScope`) precisely to materialize this cleanup on both edges.
_______________________________________________
llvm-bugs mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-bugs