Skip to content

Control

MIR blocks, values, edges, terminators, continuations, and suspension.

Updated View as Markdown

A MIR function is a directed graph of basic blocks. BlockId and LocalId are one-based nonzero handles into dense vectors. Zero is not a valid identity, and raw numbers have no meaning outside their function.

Blocks

A block contains:

  • a source range;
  • ordered block parameters;
  • straight-line operations;
  • exactly one terminator;
  • sorted live-in and live-out sets after liveness.

Block parameters are phi-like definitions. Every incoming edge supplies an argument for each target parameter in the same position and with the same type.

block 4(value: local 9):
    local 10 = add value, local 2
    jump block 6(local 10)

This is illustrative notation, not a serialized syntax. The invariant is that the edge argument list and target parameter list agree exactly.

The entry block has no incoming edge contract. Its values come from function parameters and entry setup.

Operations and terminators

Non-suspending work is an operation. Control transfer and suspension are terminators:

Terminator Meaning
Jump Transfer to one block with ordered arguments
Branch Select one of two blocks and supply its arguments
Return Complete with a success value
TryPropagate Return an existing error channel
ErrorReturn Complete through the explicit error channel
SuspendingCall End the block, yield if needed, and resume at a named block
TailCall Re-enter a statically known compatible callee without returning here
Diverge Intentional non-returning path
Unreachable Construction or recovery placeholder
WasmReturn Post-canonicalization primitive return to the trampoline

Unreachable and Diverge can encode the same WebAssembly trap instruction, but they do not mean the same thing. Divergence is a proven property of the program path. An unfinished reachable placeholder is an invalid finished MIR function.

Suspension edge

A suspending call names:

  • a static or dynamic callee;
  • ordered arguments;
  • the logical result type and result local;
  • a resume block;
  • explicit extra resume arguments.

The call’s result is an implicit definition at the resume site. It is separate from resume_args because the runtime result channel and ordinary phi-like edge values have different storage and delivery rules.

Conceptually:

before:
    result = suspending_call target(args)
    use result

MIR:
    block A:
        suspend target(args), result -> block B(extra_args)

    block B(extra_params):
        # result is defined by resumption
        use result

Values live across that edge must survive in frame storage. The split pass and frame layout make this obligation concrete; the backend cannot decide to recompute a value after resume.

Continuations

After splitting, each continuation owns an ordered chain of blocks, a kind, reach classification, and source range. Optional flags identify continuations safe for tail-only elision or splicing into a predecessor.

A continuation chain ends by:

  • suspending and handing control to another continuation;
  • completing or returning;
  • intentionally diverging.

Confined continuations are reachable only inside their parent function and can be clustered behind a state tag. A continuation becomes reified when a callback, future, runtime registration, stored handle, or other external observer needs its identity. Reified is conservative: a missed clustering opportunity is slower; incorrectly confining an escaping continuation is unsound.

Loops

Loops are CFG cycles, with loop-header and scope-exit facts maintained beside the graph. A LoopStep continuation can yield cooperatively through the ready queue rather than monopolizing a runtime thread.

Optimizations may rotate, unroll, simplify, or merge blocks only when they preserve:

  • target argument/parameter agreement;
  • source-visible evaluation and effect order;
  • suspension boundaries and live-across state;
  • loop and scope-exit facts required by later passes;
  • provenance side-table keys.

Tail calls

Current tail-call formation is deliberately narrow. A TailCall is static and self-recursive, with arguments matching the callee’s parameter-slot order. Supporting mutual tail recursion would require coordinated compatible frame layouts; it must not be inferred merely because two calls are textually in tail position.

Recursive non-tail calls remain legal. Recursion analysis determines when a child frame must be flex allocated rather than reserved as an infinitely nested fixed frame.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close