Phase 1, week 3retrospective

Regions, blocks, and block arguments

Regions own blocks, and block arguments replace phi nodes, so control flow is expressed by ops.


Regions, blocks, operations

Containment has three levels. An operation may own regions; a region is an ordered list of blocks; a block is an ordered list of operations plus a list of arguments. func.func has one region holding one block, and that block’s arguments are the function parameters: the %x in func.func @f(%x: i32) is a block argument printed as part of the signature.

Two kinds of emptiness differ. A region with zero blocks is a declaration body; a block with zero arguments is a parameterless entry point. func.func private @flush() -> i32 has an empty region and no entry block at all.

Block arguments replace phi

There is no phi node. A branch passes values to its successor, and the successor receives them as arguments:

func.func @abs(%x: i32) -> i32 {
  %c0 = arith.constant 0 : i32
  %neg = arith.cmpi slt, %x, %c0 : i32
  cf.cond_br %neg, ^bb1(%x : i32), ^bb2(%x : i32)
^bb1(%v: i32):
  %0 = arith.subi %c0, %v : i32
  cf.br ^bb2(%0 : i32)
^bb2(%r: i32):
  return %r : i32
}

^bb2 is the join, and the value entering it is a branch operand rather than a phi. The verifier checks arity and types on every edge, so a mismatch is a compile error instead of a dominance puzzle.

Structured control flow is also ops

scf.for and scf.if are operations with regions. A loop-carried value is a body block argument, yielded at the end:

%sum = scf.for %i = %c0 to %n step %c1 iter_args(%acc = %init) -> (f32) {
  %v = memref.load %buf[%i] : memref<?xf32>
  %acc2 = arith.addf %acc, %v : f32
  scf.yield %acc2 : f32
}

%i and %acc are that body block’s arguments. -convert-scf-to-cf rewrites the loop into blocks and branches, so both spellings describe the same CFG; confirm the pass name on your release.

Things that break

region.front() on a declaration asserts. Check region.empty() first, and do not read it as “a block with no arguments”. A branch whose operand count or types disagree with the target block’s arguments fails verification with an error naming the edge, which is the good case. The bad case is a cf.cond_br built by hand towards a block that does not exist yet: that asserts inside the builder rather than reporting a diagnostic.

Sources

A version records the documentation the claim was checked against, not a house pin. Confirm flags with --help on the release you installed.

Check yourself

A `func.func private` declaration prints with no body. What is its body region?

Where does a value that LLVM IR would put in a phi node appear in MLIR?

In `scf.for ... iter_args(%acc = %init) -> (f32) { ... scf.yield %next : f32 }`, where does the loop-carried value live?

0 / 3 correct

← back to the map