[llvm-branch-commits] [clang] [CIR] Fix cir.try successor regions (PR #231449)
Henrich Lauko via llvm-branch-commits
llvm-branch-commits at lists.llvm.org
Mon Oct 26 05:48:26 PDT 2026
https://github.com/xlauko updated https://github.com/llvm/llvm-project/pull/231449
>From 2ac6a2f844c3d9d309818522f64232af2c6de095 Mon Sep 17 00:00:00 2001
From: Henrich Lauko <hlauko at nvidia.com>
Date: Tue, 13 Oct 2026 10:40:32 +0000
Subject: [PATCH] [CIR] Fix cir.try successor regions
`TryOp::getSuccessorRegions` sent every exit from the try region back to the
parent, so nothing led from the try region to the handlers. A `cir.resume`
ending the try region, left there once FlattenCFG has flattened an inner
cleanup scope, unwinds into a handler, and `CIRTryOpFlattening` branches it
to the handler dispatch block. Now every exit from the try region can enter
the handlers, and a `cir.resume` leaves the op only when there are none. The
parent keeps its edges to the handlers. An unwind out of a call in the try
region is not a region branch point, so these edges stand in for it, and
without them -remove-dead-values empties a handler that is reached only that
way.
Co-Authored-By: Claude Opus 5.5 <noreply at anthropic.com>
---
clang/lib/CIR/Dialect/IR/CIRDialect.cpp | 38 ++++-
.../CIR/Transforms/remove-dead-values-try.cir | 40 +++++
clang/unittests/CIR/ControlFlowTest.cpp | 143 ++++++++++++++++--
3 files changed, 201 insertions(+), 20 deletions(-)
create mode 100644 clang/test/CIR/Transforms/remove-dead-values-try.cir
diff --git a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
index d585a0c5e108fc..cebdb9c4604a51 100644
--- a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
+++ b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
@@ -834,7 +834,7 @@ cir::ConditionOp::getMutableSuccessorOperands(RegionSuccessor point) {
MutableOperandRange
cir::ResumeOp::getMutableSuccessorOperands(RegionSuccessor point) {
- // The eh_token operand is not forwarded to the parent region.
+ // The eh_token operand is not modeled as forwarded to any successor.
return MutableOperandRange(getOperation(), 0, 0);
}
@@ -5176,18 +5176,40 @@ LogicalResult cir::TypeInfoAttr::verify(
void cir::TryOp::getSuccessorRegions(
mlir::RegionBranchPoint point,
llvm::SmallVectorImpl<mlir::RegionSuccessor> ®ions) {
- // The `try` and the `catchers` region branch back to the parent operation.
- if (!point.isParent()) {
+ // The interface has no branch point for an unwind out of a call in the try
+ // region, which is how most handlers are entered. The handlers are
+ // therefore potential successors of the parent and of every exit from the
+ // try region. The edge from the parent stands in for a throw before the try
+ // region has any effect, and the edges from its exits for one after some of
+ // its effects.
+ //
+ // TODO(CIR): If we know a target function never throws a specific type, we
+ // can remove the catch handler.
+ auto addHandlers = [&] {
+ for (mlir::Region &handlerRegion : getHandlerRegions())
+ regions.emplace_back(&handlerRegion);
+ };
+
+ if (point.isParent()) {
+ regions.emplace_back(&getTryRegion());
+ addHandlers();
+ return;
+ }
+
+ // A handler exit leaves the operation.
+ mlir::Operation *term = point.getTerminatorPredecessorOrNull();
+ if (term->getParentRegion() != &getTryRegion()) {
regions.emplace_back(getOperation());
return;
}
- regions.push_back(mlir::RegionSuccessor(&getTryRegion()));
+ addHandlers();
- // TODO(CIR): If we know a target function never throws a specific type, we
- // can remove the catch handler.
- for (mlir::Region &handlerRegion : this->getHandlerRegions())
- regions.push_back(mlir::RegionSuccessor(&handlerRegion));
+ // A cir.resume ending the try region unwinds into a handler. It leaves the
+ // operation only when there is no handler to unwind into. Every other exit
+ // can leave the operation.
+ if (!isa<cir::ResumeOp>(term) || getHandlerRegions().empty())
+ regions.emplace_back(getOperation());
}
/// Verify that each cir.init_catch_param in \p handlerRegion, outside any
diff --git a/clang/test/CIR/Transforms/remove-dead-values-try.cir b/clang/test/CIR/Transforms/remove-dead-values-try.cir
new file mode 100644
index 00000000000000..446f0355183b04
--- /dev/null
+++ b/clang/test/CIR/Transforms/remove-dead-values-try.cir
@@ -0,0 +1,40 @@
+// RUN: cir-opt %s -remove-dead-values -o - | FileCheck %s
+
+// The handler runs only when @may_throw throws, which is not a region branch
+// point. The handler region must still count as reachable, or generic passes
+// such as -remove-dead-values erase its contents.
+
+!s32i = !cir.int<s, 32>
+!void = !cir.void
+
+module {
+ cir.func private @may_throw()
+
+ cir.func @catch_all(%p : !cir.ptr<!s32i>) {
+ cir.scope {
+ cir.try {
+ cir.call @may_throw() : () -> ()
+ cir.yield
+ } catch all (%eh : !cir.eh_token) {
+ %ct, %exn = cir.begin_catch %eh : !cir.eh_token -> (!cir.catch_token, !cir.ptr<!void>)
+ %c = cir.const #cir.int<7> : !s32i
+ cir.store %c, %p : !s32i, !cir.ptr<!s32i>
+ cir.end_catch %ct : !cir.catch_token
+ cir.yield
+ }
+ }
+ cir.return
+ }
+
+ // CHECK-LABEL: cir.func @catch_all
+ // CHECK: cir.try {
+ // CHECK-NEXT: cir.call @may_throw() : () -> ()
+ // CHECK-NEXT: cir.yield
+ // CHECK-NEXT: } catch all (%[[EH:.*]]: !cir.eh_token) {
+ // CHECK-NEXT: %[[CT:.*]], %{{.*}} = cir.begin_catch %[[EH]]
+ // CHECK-NEXT: %[[C:.*]] = cir.const #cir.int<7> : !s32i
+ // CHECK-NEXT: cir.store %[[C]], %{{.*}} : !s32i, !cir.ptr<!s32i>
+ // CHECK-NEXT: cir.end_catch %[[CT]] : !cir.catch_token
+ // CHECK-NEXT: cir.yield
+ // CHECK-NEXT: }
+}
diff --git a/clang/unittests/CIR/ControlFlowTest.cpp b/clang/unittests/CIR/ControlFlowTest.cpp
index c7f6a7eee8cc55..c914ad47a6c4b0 100644
--- a/clang/unittests/CIR/ControlFlowTest.cpp
+++ b/clang/unittests/CIR/ControlFlowTest.cpp
@@ -67,6 +67,19 @@ static RegionBranchTerminatorOpInterface getTerminator(Region ®ion) {
return branchTerm;
}
+/// Return the RegionBranchTerminatorOpInterface terminators of `region`'s
+/// blocks, in block order. These are the region's branch points. A block that
+/// ends in a plain branch such as cir.brcond is not one and is skipped.
+static SmallVector<RegionBranchTerminatorOpInterface>
+getExitTerminators(Region ®ion) {
+ SmallVector<RegionBranchTerminatorOpInterface> terms;
+ for (Block &block : region)
+ if (auto term =
+ dyn_cast<RegionBranchTerminatorOpInterface>(block.getTerminator()))
+ terms.push_back(term);
+ return terms;
+}
+
/// Check that the terminator of `region` has the given successor regions.
static void expectTerminatorSuccessors(Region ®ion,
ArrayRef<Region *> expected) {
@@ -640,15 +653,8 @@ TEST_F(CIRControlFlowTest, CleanupScopeOpMultiBlockBodyNormal) {
cleanupScopeOp, RegionBranchPoint::parent(),
{&cleanupScopeOp.getBodyRegion(), &cleanupScopeOp.getCleanupRegion()});
- // The cir.brcond block is not a branch point, so only the two exiting
- // blocks show up here.
- SmallVector<RegionBranchTerminatorOpInterface> bodyTerms;
- for (RegionBranchPoint point :
- asRegionBranch(cleanupScopeOp).getAllRegionBranchPoints())
- if (!point.isParent() &&
- point.getTerminatorPredecessorOrNull()->getParentRegion() ==
- &cleanupScopeOp.getBodyRegion())
- bodyTerms.push_back(point.getTerminatorPredecessorOrNull());
+ SmallVector<RegionBranchTerminatorOpInterface> bodyTerms =
+ getExitTerminators(cleanupScopeOp.getBodyRegion());
ASSERT_EQ(bodyTerms.size(), 2u);
EXPECT_TRUE(isa<cir::YieldOp>(*bodyTerms[0]));
EXPECT_TRUE(isa<cir::ResumeOp>(*bodyTerms[1]));
@@ -731,15 +737,128 @@ TEST_F(CIRControlFlowTest, TryOpWithCatchAll) {
MutableArrayRef<Region> handlerRegions = tryOp.getHandlerRegions();
ASSERT_EQ(handlerRegions.size(), 1u);
+ // A throw from the middle of the body is not a branch point, so the handler
+ // is a successor of both the parent and the body's cir.yield.
expectSuccessors(tryOp, RegionBranchPoint::parent(),
{&tryRegion, &handlerRegions[0]});
- expectTerminatorSuccessors(tryRegion, {nullptr});
+ expectTerminatorSuccessors(tryRegion, {&handlerRegions[0], nullptr});
+ expectTerminatorSuccessors(handlerRegions[0], {nullptr});
+
+ EXPECT_FALSE(asRegionBranch(tryOp).hasLoop());
+
+ verifyControlFlowInterfaceConsistency(tryOp);
+}
+
+TEST_F(CIRControlFlowTest, TryOpResumeInBody) {
+ // A cir.resume left behind in the body by an already-flattened inner
+ // cleanup scope unwinds into the handlers.
+ OwningOpRef<ModuleOp> module = parse(R"CIR(
+ !u8i = !cir.int<u, 8>
+ !void = !cir.void
+ cir.func @f() {
+ cir.scope {
+ cir.try {
+ %tok = cir.eh.initiate cleanup : !cir.eh_token
+ cir.resume %tok : !cir.eh_token
+ } catch [type #cir.global_view<@_ZTIi> : !cir.ptr<!u8i>]
+ (%eh : !cir.eh_token) {
+ %ct, %exn = cir.begin_catch %eh
+ : !cir.eh_token -> (!cir.catch_token, !cir.ptr<!void>)
+ cir.end_catch %ct : !cir.catch_token
+ cir.yield
+ } unwind (%eh : !cir.eh_token) {
+ cir.resume %eh : !cir.eh_token
+ }
+ }
+ cir.return
+ }
+ )CIR");
+ auto tryOp = findFirstOp<cir::TryOp>(*module);
+
+ Region &tryRegion = tryOp.getTryRegion();
+ MutableArrayRef<Region> handlerRegions = tryOp.getHandlerRegions();
+ ASSERT_EQ(handlerRegions.size(), 2u);
+
+ expectSuccessors(tryOp, RegionBranchPoint::parent(),
+ {&tryRegion, &handlerRegions[0], &handlerRegions[1]});
+ expectTerminatorSuccessors(tryRegion,
+ {&handlerRegions[0], &handlerRegions[1]});
+ // A cir.resume in the unwind handler leaves the op like any handler exit.
expectTerminatorSuccessors(handlerRegions[0], {nullptr});
+ expectTerminatorSuccessors(handlerRegions[1], {nullptr});
EXPECT_FALSE(asRegionBranch(tryOp).hasLoop());
- // TODO: TryOp::getSuccessorInputs returns empty for handler regions that
- // have block arguments, so verifyControlFlowInterfaceConsistency fails.
+ verifyControlFlowInterfaceConsistency(tryOp);
+}
+
+TEST_F(CIRControlFlowTest, TryOpMultiBlockBody) {
+ // One block leaves the try body normally and another unwinds. Only the
+ // normal exit can leave the op.
+ OwningOpRef<ModuleOp> module = parse(R"CIR(
+ !void = !cir.void
+ cir.func @f() {
+ cir.scope {
+ cir.try {
+ %cond = cir.const #cir.bool<true> : !cir.bool
+ cir.brcond %cond ^bb_normal, ^bb_unwind
+ ^bb_normal:
+ cir.yield
+ ^bb_unwind:
+ %tok = cir.eh.initiate cleanup : !cir.eh_token
+ cir.resume %tok : !cir.eh_token
+ } catch all (%eh : !cir.eh_token) {
+ %ct, %exn = cir.begin_catch %eh
+ : !cir.eh_token -> (!cir.catch_token, !cir.ptr<!void>)
+ cir.end_catch %ct : !cir.catch_token
+ cir.yield
+ }
+ }
+ cir.return
+ }
+ )CIR");
+ auto tryOp = findFirstOp<cir::TryOp>(*module);
+ Region &tryRegion = tryOp.getTryRegion();
+ Region &handler = tryOp.getHandlerRegions()[0];
+
+ SmallVector<RegionBranchTerminatorOpInterface> bodyTerms =
+ getExitTerminators(tryRegion);
+ ASSERT_EQ(bodyTerms.size(), 2u);
+ EXPECT_TRUE(isa<cir::YieldOp>(*bodyTerms[0]));
+ EXPECT_TRUE(isa<cir::ResumeOp>(*bodyTerms[1]));
+
+ expectSuccessors(tryOp, RegionBranchPoint(bodyTerms[0]), {&handler, nullptr});
+ expectSuccessors(tryOp, RegionBranchPoint(bodyTerms[1]), {&handler});
+ expectTerminatorSuccessors(handler, {nullptr});
+
+ // hasLoop() reports a loop whenever two exits reach the same region, so
+ // check that no region can reach itself instead.
+ RegionBranchOpInterface branch = asRegionBranch(tryOp);
+ EXPECT_FALSE(branch.isRepetitiveRegion(0));
+ EXPECT_FALSE(branch.isRepetitiveRegion(1));
+
+ verifyControlFlowInterfaceConsistency(tryOp);
+}
+
+TEST_F(CIRControlFlowTest, TryOpResumeInBodyWithoutHandlers) {
+ OwningOpRef<ModuleOp> module = parse(R"CIR(
+ cir.func @f() {
+ cir.scope {
+ cir.try {
+ %tok = cir.eh.initiate cleanup : !cir.eh_token
+ cir.resume %tok : !cir.eh_token
+ }
+ }
+ cir.return
+ }
+ )CIR");
+ auto tryOp = findFirstOp<cir::TryOp>(*module);
+
+ // With no handler to unwind into, the cir.resume leaves the op.
+ expectSuccessors(tryOp, RegionBranchPoint::parent(), {&tryOp.getTryRegion()});
+ expectTerminatorSuccessors(tryOp.getTryRegion(), {nullptr});
+
+ verifyControlFlowInterfaceConsistency(tryOp);
}
TEST_F(CIRControlFlowTest, CoroutineOp) {
More information about the llvm-branch-commits
mailing list