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

Reply via email to