[Mlir-commits] [mlir] [mlir][cf] Fix crash in simplifyBrToBlockWithSinglePred when branch operand is a block argument of its successor (PR #183797)

llvmlistbot at llvm.org llvmlistbot at llvm.org
Fri Feb 27 10:47:40 PST 2026


llvmbot wrote:


<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-mlir-cf

@llvm/pr-subscribers-mlir

Author: Mehdi Amini (joker-eph)

<details>
<summary>Changes</summary>

When `simplifyBrToBlockWithSinglePred` merges a block into its sole predecessor, it calls `inlineBlockBefore` which replaces each block argument with the corresponding value passed by the branch. If one of those values is itself a block argument of the successor block, the call `replaceAllUsesWith(arg, arg)` is a no-op. Any uses of that argument outside the block (e.g. in a downstream block) are therefore not replaced, and when the successor block is erased the argument is destroyed while those uses are still live, triggering the assertion `use_empty() && "Cannot destroy a value that still has uses\!"` in `IRObjectWithUseList::~IRObjectWithUseList`.

Guard against this by returning early when any branch operand is a block argument owned by the destination block.

Fixes #<!-- -->126213

---
Full diff: https://github.com/llvm/llvm-project/pull/183797.diff


2 Files Affected:

- (modified) mlir/lib/Dialect/ControlFlow/IR/ControlFlowOps.cpp (+8) 
- (modified) mlir/test/Dialect/ControlFlow/canonicalize.mlir (+39) 


``````````diff
diff --git a/mlir/lib/Dialect/ControlFlow/IR/ControlFlowOps.cpp b/mlir/lib/Dialect/ControlFlow/IR/ControlFlowOps.cpp
index d2078d8ab5ca5..0ce0d55f4397c 100644
--- a/mlir/lib/Dialect/ControlFlow/IR/ControlFlowOps.cpp
+++ b/mlir/lib/Dialect/ControlFlow/IR/ControlFlowOps.cpp
@@ -166,6 +166,14 @@ 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.
+  for (Value operand : op.getOperands())
+    if (auto ba = dyn_cast<BlockArgument>(operand))
+      if (ba.getOwner() == succ)
+        return failure();
+
   // Merge the successor into the current block and erase the branch.
   SmallVector<Value> brOperands(op.getOperands());
   rewriter.eraseOp(op);
diff --git a/mlir/test/Dialect/ControlFlow/canonicalize.mlir b/mlir/test/Dialect/ControlFlow/canonicalize.mlir
index 21a16784b81b2..3bf4ca006f34c 100644
--- a/mlir/test/Dialect/ControlFlow/canonicalize.mlir
+++ b/mlir/test/Dialect/ControlFlow/canonicalize.mlir
@@ -656,3 +656,42 @@ func.func @drop_unreachable_branch_2(%c: i1) {
 ^bb2:
   ub.unreachable
 }
+
+// -----
+
+// Regression test for https://github.com/llvm/llvm-project/issues/126213:
+// simplifyBrToBlockWithSinglePred must not merge a block into its sole
+// predecessor when a branch operand is itself a block argument of the
+// destination — replaceAllUsesWith would be a no-op for that argument,
+// leaving the block argument live after the block is erased and crashing.
+
+// CHECK-LABEL: @no_merge_self_arg_loop
+func.func @no_merge_self_arg_loop() {
+  %c0 = arith.constant 0 : index
+  %c1 = arith.constant 1 : index
+  %true = arith.constant true
+  cf.cond_br %true, ^bb1, ^bb2
+^bb1:
+  cf.br ^bb3(%true : i1)
+^bb2:
+  cf.br ^bb3(%true : i1)
+^bb3(%0: i1):
+  cf.br ^bb4
+^bb4:
+  cf.br ^bb5
+^bb5:
+  cf.cond_br %0, ^bb6, ^bb7
+^bb6:
+  cf.br ^bb5
+^bb7:
+  cf.br ^bb8(%c0, %true : index, i1)
+^bb8(%1: index, %2: i1):
+  %3 = arith.cmpi slt, %1, %c1 : index
+  cf.cond_br %3, ^bb9, ^bb10
+^bb9:
+  %4 = arith.addi %1, %c1 : index
+  cf.br ^bb8(%4, %2 : index, i1)
+^bb10:
+  vector.print %2 : i1
+  return
+}

``````````

</details>


https://github.com/llvm/llvm-project/pull/183797


More information about the Mlir-commits mailing list