[clang] [CIR] Add canonicalizer for CleanupScopeOp (PR #191084)
via cfe-commits
cfe-commits at lists.llvm.org
Wed Apr 8 17:03:00 PDT 2026
llvmbot wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-clang
@llvm/pr-subscribers-clangir
Author: Andy Kaylor (andykaylor)
<details>
<summary>Changes</summary>
This change adds a canonicalizer for CleanupScopeOp that erases any cleanup scope with a trivial cleanup region, inlining the contents of the body region into the block in place of the cleanup scope op. It also erases any EH-only cleanup scope whose body region contains only a yield operation, dropping the cleanup region contents even if they were not trivial because the EH cleanup is not reachable in this case.
Assisted-by: Cursor / claude-4.6-opus-high
---
Full diff: https://github.com/llvm/llvm-project/pull/191084.diff
4 Files Affected:
- (modified) clang/include/clang/CIR/Dialect/IR/CIROps.td (+1)
- (modified) clang/lib/CIR/Dialect/IR/CIRDialect.cpp (+35)
- (modified) clang/lib/CIR/Dialect/Transforms/CIRCanonicalize.cpp (+2-1)
- (added) clang/test/CIR/Transforms/canonicalize-cleanup-scope.cir (+196)
``````````diff
diff --git a/clang/include/clang/CIR/Dialect/IR/CIROps.td b/clang/include/clang/CIR/Dialect/IR/CIROps.td
index 93ca0172b2a7f..b806800275c7b 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIROps.td
+++ b/clang/include/clang/CIR/Dialect/IR/CIROps.td
@@ -1298,6 +1298,7 @@ def CIR_CleanupScopeOp : CIR_Op<"cleanup.scope", [
"llvm::function_ref<void(mlir::OpBuilder &, mlir::Location)>":$cleanupBuilder)>
];
+ let hasCanonicalizeMethod = 1;
let hasLLVMLowering = false;
}
diff --git a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
index e99faf840c15a..d5e21c26fe9be 100644
--- a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
+++ b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
@@ -1449,6 +1449,41 @@ cir::CleanupScopeOp::getSuccessorInputs(RegionSuccessor successor) {
return ValueRange();
}
+LogicalResult cir::CleanupScopeOp::canonicalize(CleanupScopeOp op,
+ PatternRewriter &rewriter) {
+ auto isRegionTrivial = [](Region ®ion) {
+ if (!region.hasOneBlock())
+ return false;
+ Block &block = region.front();
+ return llvm::hasSingleElement(block) && isa<cir::YieldOp>(block.front());
+ };
+
+ Region &body = op.getBodyRegion();
+ Region &cleanup = op.getCleanupRegion();
+
+ // An EH-only cleanup scope with an empty body can never trigger its cleanup
+ // region — there are no operations in the body that could throw. Erase it.
+ if (op.getCleanupKind() == CleanupKind::EH && isRegionTrivial(body)) {
+ rewriter.eraseOp(op);
+ return success();
+ }
+
+ // A cleanup scope with a trivial cleanup region has no cleanup to perform.
+ // Inline the body into the parent block and erase the scope.
+ if (!isRegionTrivial(cleanup) || !body.hasOneBlock())
+ return failure();
+
+ Block &bodyBlock = body.front();
+ if (!isa<cir::YieldOp>(bodyBlock.getTerminator()))
+ return failure();
+
+ Operation *yield = bodyBlock.getTerminator();
+ rewriter.inlineBlockBefore(&bodyBlock, op);
+ rewriter.eraseOp(yield);
+ rewriter.eraseOp(op);
+ return success();
+}
+
void cir::CleanupScopeOp::build(
OpBuilder &builder, OperationState &result, CleanupKind cleanupKind,
function_ref<void(OpBuilder &, Location)> bodyBuilder,
diff --git a/clang/lib/CIR/Dialect/Transforms/CIRCanonicalize.cpp b/clang/lib/CIR/Dialect/Transforms/CIRCanonicalize.cpp
index 333517956fe9c..da08f21977066 100644
--- a/clang/lib/CIR/Dialect/Transforms/CIRCanonicalize.cpp
+++ b/clang/lib/CIR/Dialect/Transforms/CIRCanonicalize.cpp
@@ -75,7 +75,8 @@ void CIRCanonicalizePass::runOnOperation() {
ComplexCreateOp, ComplexImagOp, ComplexRealOp, VecCmpOp,
VecCreateOp, VecExtractOp, VecShuffleOp, VecShuffleDynamicOp,
VecTernaryOp, BitClrsbOp, BitClzOp, BitCtzOp, BitFfsOp, BitParityOp,
- BitPopcountOp, BitReverseOp, ByteSwapOp, RotateOp, ConstantOp>(op))
+ BitPopcountOp, BitReverseOp, ByteSwapOp, RotateOp, ConstantOp,
+ CleanupScopeOp>(op))
ops.push_back(op);
});
diff --git a/clang/test/CIR/Transforms/canonicalize-cleanup-scope.cir b/clang/test/CIR/Transforms/canonicalize-cleanup-scope.cir
new file mode 100644
index 0000000000000..e6f127549fe13
--- /dev/null
+++ b/clang/test/CIR/Transforms/canonicalize-cleanup-scope.cir
@@ -0,0 +1,196 @@
+// RUN: cir-opt %s -cir-canonicalize -o - | FileCheck %s
+
+!s32i = !cir.int<s, 32>
+!rec_S = !cir.record<struct "S" {!s32i}>
+
+module {
+
+cir.func private @dtor(!cir.ptr<!rec_S>) attributes {nothrow}
+cir.func private @may_throw()
+cir.func private @side_effect()
+
+// EH cleanup with trivial body and trivial cleanup is erased (trivially dead).
+cir.func @eh_both_trivial_erased() {
+ cir.cleanup.scope {
+ cir.yield
+ } cleanup eh {
+ cir.yield
+ }
+ cir.return
+}
+// CHECK-LABEL: @eh_both_trivial_erased
+// CHECK-NEXT: cir.return
+
+// EH cleanup with trivial body and non-trivial cleanup is erased: the body
+// has no throwing operations so the EH cleanup can never fire.
+cir.func @eh_empty_body_nontrivial_cleanup(%arg0: !cir.ptr<!rec_S>) {
+ cir.cleanup.scope {
+ cir.yield
+ } cleanup eh {
+ cir.call @dtor(%arg0) nothrow : (!cir.ptr<!rec_S>) -> ()
+ cir.yield
+ }
+ cir.return
+}
+// CHECK-LABEL: @eh_empty_body_nontrivial_cleanup
+// CHECK-NEXT: cir.return
+
+// EH cleanup with non-empty body should NOT be erased.
+cir.func @eh_nonempty_body_kept(%arg0: !cir.ptr<!rec_S>) {
+ cir.cleanup.scope {
+ cir.call @may_throw() : () -> ()
+ cir.yield
+ } cleanup eh {
+ cir.call @dtor(%arg0) nothrow : (!cir.ptr<!rec_S>) -> ()
+ cir.yield
+ }
+ cir.return
+}
+// CHECK-LABEL: @eh_nonempty_body_kept
+// CHECK: cir.cleanup.scope {
+// CHECK: cir.call @may_throw()
+// CHECK: cir.yield
+// CHECK: } cleanup eh {
+// CHECK: cir.call @dtor
+// CHECK: cir.yield
+// CHECK: }
+// CHECK: cir.return
+
+// All cleanup with non-trivial cleanup should NOT be erased.
+cir.func @all_nonempty_cleanup_kept(%arg0: !cir.ptr<!rec_S>) {
+ cir.cleanup.scope {
+ cir.yield
+ } cleanup all {
+ cir.call @dtor(%arg0) nothrow : (!cir.ptr<!rec_S>) -> ()
+ cir.yield
+ }
+ cir.return
+}
+// CHECK-LABEL: @all_nonempty_cleanup_kept
+// CHECK: cir.cleanup.scope {
+// CHECK: cir.yield
+// CHECK: } cleanup all {
+// CHECK: cir.call @dtor
+// CHECK: cir.yield
+// CHECK: }
+// CHECK: cir.return
+
+// Normal cleanup with non-trivial cleanup should NOT be erased.
+cir.func @normal_nonempty_cleanup_kept(%arg0: !cir.ptr<!rec_S>) {
+ cir.cleanup.scope {
+ cir.yield
+ } cleanup normal {
+ cir.call @dtor(%arg0) nothrow : (!cir.ptr<!rec_S>) -> ()
+ cir.yield
+ }
+ cir.return
+}
+// CHECK-LABEL: @normal_nonempty_cleanup_kept
+// CHECK: cir.cleanup.scope {
+// CHECK: cir.yield
+// CHECK: } cleanup normal {
+// CHECK: cir.call @dtor
+// CHECK: cir.yield
+// CHECK: }
+// CHECK: cir.return
+
+// Trivial cleanup region: body is inlined and scope is erased.
+cir.func @trivial_cleanup_body_inlined() {
+ cir.cleanup.scope {
+ cir.call @side_effect() : () -> ()
+ cir.yield
+ } cleanup all {
+ cir.yield
+ }
+ cir.return
+}
+// CHECK-LABEL: @trivial_cleanup_body_inlined
+// CHECK-NEXT: cir.call @side_effect()
+// CHECK-NEXT: cir.return
+
+// Trivial cleanup region with normal kind: body inlined.
+cir.func @normal_trivial_cleanup_inlined() {
+ cir.cleanup.scope {
+ cir.call @side_effect() : () -> ()
+ cir.yield
+ } cleanup normal {
+ cir.yield
+ }
+ cir.return
+}
+// CHECK-LABEL: @normal_trivial_cleanup_inlined
+// CHECK-NEXT: cir.call @side_effect()
+// CHECK-NEXT: cir.return
+
+// Trivial cleanup with multiple body ops: all inlined in order.
+cir.func @trivial_cleanup_multi_ops(%arg0: !cir.ptr<!rec_S>) {
+ cir.cleanup.scope {
+ cir.call @may_throw() : () -> ()
+ cir.call @dtor(%arg0) nothrow : (!cir.ptr<!rec_S>) -> ()
+ cir.yield
+ } cleanup all {
+ cir.yield
+ }
+ cir.return
+}
+// CHECK-LABEL: @trivial_cleanup_multi_ops
+// CHECK-NEXT: cir.call @may_throw()
+// CHECK-NEXT: cir.call @dtor
+// CHECK-NEXT: cir.return
+
+// Nested: inner EH with empty body erased, outer kept.
+cir.func @nested_inner_eh_erased(%arg0: !cir.ptr<!rec_S>) {
+ cir.cleanup.scope {
+ cir.call @may_throw() : () -> ()
+ cir.cleanup.scope {
+ cir.yield
+ } cleanup eh {
+ cir.call @dtor(%arg0) nothrow : (!cir.ptr<!rec_S>) -> ()
+ cir.yield
+ }
+ cir.yield
+ } cleanup eh {
+ cir.call @dtor(%arg0) nothrow : (!cir.ptr<!rec_S>) -> ()
+ cir.yield
+ }
+ cir.return
+}
+// CHECK-LABEL: @nested_inner_eh_erased
+// CHECK: cir.cleanup.scope {
+// CHECK: cir.call @may_throw()
+// CHECK-NOT: cir.cleanup.scope
+// CHECK: cir.yield
+// CHECK: } cleanup eh {
+// CHECK: cir.call @dtor
+// CHECK: cir.yield
+// CHECK: }
+// CHECK: cir.return
+
+// Nested: inner has trivial cleanup, outer does not. Inner body inlined
+// into outer body; outer remains.
+cir.func @nested_trivial_cleanup_inlined(%arg0: !cir.ptr<!rec_S>) {
+ cir.cleanup.scope {
+ cir.cleanup.scope {
+ cir.call @may_throw() : () -> ()
+ cir.yield
+ } cleanup eh {
+ cir.yield
+ }
+ cir.yield
+ } cleanup all {
+ cir.call @dtor(%arg0) nothrow : (!cir.ptr<!rec_S>) -> ()
+ cir.yield
+ }
+ cir.return
+}
+// CHECK-LABEL: @nested_trivial_cleanup_inlined
+// CHECK: cir.cleanup.scope {
+// CHECK-NEXT: cir.call @may_throw()
+// CHECK-NEXT: cir.yield
+// CHECK-NEXT: } cleanup all {
+// CHECK: cir.call @dtor
+// CHECK: cir.yield
+// CHECK: }
+// CHECK: cir.return
+
+}
``````````
</details>
https://github.com/llvm/llvm-project/pull/191084
More information about the cfe-commits
mailing list