[Mlir-commits] [mlir] [mlir] Optimize `isOpTriviallyDead` greedy rewriter checks for deeply nested IR (PR #231423)

Ivan Butygin llvmlistbot at llvm.org
Mon Oct 26 05:55:33 PDT 2026


https://github.com/Hardcode84 updated https://github.com/llvm/llvm-project/pull/231423

>From f2dfa8c084f3ae01ad47b81899f189ad1366bcfc Mon Sep 17 00:00:00 2001
From: Ivan Butygin <ivan.butygin at gmail.com>
Date: Tue, 13 Oct 2026 10:39:45 +0200
Subject: [PATCH 01/10] [mlir] Cache recursive deadness in the greedy rewrite
 driver

Repeated deadness queries scan nested regions after each rewrite. Cache
recursive effect summaries and invalidate them through rewriter hooks.
Check uses on each query and traverse uncached regions with an explicit
worklist to handle deeply nested IR.

Find the pattern dump root only when level-two debug logging is enabled.
Add cache invalidation unit tests, debug logging coverage, and a generated
1000-loop canonicalization stress test.
---
 .../mlir/Interfaces/SideEffectInterfaces.h    |  19 ++
 mlir/lib/Interfaces/SideEffectInterfaces.cpp  | 143 ++++++++---
 .../Utils/GreedyPatternRewriteDriver.cpp      |  17 +-
 mlir/test/IR/pattern-application-debug.mlir   |  16 ++
 .../Transforms/canonicalize-deeply-nested.py  |  28 +++
 mlir/unittests/Transforms/CMakeLists.txt      |   4 +
 .../Transforms/GreedyPatternRewriteDriver.cpp | 231 ++++++++++++++++++
 7 files changed, 422 insertions(+), 36 deletions(-)
 create mode 100644 mlir/test/IR/pattern-application-debug.mlir
 create mode 100644 mlir/test/Transforms/canonicalize-deeply-nested.py
 create mode 100644 mlir/unittests/Transforms/GreedyPatternRewriteDriver.cpp

diff --git a/mlir/include/mlir/Interfaces/SideEffectInterfaces.h b/mlir/include/mlir/Interfaces/SideEffectInterfaces.h
index dfffbcaf1bbab2..f75259e37beff9 100644
--- a/mlir/include/mlir/Interfaces/SideEffectInterfaces.h
+++ b/mlir/include/mlir/Interfaces/SideEffectInterfaces.h
@@ -15,7 +15,9 @@
 #define MLIR_INTERFACES_SIDEEFFECTINTERFACES_H
 
 #include "mlir/IR/OpDefinition.h"
+#include "llvm/ADT/DenseMap.h"
 #include "llvm/ADT/Twine.h"
+#include <cstdint>
 
 namespace mlir {
 namespace SideEffects {
@@ -546,6 +548,23 @@ bool isOpTriviallyDead(Operation *op);
 /// Note: Terminators and symbols are never considered to be trivially dead.
 bool wouldOpBeTriviallyDead(Operation *op);
 
+/// A cache for repeated deadness queries on nested operations. All changes to
+/// an operation or its nested IR must invalidate that operation. Moves must
+/// also invalidate the old parent. Invalidate operations before erasing them.
+class DeadnessCache {
+public:
+  /// Return true if the operation is unused and can be erased. Terminators and
+  /// symbols cannot be erased. Uses are checked on every query.
+  bool isOpTriviallyDead(Operation *op);
+
+  /// Invalidate the operation, its descendants, and its ancestors.
+  void invalidate(Operation *op);
+
+private:
+  enum class State : uint8_t { Alive, Dead };
+  DenseMap<Operation *, State> cache;
+};
+
 /// Returns true if the given operation is free of memory effects.
 ///
 /// An operation is free of memory effects if its implementation of
diff --git a/mlir/lib/Interfaces/SideEffectInterfaces.cpp b/mlir/lib/Interfaces/SideEffectInterfaces.cpp
index b5a6888e5e1a4e..8ed3dd27b64c2c 100644
--- a/mlir/lib/Interfaces/SideEffectInterfaces.cpp
+++ b/mlir/lib/Interfaces/SideEffectInterfaces.cpp
@@ -36,6 +36,35 @@ bool mlir::isOpTriviallyDead(Operation *op) {
   return op->use_empty() && wouldOpBeTriviallyDead(op);
 }
 
+static bool hasOnlyDeadEffects(Operation *op, bool hasRecursiveEffects) {
+  // If the op has memory effects, characterize them to see if the op can be
+  // trivially dead.
+  if (auto effectInterface = dyn_cast<MemoryEffectOpInterface>(op)) {
+    // Check if this op has no effects, or only allocates/reads memory.
+    SmallVector<MemoryEffects::EffectInstance, 1> effects;
+    effectInterface.getEffects(effects);
+
+    // Gather all results of this op that are allocated.
+    SmallPtrSet<Value, 4> allocResults;
+    for (const MemoryEffects::EffectInstance &effect : effects)
+      if (isa<MemoryEffects::Allocate>(effect.getEffect()) &&
+          effect.getValue() && effect.getValue().getDefiningOp() == op)
+        allocResults.insert(effect.getValue());
+    return llvm::all_of(effects,
+                        [&](const MemoryEffects::EffectInstance &effect) {
+                          // We can drop effects on values allocated by this
+                          // operation and returned as results. Otherwise, the
+                          // effect must be a read.
+                          return allocResults.contains(effect.getValue()) ||
+                                 isa<MemoryEffects::Read>(effect.getEffect());
+                        });
+  }
+  // If the op only has recursive side effects, treat the operation itself as
+  // having no effects. The caller must also check its children. Without an
+  // effect interface or recursive effects, conservatively assume live effects.
+  return hasRecursiveEffects;
+}
+
 /// Internal implementation of `mlir::wouldOpBeTriviallyDead` that also
 /// considers terminator operations as dead if they have no side effects. This
 /// allows for marking region operations as trivially dead without always being
@@ -66,49 +95,93 @@ static bool wouldOpBeTriviallyDeadImpl(Operation *rootOp) {
       }
     }
 
-    // If the op has memory effects, try to characterize them to see if the op
-    // is trivially dead here.
-    if (auto effectInterface = dyn_cast<MemoryEffectOpInterface>(op)) {
-      // Check to see if this op either has no effects, or only allocates/reads
-      // memory.
-      SmallVector<MemoryEffects::EffectInstance, 1> effects;
-      effectInterface.getEffects(effects);
+    if (!hasOnlyDeadEffects(op, hasRecursiveEffects))
+      return false;
+  }
 
-      // Gather all results of this op that are allocated.
-      SmallPtrSet<Value, 4> allocResults;
-      for (const MemoryEffects::EffectInstance &it : effects)
-        if (isa<MemoryEffects::Allocate>(it.getEffect()) && it.getValue() &&
-            it.getValue().getDefiningOp() == op)
-          allocResults.insert(it.getValue());
-
-      if (!llvm::all_of(effects, [&allocResults](
-                                     const MemoryEffects::EffectInstance &it) {
-            // We can drop effects if the value is an allocation and is a result
-            // of the operation.
-            if (allocResults.contains(it.getValue()))
-              return true;
-            // Otherwise, the effect must be a read.
-            return isa<MemoryEffects::Read>(it.getEffect());
-          })) {
-        return false;
-      }
+  // If we get here, none of the operations had effects that prevented marking
+  // 'op' as dead.
+  return true;
+}
+
+bool DeadnessCache::isOpTriviallyDead(Operation *root) {
+  // Cache only effects. Uses can change without a change to the defining op.
+  // Nested operations may have internal uses or be terminators even when the
+  // whole root can be erased, so these checks apply only to the root.
+  if (!root->use_empty() || root->mightHaveTrait<OpTrait::IsTerminator>() ||
+      isa<SymbolOpInterface>(root))
+    return false;
+
+  auto cachedRoot = cache.find(root);
+  if (cachedRoot != cache.end())
+    return cachedRoot->second == State::Dead;
+
+  // Repeated queries on nested operations would otherwise scan the same
+  // subtrees. Use a postorder traversal to cache each subtree's effects and
+  // skip it on later queries. Keep the traversal on an explicit worklist so
+  // deeply nested IR does not consume the C++ call stack.
+  struct WorkItem {
+    Operation *complete;
+    // The unvisited range [current, end) lets a block resume after a nested
+    // operation, without adding each sibling to the worklist. The initial
+    // range contains only the root, not the rest of its block. Completion
+    // markers do not use these iterators.
+    Block::iterator current;
+    Block::iterator end;
+  };
+  SmallVector<WorkItem, 8> worklist = {
+      {nullptr, Block::iterator(root), ++Block::iterator(root)}};
+  while (!worklist.empty()) {
+    WorkItem &item = worklist.back();
+    if (item.complete) {
+      // All nested operations have been checked without finding live effects.
+      cache[item.complete] = State::Dead;
+      worklist.pop_back();
       continue;
     }
-    // Otherwise, if the op only has recursive side effects we can treat the
-    // operation itself as having no effects. We will visit its children next.
-    if (hasRecursiveEffects)
+    if (item.current == item.end) {
+      worklist.pop_back();
       continue;
-
-    // If there were no effect interfaces, we treat this op as conservatively
-    // having effects.
-    return false;
+    }
+    Operation *op = &*item.current++;
+    auto cached = cache.find(op);
+    if (cached != cache.end() && cached->second == State::Dead)
+      continue;
+    bool recursive = op->hasTrait<OpTrait::HasRecursiveMemoryEffects>();
+    if (cached != cache.end() || !hasOnlyDeadEffects(op, recursive)) {
+      // One live effect keeps every ancestor up to the root live. Cache this
+      // result and stop; the remaining subtrees cannot change the answer.
+      for (Operation *ancestor = op;; ancestor = ancestor->getParentOp()) {
+        cache[ancestor] = State::Alive;
+        if (ancestor == root)
+          break;
+      }
+      return false;
+    }
+    if (!recursive) {
+      cache[op] = State::Dead;
+      continue;
+    }
+    // Process the child ranges before this completion marker. The op can be
+    // cached as dead only after all of its recursive effects are checked.
+    worklist.push_back({op, {}, {}});
+    for (Region &region : op->getRegions())
+      for (Block &block : region)
+        worklist.push_back({nullptr, block.begin(), block.end()});
   }
-
-  // If we get here, none of the operations had effects that prevented marking
-  // 'op' as dead.
   return true;
 }
 
+void DeadnessCache::invalidate(Operation *op) {
+  op->walk([&](Operation *nested) { cache.erase(nested); });
+  // An ancestor may report nested effects through its own effect interface,
+  // without querying the intermediate cache entries. A missing entry cannot
+  // stop invalidation of its ancestors.
+  for (Operation *parent = op->getParentOp(); parent;
+       parent = parent->getParentOp())
+    cache.erase(parent);
+}
+
 template <typename EffectTy>
 bool mlir::hasSingleEffect(Operation *op) {
   auto memOp = dyn_cast<MemoryEffectOpInterface>(op);
diff --git a/mlir/lib/Transforms/Utils/GreedyPatternRewriteDriver.cpp b/mlir/lib/Transforms/Utils/GreedyPatternRewriteDriver.cpp
index 4bcde3c5bf5550..6032168940ab91 100644
--- a/mlir/lib/Transforms/Utils/GreedyPatternRewriteDriver.cpp
+++ b/mlir/lib/Transforms/Utils/GreedyPatternRewriteDriver.cpp
@@ -516,6 +516,8 @@ class GreedyPatternRewriteDriver : public RewriterBase::Listener {
   /// The low-level pattern applicator.
   PatternApplicator matcher;
 
+  DeadnessCache deadnessCache;
+
 #if MLIR_ENABLE_EXPENSIVE_PATTERN_API_CHECKS
   ExpensiveChecks expensiveChecks;
 #endif // MLIR_ENABLE_EXPENSIVE_PATTERN_API_CHECKS
@@ -809,7 +811,7 @@ bool GreedyPatternRewriteDriver::processWorklist() {
     });
 
     // If the operation is trivially dead - remove it.
-    if (isOpTriviallyDead(op)) {
+    if (deadnessCache.isOpTriviallyDead(op)) {
       rewriter.eraseOp(op);
       changed = true;
 
@@ -993,17 +995,28 @@ void GreedyPatternRewriteDriver::addSingleOpToWorklist(Operation *op) {
 
 void GreedyPatternRewriteDriver::notifyBlockInserted(
     Block *block, Region *previous, Region::iterator previousIt) {
+  if (previous)
+    if (Operation *parent = previous->getParentOp())
+      deadnessCache.invalidate(parent);
+  if (Operation *parent = block->getParentOp())
+    deadnessCache.invalidate(parent);
   if (RewriterBase::Listener *listener = config.getListener())
     listener->notifyBlockInserted(block, previous, previousIt);
 }
 
 void GreedyPatternRewriteDriver::notifyBlockErased(Block *block) {
+  if (Operation *parent = block->getParentOp())
+    deadnessCache.invalidate(parent);
   if (RewriterBase::Listener *listener = config.getListener())
     listener->notifyBlockErased(block);
 }
 
 void GreedyPatternRewriteDriver::notifyOperationInserted(
     Operation *op, OpBuilder::InsertPoint previous) {
+  if (previous.isSet())
+    if (Operation *parent = previous.getBlock()->getParentOp())
+      deadnessCache.invalidate(parent);
+  deadnessCache.invalidate(op);
   LLVM_DEBUG({
     logger.startLine() << "** Insert  : '" << op->getName() << "'(" << op
                        << ")\n";
@@ -1016,6 +1029,7 @@ void GreedyPatternRewriteDriver::notifyOperationInserted(
 }
 
 void GreedyPatternRewriteDriver::notifyOperationModified(Operation *op) {
+  deadnessCache.invalidate(op);
   LLVM_DEBUG({
     logger.startLine() << "** Modified: '" << op->getName() << "'(" << op
                        << ")\n";
@@ -1059,6 +1073,7 @@ void GreedyPatternRewriteDriver::addOperandsToWorklist(Operation *op) {
 }
 
 void GreedyPatternRewriteDriver::notifyOperationErased(Operation *op) {
+  deadnessCache.invalidate(op);
   LLVM_DEBUG({
     logger.startLine() << "** Erase   : '" << op->getName() << "'(" << op
                        << ")\n";
diff --git a/mlir/test/IR/pattern-application-debug.mlir b/mlir/test/IR/pattern-application-debug.mlir
new file mode 100644
index 00000000000000..28e4da51b8d493
--- /dev/null
+++ b/mlir/test/IR/pattern-application-debug.mlir
@@ -0,0 +1,16 @@
+// REQUIRES: asserts
+// RUN: mlir-opt %s --test-walk-pattern-rewrite-driver --debug-only=pattern-application:1 -o /dev/null 2>&1 | FileCheck %s --check-prefix=LEVEL1 --implicit-check-not="IR Dump After Pattern Application"
+// RUN: mlir-opt %s --test-walk-pattern-rewrite-driver --debug-only=pattern-application:2 -o /dev/null 2>&1 | FileCheck %s --check-prefix=LEVEL2
+
+// The pattern erases its root. The dump must still show the enclosing function.
+// LEVEL1: matchAndRewrite successful
+// LEVEL2: IR Dump After Pattern Application
+// LEVEL2: func.func @replace_with_new_op()
+// LEVEL2: "test.new_op"
+// LEVEL2-NOT: "test.replace_with_new_op"
+// LEVEL2: return
+func.func @replace_with_new_op() -> i32 {
+  %a = "test.replace_with_new_op"() : () -> i32
+  %res = arith.addi %a, %a : i32
+  return %res : i32
+}
diff --git a/mlir/test/Transforms/canonicalize-deeply-nested.py b/mlir/test/Transforms/canonicalize-deeply-nested.py
new file mode 100644
index 00000000000000..12b2c6d6f16bdd
--- /dev/null
+++ b/mlir/test/Transforms/canonicalize-deeply-nested.py
@@ -0,0 +1,28 @@
+# RUN: %PYTHON %s | mlir-opt -canonicalize | FileCheck %s --implicit-check-not=arith.addi
+
+# Identity folds change values used in nested regions. The store keeps
+# all loops live, so repeated deadness queries must account for nested effects.
+# Generate the input to keep the test small. Do not check elapsed time.
+
+# CHECK-LABEL: func.func @deeply_nested(
+# CHECK-SAME: %[[VALUE:[^ ,]+]]: i32, %[[OUT:[^ ,]+]]: memref<i32>
+# CHECK-COUNT-1000: scf.for
+# CHECK-NEXT: memref.store %[[VALUE]], %[[OUT]][] : memref<i32>
+# CHECK-NOT: scf.for
+
+depth = 1000
+print("func.func @deeply_nested(%value: i32, %out: memref<i32>) {")
+print("%c0 = arith.constant 0 : index")
+print("%c1 = arith.constant 1 : index")
+print("%c4 = arith.constant 4 : index")
+print("%zero = arith.constant 0 : i32")
+value = "%value"
+for i in range(depth):
+    print(f"scf.for %i{i} = %c0 to %c4 step %c1 {{")
+    print(f"%v{i} = arith.addi {value}, %zero : i32")
+    value = f"%v{i}"
+print(f"memref.store {value}, %out[] : memref<i32>")
+for _ in range(depth):
+    print("}")
+print("return")
+print("}")
diff --git a/mlir/unittests/Transforms/CMakeLists.txt b/mlir/unittests/Transforms/CMakeLists.txt
index dc5920087b505e..d26971492219d1 100644
--- a/mlir/unittests/Transforms/CMakeLists.txt
+++ b/mlir/unittests/Transforms/CMakeLists.txt
@@ -1,8 +1,12 @@
 add_mlir_unittest(MLIRTransformsTests
   Canonicalizer.cpp
   DialectConversion.cpp
+  GreedyPatternRewriteDriver.cpp
 )
 mlir_target_link_libraries(MLIRTransformsTests
   PRIVATE
   MLIRParser
   MLIRTransforms)
+
+target_include_directories(MLIRTransformsTests PRIVATE "${MLIR_BINARY_DIR}/test/lib/Dialect/Test")
+target_link_libraries(MLIRTransformsTests PRIVATE MLIRTestDialect)
diff --git a/mlir/unittests/Transforms/GreedyPatternRewriteDriver.cpp b/mlir/unittests/Transforms/GreedyPatternRewriteDriver.cpp
new file mode 100644
index 00000000000000..9b469776bed0d2
--- /dev/null
+++ b/mlir/unittests/Transforms/GreedyPatternRewriteDriver.cpp
@@ -0,0 +1,231 @@
+//===- GreedyPatternRewriteDriver.cpp - Greedy rewrite tests -------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "mlir/Transforms/GreedyPatternRewriteDriver.h"
+#include "mlir/IR/PatternMatch.h"
+#include "mlir/IR/Verifier.h"
+#include "mlir/Interfaces/SideEffectInterfaces.h"
+#include "mlir/Parser/Parser.h"
+#include "gtest/gtest.h"
+
+#include "../../test/lib/Dialect/Test/TestDialect.h"
+
+using namespace mlir;
+
+namespace {
+static std::string region(StringRef body) {
+  return ("%r = \"test.one_region_with_recursive_memory_effects\"() ({\n" +
+          body + "\n\"test.return\"() : () -> ()\n}) : () -> i32\n")
+      .str();
+}
+
+constexpr StringLiteral effect =
+    "%e = \"test.side_effect_op\"() {effects = [{effect = \"write\"}]} "
+    ": () -> i32\n";
+
+class RewriteRoot : public RewritePattern {
+public:
+  RewriteRoot(MLIRContext *context, Operation *root,
+              function_ref<void(PatternRewriter &)> rewrite)
+      : RewritePattern(root->getName().getStringRef(), 1, context), root(root),
+        rewrite(rewrite) {}
+
+  LogicalResult matchAndRewrite(Operation *op,
+                                PatternRewriter &rewriter) const override {
+    if (op != root || applied)
+      return failure();
+    applied = true;
+    rewrite(rewriter);
+    return success();
+  }
+
+private:
+  Operation *root;
+  function_ref<void(PatternRewriter &)> rewrite;
+  mutable bool applied = false;
+};
+
+class GreedyDeadnessTest : public testing::Test {
+protected:
+  GreedyDeadnessTest() { context.loadDialect<test::TestDialect>(); }
+
+  void run(ModuleOp moduleOp, function_ref<void(PatternRewriter &)> rewrite) {
+    RewritePatternSet patterns(&context);
+    patterns.add<RewriteRoot>(&context, &moduleOp.getBody()->front(), rewrite);
+    GreedyRewriteConfig config;
+    // Query the outer operation before the rewrite changes its nested IR.
+    // This populates the cache so the rewrite must invalidate stale entries.
+    config.setUseTopDownTraversal(true)
+        .enableFolding(false)
+        .enableConstantCSE(false)
+        .setRegionSimplificationLevel(GreedySimplifyRegionLevel::Disabled);
+    ASSERT_TRUE(succeeded(
+        applyPatternsGreedily(moduleOp, std::move(patterns), config)));
+    EXPECT_TRUE(succeeded(verify(moduleOp)));
+  }
+
+  MLIRContext context;
+};
+
+TEST_F(GreedyDeadnessTest, EraseLastEffect) {
+  // Erasing the only write must invalidate cached live effects in both
+  // enclosing operations, so the driver can delete them.
+  auto moduleOp = parseSourceString<ModuleOp>(region(region(effect)), &context);
+  ASSERT_TRUE(moduleOp);
+  Operation *outer = &moduleOp->getBody()->front();
+  Operation *inner = &outer->getRegion(0).front().front();
+  Operation *write = &inner->getRegion(0).front().front();
+  run(*moduleOp, [&](PatternRewriter &rewriter) { rewriter.eraseOp(write); });
+  EXPECT_TRUE(moduleOp->getBody()->empty());
+}
+
+TEST_F(GreedyDeadnessTest, ModifyLastEffect) {
+  // Changing the write to an effect-free operation must make both enclosing
+  // operations dead, even though no operation was erased by the pattern.
+  auto moduleOp = parseSourceString<ModuleOp>(region(region(effect)), &context);
+  ASSERT_TRUE(moduleOp);
+  Operation *outer = &moduleOp->getBody()->front();
+  Operation *inner = &outer->getRegion(0).front().front();
+  Operation *write = &inner->getRegion(0).front().front();
+  run(*moduleOp, [&](PatternRewriter &rewriter) {
+    rewriter.modifyOpInPlace(
+        write, [&] { write->setAttr("effects", rewriter.getArrayAttr({})); });
+  });
+  EXPECT_TRUE(moduleOp->getBody()->empty());
+}
+
+TEST_F(GreedyDeadnessTest, UsedInnerResultDoesNotPreventOuterDeletion) {
+  // Internal uses do not keep the unused outer operation live after its last
+  // write effect is removed. Cached summaries must describe effects, not uses.
+  auto moduleOp = parseSourceString<ModuleOp>(R"mlir(
+    %outer = "test.one_region_with_recursive_memory_effects"() ({
+      %inner = "test.one_region_with_recursive_memory_effects"() ({
+        %v = "test.side_effect_op"() {effects = [{effect = "write"}]} : () -> i32
+        "test.return"(%v) : (i32) -> ()
+      }) : () -> i32
+      "test.return"(%inner) : (i32) -> ()
+    }) : () -> i32
+  )mlir",
+                                              &context);
+  ASSERT_TRUE(moduleOp);
+  Operation *outer = &moduleOp->getBody()->front();
+  Operation *inner = &outer->getRegion(0).front().front();
+  Operation *write = &inner->getRegion(0).front().front();
+  ASSERT_FALSE(inner->use_empty());
+  run(*moduleOp, [&](PatternRewriter &rewriter) {
+    rewriter.modifyOpInPlace(
+        write, [&] { write->setAttr("effects", rewriter.getArrayAttr({})); });
+  });
+  EXPECT_TRUE(moduleOp->getBody()->empty());
+}
+
+TEST_F(GreedyDeadnessTest, InsertEffectIntoDeadRegion) {
+  // The first query caches the empty inner operation as dead before finding
+  // the write beside it. Inserting a write into it must invalidate that result;
+  // erasing the original write must not cause the new write to be deleted.
+  auto moduleOp =
+      parseSourceString<ModuleOp>(region(region("") + effect.str()), &context);
+  ASSERT_TRUE(moduleOp);
+  Operation *outer = &moduleOp->getBody()->front();
+  Operation *inner = &outer->getRegion(0).front().front();
+  Operation *write = inner->getNextNode();
+  run(*moduleOp, [&](PatternRewriter &rewriter) {
+    rewriter.setInsertionPointToStart(&inner->getRegion(0).front());
+    rewriter.clone(*write);
+    rewriter.eraseOp(write);
+  });
+  ASSERT_FALSE(moduleOp->getBody()->empty());
+  ASSERT_FALSE(outer->getRegion(0).front().empty());
+  EXPECT_EQ(&outer->getRegion(0).front().front(), inner);
+  EXPECT_EQ(inner->getRegion(0).front().front().getName().getStringRef(),
+            "test.side_effect_op");
+}
+
+TEST_F(GreedyDeadnessTest, MoveEffectBetweenRegions) {
+  // Moving the only write must invalidate both parents: the destination must
+  // survive, and the now-empty source must be deleted.
+  std::string empty = region("");
+  std::string live = region(effect);
+  live.replace(0, 2, "%s");
+  auto moduleOp = parseSourceString<ModuleOp>(region(empty + live), &context);
+  ASSERT_TRUE(moduleOp);
+  Operation *outer = &moduleOp->getBody()->front();
+  Operation *target = &outer->getRegion(0).front().front();
+  Operation *source = target->getNextNode();
+  Operation *write = &source->getRegion(0).front().front();
+  run(*moduleOp, [&](PatternRewriter &rewriter) {
+    rewriter.moveOpBefore(write, &target->getRegion(0).front(),
+                          target->getRegion(0).front().begin());
+  });
+  ASSERT_FALSE(moduleOp->getBody()->empty());
+  EXPECT_EQ(outer->getRegion(0).front().getOperations().size(), 2u);
+  EXPECT_EQ(&outer->getRegion(0).front().front(), target);
+  EXPECT_EQ(&target->getRegion(0).front().front(), write);
+}
+
+TEST_F(GreedyDeadnessTest, MoveBlocksBetweenRegions) {
+  // Moving whole blocks must update the effects of both enclosing operations,
+  // just as moving a write does. Keep the new owner of the live block and
+  // delete the operation that receives the empty block.
+  std::string empty = region("");
+  std::string live = region(effect);
+  live.replace(0, 2, "%s");
+  auto moduleOp = parseSourceString<ModuleOp>(region(empty + live), &context);
+  ASSERT_TRUE(moduleOp);
+  Operation *outer = &moduleOp->getBody()->front();
+  Operation *target = &outer->getRegion(0).front().front();
+  Operation *source = target->getNextNode();
+  Block *emptyBlock = &target->getRegion(0).front();
+  Block *liveBlock = &source->getRegion(0).front();
+  run(*moduleOp, [&](PatternRewriter &rewriter) {
+    rewriter.moveBlockBefore(liveBlock, &target->getRegion(0),
+                             target->getRegion(0).begin());
+    rewriter.moveBlockBefore(emptyBlock, &source->getRegion(0),
+                             source->getRegion(0).begin());
+  });
+  ASSERT_FALSE(moduleOp->getBody()->empty());
+  EXPECT_EQ(outer->getRegion(0).front().getOperations().size(), 2u);
+  EXPECT_EQ(&outer->getRegion(0).front().front(), target);
+  EXPECT_EQ(&target->getRegion(0).front(), liveBlock);
+}
+
+TEST_F(GreedyDeadnessTest, UsesAreNotCached) {
+  // Change uses without invalidating the effect cache. Each query must check
+  // current uses, including a new use added after caching a dead result.
+  auto moduleOp = parseSourceString<ModuleOp>(R"mlir(
+    %v = "test.side_effect_op"() : () -> i32
+    "test.use"(%v) : (i32) -> ()
+  )mlir",
+                                              &context);
+  ASSERT_TRUE(moduleOp);
+  Operation *value = &moduleOp->getBody()->front();
+  DeadnessCache cache;
+  EXPECT_FALSE(cache.isOpTriviallyDead(value));
+  value->getNextNode()->erase();
+  EXPECT_TRUE(cache.isOpTriviallyDead(value));
+  OpBuilder builder(&context);
+  builder.setInsertionPointAfter(value);
+  OperationState state(builder.getUnknownLoc(), "test.use");
+  state.addOperands(value->getResult(0));
+  builder.create(state);
+  EXPECT_FALSE(cache.isOpTriviallyDead(value));
+}
+
+TEST_F(GreedyDeadnessTest, TerminatorsRemainLiveAfterRecursiveQuery) {
+  // The outer query caches the terminator's effects as dead. A direct query
+  // must still reject deleting the terminator itself.
+  auto moduleOp = parseSourceString<ModuleOp>(region(""), &context);
+  ASSERT_TRUE(moduleOp);
+  Operation *outer = &moduleOp->getBody()->front();
+  Operation *terminator = &outer->getRegion(0).front().back();
+  DeadnessCache cache;
+  EXPECT_TRUE(cache.isOpTriviallyDead(outer));
+  EXPECT_FALSE(cache.isOpTriviallyDead(terminator));
+  EXPECT_FALSE(cache.isOpTriviallyDead(*moduleOp));
+}
+} // namespace

>From 24cdf53815f76dbfaae626b34bf53fcb81978efb Mon Sep 17 00:00:00 2001
From: Ivan Butygin <ivan.butygin at gmail.com>
Date: Tue, 13 Oct 2026 12:42:19 +0200
Subject: [PATCH 02/10] [mlir] Document the dead-effect query contract

---
 mlir/lib/Interfaces/SideEffectInterfaces.cpp | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

diff --git a/mlir/lib/Interfaces/SideEffectInterfaces.cpp b/mlir/lib/Interfaces/SideEffectInterfaces.cpp
index 8ed3dd27b64c2c..4d0ba914e97658 100644
--- a/mlir/lib/Interfaces/SideEffectInterfaces.cpp
+++ b/mlir/lib/Interfaces/SideEffectInterfaces.cpp
@@ -36,11 +36,18 @@ bool mlir::isOpTriviallyDead(Operation *op) {
   return op->use_empty() && wouldOpBeTriviallyDead(op);
 }
 
+/// Return true if the op's own memory effects do not prevent erasure. Allowed
+/// effects are reads and effects on results allocated by the op itself.
+///
+/// If the op only has recursive effects, treat the op itself as having no
+/// effects. Without a memory effect interface or recursive effects, return
+/// false to conservatively assume live effects.
+///
+/// The caller must also check nested effects when `hasRecursiveEffects` is
+/// true. This function does not check uses or restrictions on erasing
+/// terminators and symbols.
 static bool hasOnlyDeadEffects(Operation *op, bool hasRecursiveEffects) {
-  // If the op has memory effects, characterize them to see if the op can be
-  // trivially dead.
   if (auto effectInterface = dyn_cast<MemoryEffectOpInterface>(op)) {
-    // Check if this op has no effects, or only allocates/reads memory.
     SmallVector<MemoryEffects::EffectInstance, 1> effects;
     effectInterface.getEffects(effects);
 
@@ -59,9 +66,6 @@ static bool hasOnlyDeadEffects(Operation *op, bool hasRecursiveEffects) {
                                  isa<MemoryEffects::Read>(effect.getEffect());
                         });
   }
-  // If the op only has recursive side effects, treat the operation itself as
-  // having no effects. The caller must also check its children. Without an
-  // effect interface or recursive effects, conservatively assume live effects.
   return hasRecursiveEffects;
 }
 

>From 751b7049a058e44250f3e2895f2f6b85a0085a4b Mon Sep 17 00:00:00 2001
From: Ivan Butygin <ivan.butygin at gmail.com>
Date: Tue, 13 Oct 2026 13:13:10 +0200
Subject: [PATCH 03/10] [mlir] Remove pattern debug-output lit test

---
 mlir/test/IR/pattern-application-debug.mlir | 16 ----------------
 1 file changed, 16 deletions(-)
 delete mode 100644 mlir/test/IR/pattern-application-debug.mlir

diff --git a/mlir/test/IR/pattern-application-debug.mlir b/mlir/test/IR/pattern-application-debug.mlir
deleted file mode 100644
index 28e4da51b8d493..00000000000000
--- a/mlir/test/IR/pattern-application-debug.mlir
+++ /dev/null
@@ -1,16 +0,0 @@
-// REQUIRES: asserts
-// RUN: mlir-opt %s --test-walk-pattern-rewrite-driver --debug-only=pattern-application:1 -o /dev/null 2>&1 | FileCheck %s --check-prefix=LEVEL1 --implicit-check-not="IR Dump After Pattern Application"
-// RUN: mlir-opt %s --test-walk-pattern-rewrite-driver --debug-only=pattern-application:2 -o /dev/null 2>&1 | FileCheck %s --check-prefix=LEVEL2
-
-// The pattern erases its root. The dump must still show the enclosing function.
-// LEVEL1: matchAndRewrite successful
-// LEVEL2: IR Dump After Pattern Application
-// LEVEL2: func.func @replace_with_new_op()
-// LEVEL2: "test.new_op"
-// LEVEL2-NOT: "test.replace_with_new_op"
-// LEVEL2: return
-func.func @replace_with_new_op() -> i32 {
-  %a = "test.replace_with_new_op"() : () -> i32
-  %res = arith.addi %a, %a : i32
-  return %res : i32
-}

>From 4f104032eaab5635fcd6d853ce6ce3375ff47700 Mon Sep 17 00:00:00 2001
From: Ivan Butygin <ivan.butygin at gmail.com>
Date: Tue, 13 Oct 2026 13:33:27 +0200
Subject: [PATCH 04/10] [mlir] Isolate greedy deadness caching in a forwarding
 listener

Move cache invalidation from driver callbacks into DeadnessListener. Forward in-place fold notifications through the listener and preserve the expensive-check listener chain.

Invalidate parent fingerprints on block insertion and movement so the block-move test also passes with expensive pattern checks enabled.
---
 .../Utils/GreedyPatternRewriteDriver.cpp      | 85 +++++++++++++------
 1 file changed, 61 insertions(+), 24 deletions(-)

diff --git a/mlir/lib/Transforms/Utils/GreedyPatternRewriteDriver.cpp b/mlir/lib/Transforms/Utils/GreedyPatternRewriteDriver.cpp
index 6032168940ab91..b9da0c5c1ca6db 100644
--- a/mlir/lib/Transforms/Utils/GreedyPatternRewriteDriver.cpp
+++ b/mlir/lib/Transforms/Utils/GreedyPatternRewriteDriver.cpp
@@ -418,6 +418,56 @@ class ReachabilityListener : public RewriterBase::ForwardingListener {
   Region *const scope;
 };
 
+/// Provides cached deadness queries that stay valid across rewriter
+/// notifications. All notifications are forwarded to the wrapped listener
+/// after invalidating affected cache entries.
+class DeadnessListener : public RewriterBase::ForwardingListener {
+public:
+  explicit DeadnessListener(RewriterBase::Listener *listener)
+      : RewriterBase::ForwardingListener(listener) {}
+
+  /// Return true if the operation is unused and can be erased.
+  bool isOpTriviallyDead(Operation *op) { return cache.isOpTriviallyDead(op); }
+
+  void notifyBlockInserted(Block *block, Region *previous,
+                           Region::iterator previousIt) override {
+    if (previous)
+      if (Operation *parent = previous->getParentOp())
+        cache.invalidate(parent);
+    if (Operation *parent = block->getParentOp())
+      cache.invalidate(parent);
+    ForwardingListener::notifyBlockInserted(block, previous, previousIt);
+  }
+
+  void notifyBlockErased(Block *block) override {
+    if (Operation *parent = block->getParentOp())
+      cache.invalidate(parent);
+    ForwardingListener::notifyBlockErased(block);
+  }
+
+  void notifyOperationInserted(Operation *op,
+                               OpBuilder::InsertPoint previous) override {
+    if (previous.isSet())
+      if (Operation *parent = previous.getBlock()->getParentOp())
+        cache.invalidate(parent);
+    cache.invalidate(op);
+    ForwardingListener::notifyOperationInserted(op, previous);
+  }
+
+  void notifyOperationModified(Operation *op) override {
+    cache.invalidate(op);
+    ForwardingListener::notifyOperationModified(op);
+  }
+
+  void notifyOperationErased(Operation *op) override {
+    cache.invalidate(op);
+    ForwardingListener::notifyOperationErased(op);
+  }
+
+private:
+  DeadnessCache cache;
+};
+
 /// This is a worklist-driven driver for the PatternMatcher, which repeatedly
 /// applies the locally optimal patterns.
 ///
@@ -516,7 +566,7 @@ class GreedyPatternRewriteDriver : public RewriterBase::Listener {
   /// The low-level pattern applicator.
   PatternApplicator matcher;
 
-  DeadnessCache deadnessCache;
+  DeadnessListener deadnessListener;
 
 #if MLIR_ENABLE_EXPENSIVE_PATTERN_API_CHECKS
   ExpensiveChecks expensiveChecks;
@@ -735,11 +785,12 @@ GreedyPatternRewriteDriver::GreedyPatternRewriteDriver(
     MLIRContext *ctx, const FrozenRewritePatternSet &patterns,
     const GreedyRewriteConfig &config)
     : rewriter(ctx), config(config),
-      reachabilityListener(this, config.getScope()), matcher(patterns)
+      reachabilityListener(this, config.getScope()), matcher(patterns),
+      deadnessListener(&reachabilityListener)
 #if MLIR_ENABLE_EXPENSIVE_PATTERN_API_CHECKS
       // clang-format off
       , expensiveChecks(
-          /*listener=*/&reachabilityListener,
+          /*listener=*/&deadnessListener,
           /*topLevel=*/config.getScope() ? config.getScope()->getParentOp()
                                          : nullptr)
 // clang-format on
@@ -750,11 +801,11 @@ GreedyPatternRewriteDriver::GreedyPatternRewriteDriver(
 
   // Set up listener.
 #if MLIR_ENABLE_EXPENSIVE_PATTERN_API_CHECKS
-  // Chain notifications through the debug handler, reachability listener, and
-  // worklist driver, in that order.
+  // Forward notifications through expensive checks, deadness, reachability,
+  // and the worklist driver, in that order.
   rewriter.setListener(&expensiveChecks);
 #else
-  rewriter.setListener(&reachabilityListener);
+  rewriter.setListener(&deadnessListener);
 #endif // MLIR_ENABLE_EXPENSIVE_PATTERN_API_CHECKS
 }
 
@@ -811,7 +862,7 @@ bool GreedyPatternRewriteDriver::processWorklist() {
     });
 
     // If the operation is trivially dead - remove it.
-    if (deadnessCache.isOpTriviallyDead(op)) {
+    if (deadnessListener.isOpTriviallyDead(op)) {
       rewriter.eraseOp(op);
       changed = true;
 
@@ -831,10 +882,9 @@ bool GreedyPatternRewriteDriver::processWorklist() {
         Operation *dumpRootOp = getDumpRootOp(op);
 #endif // NDEBUG
         if (foldResults.empty()) {
-          // Folding modified the op in place without going through the
-          // rewriter.
-          // Notify both the reachability listener and the worklist driver.
-          reachabilityListener.notifyOperationModified(op);
+          // Folding modified the op without using the rewriter. Notify the
+          // cache listeners, which forward the modification to the driver.
+          deadnessListener.notifyOperationModified(op);
           changed = true;
           LLVM_DEBUG(logSuccessfulFolding(dumpRootOp));
 #if MLIR_ENABLE_EXPENSIVE_PATTERN_API_CHECKS
@@ -995,28 +1045,17 @@ void GreedyPatternRewriteDriver::addSingleOpToWorklist(Operation *op) {
 
 void GreedyPatternRewriteDriver::notifyBlockInserted(
     Block *block, Region *previous, Region::iterator previousIt) {
-  if (previous)
-    if (Operation *parent = previous->getParentOp())
-      deadnessCache.invalidate(parent);
-  if (Operation *parent = block->getParentOp())
-    deadnessCache.invalidate(parent);
   if (RewriterBase::Listener *listener = config.getListener())
     listener->notifyBlockInserted(block, previous, previousIt);
 }
 
 void GreedyPatternRewriteDriver::notifyBlockErased(Block *block) {
-  if (Operation *parent = block->getParentOp())
-    deadnessCache.invalidate(parent);
   if (RewriterBase::Listener *listener = config.getListener())
     listener->notifyBlockErased(block);
 }
 
 void GreedyPatternRewriteDriver::notifyOperationInserted(
     Operation *op, OpBuilder::InsertPoint previous) {
-  if (previous.isSet())
-    if (Operation *parent = previous.getBlock()->getParentOp())
-      deadnessCache.invalidate(parent);
-  deadnessCache.invalidate(op);
   LLVM_DEBUG({
     logger.startLine() << "** Insert  : '" << op->getName() << "'(" << op
                        << ")\n";
@@ -1029,7 +1068,6 @@ void GreedyPatternRewriteDriver::notifyOperationInserted(
 }
 
 void GreedyPatternRewriteDriver::notifyOperationModified(Operation *op) {
-  deadnessCache.invalidate(op);
   LLVM_DEBUG({
     logger.startLine() << "** Modified: '" << op->getName() << "'(" << op
                        << ")\n";
@@ -1073,7 +1111,6 @@ void GreedyPatternRewriteDriver::addOperandsToWorklist(Operation *op) {
 }
 
 void GreedyPatternRewriteDriver::notifyOperationErased(Operation *op) {
-  deadnessCache.invalidate(op);
   LLVM_DEBUG({
     logger.startLine() << "** Erase   : '" << op->getName() << "'(" << op
                        << ")\n";

>From 2cf160f07f757622c9b86bf607c72c56f90c0ddb Mon Sep 17 00:00:00 2001
From: Ivan Butygin <ivan.butygin at gmail.com>
Date: Tue, 20 Oct 2026 10:53:29 +0200
Subject: [PATCH 05/10] [mlir] Avoid scanning siblings when invalidating moved
 operations

---
 mlir/include/mlir/Interfaces/SideEffectInterfaces.h | 3 +++
 mlir/lib/Interfaces/SideEffectInterfaces.cpp        | 7 +++++--
 2 files changed, 8 insertions(+), 2 deletions(-)

diff --git a/mlir/include/mlir/Interfaces/SideEffectInterfaces.h b/mlir/include/mlir/Interfaces/SideEffectInterfaces.h
index f75259e37beff9..f3f9d7e018303f 100644
--- a/mlir/include/mlir/Interfaces/SideEffectInterfaces.h
+++ b/mlir/include/mlir/Interfaces/SideEffectInterfaces.h
@@ -560,6 +560,9 @@ class DeadnessCache {
   /// Invalidate the operation, its descendants, and its ancestors.
   void invalidate(Operation *op);
 
+  /// Invalidate the operation and its ancestors, preserving descendant entries.
+  void invalidateAncestors(Operation *op);
+
 private:
   enum class State : uint8_t { Alive, Dead };
   DenseMap<Operation *, State> cache;
diff --git a/mlir/lib/Interfaces/SideEffectInterfaces.cpp b/mlir/lib/Interfaces/SideEffectInterfaces.cpp
index 4d0ba914e97658..4e3a4db20574cb 100644
--- a/mlir/lib/Interfaces/SideEffectInterfaces.cpp
+++ b/mlir/lib/Interfaces/SideEffectInterfaces.cpp
@@ -178,11 +178,14 @@ bool DeadnessCache::isOpTriviallyDead(Operation *root) {
 
 void DeadnessCache::invalidate(Operation *op) {
   op->walk([&](Operation *nested) { cache.erase(nested); });
+  invalidateAncestors(op);
+}
+
+void DeadnessCache::invalidateAncestors(Operation *op) {
   // An ancestor may report nested effects through its own effect interface,
   // without querying the intermediate cache entries. A missing entry cannot
   // stop invalidation of its ancestors.
-  for (Operation *parent = op->getParentOp(); parent;
-       parent = parent->getParentOp())
+  for (Operation *parent = op; parent; parent = parent->getParentOp())
     cache.erase(parent);
 }
 

>From 2d7381f636d5e888885d07dee686e5c617f0bbe7 Mon Sep 17 00:00:00 2001
From: Ivan Butygin <ivan.butygin at gmail.com>
Date: Tue, 20 Oct 2026 10:53:29 +0200
Subject: [PATCH 06/10] [mlir] Route constant folding changes through the
 rewriter listener

---
 .../Utils/GreedyPatternRewriteDriver.cpp      | 20 +++--
 .../Transforms/GreedyPatternRewriteDriver.cpp | 83 +++++++++++++++++++
 2 files changed, 95 insertions(+), 8 deletions(-)

diff --git a/mlir/lib/Transforms/Utils/GreedyPatternRewriteDriver.cpp b/mlir/lib/Transforms/Utils/GreedyPatternRewriteDriver.cpp
index b9da0c5c1ca6db..bacea081a5ed53 100644
--- a/mlir/lib/Transforms/Utils/GreedyPatternRewriteDriver.cpp
+++ b/mlir/lib/Transforms/Utils/GreedyPatternRewriteDriver.cpp
@@ -431,25 +431,31 @@ class DeadnessListener : public RewriterBase::ForwardingListener {
 
   void notifyBlockInserted(Block *block, Region *previous,
                            Region::iterator previousIt) override {
+    // Moving a block can change the context of every operation it contains.
+    for (Operation &op : *block)
+      cache.invalidate(&op);
     if (previous)
       if (Operation *parent = previous->getParentOp())
-        cache.invalidate(parent);
+        cache.invalidateAncestors(parent);
     if (Operation *parent = block->getParentOp())
-      cache.invalidate(parent);
+      cache.invalidateAncestors(parent);
     ForwardingListener::notifyBlockInserted(block, previous, previousIt);
   }
 
   void notifyBlockErased(Block *block) override {
     if (Operation *parent = block->getParentOp())
-      cache.invalidate(parent);
+      cache.invalidateAncestors(parent);
     ForwardingListener::notifyBlockErased(block);
   }
 
   void notifyOperationInserted(Operation *op,
                                OpBuilder::InsertPoint previous) override {
+    // The old parent loses a child, but its remaining descendants are
+    // unchanged. Scanning them for every move would make block merging
+    // quadratic.
     if (previous.isSet())
       if (Operation *parent = previous.getBlock()->getParentOp())
-        cache.invalidate(parent);
+        cache.invalidateAncestors(parent);
     cache.invalidate(op);
     ForwardingListener::notifyOperationInserted(op, previous);
   }
@@ -1234,10 +1240,8 @@ LogicalResult RegionPatternRewriteDriver::simplify(bool *changed) && {
     worklist.clear();
 
     // `OperationFolder` CSE's constant ops (and may move them into parents
-    // regions to enable more aggressive CSE'ing). It must notify the
-    // reachability listener, which forwards to the driver, so that the
-    // reachability cache sees every CFG change.
-    OperationFolder folder(ctx, &reachabilityListener);
+    // regions to enable more aggressive CSE'ing).
+    OperationFolder folder(ctx, rewriter.getListener());
     auto insertKnownConstant = [&](Operation *op) {
       // Check for existing constants when populating the worklist. This avoids
       // accidentally reversing the constant order during processing.
diff --git a/mlir/unittests/Transforms/GreedyPatternRewriteDriver.cpp b/mlir/unittests/Transforms/GreedyPatternRewriteDriver.cpp
index 9b469776bed0d2..f452ffb10eb44a 100644
--- a/mlir/unittests/Transforms/GreedyPatternRewriteDriver.cpp
+++ b/mlir/unittests/Transforms/GreedyPatternRewriteDriver.cpp
@@ -11,6 +11,7 @@
 #include "mlir/IR/Verifier.h"
 #include "mlir/Interfaces/SideEffectInterfaces.h"
 #include "mlir/Parser/Parser.h"
+#include "mlir/Transforms/FoldUtils.h"
 #include "gtest/gtest.h"
 
 #include "../../test/lib/Dialect/Test/TestDialect.h"
@@ -194,6 +195,88 @@ TEST_F(GreedyDeadnessTest, MoveBlocksBetweenRegions) {
   EXPECT_EQ(&target->getRegion(0).front(), liveBlock);
 }
 
+TEST_F(GreedyDeadnessTest, ConstantHoistingNotifiesListener) {
+  // Constant CSE must report moves and replacements through the driver's
+  // listener chain, including changes to users of the replaced constant.
+  auto moduleOp = parseSourceString<ModuleOp>(region(R"mlir(
+    %a = "test.constant"() {value = 1 : i32} : () -> i32
+    %b = "test.constant"() {value = 1 : i32} : () -> i32
+    "test.use"(%a, %b) : (i32, i32) -> ()
+  )mlir"),
+                                              &context);
+  ASSERT_TRUE(moduleOp);
+  struct Listener : RewriterBase::Listener {
+    unsigned moved = 0, erased = 0, modified = 0;
+    void notifyOperationInserted(Operation *op,
+                                 OpBuilder::InsertPoint previous) override {
+      if (op->hasTrait<OpTrait::ConstantLike>()) {
+        EXPECT_TRUE(previous.isSet());
+        EXPECT_NE(previous.getBlock(), op->getBlock());
+        ++moved;
+      }
+    }
+    void notifyOperationErased(Operation *op) override {
+      if (op->hasTrait<OpTrait::ConstantLike>())
+        ++erased;
+    }
+    void notifyOperationModified(Operation *op) override {
+      if (op->getName().getStringRef() == "test.use")
+        ++modified;
+    }
+  } listener;
+  GreedyRewriteConfig config;
+  config.setListener(&listener)
+      .enableFolding(false)
+      .enableConstantCSE(true)
+      .setRegionSimplificationLevel(GreedySimplifyRegionLevel::Disabled);
+  ASSERT_TRUE(succeeded(
+      applyPatternsGreedily(*moduleOp, RewritePatternSet(&context), config)));
+  EXPECT_EQ(listener.moved, 1u);
+  EXPECT_EQ(listener.erased, 1u);
+  EXPECT_EQ(listener.modified, 1u);
+  EXPECT_TRUE(succeeded(verify(*moduleOp)));
+}
+
+TEST_F(GreedyDeadnessTest, ConstantRehoistingNotifiesListener) {
+  // A constant already owned by the folder must report each move, whether
+  // rehoisting is requested by constant insertion or by folding.
+  auto moduleOp = parseSourceString<ModuleOp>(R"mlir(
+    %a = "test.constant"() {value = 1 : i32} : () -> i32
+    "test.use"(%a) : (i32) -> ()
+  )mlir",
+                                              &context);
+  ASSERT_TRUE(moduleOp);
+  struct Listener : RewriterBase::Listener {
+    unsigned moved = 0;
+    void notifyOperationInserted(Operation *op,
+                                 OpBuilder::InsertPoint previous) override {
+      EXPECT_TRUE(previous.isSet());
+      EXPECT_EQ(previous.getBlock(), op->getBlock());
+      ++moved;
+    }
+  } listener;
+  Operation *constant = &moduleOp->getBody()->front();
+  Operation *user = constant->getNextNode();
+  OperationFolder folder(&context, &listener);
+  EXPECT_EQ(folder.insertKnownConstant(constant),
+            OperationFolder::ConstantInsertionResult::Unchanged);
+  // Inserting an operation ahead of the constant requires rehoisting
+  // without changing the constant's users or value.
+  OpBuilder builder(&context);
+  builder.setInsertionPoint(constant);
+  OperationState state(builder.getUnknownLoc(), "test.side_effect_op");
+  state.addTypes(builder.getI32Type());
+  Operation *inserted = builder.create(state);
+  EXPECT_EQ(folder.insertKnownConstant(constant),
+            OperationFolder::ConstantInsertionResult::Hoisted);
+  EXPECT_EQ(listener.moved, 1u);
+  inserted->moveBefore(constant);
+  EXPECT_TRUE(failed(folder.tryToFold(constant)));
+  EXPECT_EQ(listener.moved, 2u);
+  EXPECT_EQ(user->getOperand(0), constant->getResult(0));
+  EXPECT_TRUE(succeeded(verify(*moduleOp)));
+}
+
 TEST_F(GreedyDeadnessTest, UsesAreNotCached) {
   // Change uses without invalidating the effect cache. Each query must check
   // current uses, including a new use added after caching a dead result.

>From 00220a46363d612c8dc00fad376c4d0cc083f5dc Mon Sep 17 00:00:00 2001
From: Ivan Butygin <ivan.butygin at gmail.com>
Date: Tue, 20 Oct 2026 12:11:21 +0200
Subject: [PATCH 07/10] [mlir] Split constant move notifications into a
 separate change

---
 mlir/lib/Transforms/Utils/FoldUtils.cpp       | 10 +--
 .../Transforms/GreedyPatternRewriteDriver.cpp | 83 -------------------
 2 files changed, 5 insertions(+), 88 deletions(-)

diff --git a/mlir/lib/Transforms/Utils/FoldUtils.cpp b/mlir/lib/Transforms/Utils/FoldUtils.cpp
index 66eecd1b80164d..90e30b6282d7ef 100644
--- a/mlir/lib/Transforms/Utils/FoldUtils.cpp
+++ b/mlir/lib/Transforms/Utils/FoldUtils.cpp
@@ -80,7 +80,7 @@ LogicalResult OperationFolder::tryToFold(Operation *op, bool *inPlaceUpdate,
     // inserted before this one.
     Block *opBlock = op->getBlock();
     if (&opBlock->front() != op && !isFolderOwnedConstant(op->getPrevNode())) {
-      rewriter.moveOpBefore(op, &opBlock->front());
+      op->moveBefore(&opBlock->front());
       op->setLoc(erasedFoldedLocation);
     }
     return failure();
@@ -118,7 +118,7 @@ OperationFolder::insertKnownConstant(Operation *op, Attribute constValue) {
   // check to see if we should rehoist it.
   if (isFolderOwnedConstant(op)) {
     if (&opBlock->front() != op && !isFolderOwnedConstant(op->getPrevNode())) {
-      rewriter.moveOpBefore(op, &opBlock->front());
+      op->moveBefore(&opBlock->front());
       op->setLoc(erasedFoldedLocation);
       return ConstantInsertionResult::Hoisted;
     }
@@ -162,12 +162,12 @@ OperationFolder::insertKnownConstant(Operation *op, Attribute constValue) {
   ConstantInsertionResult result = ConstantInsertionResult::Unchanged;
   Block *insertBlock = &insertRegion->front();
   if (opBlock != insertBlock) {
-    rewriter.moveOpBefore(op, &insertBlock->front());
+    op->moveBefore(&insertBlock->front());
     op->setLoc(erasedFoldedLocation);
     result = ConstantInsertionResult::Hoisted;
   } else if (&insertBlock->front() != op &&
              !isFolderOwnedConstant(op->getPrevNode())) {
-    rewriter.moveOpBefore(op, &insertBlock->front());
+    op->moveBefore(&insertBlock->front());
     result = ConstantInsertionResult::Hoisted;
   }
 
@@ -288,7 +288,7 @@ OperationFolder::processFoldResults(Operation *op,
       // was inserted before the constant within the insertion block.
       Block *opBlock = op->getBlock();
       if (opBlock == constOp->getBlock() && &opBlock->front() != constOp)
-        rewriter.moveOpBefore(constOp, &opBlock->front());
+        constOp->moveBefore(&opBlock->front());
 
       results.push_back(constOp->getResult(0));
       continue;
diff --git a/mlir/unittests/Transforms/GreedyPatternRewriteDriver.cpp b/mlir/unittests/Transforms/GreedyPatternRewriteDriver.cpp
index f452ffb10eb44a..9b469776bed0d2 100644
--- a/mlir/unittests/Transforms/GreedyPatternRewriteDriver.cpp
+++ b/mlir/unittests/Transforms/GreedyPatternRewriteDriver.cpp
@@ -11,7 +11,6 @@
 #include "mlir/IR/Verifier.h"
 #include "mlir/Interfaces/SideEffectInterfaces.h"
 #include "mlir/Parser/Parser.h"
-#include "mlir/Transforms/FoldUtils.h"
 #include "gtest/gtest.h"
 
 #include "../../test/lib/Dialect/Test/TestDialect.h"
@@ -195,88 +194,6 @@ TEST_F(GreedyDeadnessTest, MoveBlocksBetweenRegions) {
   EXPECT_EQ(&target->getRegion(0).front(), liveBlock);
 }
 
-TEST_F(GreedyDeadnessTest, ConstantHoistingNotifiesListener) {
-  // Constant CSE must report moves and replacements through the driver's
-  // listener chain, including changes to users of the replaced constant.
-  auto moduleOp = parseSourceString<ModuleOp>(region(R"mlir(
-    %a = "test.constant"() {value = 1 : i32} : () -> i32
-    %b = "test.constant"() {value = 1 : i32} : () -> i32
-    "test.use"(%a, %b) : (i32, i32) -> ()
-  )mlir"),
-                                              &context);
-  ASSERT_TRUE(moduleOp);
-  struct Listener : RewriterBase::Listener {
-    unsigned moved = 0, erased = 0, modified = 0;
-    void notifyOperationInserted(Operation *op,
-                                 OpBuilder::InsertPoint previous) override {
-      if (op->hasTrait<OpTrait::ConstantLike>()) {
-        EXPECT_TRUE(previous.isSet());
-        EXPECT_NE(previous.getBlock(), op->getBlock());
-        ++moved;
-      }
-    }
-    void notifyOperationErased(Operation *op) override {
-      if (op->hasTrait<OpTrait::ConstantLike>())
-        ++erased;
-    }
-    void notifyOperationModified(Operation *op) override {
-      if (op->getName().getStringRef() == "test.use")
-        ++modified;
-    }
-  } listener;
-  GreedyRewriteConfig config;
-  config.setListener(&listener)
-      .enableFolding(false)
-      .enableConstantCSE(true)
-      .setRegionSimplificationLevel(GreedySimplifyRegionLevel::Disabled);
-  ASSERT_TRUE(succeeded(
-      applyPatternsGreedily(*moduleOp, RewritePatternSet(&context), config)));
-  EXPECT_EQ(listener.moved, 1u);
-  EXPECT_EQ(listener.erased, 1u);
-  EXPECT_EQ(listener.modified, 1u);
-  EXPECT_TRUE(succeeded(verify(*moduleOp)));
-}
-
-TEST_F(GreedyDeadnessTest, ConstantRehoistingNotifiesListener) {
-  // A constant already owned by the folder must report each move, whether
-  // rehoisting is requested by constant insertion or by folding.
-  auto moduleOp = parseSourceString<ModuleOp>(R"mlir(
-    %a = "test.constant"() {value = 1 : i32} : () -> i32
-    "test.use"(%a) : (i32) -> ()
-  )mlir",
-                                              &context);
-  ASSERT_TRUE(moduleOp);
-  struct Listener : RewriterBase::Listener {
-    unsigned moved = 0;
-    void notifyOperationInserted(Operation *op,
-                                 OpBuilder::InsertPoint previous) override {
-      EXPECT_TRUE(previous.isSet());
-      EXPECT_EQ(previous.getBlock(), op->getBlock());
-      ++moved;
-    }
-  } listener;
-  Operation *constant = &moduleOp->getBody()->front();
-  Operation *user = constant->getNextNode();
-  OperationFolder folder(&context, &listener);
-  EXPECT_EQ(folder.insertKnownConstant(constant),
-            OperationFolder::ConstantInsertionResult::Unchanged);
-  // Inserting an operation ahead of the constant requires rehoisting
-  // without changing the constant's users or value.
-  OpBuilder builder(&context);
-  builder.setInsertionPoint(constant);
-  OperationState state(builder.getUnknownLoc(), "test.side_effect_op");
-  state.addTypes(builder.getI32Type());
-  Operation *inserted = builder.create(state);
-  EXPECT_EQ(folder.insertKnownConstant(constant),
-            OperationFolder::ConstantInsertionResult::Hoisted);
-  EXPECT_EQ(listener.moved, 1u);
-  inserted->moveBefore(constant);
-  EXPECT_TRUE(failed(folder.tryToFold(constant)));
-  EXPECT_EQ(listener.moved, 2u);
-  EXPECT_EQ(user->getOperand(0), constant->getResult(0));
-  EXPECT_TRUE(succeeded(verify(*moduleOp)));
-}
-
 TEST_F(GreedyDeadnessTest, UsesAreNotCached) {
   // Change uses without invalidating the effect cache. Each query must check
   // current uses, including a new use added after caching a dead result.

>From 53ee804ab10180437cce873208724eee5698a239 Mon Sep 17 00:00:00 2001
From: Ivan Butygin <ivan.butygin at gmail.com>
Date: Sat, 24 Oct 2026 17:03:49 +0200
Subject: [PATCH 08/10] [mlir] Preserve upstream constant move notifications
 after rebase

---
 mlir/lib/Transforms/Utils/FoldUtils.cpp | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/mlir/lib/Transforms/Utils/FoldUtils.cpp b/mlir/lib/Transforms/Utils/FoldUtils.cpp
index 90e30b6282d7ef..66eecd1b80164d 100644
--- a/mlir/lib/Transforms/Utils/FoldUtils.cpp
+++ b/mlir/lib/Transforms/Utils/FoldUtils.cpp
@@ -80,7 +80,7 @@ LogicalResult OperationFolder::tryToFold(Operation *op, bool *inPlaceUpdate,
     // inserted before this one.
     Block *opBlock = op->getBlock();
     if (&opBlock->front() != op && !isFolderOwnedConstant(op->getPrevNode())) {
-      op->moveBefore(&opBlock->front());
+      rewriter.moveOpBefore(op, &opBlock->front());
       op->setLoc(erasedFoldedLocation);
     }
     return failure();
@@ -118,7 +118,7 @@ OperationFolder::insertKnownConstant(Operation *op, Attribute constValue) {
   // check to see if we should rehoist it.
   if (isFolderOwnedConstant(op)) {
     if (&opBlock->front() != op && !isFolderOwnedConstant(op->getPrevNode())) {
-      op->moveBefore(&opBlock->front());
+      rewriter.moveOpBefore(op, &opBlock->front());
       op->setLoc(erasedFoldedLocation);
       return ConstantInsertionResult::Hoisted;
     }
@@ -162,12 +162,12 @@ OperationFolder::insertKnownConstant(Operation *op, Attribute constValue) {
   ConstantInsertionResult result = ConstantInsertionResult::Unchanged;
   Block *insertBlock = &insertRegion->front();
   if (opBlock != insertBlock) {
-    op->moveBefore(&insertBlock->front());
+    rewriter.moveOpBefore(op, &insertBlock->front());
     op->setLoc(erasedFoldedLocation);
     result = ConstantInsertionResult::Hoisted;
   } else if (&insertBlock->front() != op &&
              !isFolderOwnedConstant(op->getPrevNode())) {
-    op->moveBefore(&insertBlock->front());
+    rewriter.moveOpBefore(op, &insertBlock->front());
     result = ConstantInsertionResult::Hoisted;
   }
 
@@ -288,7 +288,7 @@ OperationFolder::processFoldResults(Operation *op,
       // was inserted before the constant within the insertion block.
       Block *opBlock = op->getBlock();
       if (opBlock == constOp->getBlock() && &opBlock->front() != constOp)
-        constOp->moveBefore(&opBlock->front());
+        rewriter.moveOpBefore(constOp, &opBlock->front());
 
       results.push_back(constOp->getResult(0));
       continue;

>From 055bbd0402d6818dd1c66ccfd89a51dac25dc6de Mon Sep 17 00:00:00 2001
From: Ivan Butygin <ivan.butygin at gmail.com>
Date: Mon, 26 Oct 2026 13:00:18 +0100
Subject: [PATCH 09/10] [mlir] Share cached and uncached trivial deadness
 traversal

---
 .../mlir/Interfaces/SideEffectInterfaces.h    |   3 +-
 mlir/lib/Interfaces/SideEffectInterfaces.cpp  | 140 ++++++++----------
 .../Transforms/GreedyPatternRewriteDriver.cpp |  34 +++++
 3 files changed, 96 insertions(+), 81 deletions(-)

diff --git a/mlir/include/mlir/Interfaces/SideEffectInterfaces.h b/mlir/include/mlir/Interfaces/SideEffectInterfaces.h
index f3f9d7e018303f..11e763ec79b422 100644
--- a/mlir/include/mlir/Interfaces/SideEffectInterfaces.h
+++ b/mlir/include/mlir/Interfaces/SideEffectInterfaces.h
@@ -553,6 +553,8 @@ bool wouldOpBeTriviallyDead(Operation *op);
 /// also invalidate the old parent. Invalidate operations before erasing them.
 class DeadnessCache {
 public:
+  enum class State : uint8_t { Alive, Dead };
+
   /// Return true if the operation is unused and can be erased. Terminators and
   /// symbols cannot be erased. Uses are checked on every query.
   bool isOpTriviallyDead(Operation *op);
@@ -564,7 +566,6 @@ class DeadnessCache {
   void invalidateAncestors(Operation *op);
 
 private:
-  enum class State : uint8_t { Alive, Dead };
   DenseMap<Operation *, State> cache;
 };
 
diff --git a/mlir/lib/Interfaces/SideEffectInterfaces.cpp b/mlir/lib/Interfaces/SideEffectInterfaces.cpp
index 4e3a4db20574cb..f43da0456effb2 100644
--- a/mlir/lib/Interfaces/SideEffectInterfaces.cpp
+++ b/mlir/lib/Interfaces/SideEffectInterfaces.cpp
@@ -73,109 +73,89 @@ static bool hasOnlyDeadEffects(Operation *op, bool hasRecursiveEffects) {
 /// considers terminator operations as dead if they have no side effects. This
 /// allows for marking region operations as trivially dead without always being
 /// conservative of terminators.
-static bool wouldOpBeTriviallyDeadImpl(Operation *rootOp) {
-  // The set of operation intervals (end-exclusive) to consider when checking
-  // for side effects.
-  SmallVector<std::pair<Block::iterator, Block::iterator>, 1> effectingOps = {
-      std::make_pair(Block::iterator(rootOp), ++Block::iterator(rootOp))};
-  while (!effectingOps.empty()) {
-    Block::iterator &it = effectingOps.back().first;
-    Block::iterator end = effectingOps.back().second;
-    if (it == end) {
-      effectingOps.pop_back();
-      continue;
-    }
-    mlir::Operation *op = &*(it++);
-
-    // If the operation has recursive effects, push all of the nested operations
-    // on to the stack to consider.
-    bool hasRecursiveEffects =
-        op->hasTrait<OpTrait::HasRecursiveMemoryEffects>();
-    if (hasRecursiveEffects) {
-      for (Region &region : op->getRegions()) {
-        for (auto &block : region) {
-          effectingOps.push_back(std::make_pair(block.begin(), block.end()));
-        }
-      }
-    }
-
-    if (!hasOnlyDeadEffects(op, hasRecursiveEffects))
-      return false;
-  }
-
-  // If we get here, none of the operations had effects that prevented marking
-  // 'op' as dead.
-  return true;
-}
-
-bool DeadnessCache::isOpTriviallyDead(Operation *root) {
-  // Cache only effects. Uses can change without a change to the defining op.
-  // Nested operations may have internal uses or be terminators even when the
-  // whole root can be erased, so these checks apply only to the root.
-  if (!root->use_empty() || root->mightHaveTrait<OpTrait::IsTerminator>() ||
-      isa<SymbolOpInterface>(root))
-    return false;
-
-  auto cachedRoot = cache.find(root);
-  if (cachedRoot != cache.end())
-    return cachedRoot->second == State::Dead;
-
+static bool wouldOpBeTriviallyDeadImpl(
+    Operation *root,
+    DenseMap<Operation *, DeadnessCache::State> *cache = nullptr) {
+  using State = DeadnessCache::State;
   // Repeated queries on nested operations would otherwise scan the same
-  // subtrees. Use a postorder traversal to cache each subtree's effects and
-  // skip it on later queries. Keep the traversal on an explicit worklist so
-  // deeply nested IR does not consume the C++ call stack.
+  // subtrees. Cache each subtree after checking its effects, and skip cached
+  // subtrees on later queries. An explicit worklist also bounds C++ stack use
+  // for deeply nested IR. Uncached queries do not look up or record summaries.
   struct WorkItem {
-    Operation *complete;
-    // The unvisited range [current, end) lets a block resume after a nested
-    // operation, without adding each sibling to the worklist. The initial
-    // range contains only the root, not the rest of its block. Completion
-    // markers do not use these iterators.
+    // Keep the unvisited range so a block can resume after a nested operation
+    // without adding every sibling to the worklist.
     Block::iterator current;
     Block::iterator end;
+    Operation *complete = nullptr;
   };
-  SmallVector<WorkItem, 8> worklist = {
-      {nullptr, Block::iterator(root), ++Block::iterator(root)}};
+  SmallVector<WorkItem, 1> worklist = {
+      {Block::iterator(root), ++Block::iterator(root)}};
   while (!worklist.empty()) {
     WorkItem &item = worklist.back();
-    if (item.complete) {
-      // All nested operations have been checked without finding live effects.
-      cache[item.complete] = State::Dead;
-      worklist.pop_back();
-      continue;
-    }
     if (item.current == item.end) {
+      if (cache && item.complete)
+        (*cache)[item.complete] = State::Dead;
       worklist.pop_back();
       continue;
     }
     Operation *op = &*item.current++;
-    auto cached = cache.find(op);
-    if (cached != cache.end() && cached->second == State::Dead)
-      continue;
+    bool cachedAlive = false;
+    if (cache) {
+      auto cached = cache->find(op);
+      if (cached != cache->end()) {
+        if (cached->second == State::Dead)
+          continue;
+        cachedAlive = true;
+      }
+    }
     bool recursive = op->hasTrait<OpTrait::HasRecursiveMemoryEffects>();
-    if (cached != cache.end() || !hasOnlyDeadEffects(op, recursive)) {
-      // One live effect keeps every ancestor up to the root live. Cache this
-      // result and stop; the remaining subtrees cannot change the answer.
-      for (Operation *ancestor = op;; ancestor = ancestor->getParentOp()) {
-        cache[ancestor] = State::Alive;
-        if (ancestor == root)
-          break;
+    if (cachedAlive || !hasOnlyDeadEffects(op, recursive)) {
+      // One live effect keeps every ancestor up to the root live. The
+      // remaining subtrees cannot change the answer.
+      if (cache) {
+        for (Operation *ancestor = op;; ancestor = ancestor->getParentOp()) {
+          (*cache)[ancestor] = State::Alive;
+          if (ancestor == root)
+            break;
+        }
       }
       return false;
     }
     if (!recursive) {
-      cache[op] = State::Dead;
+      if (cache)
+        (*cache)[op] = State::Dead;
       continue;
     }
-    // Process the child ranges before this completion marker. The op can be
-    // cached as dead only after all of its recursive effects are checked.
-    worklist.push_back({op, {}, {}});
-    for (Region &region : op->getRegions())
-      for (Block &block : region)
-        worklist.push_back({nullptr, block.begin(), block.end()});
+    // The first pushed block is processed last. Record the parent's summary
+    // only when that block is complete and all other blocks have been checked.
+    Operation *complete = cache ? op : nullptr;
+    for (Region &region : op->getRegions()) {
+      for (Block &block : region) {
+        worklist.push_back({block.begin(), block.end(), complete});
+        complete = nullptr;
+      }
+    }
+    if (complete)
+      (*cache)[complete] = State::Dead;
   }
   return true;
 }
 
+bool DeadnessCache::isOpTriviallyDead(Operation *root) {
+  // Cache only effects. Uses can change without a change to the defining op.
+  // Nested operations may have internal uses or be terminators even when the
+  // whole root can be erased, so these checks apply only to the root.
+  if (!root->use_empty() || root->mightHaveTrait<OpTrait::IsTerminator>() ||
+      isa<SymbolOpInterface>(root))
+    return false;
+
+  auto cachedRoot = cache.find(root);
+  if (cachedRoot != cache.end())
+    return cachedRoot->second == State::Dead;
+
+  return wouldOpBeTriviallyDeadImpl(root, &cache);
+}
+
 void DeadnessCache::invalidate(Operation *op) {
   op->walk([&](Operation *nested) { cache.erase(nested); });
   invalidateAncestors(op);
diff --git a/mlir/unittests/Transforms/GreedyPatternRewriteDriver.cpp b/mlir/unittests/Transforms/GreedyPatternRewriteDriver.cpp
index 9b469776bed0d2..85a3d363c5f5e9 100644
--- a/mlir/unittests/Transforms/GreedyPatternRewriteDriver.cpp
+++ b/mlir/unittests/Transforms/GreedyPatternRewriteDriver.cpp
@@ -194,6 +194,40 @@ TEST_F(GreedyDeadnessTest, MoveBlocksBetweenRegions) {
   EXPECT_EQ(&target->getRegion(0).front(), liveBlock);
 }
 
+TEST_F(GreedyDeadnessTest, MultipleBlocksAgreeWithUncachedQuery) {
+  // A write in either block must keep the parent live on repeated queries.
+  // An effect-free parent becomes dead only after both blocks are checked.
+  for (int writeBlock : {-1, 0, 1}) {
+    std::string body = "\"test.any_cond\"() ({\n";
+    for (int block = 0; block < 2; ++block) {
+      body += "^bb" + std::to_string(block) + ":\n";
+      if (block == writeBlock)
+        body += effect.str();
+      body += "\"test.return\"() : () -> ()\n";
+    }
+    body += "}) : () -> ()\n";
+    auto moduleOp = parseSourceString<ModuleOp>(body, &context);
+    ASSERT_TRUE(moduleOp);
+    Operation *root = &moduleOp->getBody()->front();
+    DeadnessCache cache;
+    EXPECT_EQ(wouldOpBeTriviallyDead(root), writeBlock == -1);
+    EXPECT_EQ(cache.isOpTriviallyDead(root), writeBlock == -1);
+    EXPECT_EQ(cache.isOpTriviallyDead(root), writeBlock == -1);
+  }
+}
+
+TEST_F(GreedyDeadnessTest, EmptyRegionAgreesWithUncachedQuery) {
+  // A recursive-effect operation with no blocks has no nested live effects.
+  auto moduleOp = parseSourceString<ModuleOp>(
+      "\"test.any_cond\"() ({}) : () -> ()", &context);
+  ASSERT_TRUE(moduleOp);
+  Operation *root = &moduleOp->getBody()->front();
+  DeadnessCache cache;
+  EXPECT_TRUE(wouldOpBeTriviallyDead(root));
+  EXPECT_TRUE(cache.isOpTriviallyDead(root));
+  EXPECT_TRUE(cache.isOpTriviallyDead(root));
+}
+
 TEST_F(GreedyDeadnessTest, UsesAreNotCached) {
   // Change uses without invalidating the effect cache. Each query must check
   // current uses, including a new use added after caching a dead result.

>From b351718d3b6b4c7ebe6db8745d336c7807e99422 Mon Sep 17 00:00:00 2001
From: Ivan Butygin <ivan.butygin at gmail.com>
Date: Mon, 26 Oct 2026 13:00:59 +0100
Subject: [PATCH 10/10] [mlir] Rename trivial deadness effect helper

---
 mlir/lib/Interfaces/SideEffectInterfaces.cpp | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/mlir/lib/Interfaces/SideEffectInterfaces.cpp b/mlir/lib/Interfaces/SideEffectInterfaces.cpp
index f43da0456effb2..83eea06d3baf54 100644
--- a/mlir/lib/Interfaces/SideEffectInterfaces.cpp
+++ b/mlir/lib/Interfaces/SideEffectInterfaces.cpp
@@ -46,7 +46,7 @@ bool mlir::isOpTriviallyDead(Operation *op) {
 /// The caller must also check nested effects when `hasRecursiveEffects` is
 /// true. This function does not check uses or restrictions on erasing
 /// terminators and symbols.
-static bool hasOnlyDeadEffects(Operation *op, bool hasRecursiveEffects) {
+static bool hasOnlyRemovableEffects(Operation *op, bool hasRecursiveEffects) {
   if (auto effectInterface = dyn_cast<MemoryEffectOpInterface>(op)) {
     SmallVector<MemoryEffects::EffectInstance, 1> effects;
     effectInterface.getEffects(effects);
@@ -109,7 +109,7 @@ static bool wouldOpBeTriviallyDeadImpl(
       }
     }
     bool recursive = op->hasTrait<OpTrait::HasRecursiveMemoryEffects>();
-    if (cachedAlive || !hasOnlyDeadEffects(op, recursive)) {
+    if (cachedAlive || !hasOnlyRemovableEffects(op, recursive)) {
       // One live effect keeps every ancestor up to the root live. The
       // remaining subtrees cannot change the answer.
       if (cache) {



More information about the Mlir-commits mailing list