[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 &region) {
       .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 &region, mlir::Block *continueBlock,
+                            llvm::StringRef regionDescription) {
+  mlir::Operation *terminator = region.back().getTerminator();
+  rewriter.setInsertionPointToEnd(&region.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