[Mlir-commits] [mlir] [mlir][cf] Do not merge unique-pred block when branch operand derived from successor argument (PR #225791)

Scott Manley llvmlistbot at llvm.org
Wed Sep 23 07:16:16 PDT 2026


https://github.com/rscottmanley created https://github.com/llvm/llvm-project/pull/225791



simplifyBrToBlockWithSinglePred already refused to merge when a branch operand was itself a destination block argument. That missed the loop latch case where the operand is computed from the argument, e.g. `%next = addi %iter, 1` / `br ^header(%next)`. Merging rewrote the add into a cyclic def (`addi %next, 1`), after which AddIAddConstant unbounded-doubled the step.

Walk the operand's defining chain and refuse the merge if it reaches a successor argument. Adds a canonicalize regression test.

>From 96b5fa4a3b81abc7601741cd60e51d0ea1774762 Mon Sep 17 00:00:00 2001
From: Scott Manley <scmanley at nvidia.com>
Date: Wed, 23 Sep 2026 07:08:56 -0700
Subject: [PATCH] [mlir][cf] Do not merge a unique-pred block when the branch
 operand is derived from a successor argument

simplifyBrToBlockWithSinglePred already refused to merge when a branch
operand was itself a destination block argument. That missed the loop
latch case where the operand is computed from the argument, e.g.
`%next = addi %iter, 1` / `br ^header(%next)`. Merging rewrote the add
into a cyclic def (`addi %next, 1`), after which AddIAddConstant
unbounded-doubled the step.

Walk the operand's defining chain and refuse the merge if it reaches a
successor argument. Adds a canonicalize regression test.
---
 .../Dialect/ControlFlow/IR/ControlFlowOps.cpp | 37 ++++++++++++++++---
 .../Dialect/ControlFlow/canonicalize.mlir     | 27 ++++++++++++++
 2 files changed, 58 insertions(+), 6 deletions(-)

diff --git a/mlir/lib/Dialect/ControlFlow/IR/ControlFlowOps.cpp b/mlir/lib/Dialect/ControlFlow/IR/ControlFlowOps.cpp
index 9016d1929f859c..171e41a27abd37 100644
--- a/mlir/lib/Dialect/ControlFlow/IR/ControlFlowOps.cpp
+++ b/mlir/lib/Dialect/ControlFlow/IR/ControlFlowOps.cpp
@@ -25,6 +25,7 @@
 #include "mlir/IR/Value.h"
 #include "mlir/Transforms/InliningUtils.h"
 #include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/SmallPtrSet.h"
 #include <numeric>
 
 #include "mlir/Dialect/ControlFlow/IR/ControlFlowOpsDialect.cpp.inc"
@@ -156,6 +157,28 @@ static LogicalResult collapseBranch(Block *&successor,
   return success();
 }
 
+/// Return true if `value` is a block argument of `block`, or is defined by an
+/// operation that transitively uses such an argument.
+static bool isTransitivelyDefinedFrom(Value value, Block *block) {
+  SmallVector<Value, 8> worklist{value};
+  SmallPtrSet<Value, 16> visited;
+  while (!worklist.empty()) {
+    Value current = worklist.pop_back_val();
+    if (!visited.insert(current).second)
+      continue;
+    if (auto arg = dyn_cast<BlockArgument>(current)) {
+      if (arg.getOwner() == block)
+        return true;
+      continue;
+    }
+    Operation *def = current.getDefiningOp();
+    if (!def)
+      continue;
+    llvm::append_range(worklist, def->getOperands());
+  }
+  return false;
+}
+
 /// Simplify a branch to a block that has a single predecessor. This effectively
 /// merges the two blocks.
 static LogicalResult
@@ -166,13 +189,15 @@ simplifyBrToBlockWithSinglePred(BranchOp op, PatternRewriter &rewriter) {
   if (succ == opParent || !llvm::hasSingleElement(succ->getPredecessors()))
     return failure();
 
-  // If any branch operand is itself a block argument of the successor, merging
-  // would call replaceAllUsesWith(arg, arg) — a no-op — leaving dangling uses
-  // of that argument after the successor block is erased.
+  // If any branch operand is defined from a block argument of the successor,
+  // merging replaces that argument with the operand. A direct self-argument
+  // makes replaceAllUsesWith a no-op and leaves dangling uses after the
+  // successor is erased. A value derived from the argument (for example
+  // `addi %arg, 1` on a backedge) becomes a cyclic def after the replacement,
+  // and later arith folds do not terminate.
   for (Value operand : op.getOperands())
-    if (auto ba = dyn_cast<BlockArgument>(operand))
-      if (ba.getOwner() == succ)
-        return failure();
+    if (isTransitivelyDefinedFrom(operand, succ))
+      return failure();
 
   // Merge the successor into the current block and erase the branch.
   SmallVector<Value> brOperands(op.getOperands());
diff --git a/mlir/test/Dialect/ControlFlow/canonicalize.mlir b/mlir/test/Dialect/ControlFlow/canonicalize.mlir
index c8cf5931a1297c..731b4bb4990f07 100644
--- a/mlir/test/Dialect/ControlFlow/canonicalize.mlir
+++ b/mlir/test/Dialect/ControlFlow/canonicalize.mlir
@@ -687,6 +687,33 @@ func.func @no_merge_self_arg_loop(%step: i1) -> i1 {
   return %result : i1
 }
 
+// -----
+
+// Related to the case above: after folding the constant guard, ^body is the
+// unique predecessor of ^header. The backedge operand is `addi %iter, 1`,
+// which is derived from the destination block argument. Merging would rewrite
+// that add to `addi %next, 1` (a cyclic def) and arith canonicalization would
+// then keep doubling the step.
+
+// CHECK-LABEL: @no_merge_derived_from_dest_arg
+//  CHECK-NEXT:   %[[C0:.*]] = arith.constant 0 : i32
+//  CHECK-NEXT:   return %[[C0]]
+func.func @no_merge_derived_from_dest_arg(%loop: i1, %initial: i32) -> i32 {
+  %true = arith.constant true
+  %c1 = arith.constant 1 : i32
+  %c0 = arith.constant 0 : i32
+  cf.br ^guard(%true : i1)
+^guard(%enter: i1):
+  cf.cond_br %enter, ^exit(%c0 : i32), ^header(%initial : i32)
+^header(%iter: i32):
+  cf.cond_br %loop, ^body, ^exit(%iter : i32)
+^body:
+  %next = arith.addi %iter, %c1 : i32
+  cf.br ^header(%next : i32)
+^exit(%result: i32):
+  return %result : i32
+}
+
 // Verify that block arguments are replaced with a uniform incoming value
 // when all predecessors pass the same SSA value
 



More information about the Mlir-commits mailing list