10  Block

A block is an ordered list of operations with an optional ordered list of block arguments. It is MLIR’s basic unit of local sequencing. In a control-flow region, blocks are conventional basic blocks connected by branch successors. In a structured region, a block is often simply the body of a function, loop, or conditional branch.

^entry(%x: i32):
  %one = arith.constant 1 : i32
  %next = arith.addi %x, %one : i32
  cf.br ^exit(%next : i32)
^exit(%result: i32):
  func.return %result : i32

^entry and ^exit are blocks. %x and %result are block arguments. The operations in each block execute in printed order, subject to the semantics of the surrounding dialect and control-flow graph.

10.1 Blocks Are Where SSA Merges

Block arguments are MLIR’s representation for values arriving along control flow edges. They play the role traditionally assigned to SSA phi nodes, but they live at the destination block rather than in a special operation.

cf.cond_br %test, ^then(%a : i32), ^else(%b : i32)
^then(%chosen: i32):
  cf.br ^join(%chosen : i32)
^else(%chosen: i32):
  cf.br ^join(%chosen : i32)
^join(%merged: i32):
  %out = arith.addi %merged, %merged : i32

Each successor edge supplies operands matching the destination arguments by position and type. %merged is not assigned imperatively; it is one SSA definition whose incoming value is chosen by the predecessor edge. If a pass adds a block argument, it must update every predecessor edge. This is one of the most common ways to accidentally create invalid CFG IR.

10.2 Ordered Operations And Terminators

Within a block, an operation normally may only use values defined earlier in the same block or visible from enclosing scopes. A terminator ends a block’s operation list when its parent region uses control flow or structured-region rules. cf.br, cf.cond_br, func.return, scf.yield, and scf.condition are examples, but each dialect defines which terminator belongs in which context.

Do not move an operation after a terminator. Do not append ordinary operations to a terminated block. A block can be empty only when its parent operation and region rules allow it. The IsTerminator trait and parent-operation verification are how MLIR enforces much of this structure.

Structured dialects sometimes hide block labels because a region has one entry block and no explicit branches in its friendly syntax. The block still exists. For example, the body of scf.for has a block whose arguments include the induction variable and any loop-carried values:

scf.for %i = %lower to %upper step %step {
  %value = memref.load %data[%i] : memref<?xi32>
  // The body is a block even though its label is elided.
}

10.3 Entry Blocks And Region Semantics

The first block of a region is its entry block. Whether a region permits zero, one, or many blocks is defined by its owning operation. A function body may contain a CFG with many blocks. The body of scf.for is generally single-block structured IR. A graph-like region may permit arbitrary block connections; single-block regions make stronger lexical guarantees useful for transformations.

An entry block is not always reached by an explicit branch. For a function or structured operation, invoking the enclosing operation enters its entry block. Its arguments are supplied by the function call or enclosing operation’s operands according to that operation’s semantics. This relationship is checked by the owning operation, not by a generic promise that all regions work alike.

10.4 Manipulating Blocks In A Pass

At the C++ level, a block owns intrusive lists of operations and block-argument objects. Rewriters provide safe APIs to insert operations before/after an operation, split a block, merge blocks, or replace branches. These actions are not cosmetic. Splitting a block changes dominance and may require a branch; merging blocks may require forwarding arguments; erasing a block requires removing incoming branches and ensuring no live uses remain.

Use the control-flow utilities or PatternRewriter/IRRewriter APIs rather than manually splicing lists unless your pass explicitly owns CFG maintenance. After structural edits, run verification and, where relevant, invalidate or recompute dominance, liveness, and alias analyses. Analyses that cache the old CFG are no longer trustworthy.

10.5 A Worked CFG Example

This function selects the larger input using block arguments:

func.func @maximum(%a: i32, %b: i32) -> i32 {
  %is_larger = arith.cmpi sgt, %a, %b : i32
  cf.cond_br %is_larger, ^take_a, ^take_b
^take_a:
  cf.br ^done(%a : i32)
^take_b:
  cf.br ^done(%b : i32)
^done(%winner: i32):
  return %winner : i32
}

There is no mutable local winner and no explicit phi instruction. The predecessor branches agree that ^done needs one i32; the block argument provides the merged definition. Converting this into scf.if may preserve the same dataflow while recovering structured control flow. Lowering scf.if often performs the inverse transformation, creating blocks and branches.