[clang] [CIR] FlattenCFG: accept cir.trap as a ternary region terminator (PR #194497)
Bruno Cardoso Lopes via cfe-commits
cfe-commits at lists.llvm.org
Mon Apr 27 16:58:50 PDT 2026
https://github.com/bcardosolopes updated https://github.com/llvm/llvm-project/pull/194497
>From b7fc4e53e2aa3968870da2ce4d90ccf825e80c03 Mon Sep 17 00:00:00 2001
From: Bruno Cardoso Lopes <bruno.cardoso at gmail.com>
Date: Mon, 27 Apr 2026 16:43:27 -0700
Subject: [PATCH] [CIR] FlattenCFG: accept cir.trap as a ternary region
terminator
The CIRTernaryOpFlattening pattern only accepted cir.yield and
cir.unreachable as the terminator of a ternary region. Any other
terminator caused a verifier error ("unexpected terminator in ternary
false region") and aborted the backend with "IR failed to verify after
pattern application".
Real-world C++ code that uses the libc++ assertion macros hits this
path: _LIBCPP_ASSERT_NON_NULL expands to `cond ? (void)0 :
__builtin_trap()`, which lowers to a `cir.ternary` whose false region
ends in `cir.trap`. This is structurally identical to the
`cir.unreachable` case (both are non-returning Terminators), so the
flattening pattern just needs to leave the trap in place rather than
trying to rewrite it into a branch to the continue block.
Concretely this unblocked ~1,510 libc++ tests (~17% of the suite) that
all failed with the same diagnostic at construct_at.h, sample.h, and
pop_heap.h.
Extends clang/test/CIR/Transforms/ternary.cir with regression coverage
for cir.trap (true and false region) and cir.unreachable (false
region).
---
.../lib/CIR/Dialect/Transforms/FlattenCFG.cpp | 88 +++++++++++--------
clang/test/CIR/Transforms/ternary.cir | 70 +++++++++++++++
2 files changed, 123 insertions(+), 35 deletions(-)
diff --git a/clang/lib/CIR/Dialect/Transforms/FlattenCFG.cpp b/clang/lib/CIR/Dialect/Transforms/FlattenCFG.cpp
index 01c0af62c3ab2..d07316b1cf3a4 100644
--- a/clang/lib/CIR/Dialect/Transforms/FlattenCFG.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/FlattenCFG.cpp
@@ -77,6 +77,51 @@ static bool hasNestedOpsToFlatten(mlir::Region ®ion) {
.wasInterrupted();
}
+/// True if `op` is a non-returning terminator — currently `cir.unreachable`
+/// or `cir.trap`. Such terminators don't fall through and don't yield a
+/// value, so when flattening a region they can be left in place rather than
+/// being replaced with a branch to the continuation block. Add new ops here
+/// (e.g. a hypothetical `cir.abort`) so every flattening pattern picks them
+/// up at once.
+static bool isNonReturningTerminator(mlir::Operation *op) {
+ return mlir::isa_and_nonnull<cir::UnreachableOp, cir::TrapOp>(op);
+}
+
+/// Rewrite the terminator of `region`'s exit block so that, after
+/// flattening, control falls through to `continueBlock`. The exit
+/// terminator is expected to be either:
+/// - `cir.yield`: replaced with `cir.br` to `continueBlock` (yielded
+/// args become the destination block's arguments).
+/// - non-returning (`cir.unreachable`, `cir.trap`): left in place — no
+/// branch is needed.
+///
+/// On success returns `success()`. If the terminator is anything else, an
+/// error is emitted and `failure()` is returned. NOTE: callers in this
+/// file have typically already mutated IR (splitBlock / createBlock) by
+/// the time this is invoked, so the MLIR pattern rewriter contract
+/// requires them to still return `success()` from the surrounding
+/// pattern; the `failure()` here just signals "stop trying to wire up
+/// this region".
+static mlir::LogicalResult
+rewriteRegionExitToContinue(mlir::PatternRewriter &rewriter,
+ mlir::Region ®ion, mlir::Block *continueBlock,
+ llvm::StringRef regionDescription) {
+ mlir::Operation *terminator = region.back().getTerminator();
+ rewriter.setInsertionPointToEnd(®ion.back());
+ if (auto yieldOp = mlir::dyn_cast<cir::YieldOp>(terminator)) {
+ rewriter.replaceOpWithNewOp<cir::BrOp>(yieldOp, yieldOp.getArgs(),
+ continueBlock);
+ return mlir::success();
+ }
+ if (isNonReturningTerminator(terminator))
+ return mlir::success();
+ terminator->emitError("unexpected terminator in ")
+ << regionDescription
+ << " region, expected yield, unreachable, or trap, got: "
+ << terminator->getName();
+ return mlir::failure();
+}
+
struct CIRFlattenCFGPass : public impl::CIRFlattenCFGBase<CIRFlattenCFGPass> {
CIRFlattenCFGPass() = default;
@@ -551,49 +596,22 @@ class CIRTernaryOpFlattening : public mlir::OpRewritePattern<cir::TernaryOp> {
Region &trueRegion = op.getTrueRegion();
Block *trueBlock = &trueRegion.front();
- mlir::Operation *trueTerminator = trueRegion.back().getTerminator();
- rewriter.setInsertionPointToEnd(&trueRegion.back());
-
- // Handle both yield and unreachable terminators (throw expressions).
- // Note: IR has already been modified (splitBlock, createBlock above), so
- // we must not return failure() from this point onward per the MLIR pattern
- // rewriter contract.
- if (auto trueYieldOp = dyn_cast<cir::YieldOp>(trueTerminator)) {
- rewriter.replaceOpWithNewOp<cir::BrOp>(trueYieldOp, trueYieldOp.getArgs(),
- continueBlock);
- } else if (isa<cir::UnreachableOp>(trueTerminator)) {
- // Terminator is unreachable (e.g., from throw), just keep it
- } else {
- trueTerminator->emitError("unexpected terminator in ternary true region, "
- "expected yield or unreachable, got: ")
- << trueTerminator->getName();
- // Return success because IR was already modified
- // (splitBlock/createBlock).
+ // Wire up the true region's exit (cir.yield -> br, cir.unreachable /
+ // cir.trap kept as-is). IR has already been modified by splitBlock /
+ // createBlock above, so per the MLIR pattern rewriter contract we must
+ // still return success() if the terminator turns out to be unexpected.
+ if (failed(rewriteRegionExitToContinue(rewriter, trueRegion, continueBlock,
+ "ternary true")))
return mlir::success();
- }
rewriter.inlineRegionBefore(trueRegion, continueBlock);
Block *falseBlock = continueBlock;
Region &falseRegion = op.getFalseRegion();
falseBlock = &falseRegion.front();
- mlir::Operation *falseTerminator = falseRegion.back().getTerminator();
- rewriter.setInsertionPointToEnd(&falseRegion.back());
-
- // Handle both yield and unreachable terminators (throw expressions)
- if (auto falseYieldOp = dyn_cast<cir::YieldOp>(falseTerminator)) {
- rewriter.replaceOpWithNewOp<cir::BrOp>(
- falseYieldOp, falseYieldOp.getArgs(), continueBlock);
- } else if (isa<cir::UnreachableOp>(falseTerminator)) {
- // Terminator is unreachable (e.g., from throw), just keep it
- } else {
- falseTerminator->emitError("unexpected terminator in ternary false "
- "region, expected yield or unreachable, got: ")
- << falseTerminator->getName();
- // Return success because IR was already modified
- // (splitBlock/createBlock).
+ if (failed(rewriteRegionExitToContinue(rewriter, falseRegion, continueBlock,
+ "ternary false")))
return mlir::success();
- }
rewriter.inlineRegionBefore(falseRegion, continueBlock);
rewriter.setInsertionPointToEnd(condBlock);
diff --git a/clang/test/CIR/Transforms/ternary.cir b/clang/test/CIR/Transforms/ternary.cir
index cb5f3d0fc9919..4120e4e992c32 100644
--- a/clang/test/CIR/Transforms/ternary.cir
+++ b/clang/test/CIR/Transforms/ternary.cir
@@ -63,6 +63,76 @@ module {
// CHECK: cir.br ^bb4
// CHECK: ^bb4: // pred: ^bb3
// CHECK: cir.return
+// CHECK: }
+
+ // Ternary with cir.trap as the false-region terminator. This pattern is
+ // emitted by libc++ assertion macros that expand to
+ // `cond ? (void)0 : __builtin_trap()`. The flatten-cfg pass must accept
+ // cir.trap (a non-returning Terminator) as a valid region terminator and
+ // leave it in place rather than rewriting it into a branch.
+ cir.func @ternary_trap_false(%arg0: !cir.bool) {
+ cir.ternary(%arg0, true {
+ cir.yield
+ }, false {
+ cir.trap
+ }) : (!cir.bool) -> ()
+ cir.return
+ }
+
+// CHECK: cir.func{{.*}} @ternary_trap_false(%arg0: !cir.bool) {
+// CHECK: cir.brcond %arg0 ^bb1, ^bb2
+// CHECK: ^bb1: // pred: ^bb0
+// CHECK: cir.br ^bb3
+// CHECK: ^bb2: // pred: ^bb0
+// CHECK: cir.trap
+// CHECK: ^bb3: // pred: ^bb1
+// CHECK: cir.br ^bb4
+// CHECK: ^bb4: // pred: ^bb3
+// CHECK: cir.return
+// CHECK: }
+
+ // Mirror case: cir.trap in the true region.
+ cir.func @ternary_trap_true(%arg0: !cir.bool) {
+ cir.ternary(%arg0, true {
+ cir.trap
+ }, false {
+ cir.yield
+ }) : (!cir.bool) -> ()
+ cir.return
+ }
+
+// CHECK: cir.func{{.*}} @ternary_trap_true(%arg0: !cir.bool) {
+// CHECK: cir.brcond %arg0 ^bb1, ^bb2
+// CHECK: ^bb1: // pred: ^bb0
+// CHECK: cir.trap
+// CHECK: ^bb2: // pred: ^bb0
+// CHECK: cir.br ^bb3
+// CHECK: ^bb3: // pred: ^bb2
+// CHECK: cir.br ^bb4
+// CHECK: ^bb4: // pred: ^bb3
+// CHECK: cir.return
+// CHECK: }
+
+ // cir.unreachable as the false-region terminator (e.g. throw expressions).
+ cir.func @ternary_unreachable_false(%arg0: !cir.bool) {
+ cir.ternary(%arg0, true {
+ cir.yield
+ }, false {
+ cir.unreachable
+ }) : (!cir.bool) -> ()
+ cir.return
+ }
+
+// CHECK: cir.func{{.*}} @ternary_unreachable_false(%arg0: !cir.bool) {
+// CHECK: cir.brcond %arg0 ^bb1, ^bb2
+// CHECK: ^bb1: // pred: ^bb0
+// CHECK: cir.br ^bb3
+// CHECK: ^bb2: // pred: ^bb0
+// CHECK: cir.unreachable
+// CHECK: ^bb3: // pred: ^bb1
+// CHECK: cir.br ^bb4
+// CHECK: ^bb4: // pred: ^bb3
+// CHECK: cir.return
// CHECK: }
}
More information about the cfe-commits
mailing list