https://github.com/xlauko updated 
https://github.com/llvm/llvm-project/pull/232217

>From 09d23bcd5375d96279b447ceb7552ff6daaf5b71 Mon Sep 17 00:00:00 2001
From: Henrich Lauko <[email protected]>
Date: Thu, 15 Oct 2026 07:46:35 +0000
Subject: [PATCH] [CIR] Keep cir.cleanup.scope cleanup regions reachable

RegionBranchOpInterface::getSuccessorRegions must return every region that
may run next, but an unwind out of a call in the body, or a cir.return,
cir.break, cir.continue or cir.goto out of it, runs the cleanup without
passing a region branch point. A cleanup reached only that way had no
predecessor, so generic passes treated it as dead, and -remove-dead-values
erased an EH cleanup that stores to memory. Now the cleanup region is a
successor of the parent and of every body exit, and a body exit that the
cleanup kind does not cover can also leave the op.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 clang/lib/CIR/Dialect/IR/CIRDialect.cpp       | 34 ++++-----
 .../remove-dead-values-cleanup-scope.cir      | 33 +++++++++
 clang/unittests/CIR/ControlFlowTest.cpp       | 70 ++++++++++---------
 3 files changed, 88 insertions(+), 49 deletions(-)
 create mode 100644 
clang/test/CIR/Transforms/remove-dead-values-cleanup-scope.cir

diff --git a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp 
b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
index 5fecc433d05ae8..15f5ccaef130b7 100644
--- a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
+++ b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
@@ -2069,31 +2069,31 @@ LogicalResult cir::CleanupScopeOp::verify() {
 
 void cir::CleanupScopeOp::getSuccessorRegions(
     mlir::RegionBranchPoint point, SmallVectorImpl<RegionSuccessor> &regions) {
-  // Execution always starts in the body region.
+  // The cleanup can also run from the middle of the body, on an unwind out of
+  // a call or on a cir.return, cir.break, cir.continue or cir.goto. None of
+  // these is a branch point, so the cleanup region is a successor of the
+  // parent and of every body exit to stand in for them.
   if (point.isParent()) {
+    regions.reserve(regions.size() + 2);
     regions.emplace_back(&getBodyRegion());
+    regions.emplace_back(&getCleanupRegion());
     return;
   }
 
   mlir::Operation *term = point.getTerminatorPredecessorOrNull();
-  assert(term && "expected a terminator predecessor");
-
-  if (term->getParentRegion() == &getBodyRegion()) {
-    // cir.resume unwinds out of the body. Every other terminator, such as
-    // cir.yield or cir.co_return, is a normal exit.
-    cir::CleanupKindAttr kind = getCleanupKindAttr();
-    if (isa<cir::ResumeOp>(term) ? kind.isEH() : kind.isNormal()) {
-      regions.emplace_back(&getCleanupRegion());
-      return;
-    }
+  if (term->getParentRegion() == &getCleanupRegion()) {
+    regions.emplace_back(getOperation());
+    return;
   }
 
-  // Exiting the cleanup region, or a body exit that skips it, leaves the
-  // operation. An unwind out of the middle of the body is not a
-  // RegionBranchPoint, and neither are cir.return, cir.break, cir.continue or
-  // cir.goto, so a cleanup region reached only that way has no predecessor
-  // here.
-  regions.emplace_back(getOperation());
+  regions.emplace_back(&getCleanupRegion());
+
+  // cir.resume unwinds out of the body, and every other terminator is a
+  // normal exit. An exit that the cleanup kind does not cover can also leave
+  // the operation directly.
+  cir::CleanupKindAttr kind = getCleanupKindAttr();
+  if (isa<cir::ResumeOp>(term) ? !kind.isEH() : !kind.isNormal())
+    regions.emplace_back(getOperation());
 }
 
 LogicalResult cir::CleanupScopeOp::canonicalize(CleanupScopeOp op,
diff --git a/clang/test/CIR/Transforms/remove-dead-values-cleanup-scope.cir 
b/clang/test/CIR/Transforms/remove-dead-values-cleanup-scope.cir
new file mode 100644
index 00000000000000..5aebe22a04739a
--- /dev/null
+++ b/clang/test/CIR/Transforms/remove-dead-values-cleanup-scope.cir
@@ -0,0 +1,33 @@
+// RUN: cir-opt %s -remove-dead-values -o - | FileCheck %s
+
+// The EH cleanup runs only when @may_throw unwinds, which is not a region
+// branch point. The cleanup region must still count as reachable, or generic
+// passes such as -remove-dead-values erase it.
+
+!s32i = !cir.int<s, 32>
+
+module {
+  cir.func private @may_throw()
+
+  cir.func @eh_cleanup(%p : !cir.ptr<!s32i>) {
+    cir.cleanup.scope {
+      cir.call @may_throw() : () -> ()
+      cir.yield
+    } cleanup eh {
+      %c = cir.const #cir.int<7> : !s32i
+      cir.store %c, %p : !s32i, !cir.ptr<!s32i>
+      cir.yield
+    }
+    cir.return
+  }
+
+  // CHECK-LABEL: cir.func @eh_cleanup
+  // CHECK:         cir.cleanup.scope {
+  // CHECK-NEXT:      cir.call @may_throw() : () -> ()
+  // CHECK-NEXT:      cir.yield
+  // CHECK-NEXT:    } cleanup eh {
+  // CHECK-NEXT:      %[[C:.*]] = cir.const #cir.int<7> : !s32i
+  // CHECK-NEXT:      cir.store %[[C]], %{{.*}} : !s32i, !cir.ptr<!s32i>
+  // CHECK-NEXT:      cir.yield
+  // CHECK-NEXT:    }
+}
diff --git a/clang/unittests/CIR/ControlFlowTest.cpp 
b/clang/unittests/CIR/ControlFlowTest.cpp
index 1cad991d860b72..a83d1098a17cbc 100644
--- a/clang/unittests/CIR/ControlFlowTest.cpp
+++ b/clang/unittests/CIR/ControlFlowTest.cpp
@@ -67,6 +67,19 @@ static RegionBranchTerminatorOpInterface 
getTerminator(Region &region) {
   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 &region) {
+  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 &region,
                                        ArrayRef<Region *> expected) {
@@ -80,13 +93,10 @@ static void expectTerminatorSuccessors(Region &region,
 }
 
 /// Verify control flow interface consistency beyond what mlir::verify checks:
-///   - Every non-empty region is reachable, unless `requireAllReachable` is
-///     false
+///   - Every non-empty region is reachable
 ///   - Every non-empty region has at least one branching terminator
 ///   - Op and terminator agree on successor regions at every branch point
-static void
-verifyControlFlowInterfaceConsistency(RegionBranchOpInterface op,
-                                      bool requireAllReachable = true) {
+static void verifyControlFlowInterfaceConsistency(RegionBranchOpInterface op) {
   EXPECT_TRUE(succeeded(mlir::verify(op)))
       << "MLIR verifier failed for '" << op->getName().getStringRef().str()
       << "'";
@@ -123,7 +133,7 @@ 
verifyControlFlowInterfaceConsistency(RegionBranchOpInterface op,
   for (Region &region : op->getRegions()) {
     if (region.empty())
       continue;
-    EXPECT_TRUE(!requireAllReachable || allReachable.contains(&region))
+    EXPECT_TRUE(allReachable.contains(&region))
         << "Region #" << region.getRegionNumber()
         << " is non-empty but not reachable from any branch point";
     EXPECT_TRUE(regionsWithExit.contains(&region))
@@ -135,18 +145,21 @@ 
verifyControlFlowInterfaceConsistency(RegionBranchOpInterface op,
 }
 
 /// Check a cir.cleanup.scope whose body has a single exit. The parent enters
-/// the body, the body exit enters the cleanup region if `bodyRunsCleanup` and
-/// leaves the op otherwise, and the cleanup region leaves the op.
+/// the body or the cleanup region. The body exit enters the cleanup region,
+/// and also leaves the op unless `bodyRunsCleanup`. The cleanup region leaves
+/// the op.
 static void expectCleanupScopeSuccessors(cir::CleanupScopeOp op,
                                          bool bodyRunsCleanup) {
   Region &body = op.getBodyRegion();
   Region &cleanup = op.getCleanupRegion();
-  expectSuccessors(op, RegionBranchPoint::parent(), {&body});
+  expectSuccessors(op, RegionBranchPoint::parent(), {&body, &cleanup});
 
   RegionBranchTerminatorOpInterface bodyTerm = getTerminator(body);
   ASSERT_TRUE(bodyTerm);
-  expectSuccessors(op, RegionBranchPoint(bodyTerm),
-                   {bodyRunsCleanup ? &cleanup : nullptr});
+  if (bodyRunsCleanup)
+    expectSuccessors(op, RegionBranchPoint(bodyTerm), {&cleanup});
+  else
+    expectSuccessors(op, RegionBranchPoint(bodyTerm), {&cleanup, nullptr});
   expectTerminatorSuccessors(cleanup, {nullptr});
 
   RegionBranchOpInterface branch = op;
@@ -154,10 +167,7 @@ static void 
expectCleanupScopeSuccessors(cir::CleanupScopeOp op,
   EXPECT_FALSE(branch.isRepetitiveRegion(1));
   EXPECT_FALSE(branch.hasLoop());
 
-  // Without the body edge nothing reaches the cleanup region.
-  verifyControlFlowInterfaceConsistency(
-      op,
-      /*requireAllReachable=*/bodyRunsCleanup);
+  verifyControlFlowInterfaceConsistency(op);
 }
 
 
//===----------------------------------------------------------------------===//
@@ -573,7 +583,8 @@ TEST_F(CIRControlFlowTest, CleanupScopeOpYieldEH) {
       cir.return
     }
   )CIR");
-  // An EH-only cleanup is skipped by the body's normal cir.yield.
+  // An EH-only cleanup does not run on the body's normal cir.yield, but stays
+  // a successor for an unwind from the middle of the body.
   expectCleanupScopeSuccessors(findFirstOp<cir::CleanupScopeOp>(*module),
                                /*bodyRunsCleanup=*/false);
 }
@@ -609,7 +620,8 @@ TEST_F(CIRControlFlowTest, CleanupScopeOpResumeNormal) {
       cir.return
     }
   )CIR");
-  // A normal-only cleanup is skipped by the body's unwinding cir.resume.
+  // A normal-only cleanup does not run on the body's unwinding cir.resume, but
+  // stays a successor for a cir.return or similar from the middle of the body.
   expectCleanupScopeSuccessors(findFirstOp<cir::CleanupScopeOp>(*module),
                                /*bodyRunsCleanup=*/false);
 }
@@ -637,31 +649,25 @@ TEST_F(CIRControlFlowTest, 
CleanupScopeOpMultiBlockBodyNormal) {
   )CIR");
   auto cleanupScopeOp = findFirstOp<cir::CleanupScopeOp>(*module);
 
-  expectSuccessors(cleanupScopeOp, RegionBranchPoint::parent(),
-                   {&cleanupScopeOp.getBodyRegion()});
-
-  // 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());
+  expectSuccessors(
+      cleanupScopeOp, RegionBranchPoint::parent(),
+      {&cleanupScopeOp.getBodyRegion(), &cleanupScopeOp.getCleanupRegion()});
+
+  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]));
 
-  // The normal exit runs the cleanup, the unwind exit skips it.
+  // The normal exit runs the cleanup. The unwind exit can also leave the op.
   expectSuccessors(cleanupScopeOp, RegionBranchPoint(bodyTerms[0]),
                    {&cleanupScopeOp.getCleanupRegion()});
-  expectSuccessors(cleanupScopeOp, RegionBranchPoint(bodyTerms[1]), {nullptr});
+  expectSuccessors(cleanupScopeOp, RegionBranchPoint(bodyTerms[1]),
+                   {&cleanupScopeOp.getCleanupRegion(), nullptr});
   expectTerminatorSuccessors(cleanupScopeOp.getCleanupRegion(), {nullptr});
 
   EXPECT_FALSE(asRegionBranch(cleanupScopeOp).hasLoop());
 
-  // The cleanup region does have a predecessor here, via the normal exit.
   verifyControlFlowInterfaceConsistency(cleanupScopeOp);
 }
 

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to