Control Flow
WebAssembly has structured control flow.
Unlike native CPU instructions which are unrestricted in their jumps, control flow in WebAssembly is always expressed in function calls and blocks inside a function.
nop
and unreachable
The unreachable
instruction aborts, or traps, the execution of a WASM module. When unreachable
instruction is executed, the host of the WASM environments gets something similar to an exception in OOP-style languages or panic!()
in Rust:
unreachable ;; bye WASM world, the host must handle this.
Handling this situation is up to the host environment and is outside of the WASM spec. The host environment can report the failure to the user; if this is a breakpoint, it can patch back the real instruction and resume execution; etc.
Helpfully, unreachable
is encoded as 0x00
in binary, so any zero-initialized memory is full of unreachable
when (mis)executed.
Another occasionally useful instruction is nop
. It does not do anything and can be used as padding to a nicer address or a placeholder for other commands written in its place later.
nop ;; just blank space, nothing to see here, go ahead
nop
;; actually useful instructions
blocks and branching
Jumps (i.e. non-sequential execution of instructions) inside a function are called branching in WASM and always happen to (maybe nested) blocks. Branching instruction names usually start with br
.
Unconditional branching is done with the br <blockidx>
instruction.
The simplest kind of block is introduced by block
and end
WASM instructions (indentation is not significant here):
block
br 0 ;; jump to the end of the innermost, "0th" block
unreachable ;; any instructions until `end` are unreachable
end
;; execution continues from here
A more high-level, folded representation uses parenthesis and omits end
:
(block
br 0
unreachable)
;; the jump destination is at the end of the `block`
nested blocks
When nested, blocks are indexed by their distance from the current instruction: the innermost current block has index 0, its outer block has index 1, the next outer block is indexed 2, and so on and so forth.
(block ;; 1st
(block ;; 0th
(br 1) ;; skip one level of blocks, jump to the end
;; unreachable
)
;; also unreachable
(br 0) ;; would jump out one level up
)
;; jumps here
Blocks can be given aliases that can be used by branching instructions. For example:
(block $outer ;; blockidx 1
(block $inner ;; blockidx 0
(br $outer)
unreachable)
unreachable)
;; to here
block results
If a block leaves something on the stack, this must be declared as a (result <type>)
just after block
:
;; stack before: [...]
(block (result i32)
i32.const 42
br 0
unreachable)
;; stack after: [... 42]
TODO: more than one result
TODO: stack validation
conditional branching
The only thing that the previous example can do is to introduce dead code which cannot be executed. Let's do something useful, e.g. TODO
TODO: if
/else
/end
, (if ... (then ...) (else ...))
, select
TODO: br_if
loops
Another kind of blocks is loop
. Unlike block
, jump destination is at the start of the loop
:
;; this is an infinite loop
(loop
(br 0) ;; continue
unreachable)
TODO: a simple counter loop example
TODO: while loop example
TODO: break and continue example