[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 ®ion) 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 ®ion) = 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 ®ion) {
+ 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 ®ion, 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 ®ion,
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