Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Instructions

Wax instructions are expression-oriented.

Literals

A character literal is an i32 constant (its Unicode code point); a string literal builds an i8 or i16 array with array.new_fixed. An i8 array holds the raw UTF-8 bytes; an i16 array holds the UTF-16 code units (so the string must be valid Unicode).

WasmWax
i32.const <code point>'c'
array.new_fixed $t (constant elements)"..." or t # "..."

In WAT both forms are written with annotations ((@char …) and (@string …)), so they round-trip faithfully through WAT. A Wasm binary keeps only the underlying i32.const / array.new_fixed: a character decompiles to a plain integer, and an array.new_fixed is recovered as a string only when its elements, decoded by the array’s element type (UTF-8 for i8, UTF-16 for i16), form a reasonable string (valid text, no control characters other than tab/newline/carriage return); otherwise it stays an array literal.

Numeric Instructions

Binary and unary operations use standard mathematical operators. Signedness is often explicit in the operator.

WasmWax
i32.add / i64.add+
i32.sub / i64.sub-
i32.mul / i64.mul*
i32.div_s / i64.div_s/s
i32.div_u / i64.div_u/u
i32.rem_s / i64.rem_s%s
i32.rem_u / i64.rem_u%u
i32.and / i64.and&
i32.or / i64.or|
i32.xor / i64.xor^
i32.shl / i64.shl<<
i32.shr_s / i64.shr_s>>s
i32.shr_u / i64.shr_u>>u
i32.eqz / i64.eqz!x (logical not)

Floating Point Operations

WasmWax
f32.add / f64.add+
f32.sub / f64.sub-
f32.mul / f64.mul*
f32.div / f64.div/
f32.absf64.sqrtval.abs(), val.ceil(), val.floor(), val.trunc(), val.nearest(), val.sqrt()
f32.minf64.copysignv1.min(v2), v1.max(v2), v1.copysign(v2)

Advanced Integer Operations

WasmWax
i32.clzi64.popcntval.clz(), val.ctz(), val.popcnt()
i32.extend8_si64.extend16_sval.extend8_s(), val.extend16_s()
i32.rotli64.rotrv1.rotl(v2), v1.rotr(v2)

Wide Arithmetic

These 128-bit operations take and return their operands as (low, high) pairs of i64 values, so each is written as a call returning two results (typically bound with a multi-value let).

WasmWax
i64.add128i64::add128(a_lo, a_hi, b_lo, b_hi)
i64.sub128i64::sub128(a_lo, a_hi, b_lo, b_hi)
i64.mul_wide_si64::mul_wide_s(a, b)
i64.mul_wide_ui64::mul_wide_u(a, b)

Conversions

WasmWax
i32.wrap_i64val as i32
i64.extend_i32_sval as i64_s
i64.extend_i32_uval as i64_u
i32.trunc_f32_sval as i32_s
f32.convert_i32_sval as f32_s
f32.demote_f64val as f32
f64.promote_f32val as f64
i32.reinterpret_f32val.to_bits()
f32.reinterpret_i32val.from_bits()

Comparison

WasmWax
eq==
ne!=
lt_s / lt<s / <
lt_u<u
le_s / le<=s / <=
le_u<=u
gt_s / gt>s / >
gt_u>u
ge_s / ge>=s / >=
ge_u>=u

On reference operands (any subtype of &eq), == and != are reference equality: a == b is ref.eq, and a != b is ref.eq followed by i32.eqz (there is no ref.ne instruction).

Variable Instructions

WasmWax
local.get $xx
local.set $xx = val
local.tee $xx := val
global.get $gg
global.set $gg = val
(local $x t)let x : t

When an expression leaves several values on the stack (typically a call to a multi-result function) and a run of local.set instructions immediately stores them into locals, the two collapse into a single multi-binding let. A drop of one of the results becomes a _ binding:

(call $divmod ...)      let (q, r) = divmod(...);
(local.set $r)
(local.set $q)

The number of stores folded is exactly the producer’s result arity, so a local.set that consumes a value already on the stack below the call is left as its own assignment.

A compound assignment x op= val is shorthand for x = x op val; it lowers to a local.get/global.get of x, the operand, the binary operator, and a local.set/global.set back into x. The reverse direction recognises exactly this shape (a set of x whose value is a binary operator with x as its left operand) and reconstructs the compound form (x = x + 1 becomes x += 1). A comparison, or x in the right operand (x = 1 - x), is left as an ordinary assignment.

Control Instructions

WasmWax
blockdo { ... } or { ... }
looploop { ... }
loop + leading back-br idiomwhile cond { ... }
loop { if c { block 'l {...}; step; br } }'l: while c : (step) { ... } (continue-expression)
loop + trailing back-br_if idiomloop { ... br_if 'l cond; } (kept as a plain loop)
if ... else ...if cond { ... } else { ... }
br $lbr 'l
br_if $lbr_if 'l cond
br_table $l* $ldbr_table ['l* else 'ld] val
br_on_null $lbr_on_null 'l val
br_on_non_null $lbr_on_non_null 'l val
br_on_cast $l $t1 $t2br_on_cast 'l t2 val
br_on_cast_fail $l $t1 $t2br_on_cast_fail 'l t2 val
br_on_cast_desc_eq $l $t1 $t2br_on_cast 'l [?]descriptor(d) val
br_on_cast_desc_eq_fail $l $t1 $t2br_on_cast_fail 'l [?]descriptor(d) val
returnreturn val
call $ff(args)
call_ref $t(val as &?t)(args)
return_call $fbecome f(args)
return_call_ref $tbecome (val as &?t)(args)
unreachableunreachable
nopnop
selectcond ? v1 : v2
drop_ = val

A drop becomes an assignment to _, an anonymous binding that evaluates its value and discards it (see Discarding a value). When the dropped value is a bare numeric expression whose width the Wax syntax would not otherwise carry, the conversion pins it with a type annotation, _: t = val, so re-reading the Wax does not re-default the value to i32/f64 and silently change its width. The annotation is emitted only where it is load-bearing; a value already anchored by a typed sub-expression (a local, a call) needs none.

The rows above are the raw instructions. Two common shapes built from them are recovered as higher-level constructs on the way back to Wax: a br_table in the conventional dense void-switch shape becomes a dispatch, and a br_on_cast/br_on_null ladder becomes a match (both below).

Block Labels and Types

Blocks, loops, and ifs can be labeled and typed. Labels are identifiers starting with ' followed by a colon.

'my_block: do { ... }
'my_loop: loop { ... }

Block types (signatures) can be specified using (param) -> result syntax or a single value type. If no type is specified, the do keyword is optional.

do (i32) -> i32 { ... }
do i32 { ... }
{ ... }                  ;; equivalent to do { ... }
if cond => (i32) -> i32 { ... } else { ... }

A single result type can be inferred, so converting from WebAssembly drops it wherever it is redundant: a do i32 { ... } becomes do { ... }, an if cond => i32 { ... } loses its => i32, and likewise for loop, try, and try_table. The type is recovered when reading the Wax back, from the values reaching the block’s exit or from the surrounding context (see Type inference). Parameterized and multi-result block types are always kept.

Branch Hints

The branch-hinting proposal annotates a conditional branch as likely or unlikely taken. Wax writes the hint as an attribute on the branch and preserves it on every round-trip (it is stored in the metadata.code.branch_hint custom section; no feature flag is needed):

#[likely] if cond { ... } else { ... }
#[unlikely] br_if 'l cond

Any conditional branch may be hinted: if, br_if, br_on_null, br_on_non_null, br_on_cast, and br_on_cast_fail. In WAT the same hint is the (@metadata.code.branch_hint "\00"|"\01") annotation preceding the branch ("\01" = likely, "\00" = unlikely).

Dispatch and Match

Two Wax control constructs have no instruction of their own: they are readable spellings of a lower-level shape, so they lower to that shape and are recovered from it on decompilation (both round-trip through the binary). See the language guide for the surface syntax: dispatch and match.

dispatch is a jump table. It lowers to one nested block per case with a br_table in the innermost block and the case bodies (which fall through) after their blocks; a br_table matching that dense void-switch shape decompiles back to dispatch:

(block $big (block $b (block $a           dispatch x ['zero 'one else 'big] {
  (br_table $a $b $big (local.get $x)))     'big:  { … }
  … 'one body …) … 'big body …)             'one:  { … }
                                            'zero: { … } }

match is a multi-way type test. It lowers to a ladder of blocks threading the scrutinee through br_on_cast (and br_on_null for a null arm), each branch delivering the narrowed value out to its arm; such a ladder decompiles back to match:

(block $default (block $arm_1                match v {
  (block $arm (result (ref $point))            p: &point => { … }
    (br_on_null $arm_1                          null      => { … }
      (br_on_cast $arm eqref (ref $point) …)))  _         => { … } }
  … arm body …) … default …)

Reference Instructions

WasmWax
ref.nullnull
ref.func $ff
ref.is_null!val
ref.as_non_nullval!
ref.i31val as &i31
i31.get_sval as i32_s
i31.get_uval as i32_u
ref.castval as &type
ref.testval is &type
ref.cast (ref (exact $t))val as &!t
ref.test (ref (exact $t))val is &!t
ref.cast_desc_eq $tval as descriptor(d)
ref.cast_desc_eq (ref null …) $tval as ?descriptor(d)

A bare function name used as a value, f rather than the call f(args), is ref.func $f: it produces a reference to the function. This works in a global initializer and anywhere a &func reference is expected (for example the callee of an indirect call).

These casts can also chain through a forced intermediate, written as a single as:

  • &ref as i32_s/as i32_u on a reference that is not already &i31 (e.g. an &any or &eq) inserts a ref.cast (ref i31) before the i31.get, so it covers both i31.get_s and ref.cast (ref i31) + i31.get_s.
  • &ref as i64_s/as i64_u widens that further: the i31.get (with the ref.cast above as needed) is followed by i64.extend_i32_s/_u.
  • i64 as &i31 wraps to i32 (i32.wrap_i64) before ref.i31.
  • extern_val as &T for an any-hierarchy T (e.g. a struct type) inserts any.convert_extern before the ref.cast (ref T); likewise an any-hierarchy value as &extern uses extern.convert_any.
  • i32_val as &extern boxes the i32 with ref.i31 before extern.convert_any.

Aggregate Instructions

A struct or array literal takes an optional type prefix ({t| … } for a struct, [t| … ] for an array), but it is omitted whenever the literal’s type can be inferred from context (an assignment, a return, a call argument), which is the common case shown below. Write the prefix only when the type is ambiguous. The {descriptor(d)| … } forms are different: descriptor(d) supplies the runtime descriptor and is part of the construction, not a prefix that can be dropped.

WasmWax
struct.new $t{ field: val, ... }
struct.new_default $t{ .. }
struct.new_desc $t{descriptor(d)| field: val, ... }
struct.new_default_desc $t{descriptor(d)| .. }
struct.get $t $fval.field
struct.set $t $fval.field = new_val
ref.get_desc $tval.descriptor
array.new $t[ val; len ]
array.new_default $t[ ..; len ]
array.new_fixed $t[ val, ... ]
array.new_data $t $d[ d @ offset; count ]
array.new_elem $t $e[ e @ offset; count ]
array.init_data $t $darr.init(d, dest, src, count)
array.init_elem $t $earr.init(e, dest, src, count)
array.fill $tarr.fill(idx, val, count)
array.copy $t1 $t2arr.copy(idx, src_arr, src_idx, count)
array.get $tarr[idx]
array.get_s $tarr[idx] as i32_s
array.get_u $tarr[idx] as i32_u
array.set $tarr[idx] = val
array.lenarr.length()

A packed (i8/i16) struct field or array element read sign- or zero-extends to i32 via the as i32_s/as i32_u cast, as shown above (struct.get_s/_u, array.get_s/_u). Widening straight to i64 (val.field as i64_s or arr[idx] as i64_u) emits the packed read followed by i64.extend_i32_s/_u.

SIMD (Vector) Instructions

v128 vector operations are written as method intrinsics with the lane shape baked into the name. The Wax name is the WebAssembly mnemonic with the leading shape moved to the end: i32x4.add becomes add_i32x4, f32x4.sqrt becomes sqrt_f32x4, and the whole-vector v128.and becomes and_v128. Signed/unsigned variants keep the _s/_u (min_s_i8x16, extract_lane_u_i8x16). Constant lane immediates (lane indices, shuffle indices) come first in the argument list, before any remaining stack operands.

Lanewise unary and binary operations (the receiver is the first operand):

WasmWax
i32x4.adda.add_i32x4(b)
f32x4.mula.mul_f32x4(b)
i8x16.min_s / i8x16.min_ua.min_s_i8x16(b) / a.min_u_i8x16(b)
i16x8.lt_ua.lt_u_i16x8(b)
i32x4.negv.neg_i32x4()
f64x2.sqrtv.sqrt_f64x2()
i8x16.narrow_i16x8_sa.narrow_i16x8_s_i8x16(b)
f32x4.convert_i32x4_sv.convert_i32x4_s_f32x4()

Whole-vector bitwise operations, bit selection (a free function):

WasmWax
v128.and / or / xor / andnota.and_v128(b), a.or_v128(b), a.xor_v128(b), a.andnot_v128(b)
v128.notv.not_v128()
v128.bitselectv128::bitselect(a, b, mask)

Splat, lane access, and shuffle:

WasmWax
i32x4.splatx.splat_i32x4()
i32x4.extract_lanev.extract_lane_i32x4(lane)
i8x16.extract_lane_uv.extract_lane_u_i8x16(lane)
i32x4.replace_lanev.replace_lane_i32x4(lane, x)
i8x16.shufflea.shuffle_i8x16(l0, ..., l15, b)

Shifts, tests, and bitmask:

WasmWax
i16x8.shlv.shl_i16x8(n)
i32x4.shr_s / i32x4.shr_uv.shr_s_i32x4(n) / v.shr_u_i32x4(n)
v128.any_truev.any_true_v128()
i32x4.all_truev.all_true_i32x4()
i8x16.bitmaskv.bitmask_i8x16()

Constants are v128:: free functions (one argument per lane: 16, 8, 4, or 2). v128.const is a constant expression, so it may initialise a global:

WasmWax
v128.const i32x4 1 2 3 4v128::const_i32x4(1, 2, 3, 4)
v128.const f32x4 1.5 2.5 3.5 4.5v128::const_f32x4(1.5, 2.5, 3.5, 4.5)
v128.const i8x16 0 1 ... 15v128::const_i8x16(0, 1, ..., 15)

Memory loads and stores are methods on a memory, like the scalar memory accesses (the optional trailing align/offset arguments work the same way):

WasmWax
v128.loadm.v128_load(p)
v128.storem.v128_store(p, v)
v128.load8x8_s (and 16x4/32x2, _s/_u)m.v128_load8x8_s(p)
v128.load32_zero (and 64_zero)m.v128_load32_zero(p)
v128.load8_splat (and 16/32/64)m.v128_load8_splat(p)
v128.load8_lane (and 16/32/64)m.v128_load8_lane(p, v, lane)
v128.store8_lane (and 16/32/64)m.v128_store8_lane(p, v, lane)

Relaxed-SIMD operations follow the same scheme:

WasmWax
f32x4.relaxed_madda.relaxed_madd_f32x4(b, c)
i8x16.relaxed_swizzlea.relaxed_swizzle_i8x16(b)

No intrinsic can clash with a module entity name: the free-function intrinsics are written as v128::/i64:: qualified paths and every other is a method on a receiver, so a function may freely be named e.g. v128_bitselect without any renaming.

Memory Access

Loads and stores are method calls on a memory. The method name carries the access width; the value’s signedness (for narrow loads) and its i32/i64 type are expressed with the surrounding as iN_s/as iN_u cast, mirroring packed array access.

WasmWax
i32.loadm.load32(p)
i64.loadm.load64(p)
f32.load / f64.loadm.loadf32(p) / m.loadf64(p)
i32.load8_sm.load8(p) as i32_s
i32.load16_um.load16(p) as i32_u
i64.load32_sm.load32(p) as i64_s
i64.load8_sm.load8(p) as i64_s
i64.load16_um.load16(p) as i64_u
i32.store (v: i32) or i64.store32 (v: i64)m.store32(p, v)
i32.store16 or i64.store16 (by v’s type)m.store16(p, v)
i64.store / f64.storem.store64(p, v) / m.storef64(p, v)

A bare narrow load (m.load8(p) with no cast) defaults to unsigned i32, like array.get_u. The store value’s i32/i64 type is inferred from the operand.

Two optional trailing arguments give the align and offset of the access (both constant integers); offset requires align to fill its positional slot:

m.load32(p);          // i32.load
m.load32(p, 1);       // i32.load align=1
m.load32(p, 1, 16);   // i32.load align=1 offset=16

The alignment defaults to the access’s natural alignment and is only printed when it differs; the offset defaults to 0.

The remaining memory operations are also methods on the memory (a data segment is named directly, not as a value):

WasmWax
memory.sizem.size()
memory.growm.grow(n)
memory.fillm.fill(dest, val, len)
memory.copy (within m)m.copy(dest, src, len)
memory.copy m m2 (copy from m2 into m)m.copy(m2, dest, src, len)
memory.init segm.init(seg, dest, src, len)
data.drop segseg.drop()

Atomics

Atomic accesses (the threads proposal) are methods on a memory, named after the WebAssembly mnemonic with . rewritten as _ (the value type stays in the name). They require exactly their natural alignment. atomic.fence has no memory operand and is the atomic::fence() intrinsic.

WasmWax
i32.atomic.loadm.i32_atomic_load(p)
i64.atomic.store8m.i64_atomic_store8(p, v)
i32.atomic.rmw.addm.i32_atomic_rmw_add(p, v)
i64.atomic.rmw16.add_um.i64_atomic_rmw16_add_u(p, v)
i32.atomic.rmw.cmpxchgm.i32_atomic_rmw_cmpxchg(p, expected, replacement)
memory.atomic.notifym.atomic_notify(p, count)
memory.atomic.wait32m.atomic_wait32(p, expected, timeout)
atomic.fenceatomic::fence()

Table Access

A table is indexed like an array, and an indirect call is written as a call through a table slot cast to the callee’s function type.

WasmWax
table.get $tt[i]
table.set $tt[i] = v
call_indirect $t (type $ft)(t[i] as &?ft)(args)
call_indirect $t (result i32)(t[i] as &?fn() -> i32)(args)
return_call_indirect $t (type $ft)return (t[i] as &?ft)(args)

call_indirect is reconstructed from this pattern on conversion to WAT/Wasm, so it round-trips. The cast target names the callee’s function type, a defined type (ft) or an inline one written fn(params) -> results (used when the WAT type is anonymous), and matches the table’s element type: for a funcref table (the usual case) it is the nullable &?ft, as shown. When the element type is already a non-null &ft, the cast may be omitted (t[i](args)).

The other table operations are methods on the table (an element segment is named directly):

WasmWax
table.sizet.size()
table.growt.grow(val, n)
table.fillt.fill(dest, val, len)
table.copy (within t)t.copy(dest, src, len)
table.copy t t2 (copy from t2 into t)t.copy(t2, dest, src, len)
table.init segt.init(seg, dest, src, len)
elem.drop segseg.drop()

A GC array can also be filled from a segment with arr.init(seg, dest, src, len) (array.init_data/array.init_elem, selected by the array’s element type), the segment-from-array counterpart of array.new_data/array.new_elem.

Exception Instructions

WasmWax
throw $tagthrow tag (no payload), throw tag x (one), throw tag (x, y) (several)
throw_refthrow_ref
try_table ... catch $tag $l ...try { ... } catch [ tag -> 'l, ... ]
try_table ... catch_ref $tag $l ...try { ... } catch [ tag & -> 'l, ... ]
try_table ... catch_all $l ...try { ... } catch [ _ -> 'l, ... ]
try_table ... catch_all_ref $l ...try { ... } catch [ _ & -> 'l, ... ]
try ... catch $tag ...try { ... } catch { tag => { ... } ... }
try ... catch_all ...try { ... } catch { _ => { ... } }

The try { ... } catch [ ... ] syntax compiles to WebAssembly’s try_table instruction (the current standard). The try { ... } catch { tag => { ... } } syntax compiles to the older try/catch instructions (deprecated but still supported for compatibility).

Stack Switching Instructions

These correspond to the WebAssembly stack-switching proposal. A continuation type is declared with type k = cont ft; (see Types); <k> and <tag> below are the names of a continuation type and a tag respectively.

WasmWax
cont.new $kcont_new k (func)
cont.bind $k1 $k2cont_bind k1 k2 (args, cont)
suspend $tagsuspend tag (args)
resume $kresume k [] (args, cont)
resume $k (on $tag $l) ...resume k [ tag -> 'l, ... ] (args, cont)
resume $k (on $tag switch) ...resume k [ tag -> switch, ... ] (args, cont)
resume_throw $k $tag ...resume_throw k tag [ ... ] (args, cont)
resume_throw_ref $k ...resume_throw_ref k [ ... ] (exn, cont)
switch $k $tagswitch k tag (args, cont)

Operands are written in WebAssembly stack order, so the continuation reference is the last argument. The handler list in [ ... ] mirrors the try ... catch [ ... ] syntax: tag -> 'l is an (on $tag $l) clause and tag -> switch is an (on $tag switch) clause.

Tags used with suspend/resume may have result types (unlike exception tags); see Tags. When a function reference is passed to cont_new for a continuation whose function type belongs to a recursion group, declare the function with an explicit type (fn f: ft (...) { ... }) so its type matches the continuation’s.