WasmIR retains a CFG long enough for local optimization and verification. It does not force every earlier MIR block to become a WebAssembly function, nor does it require the encoder to recover arbitrary unstructured control flow.
Blocks and parameters
BlockId is a one-based nonzero index into the function’s block vector. A
block contains ordered SSA parameters, straight-line instructions, one
terminator, and an optional debug label.
Function parameters and block parameters are different:
- function parameters are defined at function entry and occupy the first virtual registers;
- block parameters are defined on entry to one block and receive values from predecessor edges;
- the entry block has no block parameters.
Every jump and branch arm supplies exactly the target parameter count, in
order, with matching WasmTys.
Terminators
| Terminator | Encoding role |
|---|---|
Jump |
Continue at a target with block arguments |
Branch |
Select one of two targets from an I32 condition |
Return |
Return values matching the function result signature |
ReturnCall |
Tail-call a known WebAssembly function index |
YieldReturn |
Bare return after an asynchronous barrier |
LoopBackedge |
Branch to the active structured loop header |
Unreachable |
Emit a trap |
YieldReturn is distinct from an empty ordinary return. It proves that runtime
handoff instructions intentionally ended the current activation and that the
trampoline will re-enter through a continuation.
LoopBackedge names the loop header whose relative structured branch depth
will be known during encoding. It is not an arbitrary CFG successor for
reachability calculation.
Calls
Calls are instructions rather than CFG terminators because canonical MIR has already expanded suspension into explicit runtime operations and return shapes. A call identifies:
- zero, one, or multiple result registers;
- a resolved function or import index;
- ordered argument registers;
HostImportorIntraUnitcall kind.
Both kinds conservatively clobber memory for optimization. Async handoff uses dedicated instructions, not a magic call-index list interpreted by the optimizer.
Structuring
The CFG is transformed into encodable WebAssembly constructs after local optimization. Depending on graph shape, a transfer can become:
- fall-through inside an emitted region;
if/else;- a branch to an enclosing block or loop;
- a call or return-call to a hoisted block function.
Hoisted BlockFn functions receive the arena base and frame offset followed
by typed live-ins. Their signature therefore records the values that were CFG
edges before hoisting.
Structuring must preserve:
- edge argument/parameter correspondence;
- virtual-register definitions and dominance;
- effect and async-barrier order;
- loop backedge identity;
- instruction provenance or an explicit synthetic absence;
- function kind and signature.
It may change physical nesting and choose block functions without changing observable Atoll control flow.
Direct and continuation shapes
A direct function has natural value parameters/results and no frame prefix.
A continuation has exactly the five-I32 runtime prefix:
(arena_base, frame_off, handle, tag, value)A block function has:
(arena_base, frame_off, ...live_ins)The verifier checks these shapes. An encoder must not add missing parameters based on a continuation kind, because doing so would hide a malformed WasmIR producer.