Skip to main content

Opcode catalog

The scalar operations the wasm.<opcode>(...) escape hatch accepts: 211 of them, in catalog reedc-core-gc-eh-simd-threads-1. Anything absent from this page or the two tables noted below is a feature error, not a fallback to raw WAT.

SIMD and atomics are not listed here. Their 323 opcodes are catalogued separately (simd::SIMD_OPS and atomics::ATOMIC_OPS in reedc-core, each checked against vendor/wasm-ops/data/ by its own invariant script) because they are shape-driven rather than one-arm-per-opcode: every fixed-width and relaxed SIMD instruction, and every atomic memory operation, is available under its standard name. See lane-wise operations and shared memory and atomics.

10 further rows below (memory.fill, memory.copy, table.fill, table.copy, memory.init, table.init, data.drop, elem.drop, array.new_data, array.new_elem) are listed for reference even though the escape hatch itself does not accept them -- they have no wasm.<opcode>(...) spelling at all, only dedicated surface syntax (.fill(...), .copy<Other>(...), .init<Other>(...), .drop(), or new Name[n] data<D>(...)/elem<E>(...)), noted in their own Immediates column.

Where this comes from

The opcode list is extracted from KNOWN_OPCODES in reedc-core -- the compiler decides what actually parses. Stack signatures, descriptions, and byte encodings come from vendor/wasm-ops/data/, last checked against the specification in July 2026. Immediate syntax is the Reed source form, which is not what the vendor data records (it describes binary immediates). Regenerate with mise run docs:opcodes.

Reading the tables

Stack is the WebAssembly stack effect: operands consumed on the left, results produced on the right. It is the instruction's contract, and it is why the escape hatch can type-check your call rather than pasting text.

In Reed the operands are ordinary expressions and the immediates go in <...>, so [i32 i32] → [i32] with no immediates reads as wasm.i32.add(a, b). t is a type the immediate supplies; field is a struct field or array element type.

wasm.i32.add(left, right)                      // [i32 i32] → [i32]
wasm.struct.get<Circle, radius>(circle)        // [(ref null $t)] → [field]
wasm.i32.store<Heap, offset=0, align=4>(a, v)  // [i32 i32] → []

A Const mark means the operation is valid in a global initializer. Only 19 are: array.new, array.new_default, array.new_fixed, f32.const, f64.const, global.get, i32.add, i32.const, i32.mul, i32.sub, i64.add, i64.const, i64.mul, i64.sub, ref.func, ref.i31, ref.null, struct.new, struct.new_default (global.get restricted to imported immutable globals).

A Method mark (ᵐ) means the opcode also has dedicated .method(args) postfix syntax (section 7 of the specification) in addition to the wasm.<opcode>(...) escape hatch spelling shown here -- for example x.clz() and wasm.i32.clz(x) lower to the identical instruction. 20 method names carry this mark across 40 opcodes (each name applies to two receiver types): clz/ctz/popcnt/rotl/rotr/div_u/rem_u/lt_u/le_u/gt_u/ge_u on i32/i64, and abs/sqrt/ceil/floor/trunc/nearest/min/max/copysign on f32/f64. See Raw WebAssembly operations for the full method catalog and a compiling example. The same mark also covers two array bulk-op methods on a non-null declared-array receiver instead of a numeric one -- dest.copy(dest_offset, src, src_offset, count) for array.copy and arr.fill(offset, value, count) for array.fill -- which are arity-zero statement forms, not value-producing expressions, so they're dispatched separately from the numeric names above rather than being one more entry in that catalog. See Raw WebAssembly operations for a compiling example.

A New mark (ⁿ) means the opcode is also reachable through dedicated construction syntax (section 6/6.1 of the specification): new Name {} for a struct Name with one or more fields lowers to struct.new_default, new Name[count]{} for an array Name lowers to array.new_default, and new Name[count]{value} lowers to array.new. The two default-initialized forms require every field/element type to be defaultable -- numeric, or a nullable ?T reference -- else the construction is a compile error naming the offending field or element type; a non-null &T field or element has no default value. The fill form has no such requirement, since it supplies a value, and is the only construction that fills a non-null-reference array without listing every element. See Raw WebAssembly operations for a compiling example.

An As mark (ᵃ) means the opcode is also reachable through the as cast-expression (section 7 of the specification): 26 opcodes carry it -- the numeric conversions (wrap_i64, promote_f32, demote_f64, extend_i32_s/_u, convert_i32_s/_u, convert_i64_s/_u, trunc_f32_s/_u, trunc_f64_s/_u, plain as when the pair is unambiguous, as <type>.s/as <type>.u when it isn't) and the host reference bridge (any.convert_extern, extern.convert_any, via as ?any/as &any/as ?extern/as &extern), and i31 boxing/unboxing (ref.i31 via as &i31; i31.get_s/i31.get_u via as i32.s/as i32.u). The reinterpret_* rows deliberately carry no As mark; see the note above the Conversions table for why. See Raw WebAssembly operations for a compiling example.

A Growth mark (ᵍ) means the opcode also has dedicated surface syntax outside the escape hatch (section 10 of the specification): memory.grow, memory.size, table.grow. #Mem/Mem.grow(delta) reach memory.size/memory.grow, and Table.grow(init, delta) reaches table.grow -- lowering to the identical instruction as the wasm.* spelling shown here. Four of the further rows below (memory.fill, memory.copy, table.fill, table.copy) carry no mark at all because they have no wasm.<opcode>(...) spelling to mark as an alternative -- dedicated syntax (.fill(...), .copy<Other>(...)) is the only way to reach them, which their Immediates column says directly. See Raw WebAssembly operations for compiling examples of all seven. The remaining six further rows (memory.init, table.init, data.drop, elem.drop, array.new_data, array.new_elem) carry no mark for the identical reason -- dedicated syntax (.init<Other>(...), .drop(), new Name[n] data<D>(...)/elem<E>(...)) is the only way to reach them. See Raw WebAssembly operations and Raw WebAssembly operations for compiling examples of those six.

Constants

No operands. Immediates do not accept a leading -; write the unsigned bit pattern instead (wasm.i32.const<4294967295> for -1).

OpcodeImmediatesStackDescriptionByte
i32.const<n>[] → [i32]Push a 32-bit integer value to the stack0x41
i64.const<n>[] → [i64]Push a 64-bit integer value to the stack0x42
f32.const<x.y>[] → [f32]Push a 32-bit float value to the stack0x43
f64.const<x.y>[] → [f64]Push a 64-bit float value to the stack0x44

i32 arithmetic, bitwise, and comparison

OpcodeImmediatesStackDescriptionByte
i32.addnone[i32 i32] → [i32]sign-agnostic addition0x6A
i32.subnone[i32 i32] → [i32]sign-agnostic subtraction0x6B
i32.mulnone[i32 i32] → [i32]sign-agnostic multiplication, modulo 2³²0x6C
i32.div_snone[i32 i32] → [i32]signed division (result is truncated toward zero)0x6D
i32.div_unone[i32 i32] → [i32]unsigned division (result is floored)0x6E
i32.rem_snone[i32 i32] → [i32]signed remainder (result has the sign of the dividend)0x6F
i32.rem_unone[i32 i32] → [i32]unsigned remainder0x70
i32.andnone[i32 i32] → [i32]sign-agnostic bitwise and. Return the bitwise conjunction of 𝑖1 and 𝑖20x71
i32.ornone[i32 i32] → [i32]sign-agnostic bitwise inclusive or. Return the bitwise disjunction of 𝑖1 and 𝑖20x72
i32.xornone[i32 i32] → [i32]sign-agnostic bitwise exclusive or. Return the bitwise exclusive disjunction of 𝑖1 and 𝑖20x73
i32.shlnone[i32 i32] → [i32]sign-agnostic shift left. Return the result of shifting i1 left by k bits, modulo 2³²0x74
i32.shr_snone[i32 i32] → [i32]sign-replicating (arithmetic) shift right. Return the result of shifting i1 right by k bits, extended with the most significant bit of the original value0x75
i32.shr_unone[i32 i32] → [i32]zero-replicating (logical) shift right. Return the result of shifting i1 right by k bits, extended with 0 bits0x76
i32.rotlnone[i32 i32] → [i32]sign-agnostic rotate left. Return the result of rotating i1 left by k bits0x77
i32.rotrnone[i32 i32] → [i32]sign-agnostic rotate right. Return the result of rotating i1 right by k bits0x78
i32.eqnone[i32 i32] → [i32]== sign-agnostic compare equal0x46
i32.nenone[i32 i32] → [i32] sign-agnostic compare unequal0x47
i32.lt_snone[i32 i32] → [i32]< signed less than0x48
i32.lt_unone[i32 i32] → [i32]< unsigned less than0x49
i32.gt_snone[i32 i32] → [i32]> signed greater than0x4A
i32.gt_unone[i32 i32] → [i32]> unsigned greater than0x4B
i32.le_snone[i32 i32] → [i32] signed less than or equal0x4C
i32.le_unone[i32 i32] → [i32] unsigned less than or equal0x4D
i32.ge_snone[i32 i32] → [i32] signed greater than or equal0x4E
i32.ge_unone[i32 i32] → [i32] unsigned greater than or equal0x4F
i32.clznone[i32] → [i32]sign-agnostic count leading zero bits. Return the count of leading zero bits in i. All zero bits are considered leading if the value is zero0x67
i32.ctznone[i32] → [i32]sign-agnostic count trailing zero bits. Return the count of trailing zero bits in i. All zero bits are considered trailing if the value is zero0x68
i32.popcntnone[i32] → [i32]sign-agnostic count number of one bits. Return the count of non-zero bits in i0x69
i32.eqznone[i32] → [i32]compare equal to zero. Return 1 if operand is zero, 0 otherwise0x45
i32.trunc_sat_f32_snone[f32] → [i32]saturating form of i32.trunc_f32_s0xFC.0
i32.trunc_sat_f32_unone[f32] → [i32]saturating form of i32.trunc_f32_u0xFC.1
i32.trunc_sat_f64_snone[f64] → [i32]saturating form of i32.trunc_f64_s0xFC.2
i32.trunc_sat_f64_unone[f64] → [i32]saturating form of i32.trunc_f64_u0xFC.3

i64 arithmetic, bitwise, and comparison

OpcodeImmediatesStackDescriptionByte
i64.addnone[i64 i64] → [i64]sign-agnostic addition0x7C
i64.subnone[i64 i64] → [i64]sign-agnostic subtraction0x7D
i64.mulnone[i64 i64] → [i64]sign-agnostic multiplication, modulo 2⁶⁴0x7E
i64.div_snone[i64 i64] → [i64]signed division (result is truncated toward zero)0x7F
i64.div_unone[i64 i64] → [i64]unsigned division (result is floored)0x80
i64.rem_snone[i64 i64] → [i64]signed remainder (result has the sign of the dividend)0x81
i64.rem_unone[i64 i64] → [i64]unsigned remainder0x82
i64.andnone[i64 i64] → [i64]sign-agnostic bitwise and. Return the bitwise conjunction of 𝑖1 and 𝑖20x83
i64.ornone[i64 i64] → [i64]sign-agnostic bitwise inclusive or. Return the bitwise disjunction of 𝑖1 and 𝑖20x84
i64.xornone[i64 i64] → [i64]sign-agnostic bitwise exclusive or. Return the bitwise exclusive disjunction of 𝑖1 and 𝑖20x85
i64.shlnone[i64 i64] → [i64]sign-agnostic shift left. Return the result of shifting i1 left by k bits, modulo 2⁶⁴0x86
i64.shr_snone[i64 i64] → [i64]sign-replicating (arithmetic) shift right. Return the result of shifting i1 right by k bits, extended with the most significant bit of the original value0x87
i64.shr_unone[i64 i64] → [i64]zero-replicating (logical) shift right. Return the result of shifting i1 right by k bits, extended with 0 bits0x88
i64.rotlnone[i64 i64] → [i64]sign-agnostic rotate left. Return the result of rotating i1 left by k bits0x89
i64.rotrnone[i64 i64] → [i64]sign-agnostic rotate right. Return the result of rotating i1 right by k bits0x8A
i64.eqnone[i64 i64] → [i32]== sign-agnostic compare equal0x51
i64.nenone[i64 i64] → [i32] sign-agnostic compare unequal0x52
i64.lt_snone[i64 i64] → [i32]< signed less than0x53
i64.lt_unone[i64 i64] → [i32]< unsigned less than0x54
i64.gt_snone[i64 i64] → [i32]> signed greater than0x55
i64.gt_unone[i64 i64] → [i32]> unsigned greater than0x56
i64.le_snone[i64 i64] → [i32] signed less than or equal0x57
i64.le_unone[i64 i64] → [i32] unsigned less than or equal0x58
i64.ge_snone[i64 i64] → [i32] signed greater than or equal0x59
i64.ge_unone[i64 i64] → [i32] unsigned greater than or equal0x5A
i64.clznone[i64] → [i64]sign-agnostic count leading zero bits. Return the count of leading zero bits in i. All zero bits are considered leading if the value is zero0x79
i64.ctznone[i64] → [i64]sign-agnostic count trailing zero bits. Return the count of trailing zero bits in i. All zero bits are considered trailing if the value is zero0x7A
i64.popcntnone[i64] → [i64]sign-agnostic count number of one bits. Return the count of non-zero bits in i0x7B
i64.eqznone[i64] → [i32]compare equal to zero. Return 1 if operand is zero, 0 otherwise0x50
i64.trunc_sat_f32_snone[f32] → [i64]saturating form of i64.trunc_f32_s0xFC.4
i64.trunc_sat_f32_unone[f32] → [i64]saturating form of i64.trunc_f32_u0xFC.5
i64.trunc_sat_f64_snone[f64] → [i64]saturating form of i64.trunc_f64_s0xFC.6
i64.trunc_sat_f64_unone[f64] → [i64]saturating form of i64.trunc_f64_u0xFC.7
i64.mul_wide_snone[i64 i64] → [i64 i64]--0xFC.21
i64.mul_wide_unone[i64 i64] → [i64 i64]--0xFC.22
i64.add128none[i64 i64 i64 i64] → [i64 i64]--0xFC.19
i64.sub128none[i64 i64 i64 i64] → [i64 i64]--0xFC.20

f32 arithmetic and comparison

OpcodeImmediatesStackDescriptionByte
f32.addnone[f32 f32] → [f32]addition0x92
f32.subnone[f32 f32] → [f32]subtraction0x93
f32.mulnone[f32 f32] → [f32]multiplication0x94
f32.divnone[f32 f32] → [f32]division. partial function: division by 0 is undefined0x95
f32.minnone[f32 f32] → [f32]minimum (binary operator); if either operand is NaN, returns NaN0x96
f32.maxnone[f32 f32] → [f32]maximum (binary operator); if either operand is NaN, returns NaN0x97
f32.copysignnone[f32 f32] → [f32]If z1 and z2 have the same sign, then return z1. Else return z1 with negated sign0x98
f32.eqnone[f32 f32] → [i32]== compare equal0x5B
f32.nenone[f32 f32] → [i32] compare unordered or unequal0x5C
f32.ltnone[f32 f32] → [i32]< less than0x5D
f32.gtnone[f32 f32] → [i32]> greater than0x5E
f32.lenone[f32 f32] → [i32] less than or equal0x5F
f32.genone[f32 f32] → [i32] greater than or equal0x60
f32.negnone[f32] → [f32]negation0x8C
f32.absnone[f32] → [f32]absolute value0x8B
f32.sqrtnone[f32] → [f32]square root0x91
f32.ceilnone[f32] → [f32]ceiling operator0x8D
f32.floornone[f32] → [f32]floor operator0x8E
f32.truncnone[f32] → [f32]round to nearest integer towards zero0x8F
f32.nearestnone[f32] → [f32]round to nearest integer, ties to even0x90

f64 arithmetic and comparison

OpcodeImmediatesStackDescriptionByte
f64.addnone[f64 f64] → [f64]addition0xA0
f64.subnone[f64 f64] → [f64]subtraction0xA1
f64.mulnone[f64 f64] → [f64]multiplication0xA2
f64.divnone[f64 f64] → [f64]division. partial function: division by 0 is undefined0xA3
f64.minnone[f64 f64] → [f64]minimum (binary operator); if either operand is NaN, returns NaN0xA4
f64.maxnone[f64 f64] → [f64]maximum (binary operator); if either operand is NaN, returns NaN0xA5
f64.copysignnone[f64 f64] → [f64]If z1 and z2 have the same sign, then return z1. Else return z1 with negated sign0xA6
f64.eqnone[f64 f64] → [i32]== compare equal0x61
f64.nenone[f64 f64] → [i32] compare unordered or unequal0x62
f64.ltnone[f64 f64] → [i32]< less than0x63
f64.gtnone[f64 f64] → [i32]> greater than0x64
f64.lenone[f64 f64] → [i32] less than or equal0x65
f64.genone[f64 f64] → [i32] greater than or equal0x66
f64.negnone[f64] → [f64]negation0x9A
f64.absnone[f64] → [f64]absolute value0x99
f64.sqrtnone[f64] → [f64]square root0x9F
f64.ceilnone[f64] → [f64]ceiling operator0x9B
f64.floornone[f64] → [f64]floor operator0x9C
f64.truncnone[f64] → [f64]round to nearest integer towards zero0x9D
f64.nearestnone[f64] → [f64]round to nearest integer, ties to even0x9E

Conversions

No saturating (_sat_) truncation: a float-to-int conversion traps when the value is out of range or NaN. The reinterpret_* rows carry no As mark and stay escape-hatch-only: for a same-width int/float pair, reinterpreting bits and converting a value are two different, equally valid operations, and the (source, target) type pair alone can't tell as which one you mean, unlike the unambiguous wrap/promote/demote rows and the suffix-disambiguated extend/convert/trunc rows above them.

OpcodeImmediatesStackDescriptionByte
i32.wrap_i64none[i64] → [i32]wrap a 64-bit integer to a 32-bit integer. Return i modulo 2³²0xA7
i64.extend_i32_snone[i32] → [i64]extend a signed 32-bit integer to a 64-bit integer0xAC
i64.extend_i32_unone[i32] → [i64]extend an unsigned 32-bit integer to a 64-bit integer0xAD
i32.extend8_snone[i32] → [i32]extend a signed 8-bit integer to a 32-bit integer. Sign-extension operators extension0xC0
i32.extend16_snone[i32] → [i32]extend a signed 16-bit integer to a 32-bit integer. Sign-extension operators extension0xC1
i64.extend8_snone[i64] → [i64]extend a signed 8-bit integer to a 64-bit integer. Sign-extension operators extension0xC2
i64.extend16_snone[i64] → [i64]extend a signed 16-bit integer to a 64-bit integer. Sign-extension operators extension0xC3
i64.extend32_snone[i64] → [i64]extend a signed 32-bit integer to a 64-bit integer. Sign-extension operators extension0xC4
f32.convert_i32_snone[i32] → [f32]convert a signed 32-bit integer to a 32-bit float0xB2
f32.convert_i32_unone[i32] → [f32]convert an unsigned 32-bit integer to a 32-bit float0xB3
f32.convert_i64_snone[i64] → [f32]convert a signed 64-bit integer to a 32-bit float0xB4
f32.convert_i64_unone[i64] → [f32]convert an unsigned 64-bit integer to a 32-bit float0xB5
f64.convert_i32_snone[i32] → [f64]convert a signed 32-bit integer to a 64-bit float0xB7
f64.convert_i32_unone[i32] → [f64]convert an unsigned 32-bit integer to a 64-bit float0xB8
f64.convert_i64_snone[i64] → [f64]convert a signed 64-bit integer to a 64-bit float0xB9
f64.convert_i64_unone[i64] → [f64]convert an unsigned 64-bit integer to a 64-bit float0xBA
i32.trunc_f32_snone[f32] → [i32]truncate a 32-bit float to a signed 32-bit integer0xA8
i32.trunc_f32_unone[f32] → [i32]truncate a 32-bit float to an unsigned 32-bit integer0xA9
i32.trunc_f64_snone[f64] → [i32]truncate a 64-bit float to a signed 32-bit integer0xAA
i32.trunc_f64_unone[f64] → [i32]truncate a 64-bit float to an unsigned 32-bit integer0xAB
i64.trunc_f32_snone[f32] → [i64]truncate a 32-bit float to a signed 64-bit integer0xAE
i64.trunc_f32_unone[f32] → [i64]truncate a 32-bit float to an unsigned 64-bit integer0xAF
i64.trunc_f64_snone[f64] → [i64]truncate a 64-bit float to a signed 64-bit integer0xB0
i64.trunc_f64_unone[f64] → [i64]truncate a 64-bit float to an unsigned 64-bit integer0xB1
f32.demote_f64none[f64] → [f32]demote a 64-bit float to a 32-bit float0xB6
f64.promote_f32none[f32] → [f64]promote a 32-bit float to a 64-bit float0xBB
i32.reinterpret_f32none[f32] → [i32]reinterpret the bits of a 32-bit float as a 32-bit integer0xBC
f32.reinterpret_i32none[i32] → [f32]reinterpret the bits of a 32-bit integer as a 32-bit float0xBE
i64.reinterpret_f64none[f64] → [i64]reinterpret the bits of a 64-bit float as a 64-bit integer0xBD
f64.reinterpret_i64none[i64] → [f64]reinterpret the bits of a 64-bit integer as a 64-bit float0xBF

Linear memory

Every access names its memory -- there is no default memory. align is a byte count, must be a power of two, and must not exceed the natural alignment of the access. Addresses are always i32. memory.size/memory.grow (marked Growth) also have dedicated surface syntax, #Mem/Mem.grow(delta) (spec section 10); memory.fill/memory.copy/memory.init/data.drop have only dedicated syntax -- Mem.fill(offset, value, len), Dest.copy<Src>(dest_offset, src_offset, len), Mem.init<Data>(dest_offset, src_offset, len), and SomeData.drop() -- see the Immediates column for those four, and Raw WebAssembly operations for a compiling example of the latter two.

OpcodeImmediatesStackDescriptionByte
i32.load<Memory[, offset=N][, align=N]>[i32] → [i32]load 4 bytes as i320x28
i64.load<Memory[, offset=N][, align=N]>[i32] → [i64]load 8 bytes as i640x29
f32.load<Memory[, offset=N][, align=N]>[i32] → [f32]load 4 bytes as f320x2A
f64.load<Memory[, offset=N][, align=N]>[i32] → [f64]load 8 bytes as f640x2B
i32.load8_s<Memory[, offset=N][, align=N]>[i32] → [i32]load 1 byte and sign-extend i8 to i320x2C
i32.load8_u<Memory[, offset=N][, align=N]>[i32] → [i32]load 1 byte and zero-extend i8 to i320x2D
i32.load16_s<Memory[, offset=N][, align=N]>[i32] → [i32]load 2 bytes and sign-extend i16 to i320x2E
i32.load16_u<Memory[, offset=N][, align=N]>[i32] → [i32]load 2 bytes and zero-extend i16 to i320x2F
i64.load8_s<Memory[, offset=N][, align=N]>[i32] → [i64]load 1 byte and sign-extend i8 to i640x30
i64.load8_u<Memory[, offset=N][, align=N]>[i32] → [i64]load 1 byte and zero-extend i8 to i640x31
i64.load16_s<Memory[, offset=N][, align=N]>[i32] → [i64]load 2 bytes and sign-extend i16 to i640x32
i64.load16_u<Memory[, offset=N][, align=N]>[i32] → [i64]load 2 bytes and zero-extend i16 to i640x33
i64.load32_s<Memory[, offset=N][, align=N]>[i32] → [i64]load 4 bytes and sign-extend i32 to i640x34
i64.load32_u<Memory[, offset=N][, align=N]>[i32] → [i64]load 4 bytes and zero-extend i32 to i640x35
i32.store<Memory[, offset=N][, align=N]>[i32 i32] → []store 4 bytes (no conversion)0x36
i64.store<Memory[, offset=N][, align=N]>[i32 i64] → []store 8 bytes (no conversion)0x37
f32.store<Memory[, offset=N][, align=N]>[i32 f32] → []store 4 bytes (no conversion)0x38
f64.store<Memory[, offset=N][, align=N]>[i32 f64] → []store 8 bytes (no conversion)0x39
i32.store8<Memory[, offset=N][, align=N]>[i32 i32] → []wrap i32 to i8 and store 1 byte0x3A
i32.store16<Memory[, offset=N][, align=N]>[i32 i32] → []wrap i32 to i16 and store 2 bytes0x3B
i64.store8<Memory[, offset=N][, align=N]>[i32 i64] → []wrap i64 to i8 and store 1 byte0x3C
i64.store16<Memory[, offset=N][, align=N]>[i32 i64] → []wrap i64 to i16 and store 2 bytes0x3D
i64.store32<Memory[, offset=N][, align=N]>[i32 i64] → []wrap i64 to i32 and store 4 bytes0x3E
memory.size<Memory>[] → [i32]The memory.size instruction returns the current size of a memory. Operates in units of page size. Each page is 65,536 bytes (64KB)0x3F
memory.grow<Memory>[i32] → [i32]The memory.grow instruction grows memory by a given delta and returns the previous size, or −1 if enough memory cannot be allocated. Operates in units of page size. Each page is 65,536 bytes (64KB)0x40
memory.fillnone -- no wasm.* form; use Mem.fill(...)[i32 i32 i32] → []fill a region of linear memory with a given byte value. Bulk Memory Operations0xFC.11
memory.copynone -- no wasm.* form; use Dest.copy<Src>(...)[i32 i32 i32] → []copy from one region of linear memory to another region. Bulk Memory Operations0xFC.10
memory.initnone -- no wasm.* form; use Mem.init<Data>(...)[i32 i32 i32] → []copy from a passive data segment to linear memory. Bulk Memory Operations0xFC.8
data.dropnone -- no wasm.* form; use SomeData.drop()[] → []prevent further use of passive data segment. Bulk Memory Operations0xFC.9

Locals and globals

local.set/local.tee require a var binding; global.set requires a mut global. A local var also has dedicated surface syntax for the local.tee shape: (name = value), parenthesized, used as an expression (spec section 7) -- restricted to a mutable local var because WebAssembly has no global.tee/struct.tee/array.tee, only local.tee.

OpcodeImmediatesStackDescriptionByte
local.get<local>[] → [t]This instruction gets the value of a variable0x20
local.set<local>[t] → []This instruction sets the value of a variable0x21
local.tee<local>[t] → [t]The local.tee instruction is like local.set but also returns its argument0x22
global.get<Global>[] → [t]This instruction gets the value of a variable0x23
global.set<Global>[t] → []This instruction sets the value of a variable0x24

References and casts

ref.test/ref.cast only ever downcast: the immediate's heap type must be a subtype of the operand's. A bare heap keyword is not a type immediate -- write <&struct>, never <struct>. ref.test/ref.is_null also have dedicated surface syntax, value is &Heap/value is ?Heap/value is null (spec section 9), usable as a plain i32-valued expression anywhere, not only inside an if-condition -- flow narrowing, however, remains exclusive to the if-condition position.

OpcodeImmediatesStackDescriptionByte
ref.null<?T>[] → [(ref null t)]evaluates to the null reference constant0xD0
ref.is_nullnone[(ref null t)] → [i32]checks for null0xD1
ref.eqnone[eqref eqref] → [i32][eqref eqref] -> [i32]0xD3
ref.func<func>[] → [(ref f)]creates a reference to a given function0xD2
ref.test<&T> / <?T>[(ref null t)] → [i32]checks whether a reference has a given heap type0xFB.20
ref.cast<&T> / <?T>[(ref null t₁)] → [(ref t₂)]tries to convert to a given heap type0xFB.22

i31

Note the asymmetry: ref.i31 produces a non-null &i31, but both accessors take a nullable ?i31. All three also have dedicated as-cast syntax (marked As): as &i31 for ref.i31, as i32.s/as i32.u for the two accessors -- unboxing a statically nullable ?i31 this way also triggers the nullable-i31-unbox lint.

OpcodeImmediatesStackDescriptionByte
ref.i31 ᶜᵃnone[i32] → [(ref i31)]creates an i31ref from a 32 bit value, truncating high bit0xFB.28
i31.get_snone[i31ref] → [i32]extracts the value, sign-extending0xFB.29
i31.get_unone[i31ref] → [i32]extracts the value, zero-extending0xFB.30

GC structs

The immediate names the field; the emitted WAT uses its flattened parent-first index. A packed (i8/i16) field requires _s/_u, and a non-packed field rejects them. struct.new_default also has dedicated new Name {} default-init syntax (marked New, spec section 6) once Name has one or more fields, reinterpreting the already-legal empty-braces case rather than adding new grammar.

OpcodeImmediatesStackDescriptionByte
struct.new<Struct>[field*] → [(ref $t)]allocates a structure with canonical RTT (runtime type) and initialises its fields with given values0xFB.0
struct.get<Struct, field>[(ref null $t)] → [field]reads field i from a structure0xFB.2
struct.get_s<Struct, field>[(ref null $t)] → [i32]--0xFB.3
struct.get_u<Struct, field>[(ref null $t)] → [i32]--0xFB.4
struct.set<Struct, field>[(ref null $t) field] → []writes field i of a structure0xFB.5
struct.new_default ᶜⁿ<Struct>[] → [(ref $t)]allocates a structure of type $t with canonical RTT (runtime type) and initialises its fields with default values0xFB.1

GC arrays

array.copy takes the destination type first and requires both arrays to agree on packedness. array.new_default also has dedicated new Name[count]{} default-init syntax, and array.new has new Name[count]{value} fill syntax (both marked New, spec section 6.1). array.copy/array.fill also have dedicated .copy(...)/.fill(...) method-postfix syntax (marked Method, spec section 7) on a non-null declared-array receiver. array.new_data/array.new_elem have only dedicated syntax, new Name[count] data<D>(offset)/elem<E>(offset) (spec section 6.1) -- see the Immediates column, and Raw WebAssembly operations for a compiling example.

OpcodeImmediatesStackDescriptionByte
array.new_fixed<Array>[field^n] → [(ref $t)]allocates an array with canonical RTT (runtime type) of fixed size and initialises it from operands0xFB.8
array.get<Array>[(ref null $t) i32] → [field]reads an element from an array0xFB.11
array.get_s<Array>[(ref null $t) i32] → [i32]--0xFB.12
array.get_u<Array>[(ref null $t) i32] → [i32]--0xFB.13
array.set<Array>[(ref null $t) i32 field] → []writes an element to an array0xFB.14
array.lennone[arrayref] → [i32]inquires the length of an array0xFB.15
array.new ᶜⁿ<Array>[field i32] → [(ref $t)]allocates an array with canonical RTT (runtime type)0xFB.6
array.new_default ᶜⁿ<Array>[i32] → [(ref $t)]allocates an array with canonical RTT (runtime type) and initialises its fields with the default value0xFB.7
array.fill<Array>[(ref null $t) i32 field i32] → []--0xFB.16
array.copy<Dest, Src>[(ref null $t₁) i32 (ref null $t₂) i32 i32] → []--0xFB.17
array.new_datanone -- no wasm.* form; use new Name[n] data<D>(offset)[i32 i32] → [(ref $t)]allocates an array with canonical RTT (runtime type) and initialises it from a data segment0xFB.9
array.new_elemnone -- no wasm.* form; use new Name[n] elem<E>(offset)[i32 i32] → [(ref $t)]allocates an array with canonical RTT (runtime type) and initialises it from an element segment0xFB.10

Tables

table.grow (marked Growth) takes the fill value first, then the delta, and also has dedicated surface syntax, Table.grow(init, delta) (spec section 10). table.fill/table.copy/table.init/elem.drop have only dedicated syntax -- Table.fill(offset, value, len), Dest.copy<Src>(dest_offset, src_offset, len), Table.init<Elem>(dest_offset, src_offset, len), and Handlers.drop() -- see the Immediates column for those four, and Raw WebAssembly operations for a compiling example of the latter two.

OpcodeImmediatesStackDescriptionByte
table.get<Table>[i32] → [t]Access tables0x25
table.set<Table>[i32 t] → []Access tables0x26
table.size<Table>[] → [i32]manipulate the size of a table0xFC.16
table.grow<Table>[t i32] → [i32]manipulate the size of a table0xFC.15
table.fillnone -- no wasm.* form; use Table.fill(...)[i32 t i32] → []fills a range in a table with a value0xFC.17
table.copynone -- no wasm.* form; use Dest.copy<Src>(...)[i32 i32 i32] → []copy from one region of a table to another region. Bulk Memory Operations0xFC.14
table.initnone -- no wasm.* form; use Table.init<Elem>(...)[i32 i32 i32] → []copy from a passive element segment to a table. Bulk Memory Operations0xFC.12
elem.dropnone -- no wasm.* form; use Handlers.drop()[] → []prevent further use of a passive element segment. Bulk Memory Operations0xFC.13

Host reference bridge

Both rows also have dedicated as cast-expression syntax (marked As, spec section 7): x as ?any/x as &any for any.convert_extern, x as ?extern/x as &extern for extern.convert_any. The result is always nullable regardless of which nullability marker (?/&) the source wrote on the target.

OpcodeImmediatesStackDescriptionByte
any.convert_externnone[externref] → [anyref]converts an external value into the internal representation0xFB.26
extern.convert_anynone[anyref] → [externref]converts an internal value into the external representation0xFB.27

Calls

call_indirect takes the table index as its last argument, after the call arguments.

OpcodeImmediatesStackDescriptionByte
call<func>[t∗_1] → [t∗_2]The call instruction invokes another function, consuming the necessary arguments from the stack and returning the result values of the call0x10
call_indirect<Table, FuncType>[t? i32] → [t?]The call_indirect instruction calls a function indirectly through an operand indexing into a table0x11

Stack and control

unreachable is polymorphic and marks the path diverging; it also has dedicated surface syntax, the unreachable keyword (spec section 7), which is the preferred spelling -- unreachable; as a statement, or a bare unreachable in any value position. select emits an explicit result annotation only when the result is a reference. select also has dedicated surface syntax, the ternary condition ? then : else (spec section 7) -- both branches are still evaluated unconditionally, matching select's own non-short-circuiting behavior, unlike if/else or &&/||.

OpcodeImmediatesStackDescriptionByte
dropnone[t] → []The drop instruction simply throws away a single operand. Arguably the only stack manipulation instruction WebAssembly has. There is no dup, swap or rot, and no way to reach past the top of the stack: select consumes three operands and computes with them, and local.tee does the work dup would by going out to a local and back. See WebAssembly is not quite a stack machine, by purplesyringa0x1A
selectnone (rejects any)[t t i32] → [t]The select instruction selects one of its first two operands based on whether its third operand is zero or not0x1B
nopnone[] → []The nop instruction does nothing0x01
unreachablenone[t∗_1] → [t∗_2]The unreachable instruction causes an unconditional trap. A trap immediately aborts execution. Traps cannot be handled by WebAssembly code, but are reported to the outside environment, where they typically can be caught0x00

Not in the catalog

These are absent from the wasm.<opcode>(...) escape hatch on purpose. Reaching for one that way is a feature error, even where a dedicated non-escape-hatch spelling exists (wasm.memory.copy(...) below, unlike Dest.copy<Src>(...)):

feature error at L:C: 'memory.copy' is not available in the selected
target's instruction catalog (reedc-core-gc-eh-simd-threads-1)
ExcludedWhy
br, br_if, br_table, block, loop, if, return, br_on_null, br_on_castThe escape hatch's grammar is an expression producing a value; a branch is a control-flow edge and has no such shape. The language's own structured control flow covers these, and always will.
return_call, return_call_ref, raw call_refTail calls are a compiler output mode (--tail-calls) applied to your own return, never source syntax.
i32.trunc_sat_*, i64.trunc_sat_*The fc proposal is out of scope, so out-of-range float-to-int conversion traps rather than saturating.
throwDiverges and produces no value, so it doesn't fit the escape hatch's expression-producing grammar -- same reason as the branch instructions above. Use source-level throw Name(...) (spec section 10) instead.
SIMD, threads/atomics, stringref, memory64Outside the target feature set entirely -- see limitations.

Two behaviours that will surprise you

Numeric opcodes ignore their immediates. wasm.i32.add<Nonsense>(a, b) type-checks and silently discards the immediate. It is unchecked, not rejected. select is the only opcode that errors on a stray immediate.

A typo reads as a missing feature. The unknown-opcode arm is the same one that reports out-of-scope instructions, so wasm.i32.addd(...) claims the catalog doesn't have it rather than telling you it isn't a real opcode.

See also