[clang] [CIR] Add RecursiveMemoryEffects to region-bearing ops (PR #187865)
Henrich Lauko via cfe-commits
cfe-commits at lists.llvm.org
Sat Mar 21 06:52:01 PDT 2026
https://github.com/xlauko created https://github.com/llvm/llvm-project/pull/187865
Add the RecursiveMemoryEffects trait to cir.if, cir.case, loop ops
(cir.while/cir.do/cir.for), cir.ternary, cir.await,
cir.array.ctor, cir.array.dtor, and cir.try. Without this trait,
MLIR conservatively assumes unknown memory effects for ops with
regions, preventing DCE of ops whose bodies are provably pure.
Also fix a crash in ConditionOp::getSuccessorRegions where the
missing early return after the loop-op case would fall through to
cast<AwaitOp>(...), which asserts when the parent is a loop rather
than an await op.
Add tests verifying that region ops with pure bodies are eliminated
and ops with stores or calls are preserved, including two-level nested
propagation (cir.if inside cir.while).
>From f5de28f72ca21bd8a78c7f4dffacecd01314d7a7 Mon Sep 17 00:00:00 2001
From: xlauko <xlauko at mail.muni.cz>
Date: Sat, 21 Mar 2026 14:50:35 +0100
Subject: [PATCH] [CIR] Add RecursiveMemoryEffects to region-bearing ops
Add the RecursiveMemoryEffects trait to cir.if, cir.case, loop ops
(cir.while/cir.do/cir.for), cir.ternary, cir.await,
cir.array.ctor, cir.array.dtor, and cir.try. Without this trait,
MLIR conservatively assumes unknown memory effects for ops with
regions, preventing DCE of ops whose bodies are provably pure.
Also fix a crash in ConditionOp::getSuccessorRegions where the
missing early return after the loop-op case would fall through to
cast<AwaitOp>(...), which asserts when the parent is a loop rather
than an await op.
Add tests verifying that region ops with pure bodies are eliminated
and ops with stores or calls are preserved, including two-level nested
propagation (cir.if inside cir.while).
---
clang/include/clang/CIR/Dialect/IR/CIROps.td | 18 +-
clang/lib/CIR/Dialect/IR/CIRDialect.cpp | 1 +
.../Transforms/recursive-memory-effects.cir | 304 ++++++++++++++++++
3 files changed, 315 insertions(+), 8 deletions(-)
create mode 100644 clang/test/CIR/Transforms/recursive-memory-effects.cir
diff --git a/clang/include/clang/CIR/Dialect/IR/CIROps.td b/clang/include/clang/CIR/Dialect/IR/CIROps.td
index 41858a61480a8..4c063e29e2b1e 100644
--- a/clang/include/clang/CIR/Dialect/IR/CIROps.td
+++ b/clang/include/clang/CIR/Dialect/IR/CIROps.td
@@ -836,7 +836,8 @@ def CIR_ReturnOp : CIR_Op<"return", [
def CIR_IfOp : CIR_Op<"if", [
DeclareOpInterfaceMethods<RegionBranchOpInterface, ["getSuccessorInputs"]>,
- RecursivelySpeculatable, AutomaticAllocationScope, NoRegionArguments
+ RecursivelySpeculatable, AutomaticAllocationScope, NoRegionArguments,
+ RecursiveMemoryEffects
]> {
let summary = "the if-then-else operation";
let description = [{
@@ -1364,7 +1365,7 @@ def CIR_CaseOpKind : CIR_I32EnumAttr<"CaseOpKind", "case kind", [
def CIR_CaseOp : CIR_Op<"case", [
DeclareOpInterfaceMethods<RegionBranchOpInterface, ["getSuccessorInputs"]>,
- RecursivelySpeculatable, AutomaticAllocationScope
+ RecursivelySpeculatable, AutomaticAllocationScope, RecursiveMemoryEffects
]> {
let summary = "Case operation";
let description = [{
@@ -2004,7 +2005,7 @@ def CIR_IndirectBrOp : CIR_Op<"indirect_br", [
//===----------------------------------------------------------------------===//
class CIR_LoopOpBase<string mnemonic> : CIR_Op<mnemonic, [
- LoopOpInterface, NoRegionArguments
+ LoopOpInterface, NoRegionArguments, RecursiveMemoryEffects
]> {
let extraClassDefinition = [{
void $cppClass::getSuccessorRegions(
@@ -2703,7 +2704,8 @@ def CIR_SelectOp : CIR_Op<"select", [
def CIR_TernaryOp : CIR_Op<"ternary", [
DeclareOpInterfaceMethods<RegionBranchOpInterface, ["getSuccessorInputs"]>,
- RecursivelySpeculatable, AutomaticAllocationScope, NoRegionArguments
+ RecursivelySpeculatable, AutomaticAllocationScope, NoRegionArguments,
+ RecursiveMemoryEffects
]> {
let summary = "The `cond ? a : b` C/C++ ternary operation";
let description = [{
@@ -4040,7 +4042,7 @@ def CIR_AwaitKind : CIR_I32EnumAttr<"AwaitKind", "await kind", [
def CIR_AwaitOp : CIR_Op<"await",[
DeclareOpInterfaceMethods<RegionBranchOpInterface, ["getSuccessorInputs"]>,
- RecursivelySpeculatable, NoRegionArguments
+ RecursivelySpeculatable, NoRegionArguments, RecursiveMemoryEffects
]> {
let summary = "Wraps C++ co_await implicit logic";
let description = [{
@@ -4531,7 +4533,7 @@ def CIR_TrapOp : CIR_Op<"trap", [Terminator]> {
// ArrayCtor & ArrayDtor
//===----------------------------------------------------------------------===//
-def CIR_ArrayCtor : CIR_Op<"array.ctor"> {
+def CIR_ArrayCtor : CIR_Op<"array.ctor", [RecursiveMemoryEffects]> {
let summary = "Initialize array elements with C++ constructors";
let description = [{
Initialize each array element using the same C++ constructor. This
@@ -4573,7 +4575,7 @@ def CIR_ArrayCtor : CIR_Op<"array.ctor"> {
let hasLLVMLowering = false;
}
-def CIR_ArrayDtor : CIR_Op<"array.dtor"> {
+def CIR_ArrayDtor : CIR_Op<"array.dtor", [RecursiveMemoryEffects]> {
let summary = "Destroy array elements with C++ destructors";
let description = [{
Destroy each array element using the same C++ destructor. This operation
@@ -6765,7 +6767,7 @@ def CIR_AllocExceptionOp : CIR_Op<"alloc.exception"> {
def CIR_TryOp : CIR_Op<"try",[
DeclareOpInterfaceMethods<RegionBranchOpInterface, ["getSuccessorInputs"]>,
- RecursivelySpeculatable, AutomaticAllocationScope
+ RecursivelySpeculatable, AutomaticAllocationScope, RecursiveMemoryEffects
]> {
let summary = "C++ try block";
let description = [{
diff --git a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
index bf369bfe69991..8e384e676557c 100644
--- a/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
+++ b/clang/lib/CIR/Dialect/IR/CIRDialect.cpp
@@ -337,6 +337,7 @@ void cir::ConditionOp::getSuccessorRegions(
if (auto loopOp = dyn_cast<LoopOpInterface>(getOperation()->getParentOp())) {
regions.emplace_back(&loopOp.getBody());
regions.push_back(RegionSuccessor::parent());
+ return;
}
// Parent is an await: condition may branch to resume or suspend regions.
diff --git a/clang/test/CIR/Transforms/recursive-memory-effects.cir b/clang/test/CIR/Transforms/recursive-memory-effects.cir
new file mode 100644
index 0000000000000..9fbb4b0c9def8
--- /dev/null
+++ b/clang/test/CIR/Transforms/recursive-memory-effects.cir
@@ -0,0 +1,304 @@
+// RUN: cir-opt %s --remove-dead-values -o - | FileCheck %s
+//
+// Tests that RecursiveMemoryEffects on region-based ops enables correct DCE:
+// - Ops whose regions contain no memory effects can be eliminated when unused.
+// - Ops whose regions contain stores/calls are preserved even when unused.
+
+!s32i = !cir.int<s, 32>
+!u8i = !cir.int<u, 8>
+!void = !cir.void
+!rec_S = !cir.record<struct "S" padded {!u8i}>
+
+module {
+
+// ---------------------------------------------------------------------------
+// cir.ternary
+// ---------------------------------------------------------------------------
+
+// Unused cir.ternary with a pure body (only cir.const, cir.yield, no stores)
+// → no memory effects via RecursiveMemoryEffects → DCE'd.
+//
+// CHECK-LABEL: cir.func @ternary_pure_body_dce
+// CHECK-NOT: cir.ternary
+// CHECK: cir.return
+ cir.func @ternary_pure_body_dce(%arg0: !cir.bool, %arg1: !s32i) -> !s32i {
+ %unused = cir.ternary (%arg0, true {
+ %c = cir.const #cir.int<1> : !s32i
+ cir.yield %c : !s32i
+ }, false {
+ cir.yield %arg1 : !s32i
+ }) : (!cir.bool) -> !s32i
+ cir.return %arg1 : !s32i
+ }
+
+// Unused cir.ternary with a store in one region inherits that write effect
+// → preserved despite unused result.
+//
+// CHECK-LABEL: cir.func @ternary_store_preserved
+// CHECK: cir.ternary
+// CHECK: cir.store
+// CHECK: cir.return
+ cir.func @ternary_store_preserved(%arg0: !cir.bool,
+ %arg1: !cir.ptr<!s32i>) -> !s32i {
+ %c0 = cir.const #cir.int<0> : !s32i
+ %unused = cir.ternary (%arg0, true {
+ %c = cir.const #cir.int<42> : !s32i
+ cir.store %c, %arg1 : !s32i, !cir.ptr<!s32i>
+ cir.yield %c : !s32i
+ }, false {
+ cir.yield %c0 : !s32i
+ }) : (!cir.bool) -> !s32i
+ cir.return %c0 : !s32i
+ }
+
+// ---------------------------------------------------------------------------
+// cir.if
+// ---------------------------------------------------------------------------
+
+// cir.if with a pure body (no store) → no effects → DCE'd.
+//
+// CHECK-LABEL: cir.func @if_pure_body_dce
+// CHECK-NOT: cir.if
+// CHECK: cir.return
+ cir.func @if_pure_body_dce(%arg0: !cir.bool) {
+ cir.if %arg0 {
+ %c = cir.const #cir.int<1> : !s32i
+ cir.yield
+ }
+ cir.return
+ }
+
+// cir.if with a store in its body → preserved.
+//
+// CHECK-LABEL: cir.func @if_store_preserved
+// CHECK: cir.if
+// CHECK: cir.store
+// CHECK: cir.return
+ cir.func @if_store_preserved(%arg0: !cir.bool, %arg1: !cir.ptr<!s32i>) {
+ %c = cir.const #cir.int<1> : !s32i
+ cir.if %arg0 {
+ cir.store %c, %arg1 : !s32i, !cir.ptr<!s32i>
+ cir.yield
+ }
+ cir.return
+ }
+
+// ---------------------------------------------------------------------------
+// cir.switch / cir.case
+// ---------------------------------------------------------------------------
+
+// cir.switch with all-pure cases (only cir.const, cir.yield) → no effects
+// via RecursiveMemoryEffects on cir.case and cir.switch → DCE'd.
+//
+// CHECK-LABEL: cir.func @switch_pure_body_dce
+// CHECK-NOT: cir.switch
+// CHECK: cir.return
+ cir.func @switch_pure_body_dce(%arg0: !s32i) {
+ cir.switch (%arg0 : !s32i) {
+ cir.case (equal, [#cir.int<1> : !s32i]) {
+ %c = cir.const #cir.int<42> : !s32i
+ cir.yield
+ }
+ cir.case (default, []) {
+ cir.yield
+ }
+ cir.yield
+ }
+ cir.return
+ }
+
+// cir.switch whose cases contain stores → preserved.
+//
+// CHECK-LABEL: cir.func @switch_store_preserved
+// CHECK: cir.switch
+// CHECK: cir.store
+// CHECK: cir.return
+ cir.func @switch_store_preserved(%arg0: !s32i, %arg1: !cir.ptr<!s32i>) {
+ cir.switch (%arg0 : !s32i) {
+ cir.case (equal, [#cir.int<1> : !s32i]) {
+ %c = cir.const #cir.int<42> : !s32i
+ cir.store %c, %arg1 : !s32i, !cir.ptr<!s32i>
+ cir.yield
+ }
+ cir.case (default, []) {
+ cir.yield
+ }
+ cir.yield
+ }
+ cir.return
+ }
+
+// ---------------------------------------------------------------------------
+// cir.while / cir.do / cir.for
+// ---------------------------------------------------------------------------
+
+// cir.while with a store in its body → preserved.
+//
+// CHECK-LABEL: cir.func @while_store_preserved
+// CHECK: cir.while
+// CHECK: cir.store
+// CHECK: cir.return
+ cir.func @while_store_preserved(%arg0: !cir.bool, %arg1: !cir.ptr<!s32i>) {
+ %c = cir.const #cir.int<1> : !s32i
+ cir.while {
+ cir.condition(%arg0)
+ } do {
+ cir.store %c, %arg1 : !s32i, !cir.ptr<!s32i>
+ cir.yield
+ }
+ cir.return
+ }
+
+// cir.do with a store in its body → preserved.
+//
+// CHECK-LABEL: cir.func @do_store_preserved
+// CHECK: cir.do
+// CHECK: cir.store
+// CHECK: cir.return
+ cir.func @do_store_preserved(%arg0: !cir.bool, %arg1: !cir.ptr<!s32i>) {
+ %c = cir.const #cir.int<1> : !s32i
+ cir.do {
+ cir.store %c, %arg1 : !s32i, !cir.ptr<!s32i>
+ cir.yield
+ } while {
+ cir.condition(%arg0)
+ }
+ cir.return
+ }
+
+// cir.for with a store in its body → preserved.
+//
+// CHECK-LABEL: cir.func @for_store_preserved
+// CHECK: cir.for
+// CHECK: cir.store
+// CHECK: cir.return
+ cir.func @for_store_preserved(%arg0: !cir.bool, %arg1: !cir.ptr<!s32i>) {
+ %c = cir.const #cir.int<1> : !s32i
+ cir.for : cond {
+ cir.condition(%arg0)
+ } body {
+ cir.store %c, %arg1 : !s32i, !cir.ptr<!s32i>
+ cir.yield
+ } step {
+ cir.yield
+ }
+ cir.return
+ }
+
+// cir.while containing a cir.if that stores: write effect propagates through
+// two nesting levels via RecursiveMemoryEffects → while preserved.
+//
+// CHECK-LABEL: cir.func @nested_if_in_while_store_preserved
+// CHECK: cir.while
+// CHECK: cir.if
+// CHECK: cir.store
+// CHECK: cir.return
+ cir.func @nested_if_in_while_store_preserved(%arg0: !cir.bool,
+ %arg1: !cir.bool,
+ %arg2: !cir.ptr<!s32i>) {
+ %c = cir.const #cir.int<1> : !s32i
+ cir.while {
+ cir.condition(%arg0)
+ } do {
+ cir.if %arg1 {
+ cir.store %c, %arg2 : !s32i, !cir.ptr<!s32i>
+ cir.yield
+ }
+ cir.yield
+ }
+ cir.return
+ }
+
+// ---------------------------------------------------------------------------
+// cir.try
+// ---------------------------------------------------------------------------
+
+// cir.try with a store in the try body → preserved.
+//
+// CHECK-LABEL: cir.func @try_store_preserved
+// CHECK: cir.try
+// CHECK: cir.store
+// CHECK: cir.return
+ cir.func @try_store_preserved(%arg0: !cir.ptr<!s32i>) {
+ %c = cir.const #cir.int<1> : !s32i
+ cir.try {
+ cir.store %c, %arg0 : !s32i, !cir.ptr<!s32i>
+ cir.yield
+ } catch all (%eh_token : !cir.eh_token) {
+ %catch_token, %exn = cir.begin_catch %eh_token
+ : !cir.eh_token -> (!cir.catch_token, !cir.ptr<!void>)
+ cir.cleanup.scope {
+ cir.yield
+ } cleanup eh {
+ cir.end_catch %catch_token : !cir.catch_token
+ cir.yield
+ }
+ cir.yield
+ }
+ cir.return
+ }
+
+// ---------------------------------------------------------------------------
+// cir.await
+// ---------------------------------------------------------------------------
+
+// cir.await whose resume region contains a store → preserved.
+//
+// CHECK-LABEL: cir.func coroutine @await_store_preserved
+// CHECK: cir.await
+// CHECK: cir.store
+// CHECK: cir.return
+ cir.func coroutine @await_store_preserved(%arg0: !cir.bool,
+ %arg1: !cir.ptr<!s32i>) {
+ %c = cir.const #cir.int<1> : !s32i
+ cir.await(user, ready : {
+ cir.condition(%arg0)
+ }, suspend : {
+ cir.yield
+ }, resume : {
+ cir.store %c, %arg1 : !s32i, !cir.ptr<!s32i>
+ cir.yield
+ },)
+ cir.return
+ }
+
+// ---------------------------------------------------------------------------
+// cir.array.ctor / cir.array.dtor
+// ---------------------------------------------------------------------------
+
+// cir.array.ctor calls a constructor inside its region → call has effects
+// → preserved.
+//
+// CHECK-LABEL: cir.func @array_ctor_preserved
+// CHECK: cir.array.ctor
+// CHECK: cir.call
+// CHECK: cir.return
+ cir.func private @_ZN1SC1Ev(!cir.ptr<!rec_S>)
+ cir.func @array_ctor_preserved(
+ %arg0: !cir.ptr<!cir.array<!rec_S x 4>>) {
+ cir.array.ctor %arg0 : !cir.ptr<!cir.array<!rec_S x 4>> {
+ ^bb0(%elem: !cir.ptr<!rec_S>):
+ cir.call @_ZN1SC1Ev(%elem) : (!cir.ptr<!rec_S>) -> ()
+ cir.yield
+ }
+ cir.return
+ }
+
+// cir.array.dtor calls a destructor inside its region → preserved.
+//
+// CHECK-LABEL: cir.func @array_dtor_preserved
+// CHECK: cir.array.dtor
+// CHECK: cir.call
+// CHECK: cir.return
+ cir.func private @_ZN1SD1Ev(!cir.ptr<!rec_S>)
+ cir.func @array_dtor_preserved(
+ %arg0: !cir.ptr<!cir.array<!rec_S x 4>>) {
+ cir.array.dtor %arg0 : !cir.ptr<!cir.array<!rec_S x 4>> {
+ ^bb0(%elem: !cir.ptr<!rec_S>):
+ cir.call @_ZN1SD1Ev(%elem) : (!cir.ptr<!rec_S>) -> ()
+ cir.yield
+ }
+ cir.return
+ }
+
+}
More information about the cfe-commits
mailing list