https://github.com/andykaylor created https://github.com/llvm/llvm-project/pull/191084
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 >From 64ccca1fc79e689ae6bd976faf17431ffc019bcf Mon Sep 17 00:00:00 2001 From: Andy Kaylor <[email protected]> Date: Wed, 8 Apr 2026 16:29:58 -0700 Subject: [PATCH] [CIR] Add canonicalizer for CleanupScopeOp 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 --- clang/include/clang/CIR/Dialect/IR/CIROps.td | 1 + clang/lib/CIR/Dialect/IR/CIRDialect.cpp | 35 ++++ .../Dialect/Transforms/CIRCanonicalize.cpp | 3 +- .../Transforms/canonicalize-cleanup-scope.cir | 196 ++++++++++++++++++ 4 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 clang/test/CIR/Transforms/canonicalize-cleanup-scope.cir 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 + +} _______________________________________________ cfe-commits mailing list [email protected] https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits
