[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