From ae79c326133ce47662c318cf2f1940dd8c72cddf Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Sun, 17 May 2026 22:20:40 +0200 Subject: [PATCH] Implement WebAssembly post-MVP runtime ops Implements reference instructions and bulk memory/table runtime dispatch for post-MVP wasm, with regression coverage for traps and multi-value branch carrying. References isu issue 228 --- crates/wasm/src/runtime/exec.rs | 233 ++++++++++++++++--- crates/wasm/tests/engine_tests.rs | 25 +++ crates/wasm/tests/exec_tests.rs | 360 +++++++++++++++++++++++++++++- 3 files changed, 591 insertions(+), 27 deletions(-) diff --git a/crates/wasm/src/runtime/exec.rs b/crates/wasm/src/runtime/exec.rs index 3bc6dc0..ed3bafb 100644 --- a/crates/wasm/src/runtime/exec.rs +++ b/crates/wasm/src/runtime/exec.rs @@ -20,7 +20,7 @@ //! `*_trunc_sat_*` variants. use crate::instruction::Instruction; -use crate::runtime::store::{ModuleAddr, Store}; +use crate::runtime::store::{DataAddr, ElementAddr, MemoryAddr, ModuleAddr, Store, TableAddr}; use crate::runtime::trap::Trap; use crate::runtime::value::{Ref, WasmValue}; use crate::types::MemArg; @@ -144,10 +144,10 @@ impl<'a> Interpreter<'a> { /// Execute one straight-line instruction against the supplied stack and locals. /// -/// Control-flow (`block`/`loop`/`if`/`else`/`br*`/`return`), call (`call`/ -/// `call_indirect`), reference (`ref.*`), and bulk-memory/table instructions -/// are NOT handled here; passing one is a programming error and panics. The -/// engine driver layers above us must filter them out before dispatch. +/// Control-flow (`block`/`loop`/`if`/`else`/`br*`/`return`) and call (`call`/ +/// `call_indirect`) instructions are NOT handled here; passing one is a +/// programming error and panics. The engine driver layers above us must filter +/// them out before dispatch. pub(crate) fn step_straight( store: &mut Store, module: ModuleAddr, @@ -179,28 +179,107 @@ pub(crate) fn step_straight( ) } - // ---- Reference instructions (post-MVP issue) ------------- - Instruction::RefNull(_) | Instruction::RefIsNull | Instruction::RefFunc(_) => { - unreachable!( - "reference instruction {:?} is handled by the reference-types \ - follow-up issue, not step_straight", - instr - ) + // ---- Reference instructions ------------------------------ + Instruction::RefNull(rt) => { + stack.push(WasmValue::Ref(Ref::Null(*rt))); + Ok(StepOutcome::Continue) + } + Instruction::RefIsNull => { + let r = pop_ref(stack); + stack.push(WasmValue::I32(bool_to_i32(r.is_null()))); + Ok(StepOutcome::Continue) + } + Instruction::RefFunc(idx) => { + let addr = store + .module(module) + .resolve_func(*idx) + .expect("validator: function index out of range"); + stack.push(WasmValue::Ref(Ref::Func(addr))); + Ok(StepOutcome::Continue) } - // ---- Bulk table / memory (post-MVP issue) ----------------- - Instruction::TableInit { .. } - | Instruction::ElemDrop(_) - | Instruction::TableCopy { .. } - | Instruction::MemoryInit { .. } - | Instruction::DataDrop(_) - | Instruction::MemoryCopy { .. } - | Instruction::MemoryFill(_) => { - unreachable!( - "bulk instruction {:?} is handled by the post-MVP issue, \ - not step_straight", - instr - ) + // ---- Bulk table ------------------------------------------ + Instruction::TableInit { + elem_index, + table_index, + } => { + let n = pop_i32(stack) as u32; + let src = pop_i32(stack) as u32; + let dst = pop_i32(stack) as u32; + let element_addr = store + .module(module) + .resolve_element(*elem_index) + .expect("validator: element index out of range"); + let table_addr = store + .module(module) + .resolve_table(*table_index) + .expect("validator: table index out of range"); + let refs = element_slice(store, element_addr, src, n)?.to_vec(); + store.table_mut(table_addr).init(dst, &refs)?; + Ok(StepOutcome::Continue) + } + Instruction::ElemDrop(idx) => { + let addr = store + .module(module) + .resolve_element(*idx) + .expect("validator: element index out of range"); + store.element_mut(addr).drop_segment(); + Ok(StepOutcome::Continue) + } + Instruction::TableCopy { + dst_table, + src_table, + } => { + let n = pop_i32(stack) as u32; + let src = pop_i32(stack) as u32; + let dst = pop_i32(stack) as u32; + table_copy(store, module, *dst_table, *src_table, dst, src, n)?; + Ok(StepOutcome::Continue) + } + + // ---- Bulk memory ----------------------------------------- + Instruction::MemoryInit { data_index, memory } => { + let n = pop_i32(stack) as u32; + let src = pop_i32(stack) as u32; + let dst = pop_i32(stack) as u32; + let data_addr = store + .module(module) + .resolve_data(*data_index) + .expect("validator: data index out of range"); + let memory_addr = store + .module(module) + .resolve_memory(*memory) + .expect("validator: memory index out of range"); + let bytes = data_slice(store, data_addr, src, n)?.to_vec(); + store.memory_mut(memory_addr).init(dst as u64, &bytes)?; + Ok(StepOutcome::Continue) + } + Instruction::DataDrop(idx) => { + let addr = store + .module(module) + .resolve_data(*idx) + .expect("validator: data index out of range"); + store.data_mut(addr).drop_segment(); + Ok(StepOutcome::Continue) + } + Instruction::MemoryCopy { + dst_memory, + src_memory, + } => { + let n = pop_i32(stack) as u32; + let src = pop_i32(stack) as u32; + let dst = pop_i32(stack) as u32; + memory_copy(store, module, *dst_memory, *src_memory, dst, src, n)?; + Ok(StepOutcome::Continue) + } + Instruction::MemoryFill(mem) => { + let n = pop_i32(stack) as u32; + let value = pop_i32(stack) as u8; + let dst = pop_i32(stack) as u32; + with_memory_mut(store, module, *mem, |memory| { + memory.fill(dst as u64, n as u64, value) + })?; + Ok(StepOutcome::Continue) } // ---- Parametric ------------------------------------------- @@ -987,6 +1066,110 @@ where f(store.memory_mut(mem_addr)) } +fn element_slice(store: &Store, addr: ElementAddr, src: u32, len: u32) -> Result<&[Ref], Trap> { + let segment = store.element(addr); + let start = src as u64; + let end = start + .checked_add(len as u64) + .ok_or(Trap::OutOfBoundsElementSegment)?; + if end > segment.len() as u64 { + return Err(Trap::OutOfBoundsElementSegment); + } + Ok(&segment.refs()[src as usize..end as usize]) +} + +fn data_slice(store: &Store, addr: DataAddr, src: u32, len: u32) -> Result<&[u8], Trap> { + let data = store.data(addr); + let start = src as u64; + let end = start + .checked_add(len as u64) + .ok_or(Trap::OutOfBoundsDataSegment)?; + if end > data.len() as u64 { + return Err(Trap::OutOfBoundsDataSegment); + } + Ok(&data.bytes()[src as usize..end as usize]) +} + +fn table_copy( + store: &mut Store, + module: ModuleAddr, + dst_table: u32, + src_table: u32, + dst: u32, + src: u32, + len: u32, +) -> Result<(), Trap> { + let dst_addr = resolve_table(store, module, dst_table); + let src_addr = resolve_table(store, module, src_table); + + if dst_addr == src_addr { + return store.table_mut(dst_addr).copy_within(dst, src, len); + } + + let refs = table_slice(store, src_addr, src, len)?.to_vec(); + store.table_mut(dst_addr).init(dst, &refs) +} + +fn table_slice(store: &Store, addr: TableAddr, src: u32, len: u32) -> Result<&[Ref], Trap> { + let table = store.table(addr); + let start = src as u64; + let end = start + .checked_add(len as u64) + .ok_or(Trap::OutOfBoundsTable)?; + if end > table.size() as u64 { + return Err(Trap::OutOfBoundsTable); + } + Ok(&table.elements()[src as usize..end as usize]) +} + +fn memory_copy( + store: &mut Store, + module: ModuleAddr, + dst_memory: u32, + src_memory: u32, + dst: u32, + src: u32, + len: u32, +) -> Result<(), Trap> { + let dst_addr = resolve_memory(store, module, dst_memory); + let src_addr = resolve_memory(store, module, src_memory); + + if dst_addr == src_addr { + return store + .memory_mut(dst_addr) + .copy_within(dst as u64, src as u64, len as u64); + } + + let bytes = memory_slice(store, src_addr, src, len)?.to_vec(); + store.memory_mut(dst_addr).init(dst as u64, &bytes) +} + +fn memory_slice(store: &Store, addr: MemoryAddr, src: u32, len: u32) -> Result<&[u8], Trap> { + let memory = store.memory(addr); + let start = src as u64; + let end = start + .checked_add(len as u64) + .ok_or(Trap::OutOfBoundsMemory)?; + if end > memory.bytes().len() as u64 { + return Err(Trap::OutOfBoundsMemory); + } + Ok(&memory.bytes()[src as usize..end as usize]) +} + +fn resolve_table(store: &Store, module: ModuleAddr, table_idx: u32) -> TableAddr { + store + .module(module) + .resolve_table(table_idx) + .expect("validator: table index out of range") +} + +fn resolve_memory(store: &Store, module: ModuleAddr, memory_idx: u32) -> MemoryAddr { + store + .module(module) + .resolve_memory(memory_idx) + .expect("validator: memory index out of range") +} + fn bin_i32 i32>( stack: &mut Vec, f: F, diff --git a/crates/wasm/tests/engine_tests.rs b/crates/wasm/tests/engine_tests.rs index a7523d3..2ce0ca1 100644 --- a/crates/wasm/tests/engine_tests.rs +++ b/crates/wasm/tests/engine_tests.rs @@ -468,6 +468,31 @@ fn multi_value_block_with_params_and_results() { assert_eq!(engine.invoke(f, vec![]).unwrap(), vec![WasmValue::I32(-1)]); } +#[test] +fn multi_value_branch_carries_all_results() { + let mut b = ModuleBuilder::with_types(vec![ + // type 0: () -> (i32 i32) + ft(vec![], vec![ValType::I32, ValType::I32]), + ]); + let f = b.add_wasm( + ft(vec![], vec![ValType::I32]), + vec![], + vec![ + Instruction::Block(BlockType::TypeIndex(0)), + Instruction::I32Const(30), + Instruction::I32Const(12), + Instruction::Br(0), + Instruction::I32Const(0), + Instruction::I32Const(0), + Instruction::End, + Instruction::I32Add, + Instruction::End, + ], + ); + let mut engine = Engine::new(b.store_mut()); + assert_eq!(engine.invoke(f, vec![]).unwrap(), vec![WasmValue::I32(42)]); +} + // --------------------------------------------------------------------------- // br_table // --------------------------------------------------------------------------- diff --git a/crates/wasm/tests/exec_tests.rs b/crates/wasm/tests/exec_tests.rs index 6225d77..c8b3b83 100644 --- a/crates/wasm/tests/exec_tests.rs +++ b/crates/wasm/tests/exec_tests.rs @@ -6,8 +6,8 @@ //! IEEE rounding, saturating-conversion boundaries, OOB loads/stores). use we_wasm::runtime::{ - FuncAddr, FunctionInstance, Global, GlobalAddr, Interpreter, Memory, MemoryAddr, - ModuleInstance, Ref, StepOutcome, Store, Table, TableAddr, Trap, WasmValue, + DataInstance, ElementInstance, FuncAddr, FunctionInstance, Global, GlobalAddr, Interpreter, + Memory, MemoryAddr, ModuleInstance, Ref, StepOutcome, Store, Table, TableAddr, Trap, WasmValue, }; use we_wasm::{ FuncType, GlobalType, Instruction, Limits, MemArg, MemoryType, Mutability, RefType, TableType, @@ -1176,6 +1176,51 @@ fn local_get_set_tee() { assert_eq!(s, vec![WasmValue::I32(12)]); } +// =========================================================================== +// Reference types +// =========================================================================== + +#[test] +fn ref_null_and_ref_is_null() { + assert_eq!( + run_i32(&[ + Instruction::RefNull(RefType::ExternRef), + Instruction::RefIsNull, + ]) + .unwrap(), + 1 + ); +} + +#[test] +fn ref_is_null_reports_non_null_funcref() { + let (mut store, module) = fresh(); + let mut interp = Interpreter::new(&mut store, module, vec![]); + interp.push(WasmValue::Ref(Ref::Func(FuncAddr::from_raw(7)))); + interp.step(&Instruction::RefIsNull).unwrap(); + assert_eq!(interp.stack(), &[WasmValue::I32(0)]); +} + +#[test] +fn ref_func_resolves_module_function() { + let mut store = Store::new(); + let module = store.alloc_module(ModuleInstance::new()); + let func = store.alloc_func(FunctionInstance::Wasm { + ty: FuncType { + params: vec![], + results: vec![], + }, + module, + locals: vec![], + body: vec![Instruction::End], + }); + store.module_mut(module).func_addrs.push(func); + + let mut interp = Interpreter::new(&mut store, module, vec![]); + interp.step(&Instruction::RefFunc(0)).unwrap(); + assert_eq!(interp.stack(), &[WasmValue::Ref(Ref::Func(func))]); +} + // =========================================================================== // Globals // =========================================================================== @@ -1229,6 +1274,28 @@ fn build_module_with_table( (store, module_addr, t_addr) } +fn build_module_with_table_and_element( + table_min: u32, + refs: Vec, +) -> (Store, we_wasm::runtime::ModuleAddr, TableAddr) { + let mut store = Store::new(); + let table = Table::new(TableType { + elem: RefType::FuncRef, + limits: Limits { + min: table_min, + max: None, + }, + }) + .unwrap(); + let table_addr = store.alloc_table(table); + let elem_addr = store.alloc_element(ElementInstance::new(RefType::FuncRef, refs)); + let mut module = ModuleInstance::new(); + module.table_addrs.push(table_addr); + module.element_addrs.push(elem_addr); + let module_addr = store.alloc_module(module); + (store, module_addr, table_addr) +} + #[test] fn table_size_get_set() { let (mut store, module, t) = build_module_with_table(3, None); @@ -1302,6 +1369,143 @@ fn table_fill_in_range_succeeds_and_oob_traps() { ); } +#[test] +fn table_init_copies_from_element_segment() { + let refs = vec![ + Ref::Func(FuncAddr::from_raw(10)), + Ref::Func(FuncAddr::from_raw(11)), + Ref::Func(FuncAddr::from_raw(12)), + ]; + let (mut store, module, table) = build_module_with_table_and_element(4, refs); + let mut interp = Interpreter::new(&mut store, module, vec![]); + + interp.push(WasmValue::I32(1)); // destination table index + interp.push(WasmValue::I32(1)); // source element index + interp.push(WasmValue::I32(2)); // length + interp + .step(&Instruction::TableInit { + elem_index: 0, + table_index: 0, + }) + .unwrap(); + drop(interp); + + assert_eq!( + store.table(table).elements(), + &[ + Ref::Null(RefType::FuncRef), + Ref::Func(FuncAddr::from_raw(11)), + Ref::Func(FuncAddr::from_raw(12)), + Ref::Null(RefType::FuncRef), + ] + ); +} + +#[test] +fn table_init_traps_on_segment_or_table_bounds() { + let refs = vec![Ref::Func(FuncAddr::from_raw(1))]; + let (mut store, module, _) = build_module_with_table_and_element(1, refs); + let mut interp = Interpreter::new(&mut store, module, vec![]); + + interp.push(WasmValue::I32(0)); + interp.push(WasmValue::I32(1)); + interp.push(WasmValue::I32(1)); + assert_eq!( + interp.step(&Instruction::TableInit { + elem_index: 0, + table_index: 0, + }), + Err(Trap::OutOfBoundsElementSegment) + ); + + interp.push(WasmValue::I32(1)); + interp.push(WasmValue::I32(0)); + interp.push(WasmValue::I32(1)); + assert_eq!( + interp.step(&Instruction::TableInit { + elem_index: 0, + table_index: 0, + }), + Err(Trap::OutOfBoundsTable) + ); +} + +#[test] +fn elem_drop_is_idempotent_and_blocks_nonzero_table_init() { + let refs = vec![Ref::Func(FuncAddr::from_raw(1))]; + let (mut store, module, _) = build_module_with_table_and_element(1, refs); + let mut interp = Interpreter::new(&mut store, module, vec![]); + + interp.step(&Instruction::ElemDrop(0)).unwrap(); + interp.step(&Instruction::ElemDrop(0)).unwrap(); + + interp.push(WasmValue::I32(0)); + interp.push(WasmValue::I32(0)); + interp.push(WasmValue::I32(0)); + interp + .step(&Instruction::TableInit { + elem_index: 0, + table_index: 0, + }) + .unwrap(); + + interp.push(WasmValue::I32(0)); + interp.push(WasmValue::I32(0)); + interp.push(WasmValue::I32(1)); + assert_eq!( + interp.step(&Instruction::TableInit { + elem_index: 0, + table_index: 0, + }), + Err(Trap::OutOfBoundsElementSegment) + ); +} + +#[test] +fn table_copy_instruction_handles_overlap_and_bounds() { + let (mut store, module, table) = build_module_with_table(4, None); + for i in 0..4 { + store + .table_mut(table) + .set(i, Ref::Func(FuncAddr::from_raw(i))) + .unwrap(); + } + let mut interp = Interpreter::new(&mut store, module, vec![]); + + interp.push(WasmValue::I32(1)); + interp.push(WasmValue::I32(0)); + interp.push(WasmValue::I32(3)); + interp + .step(&Instruction::TableCopy { + dst_table: 0, + src_table: 0, + }) + .unwrap(); + drop(interp); + + assert_eq!( + store.table(table).elements(), + &[ + Ref::Func(FuncAddr::from_raw(0)), + Ref::Func(FuncAddr::from_raw(0)), + Ref::Func(FuncAddr::from_raw(1)), + Ref::Func(FuncAddr::from_raw(2)), + ] + ); + + let mut interp = Interpreter::new(&mut store, module, vec![]); + interp.push(WasmValue::I32(2)); + interp.push(WasmValue::I32(0)); + interp.push(WasmValue::I32(3)); + assert_eq!( + interp.step(&Instruction::TableCopy { + dst_table: 0, + src_table: 0, + }), + Err(Trap::OutOfBoundsTable) + ); +} + // =========================================================================== // Memory // =========================================================================== @@ -1319,6 +1523,20 @@ fn build_module_with_memory( (store, module_addr, m_addr) } +fn build_module_with_memory_and_data( + data: &[u8], +) -> (Store, we_wasm::runtime::ModuleAddr, MemoryAddr) { + let mut store = Store::new(); + let memory = Memory::new(MemoryType(Limits { min: 1, max: None })).unwrap(); + let memory_addr = store.alloc_memory(memory); + let data_addr = store.alloc_data(DataInstance::from_slice(data)); + let mut module = ModuleInstance::new(); + module.memory_addrs.push(memory_addr); + module.data_addrs.push(data_addr); + let module_addr = store.alloc_module(module); + (store, module_addr, memory_addr) +} + fn mem_arg(offset: u32) -> MemArg { MemArg { align: 0, @@ -1505,6 +1723,144 @@ fn memory_f32_load_store_preserves_signaling_nan() { let _ = m_addr; } +#[test] +fn memory_init_copies_from_data_segment() { + let (mut store, module, memory) = build_module_with_memory_and_data(b"abcdef"); + let mut interp = Interpreter::new(&mut store, module, vec![]); + + interp.push(WasmValue::I32(3)); // destination memory offset + interp.push(WasmValue::I32(1)); // source data offset + interp.push(WasmValue::I32(3)); // byte count + interp + .step(&Instruction::MemoryInit { + data_index: 0, + memory: 0, + }) + .unwrap(); + drop(interp); + + assert_eq!(&store.memory(memory).bytes()[3..6], b"bcd"); +} + +#[test] +fn memory_init_traps_on_data_or_memory_bounds() { + let (mut store, module, _) = build_module_with_memory_and_data(b"abc"); + let mut interp = Interpreter::new(&mut store, module, vec![]); + + interp.push(WasmValue::I32(0)); + interp.push(WasmValue::I32(2)); + interp.push(WasmValue::I32(2)); + assert_eq!( + interp.step(&Instruction::MemoryInit { + data_index: 0, + memory: 0, + }), + Err(Trap::OutOfBoundsDataSegment) + ); + + interp.push(WasmValue::I32(65_535)); + interp.push(WasmValue::I32(0)); + interp.push(WasmValue::I32(2)); + assert_eq!( + interp.step(&Instruction::MemoryInit { + data_index: 0, + memory: 0, + }), + Err(Trap::OutOfBoundsMemory) + ); +} + +#[test] +fn data_drop_is_idempotent_and_blocks_nonzero_memory_init() { + let (mut store, module, _) = build_module_with_memory_and_data(b"x"); + let mut interp = Interpreter::new(&mut store, module, vec![]); + + interp.step(&Instruction::DataDrop(0)).unwrap(); + interp.step(&Instruction::DataDrop(0)).unwrap(); + + interp.push(WasmValue::I32(0)); + interp.push(WasmValue::I32(0)); + interp.push(WasmValue::I32(0)); + interp + .step(&Instruction::MemoryInit { + data_index: 0, + memory: 0, + }) + .unwrap(); + + interp.push(WasmValue::I32(0)); + interp.push(WasmValue::I32(0)); + interp.push(WasmValue::I32(1)); + assert_eq!( + interp.step(&Instruction::MemoryInit { + data_index: 0, + memory: 0, + }), + Err(Trap::OutOfBoundsDataSegment) + ); +} + +#[test] +fn memory_copy_handles_overlap_and_bounds() { + let (mut store, module, memory) = build_module_with_memory(1, None); + store + .memory_mut(memory) + .init(0, &[1, 2, 3, 4, 5, 6]) + .unwrap(); + let mut interp = Interpreter::new(&mut store, module, vec![]); + + interp.push(WasmValue::I32(2)); + interp.push(WasmValue::I32(0)); + interp.push(WasmValue::I32(4)); + interp + .step(&Instruction::MemoryCopy { + dst_memory: 0, + src_memory: 0, + }) + .unwrap(); + drop(interp); + + assert_eq!(&store.memory(memory).bytes()[0..6], &[1, 2, 1, 2, 3, 4]); + + let mut interp = Interpreter::new(&mut store, module, vec![]); + interp.push(WasmValue::I32(0)); + interp.push(WasmValue::I32(65_535)); + interp.push(WasmValue::I32(2)); + assert_eq!( + interp.step(&Instruction::MemoryCopy { + dst_memory: 0, + src_memory: 0, + }), + Err(Trap::OutOfBoundsMemory) + ); +} + +#[test] +fn memory_fill_writes_low_byte_and_checks_bounds() { + let (mut store, module, memory) = build_module_with_memory(1, None); + let mut interp = Interpreter::new(&mut store, module, vec![]); + + interp.push(WasmValue::I32(1)); + interp.push(WasmValue::I32(0xABCD)); + interp.push(WasmValue::I32(3)); + interp.step(&Instruction::MemoryFill(0)).unwrap(); + drop(interp); + + assert_eq!( + &store.memory(memory).bytes()[0..5], + &[0, 0xCD, 0xCD, 0xCD, 0] + ); + + let mut interp = Interpreter::new(&mut store, module, vec![]); + interp.push(WasmValue::I32(65_535)); + interp.push(WasmValue::I32(0)); + interp.push(WasmValue::I32(2)); + assert_eq!( + interp.step(&Instruction::MemoryFill(0)), + Err(Trap::OutOfBoundsMemory) + ); +} + // =========================================================================== // Control: unreachable + nop + end // =========================================================================== -- 2.51.2