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 resultValues 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.