///|
/// Validate a single instruction
fn validate_leaf_instr(
ctx : ValidationContext,
stack : OperandStack,
instr : @types.Instruction,
) -> Unit raise ValidationError {
if validate_continuation_instruction(ctx, stack, instr) {
return
}
if !ctx.bulk_memory {
match instr {
MemoryInit(_, _)
| DataDrop(_)
| MemoryCopy(_, _)
| MemoryFill(_)
| TableInit(_, _)
| ElemDrop(_)
| TableCopy(_, _) =>
raise TypeMismatch("bulk memory support is not enabled")
_ => ()
}
}
match instr {
// Constants
I32Const(_) => stack.push(I32)
I64Const(_) => stack.push(I64)
F32Const(_) => stack.push(F32)
F64Const(_) => stack.push(F64)
// Local operations
LocalGet(idx) => {
if idx < 0 || idx >= ctx.locals.length() {
raise InvalidLocalIndex(idx)
}
// Check if non-nullable ref local is initialized
if idx < ctx.local_init.length() && !ctx.local_init[idx] {
raise UninitializedLocal(idx)
}
stack.push(ctx.locals[idx])
}
LocalSet(idx) => {
if idx < 0 || idx >= ctx.locals.length() {
raise InvalidLocalIndex(idx)
}
stack.pop(ctx.locals[idx])
// Mark local as initialized
if idx < ctx.local_init.length() {
ctx.local_init[idx] = true
}
}
LocalTee(idx) => {
if idx < 0 || idx >= ctx.locals.length() {
raise InvalidLocalIndex(idx)
}
stack.pop(ctx.locals[idx])
stack.push(ctx.locals[idx])
// Mark local as initialized
if idx < ctx.local_init.length() {
ctx.local_init[idx] = true
}
}
// Global operations
GlobalGet(idx) => {
if idx < 0 || idx >= ctx.globals.length() {
raise InvalidGlobalIndex(idx)
}
stack.push(ctx.globals[idx].value_type)
}
GlobalSet(idx) => {
if idx < 0 || idx >= ctx.globals.length() {
raise InvalidGlobalIndex(idx)
}
// Global must be mutable to set
if !ctx.globals[idx].mutable {
raise TypeMismatch("cannot set immutable global")
}
stack.pop(ctx.globals[idx].value_type)
}
// i32 binary operations
I32Add
| I32Sub
| I32Mul
| I32DivS
| I32DivU
| I32RemS
| I32RemU
| I32And
| I32Or
| I32Xor
| I32Shl
| I32ShrS
| I32ShrU
| I32Rotl
| I32Rotr => {
stack.pop(I32)
stack.pop(I32)
stack.push(I32)
}
// i32 comparison operations
I32Eq
| I32Ne
| I32LtS
| I32LtU
| I32GtS
| I32GtU
| I32LeS
| I32LeU
| I32GeS
| I32GeU => {
stack.pop(I32)
stack.pop(I32)
stack.push(I32)
}
// i32 unary operations
I32Eqz => {
stack.pop(I32)
stack.push(I32)
}
I32Clz | I32Ctz | I32Popcnt => {
stack.pop(I32)
stack.push(I32)
}
// i32 sign-extension operations
I32Extend8S | I32Extend16S => {
stack.pop(I32)
stack.push(I32)
}
// i64 binary operations
I64Add128 | I64Sub128 => {
stack.pop(I64)
stack.pop(I64)
stack.pop(I64)
stack.pop(I64)
stack.push(I64)
stack.push(I64)
}
I64MulWideS | I64MulWideU => {
stack.pop(I64)
stack.pop(I64)
stack.push(I64)
stack.push(I64)
}
// i64 binary operations
I64Add
| I64Sub
| I64Mul
| I64DivS
| I64DivU
| I64RemS
| I64RemU
| I64And
| I64Or
| I64Xor
| I64Shl
| I64ShrS
| I64ShrU
| I64Rotl
| I64Rotr => {
stack.pop(I64)
stack.pop(I64)
stack.push(I64)
}
// i64 comparison operations
I64Eq
| I64Ne
| I64LtS
| I64LtU
| I64GtS
| I64GtU
| I64LeS
| I64LeU
| I64GeS
| I64GeU => {
stack.pop(I64)
stack.pop(I64)
stack.push(I32)
}
// i64 unary operations
I64Eqz => {
stack.pop(I64)
stack.push(I32)
}
I64Clz | I64Ctz | I64Popcnt => {
stack.pop(I64)
stack.push(I64)
}
// i64 sign-extension operations
I64Extend8S | I64Extend16S | I64Extend32S => {
stack.pop(I64)
stack.push(I64)
}
// f32 binary operations
F32Add | F32Sub | F32Mul | F32Div | F32Min | F32Max | F32Copysign => {
stack.pop(F32)
stack.pop(F32)
stack.push(F32)
}
// f32 comparison operations
F32Eq | F32Ne | F32Lt | F32Gt | F32Le | F32Ge => {
stack.pop(F32)
stack.pop(F32)
stack.push(I32)
}
// f32 unary operations
F32Abs | F32Neg | F32Ceil | F32Floor | F32Trunc | F32Nearest | F32Sqrt => {
stack.pop(F32)
stack.push(F32)
}
// f64 binary operations
F64Add | F64Sub | F64Mul | F64Div | F64Min | F64Max | F64Copysign => {
stack.pop(F64)
stack.pop(F64)
stack.push(F64)
}
// f64 comparison operations
F64Eq | F64Ne | F64Lt | F64Gt | F64Le | F64Ge => {
stack.pop(F64)
stack.pop(F64)
stack.push(I32)
}
// f64 unary operations
F64Abs | F64Neg | F64Ceil | F64Floor | F64Trunc | F64Nearest | F64Sqrt => {
stack.pop(F64)
stack.push(F64)
}
// Conversion operations
I32WrapI64 => {
stack.pop(I64)
stack.push(I32)
}
I64ExtendI32S | I64ExtendI32U => {
stack.pop(I32)
stack.push(I64)
}
I32TruncF32S | I32TruncF32U | I32TruncSatF32S | I32TruncSatF32U => {
stack.pop(F32)
stack.push(I32)
}
I32TruncF64S | I32TruncF64U | I32TruncSatF64S | I32TruncSatF64U => {
stack.pop(F64)
stack.push(I32)
}
I64TruncF32S | I64TruncF32U | I64TruncSatF32S | I64TruncSatF32U => {
stack.pop(F32)
stack.push(I64)
}
I64TruncF64S | I64TruncF64U | I64TruncSatF64S | I64TruncSatF64U => {
stack.pop(F64)
stack.push(I64)
}
F32ConvertI32S | F32ConvertI32U => {
stack.pop(I32)
stack.push(F32)
}
F32ConvertI64S | F32ConvertI64U => {
stack.pop(I64)
stack.push(F32)
}
F64ConvertI32S | F64ConvertI32U => {
stack.pop(I32)
stack.push(F64)
}
F64ConvertI64S | F64ConvertI64U => {
stack.pop(I64)
stack.push(F64)
}
F32DemoteF64 => {
stack.pop(F64)
stack.push(F32)
}
F64PromoteF32 => {
stack.pop(F32)
stack.push(F64)
}
I32ReinterpretF32 => {
stack.pop(F32)
stack.push(I32)
}
I64ReinterpretF64 => {
stack.pop(F64)
stack.push(I64)
}
F32ReinterpretI32 => {
stack.pop(I32)
stack.push(F32)
}
F64ReinterpretI64 => {
stack.pop(I64)
stack.push(F64)
}
// Memory operations - validate memory index exists
// Natural alignment: 8-bit=0, 16-bit=1, 32-bit=2, 64-bit=3
I32Load(memidx, align, offset) => {
if memidx < 0 || memidx >= ctx.mems.length() {
raise InvalidMemoryIndex(memidx)
}
validate_mem_offset(ctx.mems[memidx], offset)
if align < 0 || align > 2 {
raise InvalidAlignment(
"i32.load alignment \{align} exceeds natural alignment 2",
)
}
stack.pop(ctx.mems[memidx].addr_type()) // address
stack.push(I32)
}
I32Load8S(memidx, align, offset) | I32Load8U(memidx, align, offset) => {
if memidx < 0 || memidx >= ctx.mems.length() {
raise InvalidMemoryIndex(memidx)
}
validate_mem_offset(ctx.mems[memidx], offset)
if align < 0 || align > 0 {
raise InvalidAlignment(
"i32.load8 alignment \{align} exceeds natural alignment 0",
)
}
stack.pop(ctx.mems[memidx].addr_type()) // address
stack.push(I32)
}
I32Load16S(memidx, align, offset) | I32Load16U(memidx, align, offset) => {
if memidx < 0 || memidx >= ctx.mems.length() {
raise InvalidMemoryIndex(memidx)
}
validate_mem_offset(ctx.mems[memidx], offset)
if align < 0 || align > 1 {
raise InvalidAlignment(
"i32.load16 alignment \{align} exceeds natural alignment 1",
)
}
stack.pop(ctx.mems[memidx].addr_type()) // address
stack.push(I32)
}
I64Load(memidx, align, offset) => {
if memidx < 0 || memidx >= ctx.mems.length() {
raise InvalidMemoryIndex(memidx)
}
validate_mem_offset(ctx.mems[memidx], offset)
if align < 0 || align > 3 {
raise InvalidAlignment(
"i64.load alignment \{align} exceeds natural alignment 3",
)
}
stack.pop(ctx.mems[memidx].addr_type()) // address
stack.push(I64)
}
I64Load8S(memidx, align, offset) | I64Load8U(memidx, align, offset) => {
if memidx < 0 || memidx >= ctx.mems.length() {
raise InvalidMemoryIndex(memidx)
}
validate_mem_offset(ctx.mems[memidx], offset)
if align < 0 || align > 0 {
raise InvalidAlignment(
"i64.load8 alignment \{align} exceeds natural alignment 0",
)
}
stack.pop(ctx.mems[memidx].addr_type()) // address
stack.push(I64)
}
I64Load16S(memidx, align, offset) | I64Load16U(memidx, align, offset) => {
if memidx < 0 || memidx >= ctx.mems.length() {
raise InvalidMemoryIndex(memidx)
}
validate_mem_offset(ctx.mems[memidx], offset)
if align < 0 || align > 1 {
raise InvalidAlignment(
"i64.load16 alignment \{align} exceeds natural alignment 1",
)
}
stack.pop(ctx.mems[memidx].addr_type()) // address
stack.push(I64)
}
I64Load32S(memidx, align, offset) | I64Load32U(memidx, align, offset) => {
if memidx < 0 || memidx >= ctx.mems.length() {
raise InvalidMemoryIndex(memidx)
}
validate_mem_offset(ctx.mems[memidx], offset)
if align < 0 || align > 2 {
raise InvalidAlignment(
"i64.load32 alignment \{align} exceeds natural alignment 2",
)
}
stack.pop(ctx.mems[memidx].addr_type()) // address
stack.push(I64)
}
F32Load(memidx, align, offset) => {
if memidx < 0 || memidx >= ctx.mems.length() {
raise InvalidMemoryIndex(memidx)
}
validate_mem_offset(ctx.mems[memidx], offset)
if align < 0 || align > 2 {
raise InvalidAlignment(
"f32.load alignment \{align} exceeds natural alignment 2",
)
}
stack.pop(ctx.mems[memidx].addr_type()) // address
stack.push(F32)
}
F64Load(memidx, align, offset) => {
if memidx < 0 || memidx >= ctx.mems.length() {
raise InvalidMemoryIndex(memidx)
}
validate_mem_offset(ctx.mems[memidx], offset)
if align < 0 || align > 3 {
raise InvalidAlignment(
"f64.load alignment \{align} exceeds natural alignment 3",
)
}
stack.pop(ctx.mems[memidx].addr_type()) // address
stack.push(F64)
}
I32Store(memidx, align, offset) => {
if memidx < 0 || memidx >= ctx.mems.length() {
raise InvalidMemoryIndex(memidx)
}
validate_mem_offset(ctx.mems[memidx], offset)
if align < 0 || align > 2 {
raise InvalidAlignment(
"i32.store alignment \{align} exceeds natural alignment 2",
)
}
stack.pop(I32) // value
stack.pop(ctx.mems[memidx].addr_type()) // address
}
I32Store8(memidx, align, offset) => {
if memidx < 0 || memidx >= ctx.mems.length() {
raise InvalidMemoryIndex(memidx)
}
validate_mem_offset(ctx.mems[memidx], offset)
if align < 0 || align > 0 {
raise InvalidAlignment(
"i32.store8 alignment \{align} exceeds natural alignment 0",
)
}
stack.pop(I32) // value
stack.pop(ctx.mems[memidx].addr_type()) // address
}
I32Store16(memidx, align, offset) => {
if memidx < 0 || memidx >= ctx.mems.length() {
raise InvalidMemoryIndex(memidx)
}
validate_mem_offset(ctx.mems[memidx], offset)
if align < 0 || align > 1 {
raise InvalidAlignment(
"i32.store16 alignment \{align} exceeds natural alignment 1",
)
}
stack.pop(I32) // value
stack.pop(ctx.mems[memidx].addr_type()) // address
}
I64Store(memidx, align, offset) => {
if memidx < 0 || memidx >= ctx.mems.length() {
raise InvalidMemoryIndex(memidx)
}
validate_mem_offset(ctx.mems[memidx], offset)
if align < 0 || align > 3 {
raise InvalidAlignment(
"i64.store alignment \{align} exceeds natural alignment 3",
)
}
stack.pop(I64) // value
stack.pop(ctx.mems[memidx].addr_type()) // address
}
I64Store8(memidx, align, offset) => {
if memidx < 0 || memidx >= ctx.mems.length() {
raise InvalidMemoryIndex(memidx)
}
validate_mem_offset(ctx.mems[memidx], offset)
if align < 0 || align > 0 {
raise InvalidAlignment(
"i64.store8 alignment \{align} exceeds natural alignment 0",
)
}
stack.pop(I64) // value
stack.pop(ctx.mems[memidx].addr_type()) // address
}
I64Store16(memidx, align, offset) => {
if memidx < 0 || memidx >= ctx.mems.length() {
raise InvalidMemoryIndex(memidx)
}
validate_mem_offset(ctx.mems[memidx], offset)
if align < 0 || align > 1 {
raise InvalidAlignment(
"i64.store16 alignment \{align} exceeds natural alignment 1",
)
}
stack.pop(I64) // value
stack.pop(ctx.mems[memidx].addr_type()) // address
}
I64Store32(memidx, align, offset) => {
if memidx < 0 || memidx >= ctx.mems.length() {
raise InvalidMemoryIndex(memidx)
}
validate_mem_offset(ctx.mems[memidx], offset)
if align < 0 || align > 2 {
raise InvalidAlignment(
"i64.store32 alignment \{align} exceeds natural alignment 2",
)
}
stack.pop(I64) // value
stack.pop(ctx.mems[memidx].addr_type()) // address
}
F32Store(memidx, align, offset) => {
if memidx < 0 || memidx >= ctx.mems.length() {
raise InvalidMemoryIndex(memidx)
}
validate_mem_offset(ctx.mems[memidx], offset)
if align < 0 || align > 2 {
raise InvalidAlignment(
"f32.store alignment \{align} exceeds natural alignment 2",
)
}
stack.pop(F32) // value
stack.pop(ctx.mems[memidx].addr_type()) // address
}
F64Store(memidx, align, offset) => {
if memidx < 0 || memidx >= ctx.mems.length() {
raise InvalidMemoryIndex(memidx)
}
validate_mem_offset(ctx.mems[memidx], offset)
if align < 0 || align > 3 {
raise InvalidAlignment(
"f64.store alignment \{align} exceeds natural alignment 3",
)
}
stack.pop(F64) // value
stack.pop(ctx.mems[memidx].addr_type()) // address
}
MemorySize(memidx) | MemoryGrow(memidx) => {
if memidx < 0 || memidx >= ctx.mems.length() {
raise InvalidMemoryIndex(memidx)
}
let addr_type = ctx.mems[memidx].addr_type()
if instr is MemoryGrow(_) {
stack.pop(addr_type) // delta
}
stack.push(addr_type)
}
MemoryInit(memidx, data_idx) => {
if memidx < 0 || memidx >= ctx.mems.length() {
raise InvalidMemoryIndex(memidx)
}
if data_idx < 0 || data_idx >= ctx.data_count {
raise InvalidDataIndex(data_idx)
}
let addr_type = ctx.mems[memidx].addr_type()
stack.pop(I32) // n (always i32 for data segment offset)
stack.pop(I32) // s (always i32 for data segment offset)
stack.pop(addr_type) // d (memory address)
}
DataDrop(data_idx) =>
if data_idx < 0 || data_idx >= ctx.data_count {
raise InvalidDataIndex(data_idx)
}
MemoryCopy(dst, src) => {
if dst < 0 || dst >= ctx.mems.length() {
raise InvalidMemoryIndex(dst)
}
if src < 0 || src >= ctx.mems.length() {
raise InvalidMemoryIndex(src)
}
let dst_addr_type = ctx.mems[dst].addr_type()
let src_addr_type = ctx.mems[src].addr_type()
// The length uses the smaller of the two address types.
let n_type = if dst_addr_type == I64 && src_addr_type == I64 {
@types.ValueType::I64
} else {
I32
}
stack.pop(n_type) // n
stack.pop(src_addr_type) // s
stack.pop(dst_addr_type) // d
}
MemoryFill(memidx) => {
if memidx < 0 || memidx >= ctx.mems.length() {
raise InvalidMemoryIndex(memidx)
}
let addr_type = ctx.mems[memidx].addr_type()
stack.pop(addr_type) // n
stack.pop(I32) // val (always i32)
stack.pop(addr_type) // d
}
Atomic(subop, memidx, align, offset) => {
// Atomics use memarg except atomic.fence (subop=3).
if subop != 3 {
if memidx < 0 || memidx >= ctx.mems.length() {
raise InvalidMemoryIndex(memidx)
}
validate_mem_offset(ctx.mems[memidx], offset)
}
let addr_type = if memidx < ctx.mems.length() {
ctx.mems[memidx].addr_type()
} else {
I32
}
fn check_align(
natural : Int32,
name : String,
) -> Unit raise ValidationError {
if align != natural {
raise InvalidAlignment(
"\{name} alignment \{align} must equal natural alignment \{natural}",
)
}
}
// Reject the legacy/reserved encoding range [4, 0x10).
// In the standard encoding, atomics (except notify/wait/fence) start at 0x10.
if subop >= 4 && subop < 0x10 {
raise TypeMismatch("unsupported atomic subopcode \{subop}")
}
// Normalize subopcode encoding: in the standard encoding most ops start at 0x10.
let op = if subop >= 0x10 { subop - 12 } else { subop }
match op {
0 => {
check_align(2, "memory.atomic.notify")
stack.pop(I32) // count
stack.pop(addr_type) // addr
stack.push(I32)
}
1 => {
check_align(2, "memory.atomic.wait32")
stack.pop(I64) // timeout
stack.pop(I32) // expected
stack.pop(addr_type) // addr
stack.push(I32)
}
2 => {
check_align(3, "memory.atomic.wait64")
stack.pop(I64) // timeout
stack.pop(I64) // expected
stack.pop(addr_type) // addr
stack.push(I32)
}
3 => () // atomic.fence
4 | 6 | 7 => {
// i32.atomic.load / load8_u / load16_u
let natural = if op == 4 { 2 } else if op == 6 { 0 } else { 1 }
check_align(natural, "i32.atomic.load")
stack.pop(addr_type)
stack.push(I32)
}
5 | 8 | 9 | 10 => {
// i64.atomic.load / load8_u / load16_u / load32_u
let natural = if op == 5 {
3
} else if op == 8 {
0
} else if op == 9 {
1
} else {
2
}
check_align(natural, "i64.atomic.load")
stack.pop(addr_type)
stack.push(I64)
}
11 | 13 | 14 => {
// i32.atomic.store / store8 / store16
let natural = if op == 11 { 2 } else if op == 13 { 0 } else { 1 }
check_align(natural, "i32.atomic.store")
stack.pop(I32)
stack.pop(addr_type)
}
12 | 15 | 16 | 17 => {
// i64.atomic.store / store8 / store16 / store32
let natural = if op == 12 {
3
} else if op == 15 {
0
} else if op == 16 {
1
} else {
2
}
check_align(natural, "i64.atomic.store")
stack.pop(I64)
stack.pop(addr_type)
}
// RMW add/sub/and/or/xor/xchg
18
| 20
| 21
| 25
| 27
| 28
| 32
| 34
| 35
| 39
| 41
| 42
| 46
| 48
| 49
| 53
| 55
| 56 => {
// i32 variants (32/8/16)
let natural = if op == 18 ||
op == 25 ||
op == 32 ||
op == 39 ||
op == 46 ||
op == 53 {
2
} else if op == 20 ||
op == 27 ||
op == 34 ||
op == 41 ||
op == 48 ||
op == 55 {
0
} else {
1
}
check_align(natural, "i32.atomic.rmw")
stack.pop(I32)
stack.pop(addr_type)
stack.push(I32)
}
19
| 22
| 23
| 24
| 26
| 29
| 30
| 31
| 33
| 36
| 37
| 38
| 40
| 43
| 44
| 45
| 47
| 50
| 51
| 52
| 54
| 57
| 58
| 59 => {
// i64 variants (64/8/16/32)
let natural = if op == 19 ||
op == 26 ||
op == 33 ||
op == 40 ||
op == 47 ||
op == 54 {
3
} else if op == 22 ||
op == 29 ||
op == 36 ||
op == 43 ||
op == 50 ||
op == 57 {
0
} else if op == 23 ||
op == 30 ||
op == 37 ||
op == 44 ||
op == 51 ||
op == 58 {
1
} else {
2
}
check_align(natural, "i64.atomic.rmw")
stack.pop(I64)
stack.pop(addr_type)
stack.push(I64)
}
// cmpxchg
60 | 62 | 63 => {
// i32 cmpxchg + 8/16
let natural = if op == 60 { 2 } else if op == 62 { 0 } else { 1 }
check_align(natural, "i32.atomic.rmw.cmpxchg")
stack.pop(I32) // replacement
stack.pop(I32) // expected
stack.pop(addr_type)
stack.push(I32)
}
61 | 64 | 65 | 66 => {
// i64 cmpxchg + 8/16/32
let natural = if op == 61 {
3
} else if op == 64 {
0
} else if op == 65 {
1
} else {
2
}
check_align(natural, "i64.atomic.rmw.cmpxchg")
stack.pop(I64) // replacement
stack.pop(I64) // expected
stack.pop(addr_type)
stack.push(I64)
}
_ => raise TypeMismatch("unsupported atomic subopcode \{subop}")
}
}
// Table operations
TableGet(idx) => {
if idx < 0 || idx >= ctx.tables.length() {
raise InvalidTableIndex(idx)
}
let table = ctx.tables[idx]
let idx_type = if table.is_table64 { @types.ValueType::I64 } else { I32 }
stack.pop(idx_type) // index
stack.push(table.elem_type)
}
TableSet(idx) => {
if idx < 0 || idx >= ctx.tables.length() {
raise InvalidTableIndex(idx)
}
let table = ctx.tables[idx]
let idx_type = if table.is_table64 { @types.ValueType::I64 } else { I32 }
stack.pop(table.elem_type) // value
stack.pop(idx_type) // index
}
TableSize(idx) => {
if idx < 0 || idx >= ctx.tables.length() {
raise InvalidTableIndex(idx)
}
let table = ctx.tables[idx]
if table.is_table64 {
stack.push(I64)
} else {
stack.push(I32)
}
}
TableGrow(idx) => {
if idx < 0 || idx >= ctx.tables.length() {
raise InvalidTableIndex(idx)
}
let table = ctx.tables[idx]
let idx_type = if table.is_table64 { @types.ValueType::I64 } else { I32 }
stack.pop(idx_type) // delta
stack.pop(table.elem_type) // init value
stack.push(idx_type) // old size or -1
}
TableFill(idx) => {
if idx < 0 || idx >= ctx.tables.length() {
raise InvalidTableIndex(idx)
}
let table = ctx.tables[idx]
let idx_type = if table.is_table64 { @types.ValueType::I64 } else { I32 }
stack.pop(idx_type) // n
stack.pop(table.elem_type) // value
stack.pop(idx_type) // i
}
TableCopy(dst_idx, src_idx) => {
if dst_idx < 0 || dst_idx >= ctx.tables.length() {
raise InvalidTableIndex(dst_idx)
}
if src_idx < 0 || src_idx >= ctx.tables.length() {
raise InvalidTableIndex(src_idx)
}
// Check that source element type is subtype of destination element type
let dst_table = ctx.tables[dst_idx]
let src_table = ctx.tables[src_idx]
if !is_type_subtype(src_table.elem_type, dst_table.elem_type) {
raise TypeMismatch(
"table.copy: source element type \{src_table.elem_type} is not subtype of destination element type \{dst_table.elem_type}",
)
}
// Length uses i64 only if BOTH tables are table64 (minimum of both types)
let len_type = if dst_table.is_table64 && src_table.is_table64 {
@types.ValueType::I64
} else {
I32
}
stack.pop(len_type) // n
// Source index uses source table's index type
stack.pop(if src_table.is_table64 { I64 } else { I32 }) // s
// Dest index uses dest table's index type
stack.pop(if dst_table.is_table64 { I64 } else { I32 }) // d
}
TableInit(table_idx, elem_idx) => {
// Validate table and elem indices
if table_idx < 0 || table_idx >= ctx.tables.length() {
raise InvalidTableIndex(table_idx)
}
if elem_idx < 0 || elem_idx >= ctx.elems.length() {
raise InvalidElemIndex(elem_idx)
}
// Check elem segment type matches table element type
let table = ctx.tables[table_idx]
let elem_type = ctx.elems[elem_idx]
if !is_type_subtype(elem_type, table.elem_type) {
raise TypeMismatch(
"element segment type \{elem_type} is not subtype of table element type \{table.elem_type}",
)
}
// For table64: d uses table's index type, s and n are always i32
// (s is offset into elem segment, n is count - both use i32 since elem is i32-indexed)
let idx_type = if table.is_table64 { @types.ValueType::I64 } else { I32 }
stack.pop(I32) // n (always i32 - elem segments are i32-indexed)
stack.pop(I32) // s (always i32 - offset into elem segment)
stack.pop(idx_type) // d (table's index type)
}
ElemDrop(elem_idx) =>
if elem_idx < 0 || elem_idx >= ctx.elems.length() {
raise InvalidElemIndex(elem_idx)
}
// Reference instructions
RefNull(ref_type) => {
validate_value_type(
ref_type,
ctx.types,
stack_switching=ctx.stack_switching,
)
stack.push(ref_type)
}
RefIsNull => {
stack.pop_any() |> ignore // any ref type
stack.push(I32)
}
RefEqInstr => {
// ref.eq: compares two references for equality
// Both operands must be subtypes of eqref
let val2 = stack.pop_any()
let val1 = stack.pop_any()
// Check that both operands are subtypes of eqref
if !is_subtype_of_eqref(val1) {
raise TypeMismatch(
"ref.eq operand must be subtype of eqref, got \{val1}",
)
}
if !is_subtype_of_eqref(val2) {
raise TypeMismatch(
"ref.eq operand must be subtype of eqref, got \{val2}",
)
}
stack.push(I32)
}
RefFunc(func_idx) => {
if func_idx < 0 || func_idx >= ctx.funcs.length() {
raise InvalidFunctionIndex(func_idx)
}
// Check that function is declared (referenced in elem segment or global init)
if !ctx.declared_funcs.contains(func_idx) {
raise UndeclaredFunctionReference(func_idx)
}
// ref.func produces (ref $t) where $t is the function's type index
let type_idx = ctx.funcs[func_idx]
stack.push(RefFuncTyped(type_idx))
}
RefAsNonNull => {
// ref.as_non_null: pops nullable ref, pushes refined non-null ref
let nullable_ref = stack.pop_ref_any()
stack.push(to_non_nullable_ref_type(nullable_ref))
}
BrOnNull(label_idx) => {
// br_on_null: pop branch args + ref; on fallthrough push args back + non-null ref
if label_idx < 0 || label_idx > ctx.labels.length() {
raise InvalidLabelIndex(label_idx)
}
let branch_types = if label_idx == ctx.labels.length() {
ctx.returns
} else {
let label = ctx.labels[ctx.labels.length() - 1 - label_idx]
ReadOnlyArray::from_array(get_label_types(ctx, label))
}
let nullable_ref = stack.pop_ref_any()
for i = branch_types.length() - 1; i >= 0; i = i - 1 {
stack.pop(branch_types[i])
}
for ty in branch_types {
stack.push(ty)
}
stack.push(to_non_nullable_ref_type(nullable_ref))
}
BrOnNonNull(label_idx) => {
// br_on_non_null: pop branch prefix + ref; on fallthrough push only prefix back
if label_idx < 0 || label_idx > ctx.labels.length() {
raise InvalidLabelIndex(label_idx)
}
let branch_types = if label_idx == ctx.labels.length() {
ctx.returns
} else {
let label = ctx.labels[ctx.labels.length() - 1 - label_idx]
ReadOnlyArray::from_array(get_label_types(ctx, label))
}
if branch_types.length() == 0 {
raise TypeMismatch("br_on_non_null target must carry a reference value")
}
let branch_ref = branch_types[branch_types.length() - 1]
let expected_nullable_ref = to_nullable_ref_type(branch_ref)
let actual_ref = stack.pop_ref_any()
if !is_type_subtype(actual_ref, expected_nullable_ref) {
raise TypeMismatch(
"br_on_non_null operand type \{actual_ref} not subtype of \{expected_nullable_ref}",
)
}
for i = branch_types.length() - 2; i >= 0; i = i - 1 {
stack.pop(branch_types[i])
}
for i in 0..<(branch_types.length() - 1) {
stack.push(branch_types[i])
}
}
// Control flow
Nop => ()
Unreachable => stack.set_polymorphic()
Drop => stack.pop_any() |> ignore
Select => {
stack.pop(I32) // condition
let t2_opt = stack.pop_any_or_unknown()
let t1_opt = stack.pop_any_or_unknown()
let selected_type = match (t1_opt, t2_opt) {
(Some(t1), Some(t2)) => {
if t1 != t2 {
raise TypeMismatch(
"select operands must have same type: \{t1} vs \{t2}",
)
}
// Untyped select (without explicit result type) only works with numeric types.
// For reference types, the typed form `select t*` must be used.
if is_ref_type(t1) {
raise TypeMismatch(
"type mismatch: select with reference type requires explicit type annotation",
)
}
t1
}
// In polymorphic-unreachable contexts, missing operands are unconstrained.
(Some(t1), None) => t1
(None, Some(t2)) => t2
(None, None) => I32
}
stack.push(selected_type)
}
SelectTyped(types) => {
// Select must have exactly one result type
if types.length() != 1 {
raise TypeMismatch("invalid result arity")
}
// Validate type indices
for ty in types {
validate_value_type(ty, ctx.types, stack_switching=ctx.stack_switching)
}
stack.pop(I32) // condition
// Pop operands matching result types in reverse
for i = types.length() - 1; i >= 0; i = i - 1 {
stack.pop(types[i])
stack.pop(types[i])
}
// Push results
for ty in types {
stack.push(ty)
}
}
// Function calls
Call(func_idx) => {
if func_idx < 0 || func_idx >= ctx.funcs.length() {
raise InvalidFunctionIndex(func_idx)
}
let type_idx = ctx.funcs[func_idx]
let func_type = ctx.get_func_type(type_idx)
// Pop parameters in reverse order
let num_params = func_type.params.length()
for offset in 0.. {
if table_idx < 0 || table_idx >= ctx.tables.length() {
raise InvalidTableIndex(table_idx)
}
// call_indirect requires a funcref table
let table = ctx.tables[table_idx]
if !ctx.subtyping_ctx.value_type_subtype(table.elem_type, FuncRef) {
raise TypeMismatch(
"call_indirect requires funcref table, got \{table.elem_type}",
)
}
if type_idx < 0 || type_idx >= ctx.types.length() {
raise InvalidTypeIndex(type_idx)
}
// Table index uses i64 for table64
let idx_type = if table.is_table64 { @types.ValueType::I64 } else { I32 }
stack.pop(idx_type) // table index
let func_type = ctx.get_func_type(type_idx)
let num_ci_params = func_type.params.length()
for offset in 0.. {
if type_idx < 0 || type_idx >= ctx.types.length() {
raise InvalidTypeIndex(type_idx)
}
let func_type = ctx.get_func_type(type_idx)
// Pop the function reference (ref null $t)
// In unreachable context, this may be a polymorphic type
stack.pop(RefNullFuncTyped(type_idx))
// Pop parameters in reverse order
let num_params = func_type.params.length()
for offset in 0.. {
if func_idx < 0 || func_idx >= ctx.funcs.length() {
raise InvalidFunctionIndex(func_idx)
}
let type_idx = ctx.funcs[func_idx]
let func_type = ctx.get_func_type(type_idx)
// Pop parameters in reverse order
let num_params = func_type.params.length()
for offset in 0..= ctx.returns.length() ||
!ctx.subtyping_ctx.value_type_subtype(
func_type.results[i],
ctx.returns[i],
) {
raise TypeMismatch("return_call result type mismatch")
}
}
if func_type.results.length() != ctx.returns.length() {
raise TypeMismatch("return_call result count mismatch")
}
// After return_call, the stack is unreachable
stack.set_polymorphic()
}
ReturnCallIndirect(type_idx, table_idx) => {
if table_idx < 0 || table_idx >= ctx.tables.length() {
raise InvalidTableIndex(table_idx)
}
// return_call_indirect requires a funcref table
let table = ctx.tables[table_idx]
if !ctx.subtyping_ctx.value_type_subtype(table.elem_type, FuncRef) {
raise TypeMismatch(
"return_call_indirect requires funcref table, got \{table.elem_type}",
)
}
if type_idx < 0 || type_idx >= ctx.types.length() {
raise InvalidTypeIndex(type_idx)
}
// Table index uses i64 for table64
let idx_type = if table.is_table64 { @types.ValueType::I64 } else { I32 }
stack.pop(idx_type) // table index
let func_type = ctx.get_func_type(type_idx)
let num_params = func_type.params.length()
for offset in 0..= ctx.returns.length() ||
!ctx.subtyping_ctx.value_type_subtype(
func_type.results[i],
ctx.returns[i],
) {
raise TypeMismatch("return_call_indirect result type mismatch")
}
}
if func_type.results.length() != ctx.returns.length() {
raise TypeMismatch("return_call_indirect result count mismatch")
}
// After return_call_indirect, the stack is unreachable
stack.set_polymorphic()
}
ReturnCallRef(type_idx) => {
if type_idx < 0 || type_idx >= ctx.types.length() {
raise InvalidTypeIndex(type_idx)
}
let func_type = ctx.get_func_type(type_idx)
// Pop the function reference (ref null $t)
stack.pop(RefNullFuncTyped(type_idx))
// Pop parameters in reverse order
let num_params = func_type.params.length()
for offset in 0..= ctx.returns.length() ||
!ctx.subtyping_ctx.value_type_subtype(
func_type.results[i],
ctx.returns[i],
) {
raise TypeMismatch("return_call_ref result type mismatch")
}
}
if func_type.results.length() != ctx.returns.length() {
raise TypeMismatch("return_call_ref result count mismatch")
}
// After return_call_ref, the stack is unreachable
stack.set_polymorphic()
}
Block(_, _) | Loop(_, _) | If(_, _, _) | TryTable(_, _, _) =>
abort("structured control must be validated by the work stack")
// Branch instructions with proper label validation
Br(label_idx) => {
if label_idx < 0 || label_idx > ctx.labels.length() {
raise InvalidLabelIndex(label_idx)
}
// label_idx == labels.length() targets the implicit function label.
// This is how `br 0` at function top-level behaves (acts like return).
let branch_types = if label_idx == ctx.labels.length() {
ctx.returns
} else {
// Get label from stack (index 0 is innermost)
let label = ctx.labels[ctx.labels.length() - 1 - label_idx]
ReadOnlyArray::from_array(get_label_types(ctx, label))
}
// Pop values that will be passed to target
for i = branch_types.length() - 1; i >= 0; i = i - 1 {
stack.pop(branch_types[i])
}
stack.set_polymorphic()
}
BrIf(label_idx) => {
stack.pop(I32) // condition
if label_idx < 0 || label_idx > ctx.labels.length() {
raise InvalidLabelIndex(label_idx)
}
let branch_types = if label_idx == ctx.labels.length() {
ctx.returns
} else {
let label = ctx.labels[ctx.labels.length() - 1 - label_idx]
ReadOnlyArray::from_array(get_label_types(ctx, label))
}
// Pop and push values (conditional branch)
for i = branch_types.length() - 1; i >= 0; i = i - 1 {
stack.pop(branch_types[i])
}
for ty in branch_types {
stack.push(ty)
}
}
BrTable(labels, default_label) => {
stack.pop(I32) // index
// Validate default label
if default_label < 0 || default_label > ctx.labels.length() {
raise InvalidLabelIndex(default_label)
}
let default_types = if default_label == ctx.labels.length() {
ctx.returns
} else {
let default_info = ctx.labels[ctx.labels.length() - 1 - default_label]
ReadOnlyArray::from_array(get_label_types(ctx, default_info))
}
// Validate all labels have same arity as default
for label_idx in labels {
if label_idx < 0 || label_idx > ctx.labels.length() {
raise InvalidLabelIndex(label_idx)
}
let label_types = if label_idx == ctx.labels.length() {
ctx.returns
} else {
let label_info = ctx.labels[ctx.labels.length() - 1 - label_idx]
ReadOnlyArray::from_array(get_label_types(ctx, label_info))
}
if label_types.length() != default_types.length() {
raise TypeMismatch(
"br_table labels must have same arity: expected \{default_types.length()}, got \{label_types.length()}",
)
}
}
// Pop values
for i = default_types.length() - 1; i >= 0; i = i - 1 {
stack.pop(default_types[i])
}
stack.set_polymorphic()
}
Return => {
// Validate return values match function signature
for i = ctx.returns.length() - 1; i >= 0; i = i - 1 {
stack.pop(ctx.returns[i])
}
stack.set_polymorphic()
}
// Exception handling instructions
Throw(tag_idx) => {
// Validate tag index is valid
if tag_idx < 0 || tag_idx >= ctx.tags.length() {
raise UnknownTag(tag_idx)
}
// Get the tag type and pop its parameters from the stack
let tag_type = ctx.exception_tag_type(tag_idx)
for i = tag_type.params.length() - 1; i >= 0; i = i - 1 {
stack.pop(tag_type.params[i])
}
// throw never returns, so set polymorphic
stack.set_polymorphic()
}
ThrowRef => {
// throw_ref pops an exnref and throws it
stack.pop(ExnRef)
stack.set_polymorphic()
}
// GC instructions - struct operations
StructNew(type_idx) => {
// Pop field values, push struct ref
let struct_type = ctx.get_struct_type(type_idx)
for i = struct_type.fields.length() - 1; i >= 0; i = i - 1 {
stack.pop(
storage_type_to_value_type(struct_type.fields[i].storage_type),
)
}
stack.push(RefStruct(type_idx))
}
StructNewDefault(type_idx) => {
// Default field values are implicit, so nothing is read from the type.
// The lookup stays because it *is* the rule that `type_idx` must name a
// struct: it raises otherwise, and that is the only reason to call it.
ignore(ctx.get_struct_type(type_idx))
stack.push(RefStruct(type_idx))
}
StructGet(type_idx, field_idx)
| StructGetS(type_idx, field_idx)
| StructGetU(type_idx, field_idx) => {
// Resolve the type before popping against it, so a `type_idx` that
// names no struct is rejected rather than shaping the operand check.
let struct_type = ctx.get_struct_type(type_idx)
stack.pop(RefNullStruct(type_idx))
let field_type = struct_type.fields[field_idx].storage_type
stack.push(storage_type_to_value_type(field_type))
}
StructSet(type_idx, field_idx) => {
// Pop value, pop struct ref
let struct_type = ctx.get_struct_type(type_idx)
let field = struct_type.fields[field_idx]
// Check that the field is mutable
if !field.mutable {
raise TypeMismatch("struct.set on immutable field")
}
stack.pop(storage_type_to_value_type(field.storage_type))
stack.pop(RefNullStruct(type_idx))
}
// GC instructions - array operations
ArrayNew(type_idx) => {
// Pop length (i32), pop init value, push array ref. The element type is
// read below; looking it up first rejects a non-array `type_idx` before
// anything is popped against it.
let array_type = ctx.get_array_type(type_idx)
stack.pop(I32)
stack.pop(storage_type_to_value_type(array_type.element.storage_type))
stack.push(RefArray(type_idx))
}
ArrayNewDefault(type_idx) => {
// Pop length (i32), push array ref. Nothing is read from the type, so
// this lookup is here only as the rule that `type_idx` names an array.
ignore(ctx.get_array_type(type_idx))
stack.pop(I32)
stack.push(RefArray(type_idx))
}
ArrayNewFixed(type_idx, len) => {
if len < 0 {
raise TypeMismatch(
"array.new_fixed element count exceeds supported stack size",
)
}
// Pop len elements, push array ref
let array_type = ctx.get_array_type(type_idx)
let elem_type = storage_type_to_value_type(
array_type.element.storage_type,
)
for _ in 0.. {
// Pop size (i32), pop offset (i32), push array ref
let array_type = ctx.get_array_type(type_idx)
if !is_numeric_or_vector_storage_type(array_type.element.storage_type) {
raise TypeMismatch("array type is not numeric or vector")
}
if data_idx < 0 || data_idx >= ctx.data_count {
raise TypeMismatch(
"array.new_data: invalid data segment index \{data_idx}",
)
}
stack.pop(I32)
stack.pop(I32)
stack.push(RefArray(type_idx))
}
ArrayNewElem(type_idx, elem_idx) => {
// Pop size (i32), pop offset (i32), push array ref
let array_type = ctx.get_array_type(type_idx)
if elem_idx < 0 || elem_idx >= ctx.elems.length() {
raise InvalidElemIndex(elem_idx)
}
let elem_type = ctx.elems[elem_idx]
let array_elem_type = storage_type_to_value_type(
array_type.element.storage_type,
)
if !is_type_subtype(elem_type, array_elem_type) {
raise TypeMismatch(
"array.new_elem: element segment type \{elem_type} incompatible with array element type \{array_elem_type}",
)
}
stack.pop(I32)
stack.pop(I32)
stack.push(RefArray(type_idx))
}
ArrayGet(type_idx) | ArrayGetS(type_idx) | ArrayGetU(type_idx) => {
// Pop index (i32), pop array ref, push element
let array_type = ctx.get_array_type(type_idx)
stack.pop(I32)
stack.pop(RefNullArray(type_idx))
stack.push(storage_type_to_value_type(array_type.element.storage_type))
}
ArraySet(type_idx) => {
// Pop value, pop index (i32), pop array ref
let array_type = ctx.get_array_type(type_idx)
// Check that the array element is mutable
if !array_type.element.mutable {
raise TypeMismatch("array.set on immutable array")
}
stack.pop(storage_type_to_value_type(array_type.element.storage_type))
stack.pop(I32)
stack.pop(RefNullArray(type_idx))
}
ArrayLen => {
// Pop array ref (any array type), push i32
// RefNullArray(-1) matches any nullable array reference
stack.pop(RefNullArray(-1))
stack.push(I32)
}
ArrayFill(type_idx) => {
// Pop n (i32), pop value, pop offset (i32), pop array ref
let array_type = ctx.get_array_type(type_idx)
// Check that the array element is mutable
if !array_type.element.mutable {
raise TypeMismatch("array.fill on immutable array")
}
stack.pop(I32)
stack.pop(storage_type_to_value_type(array_type.element.storage_type))
stack.pop(I32)
stack.pop(RefNullArray(type_idx))
}
ArrayCopy(dst_type, src_type) => {
// Pop n (i32), pop src_offset (i32), pop src, pop dst_offset (i32), pop dst
let dst_array = ctx.get_array_type(dst_type)
let src_array = ctx.get_array_type(src_type)
// Check that the destination array element is mutable
if !dst_array.element.mutable {
raise TypeMismatch("array.copy to immutable array")
}
// Check that source element type is compatible with destination element type
let dst_elem = dst_array.element.storage_type
let src_elem = src_array.element.storage_type
if dst_elem != src_elem {
raise TypeMismatch("array.copy: array types do not match")
}
stack.pop(I32)
stack.pop(I32)
stack.pop(RefNullArray(src_type))
stack.pop(I32)
stack.pop(RefNullArray(dst_type))
}
ArrayInitData(type_idx, data_idx) => {
// Pop n (i32), pop src_offset (i32), pop dst_offset (i32), pop array ref
let array_type = ctx.get_array_type(type_idx)
// Check that the array element is mutable
if !array_type.element.mutable {
raise TypeMismatch("array.init_data on immutable array")
}
if !is_numeric_or_vector_storage_type(array_type.element.storage_type) {
raise TypeMismatch("array type is not numeric or vector")
}
// Check data segment exists
if data_idx < 0 || data_idx >= ctx.data_count {
raise TypeMismatch(
"array.init_data: invalid data segment index \{data_idx}",
)
}
stack.pop(I32)
stack.pop(I32)
stack.pop(I32)
stack.pop(RefNullArray(type_idx))
}
ArrayInitElem(type_idx, elem_idx) => {
// Pop n (i32), pop src_offset (i32), pop dst_offset (i32), pop array ref
let array_type = ctx.get_array_type(type_idx)
// Check that the array element is mutable
if !array_type.element.mutable {
raise TypeMismatch("array.init_elem on immutable array")
}
// Check element segment exists and types match
if elem_idx < 0 || elem_idx >= ctx.elems.length() {
raise InvalidElemIndex(elem_idx)
}
let elem_type = ctx.elems[elem_idx]
let array_elem_type = storage_type_to_value_type(
array_type.element.storage_type,
)
// Check type compatibility - elem segment type must be subtype of array element type
if !is_type_subtype(elem_type, array_elem_type) {
raise TypeMismatch(
"array.init_elem: element segment type \{elem_type} incompatible with array element type \{array_elem_type}",
)
}
stack.pop(I32)
stack.pop(I32)
stack.pop(I32)
stack.pop(RefNullArray(type_idx))
}
// GC instructions - reference casting (simplified validation)
RefTest(_) | RefTestNull(_) => {
// Pop ref, push i32
ignore(stack.pop_any())
stack.push(I32)
}
RefCast(target_type) => {
// Pop ref, push target type
ignore(stack.pop_any())
stack.push(target_type)
}
RefCastNull(target_type) => {
// Pop ref, push nullable target type
ignore(stack.pop_any())
stack.push(target_type)
}
BrOnCast(label_idx, source_type, target_type) => {
// br_on_cast: Pop source, branch if cast succeeds (push target to label), push diff on fallthrough
// Validate: target_type must be a subtype of source_type (can only downcast)
if !ctx.subtyping_ctx.value_type_subtype(target_type, source_type) {
raise TypeMismatch(
"br_on_cast: target type \{target_type} is not a subtype of source type \{source_type}",
)
}
// Check that target_type is compatible with branch label
if label_idx < 0 || label_idx > ctx.labels.length() {
raise InvalidLabelIndex(label_idx)
}
let branch_types = if label_idx == ctx.labels.length() {
ctx.returns
} else {
let label = ctx.labels[ctx.labels.length() - 1 - label_idx]
ReadOnlyArray::from_array(get_label_types(ctx, label))
}
let diff_type = compute_br_on_cast_fail_diff_type(
source_type, target_type,
)
if branch_types.length() >= 1 {
let expected = branch_types[branch_types.length() - 1]
if !ctx.subtyping_ctx.value_type_subtype(target_type, expected) {
raise TypeMismatch(
"br_on_cast: branch target type \{target_type} not subtype of label type \{expected}",
)
}
}
// Pop source and any prefix values passed to the branch label.
stack.pop(source_type)
if branch_types.length() > 1 {
for i = branch_types.length() - 2; i >= 0; i = i - 1 {
stack.pop(branch_types[i])
}
}
// Fallthrough keeps the same prefix values and refines the cast value to diff.
if branch_types.length() > 1 {
for i in 0..<(branch_types.length() - 1) {
stack.push(branch_types[i])
}
}
stack.push(diff_type)
}
BrOnCastFail(label_idx, source_type, target_type) => {
// br_on_cast_fail: Pop source, branch if cast fails (push diff to label), push target on fallthrough
// Validate: target_type must be a subtype of source_type (can only downcast)
if !ctx.subtyping_ctx.value_type_subtype(target_type, source_type) {
raise TypeMismatch(
"br_on_cast_fail: target type \{target_type} is not a subtype of source type \{source_type}",
)
}
// Check that diff_type is compatible with branch label
// Diff type: if source is nullable and target is non-nullable, diff is nullable
// The diff type represents values that fail the cast
if label_idx < 0 || label_idx > ctx.labels.length() {
raise InvalidLabelIndex(label_idx)
}
let branch_types = if label_idx == ctx.labels.length() {
ctx.returns
} else {
let label = ctx.labels[ctx.labels.length() - 1 - label_idx]
ReadOnlyArray::from_array(get_label_types(ctx, label))
}
let diff_type = compute_br_on_cast_fail_diff_type(
source_type, target_type,
)
if branch_types.length() >= 1 {
let expected = branch_types[branch_types.length() - 1]
if !ctx.subtyping_ctx.value_type_subtype(diff_type, expected) {
raise TypeMismatch(
"br_on_cast_fail: diff type \{diff_type} not subtype of label type \{expected}",
)
}
}
// Pop source and branch-prefix values.
stack.pop(source_type)
if branch_types.length() > 1 {
for i = branch_types.length() - 2; i >= 0; i = i - 1 {
stack.pop(branch_types[i])
}
}
// Fallthrough keeps prefix values and refines to target type.
if branch_types.length() > 1 {
for i in 0..<(branch_types.length() - 1) {
stack.push(branch_types[i])
}
}
stack.push(target_type)
}
// GC instructions - i31
RefI31 => {
// Pop i32, push i31ref
stack.pop(I32)
stack.push(RefI31)
}
I31GetS | I31GetU => {
// Pop i31ref, push i32
stack.pop(RefNullI31)
stack.push(I32)
}
// GC instructions - type conversion
AnyConvertExtern => {
// Pop externref, push anyref
stack.pop(ExternRef)
stack.push(AnyRef)
}
ExternConvertAny => {
// Pop anyref, push externref
stack.pop(AnyRef)
stack.push(ExternRef)
}
// SIMD instructions
_ => validate_instr_simd(ctx, stack, instr)
}
}
///|
fn is_numeric_or_vector_value_type(ty : @types.ValueType) -> Bool {
match ty {
I32 | I64 | F32 | F64 | V128 => true
_ => false
}
}
///|
fn is_numeric_or_vector_storage_type(ty : @types.StorageType) -> Bool {
match ty {
Packed(_) => true
Val(v) => is_numeric_or_vector_value_type(v)
}
}
///|
fn to_non_nullable_ref_type(
ty : @types.ValueType,
) -> @types.ValueType raise ValidationError {
match ty {
// Nullable function/extern refs
FuncRef => RefFunc
ExternRef => RefExtern
// Typed refs
ContRef => RefCont
NullContRef => RefNoCont
RefNullContTyped(idx) => RefContTyped(idx)
RefNullFuncTyped(idx) => RefFuncTyped(idx)
RefNullStruct(idx) => RefStruct(idx)
RefNullArray(idx) => RefArray(idx)
// Abstract refs
AnyRef => RefAny
RefNullEq => RefEq
RefNullI31 => RefI31
StructRef => RefStructAbs
ArrayRef => RefArrayAbs
// Exn has no dedicated non-null type in current representation.
ExnRef => ExnRef
NullRef | NullFuncRef | NullExnRef | NullExternRef => RefNone
// Already non-null
RefFunc
| RefExtern
| RefFuncTyped(_)
| RefCont
| RefNoCont
| RefContTyped(_)
| RefStruct(_)
| RefArray(_)
| RefAny
| RefEq
| RefI31
| RefStructAbs
| RefArrayAbs
| RefNone => ty
_ => raise TypeMismatch("Expected reference type, got \{ty}")
}
}
///|
fn to_nullable_ref_type(
ty : @types.ValueType,
) -> @types.ValueType raise ValidationError {
match ty {
// Non-null to nullable
RefCont => ContRef
RefNoCont => NullContRef
RefContTyped(idx) => RefNullContTyped(idx)
RefFunc => FuncRef
RefExtern => ExternRef
RefFuncTyped(idx) => RefNullFuncTyped(idx)
RefStruct(idx) => RefNullStruct(idx)
RefArray(idx) => RefNullArray(idx)
RefAny => AnyRef
RefEq => RefNullEq
RefI31 => RefNullI31
RefStructAbs => StructRef
RefArrayAbs => ArrayRef
RefNone => NullRef
// Already nullable
FuncRef
| ExternRef
| AnyRef
| ExnRef
| StructRef
| ArrayRef
| RefNullFuncTyped(_)
| ContRef
| NullContRef
| RefNullContTyped(_)
| RefNullStruct(_)
| RefNullArray(_)
| RefNullEq
| RefNullI31
| NullRef
| NullFuncRef
| NullExnRef
| NullExternRef => ty
_ => raise TypeMismatch("Expected reference type, got \{ty}")
}
}
///|
fn get_block_results(
ctx : ValidationContext,
bt : @types.BlockType,
) -> ReadOnlyArray[@types.ValueType] raise ValidationError {
match bt {
Empty => []
Value(t) => {
validate_value_type(t, ctx.types, stack_switching=ctx.stack_switching)
[t]
}
MultiValue(types) => {
for t in types {
validate_value_type(t, ctx.types, stack_switching=ctx.stack_switching)
}
types
}
InlineType(_, results) => {
for t in results {
validate_value_type(t, ctx.types, stack_switching=ctx.stack_switching)
}
results
}
TypeIndex(idx) =>
if idx < ctx.types.length() {
ctx.get_func_type(idx).results
} else {
raise InvalidTypeIndex(idx)
}
}
}
///|
fn get_block_params(
ctx : ValidationContext,
bt : @types.BlockType,
) -> ReadOnlyArray[@types.ValueType] raise ValidationError {
match bt {
Empty => []
Value(_) => [] // Single value blocks have no params
MultiValue(_) => [] // MultiValue blocks have no params (result-only)
InlineType(params, _) => {
for t in params {
validate_value_type(t, ctx.types, stack_switching=ctx.stack_switching)
}
params
}
TypeIndex(idx) =>
if idx < ctx.types.length() {
ctx.get_func_type(idx).params
} else {
raise InvalidTypeIndex(idx)
}
}
}
///|
/// Get the types that a branch to this label should carry
/// For block/if: uses results (br jumps to end)
/// For loop: uses params (br jumps to start)
fn get_label_types(
ctx : ValidationContext,
label : LabelInfo,
) -> ReadOnlyArray[@types.ValueType] raise ValidationError {
match label.kind {
BlockLabel => get_block_results(ctx, label.block_type)
LoopLabel => get_block_params(ctx, label.block_type)
}
}