41 cf Dialect
41.1 Beginner Summary
The cf dialect is MLIR’s low-level control-flow dialect.
It defines unstructured control flow over SSA blocks:
cf.br: unconditional branch.cf.cond_br: conditional branch.cf.switch: multi-way branch.cf.assert: runtime assertion.
For beginners, cf is the dialect you see when structured control flow such as scf.if or scf.for has been lowered toward a control-flow graph.
41.2 Why This Dialect Exists
MLIR has both structured and unstructured control flow.
Structured control flow uses operations with regions:
scf.if
scf.for
scf.while
Unstructured control flow uses blocks and branches:
^bb0:
cf.cond_br %cond, ^bb1, ^bb2
^bb1:
cf.br ^bb3
^bb2:
cf.br ^bb3
^bb3:
The cf dialect exists for the unstructured form. This is useful because many low-level targets and compiler analyses operate on control-flow graphs rather than nested structured regions.
The dialect is still target-independent. A cf.cond_br is lower than scf.if, but it is not yet an LLVM branch, a SPIR-V branch, or machine code.
41.3 When It Matters
The cf dialect matters when a pipeline crosses the boundary between structured MLIR and CFG-style IR.
You often see it:
- After
convert-scf-to-cf. - Before
convert-cf-to-llvm. - Before some SPIR-V lowering paths.
- In low-level code that already looks like basic blocks.
- In canonicalization or CFG cleanup.
- Around code that needs block arguments and explicit branch operands.
You may also see it temporarily when a lowering pass needs to express control flow that no longer fits a structured scf operation.
41.4 When To Use It
Use cf when the IR needs explicit block-to-block jumps.
Good uses include:
- Lowering
scf.if,scf.while, orscf.fortoward a CFG. - Representing arbitrary branches that are not naturally structured.
- Passing values to successor blocks through block arguments.
- Modeling low-level switch dispatch.
- Preserving target-independent control flow before LLVM or SPIR-V conversion.
- Emitting runtime checks with
cf.assert.
Prefer scf when the control flow is naturally structured and you still want loop and region structure for analysis, transformations, or readability.
41.5 Core Concepts
41.5.1 Blocks And Terminators
cf.br, cf.cond_br, and cf.switch are terminators. They end a block and choose the next block.
In MLIR, successor blocks may have block arguments. Branch operands provide the values for those block arguments.
Example:
cf.br ^done(%x : i32)
^done(%y: i32):
The branch operand %x becomes the block argument %y in ^done.
41.5.2 Structured Versus Unstructured
Structured control flow nests regions:
scf.if %cond {
...
} else {
...
}
Unstructured control flow links blocks:
cf.cond_br %cond, ^then, ^else
Both can represent conditionals, but they are useful at different stages. Structured control flow is easier for many high-level transformations. Unstructured control flow is closer to LLVM IR and many target CFGs.
41.5.3 Branch Interfaces
The branch operations implement MLIR branch interfaces so generic analyses can ask:
- Which successors can this operation branch to?
- What operands are passed to each successor?
- Which successor is chosen for known operands?
cf.cond_br also implements weighted branch behavior through optional branch weights.
41.6 Operations
The current ControlFlow dialect in this LLVM checkout defines four generated operations.
41.6.1 cf.assert
cf.assert checks a one-bit integer condition at runtime.
If the condition is true, execution continues. If the condition is false, the program aborts. The operation carries a string message for diagnostics.
Use it for runtime assumptions that should fail loudly when violated.
41.6.2 cf.br
cf.br is an unconditional branch to one successor block.
It may pass operands to the destination block. The operand count and types must match the destination block arguments.
Use it when control always continues at the same successor.
41.6.3 cf.cond_br
cf.cond_br branches on an i1 condition.
If the condition is true, it jumps to the true successor. Otherwise, it jumps to the false successor. Each successor may receive a different operand list.
The operation may also carry branch weights.
Use it for low-level if/else control flow.
41.6.4 cf.switch
cf.switch branches on a signless integer value.
It has:
- A default destination.
- Zero or more integer case values.
- One destination per case.
- Optional operands for each destination.
Use it for low-level multi-way dispatch.
41.7 Transformations
The cf dialect does not have a large standalone transform-pass library like vector or memref. Its main transformations are conversions to or from other control-flow representations.
41.7.1 Canonicalization
The operations provide canonicalization hooks:
cf.asserthas canonicalization.cf.brhas canonicalization.cf.cond_brhas canonicalization.cf.switchhas canonicalization and verification.
Typical cleanups include simplifying branches whose conditions are known, removing unnecessary branch operands, or simplifying switches when cases become unreachable or redundant.
41.7.2 Bufferization And Type Conversion Support
The ControlFlow dialect has support files for:
- Buffer deallocation interfaces.
- Bufferizable operation interfaces.
- Structural type conversions.
This matters because branch operands and block arguments form type boundaries. If a conversion changes the type of a value, it must update both the branch operand and the successor block argument consistently.
41.8 Conversions And Lowering Paths
41.8.1 From SCF
convert-scf-to-cf lowers structured control flow to ControlFlow dialect branches.
This is the usual path from structured loops and conditionals to an explicit CFG. It replaces structured scf operations with blocks, cf.br, and cf.cond_br.
The pass has an allow-pattern-rollback option.
41.8.2 To SCF
lift-cf-to-scf attempts to lift ControlFlow dialect operations back into structured scf operations.
The pass is named “lift” because it is not guaranteed to replace every ControlFlow operation. If a region has a single kind of return-like operation, the pass can replace all ControlFlow operations successfully. Otherwise, it may leave a cf.switch that branches to one block per return-like operation kind.
This path is useful when a CFG can be recovered into structured control flow for further high-level optimization.
41.8.3 To LLVM
convert-cf-to-llvm lowers ControlFlow operations to the LLVM dialect.
The conversion includes patterns for:
cf.brcf.cond_brcf.switchcf.assert
cf.assert lowers through an abort-style runtime path by default.
The pass has an index-bitwidth option. The generic convert-to-llvm driver can also use the ControlFlow dialect’s LLVM conversion interface.
41.8.4 To SPIR-V
convert-cf-to-spirv lowers supported ControlFlow operations to the SPIR-V dialect.
The direct pattern set covers:
cf.brtospirv.Branchcf.cond_brtospirv.BranchConditional
This path is useful after the surrounding IR has been shaped into forms SPIR-V can represent legally.
41.9 Example IR
41.9.1 Direct Branch
func.func @direct(%x: i32) -> i32 {
cf.br ^done(%x : i32)
^done(%y: i32):
func.return %y : i32
}
The value %x is passed as the block argument %y.
41.9.2 Conditional Branch
func.func @select_cf(%a: i32, %b: i32, %cond: i1) -> i32 {
cf.cond_br %cond, ^true(%a : i32), ^false(%b : i32)
^true(%x: i32):
cf.br ^done(%x : i32)
^false(%y: i32):
cf.br ^done(%y : i32)
^done(%z: i32):
func.return %z : i32
}
This is the CFG version of a simple if/else select.
41.9.3 Switch
func.func @switch_cf(%flag: i32, %a: i32, %b: i32, %c: i32) -> i32 {
cf.switch %flag : i32, [
default: ^done(%a : i32),
42: ^done(%b : i32),
43: ^done(%c : i32)
]
^done(%x: i32):
func.return %x : i32
}
All cases branch to the same block here, but they pass different values.
41.9.4 Runtime Assert
func.func @assert_cf(%ok: i1) {
cf.assert %ok, "expected ok"
func.return
}
This keeps a runtime check in the IR until it is canonicalized away or lowered.
41.10 Mental Model
Think of cf as “MLIR basic-block control flow.”
It is lower-level than scf, because it talks directly about branches and successor blocks.
It is higher-level than LLVM IR, because it still uses MLIR block arguments, MLIR types, and dialect conversion infrastructure.
The main question is:
Do I want structured regions or explicit CFG edges?
If you want structured regions, use scf. If you want explicit edges between basic blocks, use cf.
41.11 Gotchas
cf branches are terminators.
After cf.br, cf.cond_br, or cf.switch, the current block is done.
Branch operands must match successor block arguments.
If cf.br ^bb1(%x : i32) targets ^bb1(%y: f32), verification fails because the types do not match.
cf.cond_br cannot target the entry block of a region.
This restriction prevents invalid control-flow shapes inside MLIR regions.
lift-cf-to-scf is best-effort.
Not every CFG has clean structured control flow. The pass can leave some ControlFlow operations behind when the region cannot be fully lifted.
cf.assert has runtime behavior.
It is not just a compile-time hint. If the condition is false at runtime, the lowered program is expected to abort.
41.12 Source Map
Primary source files:
mlir/include/mlir/Dialect/ControlFlow/IR/ControlFlowOps.tdmlir/lib/Dialect/ControlFlow/IR/ControlFlowOps.cppmlir/include/mlir/Dialect/ControlFlow/Transforms/StructuralTypeConversions.hmlir/lib/Dialect/ControlFlow/Transforms/StructuralTypeConversions.cppmlir/include/mlir/Conversion/Passes.tdmlir/lib/Conversion/ControlFlowToLLVM/ControlFlowToLLVM.cppmlir/lib/Conversion/ControlFlowToSCF/ControlFlowToSCF.cppmlir/lib/Conversion/ControlFlowToSPIRV/ControlFlowToSPIRV.cppmlir/lib/Conversion/SCFToControlFlow/SCFToControlFlow.cpp
Generated op documentation source:
mlir-tblgen --gen-op-doc -dialect=cf \
mlir/include/mlir/Dialect/ControlFlow/IR/ControlFlowOps.td