A stack machine in bytecode
WebAssembly is a compact, typed stack machine. Each instruction pops a fixed set of operands off the value stack and pushes its results back, which makes the bytecode easy to validate and fast to compile. This reference groups the core MVP and WebAssembly 2.0 instructions into control, parametric, variable, memory and numeric categories, each with its opcode, stack type signature and a short description.
How it works
Every instruction is described by a signature [args] → [results]. Numeric
instructions like i32.add consume their operands and push a typed result; the
integer ops use two’s-complement wrapping while float ops follow IEEE-754. Control
instructions (block, loop, if, br, br_table, call) manage labelled scopes
and jumps — branches target blocks by depth, not by name, in the binary format.
Variable instructions move values between the stack and locals or globals, and memory
instructions read and write the single linear memory using an address on the stack
plus an alignment hint and offset immediate. Opcodes are single bytes for the MVP;
later extensions such as bulk-memory use a 0xFC prefix and a sub-opcode. Filter the
list above by name, opcode or category to find what you need.
The five instruction categories
Understanding which category an instruction belongs to clarifies what it can and cannot do:
Control instructions (block, loop, if, br, br_table, return, call, call_indirect, unreachable, nop) manage program flow. A key design choice: branches in WebAssembly jump forward out of a block or back to the top of a loop — there is no arbitrary goto. This structured control flow makes validation fast and prevents many classes of jump-target bugs.
Parametric instructions (drop, select) are type-agnostic and operate on any value type. drop discards the top of the stack; select pops a condition and picks between two typed values, like a ternary operator on the stack.
Variable instructions (local.get, local.set, local.tee, global.get, global.set) move values between the stack and named locals or module-level globals. Locals are function-scoped and always typed; there are no untyped registers.
Memory instructions (i32.load, i32.store, memory.size, memory.grow, and the bulk-memory variants memory.copy, memory.fill) access the flat linear memory. Every load and store takes a static alignment hint and a byte offset immediate plus a dynamic address from the stack. Out-of-bounds access traps; there is no undefined behaviour.
Numeric instructions form the largest group: integer arithmetic (add, sub, mul, div_s, div_u, rem, bitwise ops, shifts, rotates, clz, ctz, popcnt), float arithmetic (IEEE-754 add, sub, mul, div, sqrt, min, max, abs, neg, ceil, floor, trunc, nearest), comparisons, and conversions between types.
Practical usage tips
The reference is most useful when you are reading .wat text format or inspecting a disassembled binary. Some patterns come up repeatedly:
- Trapping vs. saturating conversions.
i32.trunc_f32_straps when the float is NaN or out of the i32 range. The saturating varianti32.trunc_sat_f32_s(opcode0xFC 0x00) clamps to the min or max i32 instead — safer when you cannot guarantee a clean float. - Integer division. Both
i32.div_s(signed) andi32.div_u(unsigned) trap on divide-by-zero. The signed form also traps on the overflow case of dividing the minimum i32 by -1. Always guard or use a known-nonzero divisor. local.teepattern.local.tee $xis identical tolocal.set $xbut leaves a copy on the stack, enabling(local.tee $x) (if ...)patterns without a secondlocal.get.memory.growreturn value. It returns the previous size in 64 KiB pages, or -1 on failure. Code that ignores the return value and assumes success will silently write into unmapped memory if the host cannot satisfy the request.
Notes and gotchas
- Signed division (
i32.div_s) and the float-to-int truncations trap on divide by zero and on out-of-range conversions; use the saturating*_satvariants to clamp instead of trapping. local.teeislocal.setthat leaves the value on the stack — handy for chaining.- Memory addresses are byte offsets into one flat array starting at zero; out-of-bounds access traps deterministically.
- The text format (
.wat) names blocks and locals for readability, but the binary format resolves those to numeric indices and branch depths.