[Mlir-commits] [mlir] 176ab01 - [MLIR][Conversion] Remove func dialect dependency for CF to SCF conversion (#222594)

llvmlistbot at llvm.org llvmlistbot at llvm.org
Fri Sep 11 06:05:05 PDT 2026


Author: Yue Huang
Date: 2026-09-11T14:05:00+01:00
New Revision: 176ab0177fb9acf749b90634cae84936fcfac2c5

URL: https://github.com/llvm/llvm-project/commit/176ab0177fb9acf749b90634cae84936fcfac2c5
DIFF: https://github.com/llvm/llvm-project/commit/176ab0177fb9acf749b90634cae84936fcfac2c5.diff

LOG: [MLIR][Conversion] Remove func dialect dependency for CF to SCF conversion (#222594)

The pass used to target only `func.func` and generates a `func.return`
with poison values. Now we change it to `ub.unreachable`, as the TODO
suggests.

But this change alone can cause the following pattern to appear after
lifting:
```mlir
func.func @f(%arg0: f32, %arg1: i1) -> f32 {
    cf.cond_br %arg1, ^bb1(%arg0 : f32), ^bb2
^bb1(%0: f32):  // pred: ^bb0
    ub.unreachable
^bb2:  // pred: ^bb0
    return %arg0 : f32
}
```

This `cf.cond_br` cannot be raised further into `scf.if`, because we
need something as the operand of `return`, while `ub.unreachable`
doesn't provide this. Therefore we reused the code of synthesizing
poison values when `ub.unreachable` can be merged to other return
operations.

---------

Co-authored-by: Markus Böck <markus.boeck02 at gmail.com>

Added: 
    

Modified: 
    mlir/include/mlir/Conversion/ControlFlowToSCF/ControlFlowToSCF.h
    mlir/include/mlir/Conversion/Passes.td
    mlir/include/mlir/Transforms/CFGToSCF.h
    mlir/lib/Conversion/ControlFlowToSCF/CMakeLists.txt
    mlir/lib/Conversion/ControlFlowToSCF/ControlFlowToSCF.cpp
    mlir/lib/Transforms/Utils/CFGToSCF.cpp
    mlir/test/Conversion/ControlFlowToSCF/test.mlir

Removed: 
    


################################################################################
diff  --git a/mlir/include/mlir/Conversion/ControlFlowToSCF/ControlFlowToSCF.h b/mlir/include/mlir/Conversion/ControlFlowToSCF/ControlFlowToSCF.h
index 73ba1a8014a89..9d140af927b51 100644
--- a/mlir/include/mlir/Conversion/ControlFlowToSCF/ControlFlowToSCF.h
+++ b/mlir/include/mlir/Conversion/ControlFlowToSCF/ControlFlowToSCF.h
@@ -63,13 +63,12 @@ class ControlFlowToSCFTransformation : public CFGToSCFInterface {
   /// Creates a `ub.poison` op of the given type.
   Value getUndefValue(Location loc, OpBuilder &builder, Type type) override;
 
-  /// Creates a `func.return` op with poison for each of the return values of
-  /// the function. It is guaranteed to be directly within the function body.
-  /// TODO: This can be made independent of the `func` dialect once the UB
-  ///       dialect has a `ub.unreachable` op.
+  /// Creates a `ub.unreachable` op.
   FailureOr<Operation *> createUnreachableTerminator(Location loc,
                                                      OpBuilder &builder,
                                                      Region &region) override;
+
+  bool isUnreachableTerminator(Operation *op) override;
 };
 
 #define GEN_PASS_DECL_LIFTCONTROLFLOWTOSCFPASS

diff  --git a/mlir/include/mlir/Conversion/Passes.td b/mlir/include/mlir/Conversion/Passes.td
index f13cb9a801139..c80e1753ea2a9 100644
--- a/mlir/include/mlir/Conversion/Passes.td
+++ b/mlir/include/mlir/Conversion/Passes.td
@@ -414,19 +414,13 @@ def LiftControlFlowToSCFPass : Pass<"lift-cf-to-scf"> {
     Otherwise a single ControlFlow switch branching to one block per return-like
     operation kind remains.
 
-    This pass may need to create unreachable terminators in case of infinite
-    loops, which is only supported for 'func.func' for now. If you potentially
-    have infinite loops inside CFG regions not belonging to 'func.func',
-    consider using `transformCFGToSCF` function directly with corresponding
-    `CFGToSCFInterface::createUnreachableTerminator` implementation.
+    This pass may need to create `ub.unreachable` terminators in case of
+    statically known infinite loops.
   }];
 
   let dependentDialects = ["scf::SCFDialect",
                            "arith::ArithDialect",
-                           "ub::UBDialect",
-                           // TODO: This is only necessary until we have a
-                           //       ub.unreachable op.
-                           "func::FuncDialect"];
+                           "ub::UBDialect"];
 }
 
 //===----------------------------------------------------------------------===//

diff  --git a/mlir/include/mlir/Transforms/CFGToSCF.h b/mlir/include/mlir/Transforms/CFGToSCF.h
index 2ca019ddfe23d..af34fab8420a1 100644
--- a/mlir/include/mlir/Transforms/CFGToSCF.h
+++ b/mlir/include/mlir/Transforms/CFGToSCF.h
@@ -118,6 +118,12 @@ class CFGToSCFInterface {
   createUnreachableTerminator(Location loc, OpBuilder &builder,
                               Region &region) = 0;
 
+  /// Returns true if the given terminator is unreachable.
+  /// These terminators are allowed to be merged into
+  /// the exit block of any other return-like operation in the region, using
+  /// undefined values as operands instead of getting their own exit block.
+  virtual bool isUnreachableTerminator(Operation *op) { return false; }
+
   /// Helper function to create an unconditional branch using
   /// `createCFGSwitchOp`.
   void createSingleDestinationBranch(Location loc, OpBuilder &builder,
@@ -147,7 +153,10 @@ class CFGToSCFInterface {
 /// If the region contains only a single kind of return-like operation, all
 /// control flow graph operations will be converted successfully.
 /// Otherwise a single control flow graph operation branching to one block
-/// per return-like operation kind remains.
+/// per return-like operation kind remains. Terminators for which
+/// `CFGToSCFInterface::isUnreachableTerminator` returns true do not get their
+/// own exit block: if any other return-like exit block exists, they are
+/// turned into branches to it with undefined operands instead.
 ///
 /// The transformation currently requires that the region has no unreachable
 /// blocks and that all control flow graph operations have no side effects,

diff  --git a/mlir/lib/Conversion/ControlFlowToSCF/CMakeLists.txt b/mlir/lib/Conversion/ControlFlowToSCF/CMakeLists.txt
index e2f1677b1d069..68074aa41c30f 100644
--- a/mlir/lib/Conversion/ControlFlowToSCF/CMakeLists.txt
+++ b/mlir/lib/Conversion/ControlFlowToSCF/CMakeLists.txt
@@ -11,7 +11,6 @@ add_mlir_conversion_library(MLIRControlFlowToSCF
   MLIRAnalysis
   MLIRArithDialect
   MLIRControlFlowDialect
-  MLIRFuncDialect
   MLIRSCFDialect
   MLIRUBDialect
   MLIRPass

diff  --git a/mlir/lib/Conversion/ControlFlowToSCF/ControlFlowToSCF.cpp b/mlir/lib/Conversion/ControlFlowToSCF/ControlFlowToSCF.cpp
index 987b4b7167164..ea25fc6b30da7 100644
--- a/mlir/lib/Conversion/ControlFlowToSCF/ControlFlowToSCF.cpp
+++ b/mlir/lib/Conversion/ControlFlowToSCF/ControlFlowToSCF.cpp
@@ -14,9 +14,9 @@
 
 #include "mlir/Dialect/Arith/IR/Arith.h"
 #include "mlir/Dialect/ControlFlow/IR/ControlFlowOps.h"
-#include "mlir/Dialect/Func/IR/FuncOps.h"
 #include "mlir/Dialect/SCF/IR/SCF.h"
 #include "mlir/Dialect/UB/IR/UBOps.h"
+#include "mlir/Interfaces/FunctionInterfaces.h"
 #include "mlir/Pass/Pass.h"
 #include "mlir/Transforms/CFGToSCF.h"
 
@@ -139,22 +139,11 @@ FailureOr<Operation *>
 ControlFlowToSCFTransformation::createUnreachableTerminator(Location loc,
                                                             OpBuilder &builder,
                                                             Region &region) {
+  return ub::UnreachableOp::create(builder, loc).getOperation();
+}
 
-  // TODO: This should create a `ub.unreachable` op. Once such an operation
-  //       exists to make the pass independent of the func dialect. For now just
-  //       return poison values.
-  Operation *parentOp = region.getParentOp();
-  auto funcOp = dyn_cast<func::FuncOp>(parentOp);
-  if (!funcOp)
-    return emitError(loc, "Cannot create unreachable terminator for '")
-           << parentOp->getName() << "'";
-
-  return func::ReturnOp::create(
-             builder, loc,
-             llvm::map_to_vector(
-                 funcOp.getResultTypes(),
-                 [&](Type type) { return getUndefValue(loc, builder, type); }))
-      .getOperation();
+bool ControlFlowToSCFTransformation::isUnreachableTerminator(Operation *op) {
+  return isa<ub::UnreachableOp>(op);
 }
 
 namespace {
@@ -169,8 +158,8 @@ struct LiftControlFlowToSCF
 
     bool changed = false;
     Operation *op = getOperation();
-    WalkResult result = op->walk([&](func::FuncOp funcOp) {
-      if (funcOp.getBody().empty())
+    WalkResult result = op->walk([&](FunctionOpInterface funcOp) {
+      if (funcOp.getFunctionBody().empty())
         return WalkResult::advance();
 
       auto &domInfo = funcOp != op ? getChildAnalysis<DominanceInfo>(funcOp)

diff  --git a/mlir/lib/Transforms/Utils/CFGToSCF.cpp b/mlir/lib/Transforms/Utils/CFGToSCF.cpp
index 2cda4a26543bd..f947e889b56fa 100644
--- a/mlir/lib/Transforms/Utils/CFGToSCF.cpp
+++ b/mlir/lib/Transforms/Utils/CFGToSCF.cpp
@@ -306,9 +306,9 @@ class EdgeMultiplexer {
         continue;
       }
 
-      // Otherwise undef values for any unused block arguments used by other
+      // Otherwise poison values for any unused block arguments used by other
       // entry blocks.
-      newSuccOperands[index] = getUndefValue(argument.getType());
+      newSuccOperands[index] = getPoisonValue(argument.getType());
     }
 
     edge.setSuccessor(multiplexerBlock);
@@ -363,8 +363,8 @@ class EdgeMultiplexer {
   /// Callback used to create a constant suitable as flag for
   /// the interfaces `createCFGSwitchOp`.
   function_ref<Value(unsigned)> getSwitchValue;
-  /// Callback used to create undefined values of a given type.
-  function_ref<Value(Type)> getUndefValue;
+  /// Callback used to create poison values of a given type.
+  function_ref<Value(Type)> getPoisonValue;
 
   /// Mapping of the block arguments of an entry block to the corresponding
   /// block arguments in the multiplexer block. Block arguments of an entry
@@ -378,11 +378,11 @@ class EdgeMultiplexer {
 
   EdgeMultiplexer(Block *multiplexerBlock,
                   function_ref<Value(unsigned)> getSwitchValue,
-                  function_ref<Value(Type)> getUndefValue,
+                  function_ref<Value(Type)> getPoisonValue,
                   llvm::SmallMapVector<Block *, unsigned, 4> &&entries,
                   Value dispatchFlag)
       : multiplexerBlock(multiplexerBlock), getSwitchValue(getSwitchValue),
-        getUndefValue(getUndefValue), blockArgMapping(std::move(entries)),
+        getPoisonValue(getPoisonValue), blockArgMapping(std::move(entries)),
         discriminator(dispatchFlag) {}
 };
 
@@ -421,15 +421,35 @@ class ReturnLikeExitCombiner {
   /// Transforms `returnLikeOp` to a branch to the only block in the
   /// region with an instance of `returnLikeOp`s kind.
   void combineExit(Operation *returnLikeOp,
-                   function_ref<Value(unsigned)> getSwitchValue) {
-    auto [iter, inserted] = returnLikeToCombinedExit.try_emplace(returnLikeOp);
-    if (!inserted && iter->first == returnLikeOp)
+                   function_ref<Value(unsigned)> getSwitchValue,
+                   function_ref<Value(Type)> getUndefValue) {
+    auto existing = returnLikeToCombinedExit.find(returnLikeOp);
+    if (existing != returnLikeToCombinedExit.end() &&
+        existing->first == returnLikeOp)
       return;
 
+    // If `returnLikeOp` is an unreachable terminator and an exit block of
+    // another return-like operation already exists, it is turned into a branch
+    // to that exit block with poison operands instead of getting an exit
+    // block of its own.
+    if (interface.isUnreachableTerminator(returnLikeOp) &&
+        !orderedExitBlocks.empty()) {
+      Block *exitBlock = orderedExitBlocks.front();
+      auto builder = OpBuilder::atBlockTerminator(returnLikeOp->getBlock());
+      interface.createSingleDestinationBranch(
+          returnLikeOp->getLoc(), builder, getSwitchValue(0), exitBlock,
+          llvm::map_to_vector(exitBlock->getArgumentTypes(), getUndefValue));
+      returnLikeOp->erase();
+      return;
+    }
+
+    auto [iter, inserted] = returnLikeToCombinedExit.try_emplace(returnLikeOp);
+
     Block *exitBlock = iter->second;
     if (inserted) {
       exitBlock = new Block;
       iter->second = exitBlock;
+      orderedExitBlocks.push_back(exitBlock);
       topLevelRegion.push_back(exitBlock);
       exitBlock->addArguments(
           returnLikeOp->getOperandTypes(),
@@ -457,6 +477,8 @@ class ReturnLikeExitCombiner {
   /// as equivalent. First occurrence seen is kept in the map.
   llvm::SmallDenseMap<Operation *, Block *, 4, ReturnLikeOpEquivalence>
       returnLikeToCombinedExit;
+  /// All exit blocks in the order they were created.
+  SmallVector<Block *, 4> orderedExitBlocks;
   Region &topLevelRegion;
   CFGToSCFInterface &interface;
 };
@@ -623,7 +645,7 @@ static FailureOr<StructuredLoopProperties> createSingleExitingLatch(
         return failure();
       // Transform the just created transform operation in the case that an
       // occurrence of it existed in input IR.
-      exitCombiner.combineExit(*terminator, getSwitchValue);
+      exitCombiner.combineExit(*terminator, getSwitchValue, getUndefValue);
     }
   }
 
@@ -733,7 +755,7 @@ transformToReduceLoop(Block *loopHeader, Block *exitBlock,
           // but previously dominated an exit block with a use.
           // In this case, add a block argument to the latch and go through all
           // predecessors. If the value dominates the predecessor, pass the
-          // value as a successor operand, otherwise pass undef.
+          // value as a successor operand, otherwise pass poison.
           // The above is unnecessary if the value is a block argument of the
           // latch or if `value` dominates all predecessors.
           Value argument = value;
@@ -774,7 +796,7 @@ transformToReduceLoop(Block *loopHeader, Block *exitBlock,
   }
 
   // New block arguments may have been added to the loop header.
-  // Adjust the entry edges to pass undef values to these.
+  // Adjust the entry edges to pass poison values to these.
   for (auto iter = loopHeader->pred_begin(); iter != loopHeader->pred_end();
        ++iter) {
     // Latch successor arguments have already been handled.
@@ -1218,13 +1240,25 @@ static FailureOr<SmallVector<Block *>> transformToStructuredCFBranches(
 /// operation, it creates a single-entry and single-exit region.
 static ReturnLikeExitCombiner createSingleExitBlocksForReturnLike(
     Region &region, function_ref<Value(unsigned)> getSwitchValue,
-    CFGToSCFInterface &interface) {
+    function_ref<Value(Type)> getUndefValue, CFGToSCFInterface &interface) {
   ReturnLikeExitCombiner exitCombiner(region, interface);
 
+  // Combine the exits of all non-unreachable return-like operations first so
+  // that unreachable terminators can be merged into their exit blocks,
+  // regardless of the order in which they appear in the region.
+  for (Block &block : region.getBlocks()) {
+    if (block.getNumSuccessors() != 0 ||
+        interface.isUnreachableTerminator(block.getTerminator()))
+      continue;
+    exitCombiner.combineExit(block.getTerminator(), getSwitchValue,
+                             getUndefValue);
+  }
   for (Block &block : region.getBlocks()) {
-    if (block.getNumSuccessors() != 0)
+    if (block.getNumSuccessors() != 0 ||
+        !interface.isUnreachableTerminator(block.getTerminator()))
       continue;
-    exitCombiner.combineExit(block.getTerminator(), getSwitchValue);
+    exitCombiner.combineExit(block.getTerminator(), getSwitchValue,
+                             getUndefValue);
   }
 
   return exitCombiner;
@@ -1339,8 +1373,8 @@ FailureOr<bool> mlir::transformCFGToSCF(Region &region,
     return switchValueCache[value];
   };
 
-  ReturnLikeExitCombiner exitCombiner =
-      createSingleExitBlocksForReturnLike(region, getSwitchValue, interface);
+  ReturnLikeExitCombiner exitCombiner = createSingleExitBlocksForReturnLike(
+      region, getSwitchValue, getUndefValue, interface);
 
   // Invalidate any dominance tree on the region as the exit combiner has
   // added new blocks and edges.

diff  --git a/mlir/test/Conversion/ControlFlowToSCF/test.mlir b/mlir/test/Conversion/ControlFlowToSCF/test.mlir
index f6e7fb265790b..78ac69aec3fe4 100644
--- a/mlir/test/Conversion/ControlFlowToSCF/test.mlir
+++ b/mlir/test/Conversion/ControlFlowToSCF/test.mlir
@@ -317,8 +317,7 @@ func.func @infinite_loop(%arg: f32) -> f32 {
 // CHECK-NEXT:   %[[CALL:.*]] = func.call @bar(%[[ARG1]])
 // CHECK-NEXT:   %[[TRUNC:.*]] = arith.trunci %[[C1]]
 // CHECK-NEXT:   scf.condition(%[[TRUNC]]) %[[CALL]]
-// CHECK: %[[POISON:.*]] = ub.poison
-// CHECK: return %[[POISON]]
+// CHECK: ub.unreachable
 
 // -----
 
@@ -375,6 +374,49 @@ func.func @conditional_infinite_loop(%arg: f32, %cond: i1) -> f32 {
 
 // -----
 
+// An unreachable terminator in the input IR is merged into the exit block of
+// another return-like operation, allowing all control flow to be lifted.
+
+func.func @unreachable_merged_into_return(%cond: i1) {
+  cf.cond_br %cond, ^bb1, ^bb2
+
+^bb1:
+  ub.unreachable
+
+^bb2:
+  return
+}
+
+// CHECK-LABEL: @unreachable_merged_into_return
+// CHECK-SAME: %[[ARG0:[[:alnum:]]+]]:
+// CHECK:      scf.if %[[ARG0]]
+// CHECK-NOT:  cf.{{(switch|(cond_)?br)}}
+// CHECK:      return
+// CHECK-NOT:  ub.unreachable
+
+// -----
+
+// If a region only exits through unreachable terminators, they are combined
+// into a single exit block and the control flow is lifted regardless.
+
+func.func @only_unreachable_exits(%cond: i1) {
+  cf.cond_br %cond, ^bb1, ^bb2
+
+^bb1:
+  ub.unreachable
+
+^bb2:
+  ub.unreachable
+}
+
+// CHECK-LABEL: @only_unreachable_exits
+// CHECK-SAME: %[[ARG0:[[:alnum:]]+]]:
+// CHECK:      scf.if %[[ARG0]]
+// CHECK-NOT:  cf.{{(switch|(cond_)?br)}}
+// CHECK:      ub.unreachable
+
+// -----
+
 // Different return-like terminators lead one control flow op remaining in the top level region.
 // Each of the blocks the control flow op leads to are transformed into regions nevertheless.
 
@@ -423,7 +465,9 @@ func.func @mixing_return_like(%cond: i1, %cond2: i1, %cond3: i1) {
 // cf.switch here only has some successors with 
diff erent return-like ops.
 // This test makes sure that if there are at least two successors branching to
 // the same region, that this region gets properly turned to structured control
-// flow.
+// flow. The statically unreachable exit of the infinite loop is merged into
+// the exit block of "test.returnLike", allowing the entire function to be
+// lifted to structured control flow.
 
 func.func @some_successors_with_
diff erent_return(%flag: i32) -> i32 {
   %0 = arith.constant 5 : i32
@@ -456,19 +500,16 @@ func.func @some_successors_with_
diff erent_return(%flag: i32) -> i32 {
 // CHECK-NEXT:   scf.yield %[[C6]], %[[C1]]
 // CHECK:      default
 // CHECK-NEXT:   scf.yield %[[POISON]], %[[C0]]
-// CHECK:      cf.switch %[[INDEX_SWITCH]]#1
-// CHECK-NEXT: default: ^[[BB2:[[:alnum:]]+]]
-// CHECK-SAME: %[[INDEX_SWITCH]]#0
-// CHECK-NEXT: 0: ^[[BB1:[[:alnum:]]+]]
-// CHECK-NEXT: ]
-
-// CHECK: ^[[BB2]]{{.*}}:
-// CHECK: scf.while
-// CHECK-NOT: cf.{{(switch|(cond_)?br)}}
-// CHECK: return
-
-// CHECK: ^[[BB1]]:
-// CHECK-NEXT: "test.returnLike"
+// CHECK:      %[[INDEX_CAST2:.*]] = arith.index_castui %[[INDEX_SWITCH]]#1
+// CHECK-NEXT: scf.index_switch %[[INDEX_CAST2]]
+// CHECK:      case 0
+// CHECK-NEXT:   scf.yield
+// CHECK:      default
+// CHECK-NEXT:   scf.while
+// CHECK-SAME:     %[[INDEX_SWITCH]]#0
+// CHECK-NOT:    cf.{{(switch|(cond_)?br)}}
+// CHECK:        scf.condition
+// CHECK:        "test.returnLike"
 
 // -----
 


        


More information about the Mlir-commits mailing list