diff --git a/crates/wasm/src/runtime/instantiate.rs b/crates/wasm/src/runtime/instantiate.rs new file mode 100644 index 0000000..333a603 --- /dev/null +++ b/crates/wasm/src/runtime/instantiate.rs @@ -0,0 +1,817 @@ +//! Module instantiation and link-time import resolution. +//! +//! This module turns a parsed and validated [`Module`] into live runtime +//! objects inside a [`Store`]: it resolves imports, allocates module-defined +//! functions/tables/memories/globals, applies active element/data segments, +//! records exports, and finally invokes the start function. + +use std::collections::HashMap; +use std::fmt; + +use crate::instruction::Instruction; +use crate::module::{ + ConstExpr, DataMode, ElementItems, ElementMode, ExportDesc, Import, ImportDesc, Module, +}; +use crate::runtime::data::DataInstance; +use crate::runtime::element::ElementInstance; +use crate::runtime::engine::Engine; +use crate::runtime::function::{FunctionInstance, HostFunc}; +use crate::runtime::global::Global; +use crate::runtime::instance::{ExternVal, ModuleInstance}; +use crate::runtime::memory::Memory; +use crate::runtime::store::{DataAddr, ElementAddr, ModuleAddr, Store, TableAddr}; +use crate::runtime::table::Table; +use crate::runtime::trap::{RuntimeError, Trap}; +use crate::runtime::value::{Ref, WasmValue}; +use crate::types::{ + FuncType, GlobalType, Limits, MemoryType, Mutability, RefType, TableType, ValType, +}; +use crate::validate::{validate, ValidationError}; + +/// Host-supplied imports keyed by `(module, name)`. +#[derive(Debug, Default, Clone)] +pub struct ImportObject { + values: HashMap<(String, String), ExternVal>, +} + +impl ImportObject { + /// Build an empty import object. + pub fn new() -> Self { + Self::default() + } + + /// Insert or replace one import binding. + pub fn define( + &mut self, + module: impl Into, + name: impl Into, + value: ExternVal, + ) -> &mut Self { + self.values.insert((module.into(), name.into()), value); + self + } + + /// Builder-style variant of [`define`]. + pub fn with( + mut self, + module: impl Into, + name: impl Into, + value: ExternVal, + ) -> Self { + self.define(module, name, value); + self + } + + /// Look up one import. + pub fn get(&self, module: &str, name: &str) -> Option { + self.values + .get(&(module.to_string(), name.to_string())) + .copied() + } + + /// Number of import bindings. + pub fn len(&self) -> usize { + self.values.len() + } + + /// True when no imports are defined. + pub fn is_empty(&self) -> bool { + self.values.is_empty() + } +} + +/// A host-visible import/export kind. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ExternKind { + Func, + Table, + Memory, + Global, +} + +/// Link-time failures while resolving imports. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LinkError { + MissingImport { + module: String, + name: String, + }, + WrongImportKind { + module: String, + name: String, + expected: ExternKind, + actual: ExternKind, + }, + FunctionTypeMismatch { + module: String, + name: String, + expected: FuncType, + actual: FuncType, + }, + TableTypeMismatch { + module: String, + name: String, + expected: TableType, + actual: TableType, + }, + GlobalTypeMismatch { + module: String, + name: String, + expected: GlobalType, + actual: GlobalType, + }, + GlobalMutabilityMismatch { + module: String, + name: String, + expected: Mutability, + actual: Mutability, + }, + IncompatibleLimits { + module: String, + name: String, + kind: ExternKind, + expected_min: u32, + expected_max: Option, + actual_min: u32, + actual_max: Option, + }, +} + +impl fmt::Display for LinkError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LinkError::MissingImport { module, name } => { + write!(f, "missing import {module}.{name}") + } + LinkError::WrongImportKind { + module, + name, + expected, + actual, + } => write!( + f, + "wrong import kind for {module}.{name}: expected {expected:?}, got {actual:?}" + ), + LinkError::FunctionTypeMismatch { module, name, .. } => { + write!(f, "function import type mismatch for {module}.{name}") + } + LinkError::TableTypeMismatch { module, name, .. } => { + write!(f, "table import type mismatch for {module}.{name}") + } + LinkError::GlobalTypeMismatch { module, name, .. } => { + write!(f, "global import type mismatch for {module}.{name}") + } + LinkError::GlobalMutabilityMismatch { module, name, .. } => { + write!(f, "global import mutability mismatch for {module}.{name}") + } + LinkError::IncompatibleLimits { module, name, .. } => { + write!(f, "incompatible limits for {module}.{name}") + } + } + } +} + +impl std::error::Error for LinkError {} + +/// Errors that can occur while instantiating a module. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum InstantiationError { + Validation(ValidationError), + Link(Box), + Runtime(RuntimeError), + Trap(Trap), + InvalidConstExpr, + InvalidIndex, +} + +impl From for InstantiationError { + fn from(value: ValidationError) -> Self { + InstantiationError::Validation(value) + } +} + +impl From for InstantiationError { + fn from(value: LinkError) -> Self { + InstantiationError::Link(Box::new(value)) + } +} + +impl From for InstantiationError { + fn from(value: RuntimeError) -> Self { + InstantiationError::Runtime(value) + } +} + +impl From for InstantiationError { + fn from(value: Trap) -> Self { + InstantiationError::Trap(value) + } +} + +impl fmt::Display for InstantiationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + InstantiationError::Validation(e) => write!(f, "{e}"), + InstantiationError::Link(e) => write!(f, "{e}"), + InstantiationError::Runtime(e) => write!(f, "runtime instantiation error: {e:?}"), + InstantiationError::Trap(t) => write!(f, "start/initializer trapped: {t:?}"), + InstantiationError::InvalidConstExpr => write!(f, "invalid constant expression"), + InstantiationError::InvalidIndex => write!(f, "invalid runtime index"), + } + } +} + +impl std::error::Error for InstantiationError {} + +/// Export collection returned by instantiation. +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct Exports { + values: HashMap, +} + +impl Exports { + /// Look up an export by name. + pub fn get(&self, name: &str) -> Option { + self.values.get(name).copied() + } + + /// Number of exports. + pub fn len(&self) -> usize { + self.values.len() + } + + /// True when the instance exports nothing. + pub fn is_empty(&self) -> bool { + self.values.is_empty() + } + + /// Iterate over `(name, value)` exports. + pub fn iter(&self) -> impl Iterator + '_ { + self.values + .iter() + .map(|(name, value)| (name.as_str(), *value)) + } +} + +/// A live instantiated module. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Instance { + module: ModuleAddr, + exports: Exports, +} + +impl Instance { + /// Address of the backing [`ModuleInstance`] in the store. + pub fn module_addr(&self) -> ModuleAddr { + self.module + } + + /// All exports recorded during instantiation. + pub fn exports(&self) -> &Exports { + &self.exports + } + + /// Convenience export lookup. + pub fn export(&self, name: &str) -> Option { + self.exports.get(name) + } +} + +/// Register a host function in `store` and return an [`ExternVal`] suitable +/// for insertion into an [`ImportObject`]. +pub fn host_func(store: &mut Store, ty: FuncType, callback: HostFunc) -> ExternVal { + ExternVal::Func(store.alloc_func(FunctionInstance::Host { ty, callback })) +} + +/// Instantiate `module` inside `store`. +pub fn instantiate( + store: &mut Store, + module: &Module<'_>, + imports: &ImportObject, +) -> Result { + validate(module)?; + + let mut inst = ModuleInstance::new(); + inst.types = module.types.clone(); + + resolve_imports(store, module, imports, &mut inst)?; + + let module_addr = store.alloc_module(ModuleInstance::new()); + + allocate_defined_functions(store, module, module_addr, &mut inst)?; + allocate_tables(store, module, &mut inst)?; + allocate_memories(store, module, &mut inst)?; + allocate_globals(store, module, &mut inst)?; + + let element_actions = allocate_elements(store, module, &mut inst)?; + let data_actions = allocate_data_segments(store, module, &mut inst)?; + + build_exports(module, &mut inst)?; + let exports = Exports { + values: inst.exports.clone(), + }; + + *store.module_mut(module_addr) = inst; + + apply_element_actions(store, &element_actions)?; + apply_data_actions(store, &data_actions)?; + + if let Some(start) = module.start { + let start_addr = store + .module(module_addr) + .resolve_func(start) + .ok_or(InstantiationError::InvalidIndex)?; + Engine::new(store).invoke(start_addr, Vec::new())?; + } + + Ok(Instance { + module: module_addr, + exports, + }) +} + +fn resolve_imports( + store: &Store, + module: &Module<'_>, + imports: &ImportObject, + inst: &mut ModuleInstance, +) -> Result<(), InstantiationError> { + for import in &module.imports { + let value = imports + .get(&import.module, &import.name) + .ok_or_else(|| missing_import(import))?; + match (&import.desc, value) { + (ImportDesc::Func(type_index), ExternVal::Func(addr)) => { + let expected = module + .types + .get(*type_index as usize) + .ok_or(InstantiationError::InvalidIndex)?; + let actual = store.func(addr).ty(); + if expected != actual { + return Err(LinkError::FunctionTypeMismatch { + module: import.module.clone(), + name: import.name.clone(), + expected: expected.clone(), + actual: actual.clone(), + } + .into()); + } + inst.func_addrs.push(addr); + inst.imported_funcs += 1; + } + (ImportDesc::Table(expected), ExternVal::Table(addr)) => { + let actual = table_type(store, addr); + check_table_type(import, *expected, actual)?; + inst.table_addrs.push(addr); + inst.imported_tables += 1; + } + (ImportDesc::Memory(expected), ExternVal::Memory(addr)) => { + let actual = memory_type(store, addr); + check_memory_type(import, *expected, actual)?; + inst.memory_addrs.push(addr); + inst.imported_memories += 1; + } + (ImportDesc::Global(expected), ExternVal::Global(addr)) => { + let actual = global_type(store, addr); + check_global_type(import, *expected, actual)?; + inst.global_addrs.push(addr); + inst.imported_globals += 1; + } + (desc, actual) => { + return Err(LinkError::WrongImportKind { + module: import.module.clone(), + name: import.name.clone(), + expected: kind_for_import(desc), + actual: kind_for_extern(actual), + } + .into()); + } + } + } + Ok(()) +} + +fn allocate_defined_functions( + store: &mut Store, + module: &Module<'_>, + module_addr: ModuleAddr, + inst: &mut ModuleInstance, +) -> Result<(), InstantiationError> { + for (type_index, body) in module.functions.iter().zip(&module.code) { + let ty = module + .types + .get(*type_index as usize) + .ok_or(InstantiationError::InvalidIndex)? + .clone(); + let locals = expand_locals(&body.locals)?; + let addr = store.alloc_func(FunctionInstance::Wasm { + ty, + module: module_addr, + locals, + body: body.body.clone(), + }); + inst.func_addrs.push(addr); + } + Ok(()) +} + +fn allocate_tables( + store: &mut Store, + module: &Module<'_>, + inst: &mut ModuleInstance, +) -> Result<(), InstantiationError> { + for table_type in &module.tables { + let addr = store.alloc_table(Table::new(*table_type)?); + inst.table_addrs.push(addr); + } + Ok(()) +} + +fn allocate_memories( + store: &mut Store, + module: &Module<'_>, + inst: &mut ModuleInstance, +) -> Result<(), InstantiationError> { + for memory_type in &module.memories { + let addr = store.alloc_memory(Memory::new(*memory_type)?); + inst.memory_addrs.push(addr); + } + Ok(()) +} + +fn allocate_globals( + store: &mut Store, + module: &Module<'_>, + inst: &mut ModuleInstance, +) -> Result<(), InstantiationError> { + for global in &module.globals { + let value = eval_const_expr(store, inst, global.ty.valtype, &global.init)?; + let addr = store.alloc_global(Global::new(global.ty, value)?); + inst.global_addrs.push(addr); + } + Ok(()) +} + +#[derive(Debug, Clone)] +enum ElementAction { + Init { + table: TableAddr, + offset: u32, + refs: Vec, + segment: ElementAddr, + }, + Drop { + segment: ElementAddr, + }, +} + +fn allocate_elements( + store: &mut Store, + module: &Module<'_>, + inst: &mut ModuleInstance, +) -> Result, InstantiationError> { + let mut actions = Vec::new(); + + for element in &module.elements { + let refs = resolve_element_items(store, inst, element.ref_type, &element.items)?; + let addr = store.alloc_element(ElementInstance::new(element.ref_type, refs.clone())); + inst.element_addrs.push(addr); + + match &element.mode { + ElementMode::Passive => {} + ElementMode::Active { table, offset } => { + let table_addr = inst + .resolve_table(*table) + .ok_or(InstantiationError::InvalidIndex)?; + actions.push(ElementAction::Init { + table: table_addr, + offset: eval_i32_const_expr(store, inst, offset)?, + refs, + segment: addr, + }); + } + ElementMode::Declarative => { + actions.push(ElementAction::Drop { segment: addr }); + } + } + } + + Ok(actions) +} + +#[derive(Debug, Clone)] +struct DataAction { + memory: crate::runtime::store::MemoryAddr, + offset: u64, + bytes: Vec, + segment: DataAddr, +} + +fn allocate_data_segments( + store: &mut Store, + module: &Module<'_>, + inst: &mut ModuleInstance, +) -> Result, InstantiationError> { + let mut actions = Vec::new(); + + for data in &module.data { + let addr = store.alloc_data(DataInstance::from_slice(data.init)); + inst.data_addrs.push(addr); + + if let DataMode::Active { memory, offset } = &data.mode { + let memory_addr = inst + .resolve_memory(*memory) + .ok_or(InstantiationError::InvalidIndex)?; + actions.push(DataAction { + memory: memory_addr, + offset: eval_i32_const_expr(store, inst, offset)? as u64, + bytes: data.init.to_vec(), + segment: addr, + }); + } + } + + Ok(actions) +} + +fn apply_element_actions( + store: &mut Store, + actions: &[ElementAction], +) -> Result<(), InstantiationError> { + for action in actions { + match action { + ElementAction::Init { + table, + offset, + refs, + segment, + } => { + store.table_mut(*table).init(*offset, refs)?; + store.element_mut(*segment).drop_segment(); + } + ElementAction::Drop { segment } => { + store.element_mut(*segment).drop_segment(); + } + } + } + Ok(()) +} + +fn apply_data_actions(store: &mut Store, actions: &[DataAction]) -> Result<(), InstantiationError> { + for action in actions { + store + .memory_mut(action.memory) + .init(action.offset, &action.bytes)?; + store.data_mut(action.segment).drop_segment(); + } + Ok(()) +} + +fn build_exports(module: &Module<'_>, inst: &mut ModuleInstance) -> Result<(), InstantiationError> { + for export in &module.exports { + let value = match export.desc { + ExportDesc::Func(i) => ExternVal::Func( + inst.resolve_func(i) + .ok_or(InstantiationError::InvalidIndex)?, + ), + ExportDesc::Table(i) => ExternVal::Table( + inst.resolve_table(i) + .ok_or(InstantiationError::InvalidIndex)?, + ), + ExportDesc::Memory(i) => ExternVal::Memory( + inst.resolve_memory(i) + .ok_or(InstantiationError::InvalidIndex)?, + ), + ExportDesc::Global(i) => ExternVal::Global( + inst.resolve_global(i) + .ok_or(InstantiationError::InvalidIndex)?, + ), + }; + inst.exports.insert(export.name.clone(), value); + } + Ok(()) +} + +fn resolve_element_items( + store: &Store, + inst: &ModuleInstance, + ref_type: RefType, + items: &ElementItems, +) -> Result, InstantiationError> { + match items { + ElementItems::FunctionIndices(indices) => { + let mut refs = Vec::with_capacity(indices.len()); + for idx in indices { + refs.push(Ref::Func( + inst.resolve_func(*idx) + .ok_or(InstantiationError::InvalidIndex)?, + )); + } + Ok(refs) + } + ElementItems::Expressions(exprs) => { + let mut refs = Vec::with_capacity(exprs.len()); + for expr in exprs { + let value = eval_const_expr(store, inst, ValType::Ref(ref_type), expr)?; + match value { + WasmValue::Ref(r) => refs.push(r), + _ => return Err(InstantiationError::InvalidConstExpr), + } + } + Ok(refs) + } + } +} + +fn eval_i32_const_expr( + store: &Store, + inst: &ModuleInstance, + expr: &ConstExpr, +) -> Result { + match eval_const_expr(store, inst, ValType::I32, expr)? { + WasmValue::I32(v) => Ok(v as u32), + _ => Err(InstantiationError::InvalidConstExpr), + } +} + +fn eval_const_expr( + store: &Store, + inst: &ModuleInstance, + expected: ValType, + expr: &ConstExpr, +) -> Result { + if !matches!(expr.last(), Some(Instruction::End)) { + return Err(InstantiationError::InvalidConstExpr); + } + + let mut stack = Vec::new(); + for instr in &expr[..expr.len() - 1] { + match instr { + Instruction::I32Const(v) => stack.push(WasmValue::I32(*v)), + Instruction::I64Const(v) => stack.push(WasmValue::I64(*v)), + Instruction::F32Const(v) => stack.push(WasmValue::F32(v.to_bits())), + Instruction::F64Const(v) => stack.push(WasmValue::F64(v.to_bits())), + Instruction::RefNull(rt) => stack.push(WasmValue::Ref(Ref::Null(*rt))), + Instruction::RefFunc(idx) => stack.push(WasmValue::Ref(Ref::Func( + inst.resolve_func(*idx) + .ok_or(InstantiationError::InvalidIndex)?, + ))), + Instruction::GlobalGet(idx) => { + let addr = inst + .resolve_global(*idx) + .ok_or(InstantiationError::InvalidIndex)?; + stack.push(store.global(addr).get()); + } + _ => return Err(InstantiationError::InvalidConstExpr), + } + } + + if stack.len() != 1 { + return Err(InstantiationError::InvalidConstExpr); + } + let value = stack[0]; + if !value.matches_type(expected) { + return Err(InstantiationError::InvalidConstExpr); + } + Ok(value) +} + +fn expand_locals(locals: &[(u32, ValType)]) -> Result, InstantiationError> { + let mut out = Vec::new(); + for (count, ty) in locals { + let new_len = out + .len() + .checked_add(*count as usize) + .ok_or(InstantiationError::InvalidIndex)?; + out.resize(new_len, *ty); + } + Ok(out) +} + +fn check_table_type( + import: &Import, + expected: TableType, + actual: TableType, +) -> Result<(), InstantiationError> { + if expected.elem != actual.elem { + return Err(LinkError::TableTypeMismatch { + module: import.module.clone(), + name: import.name.clone(), + expected, + actual, + } + .into()); + } + check_limits(import, ExternKind::Table, expected.limits, actual.limits) +} + +fn check_memory_type( + import: &Import, + expected: MemoryType, + actual: MemoryType, +) -> Result<(), InstantiationError> { + check_limits(import, ExternKind::Memory, expected.0, actual.0) +} + +fn check_global_type( + import: &Import, + expected: GlobalType, + actual: GlobalType, +) -> Result<(), InstantiationError> { + if expected.valtype != actual.valtype { + return Err(LinkError::GlobalTypeMismatch { + module: import.module.clone(), + name: import.name.clone(), + expected, + actual, + } + .into()); + } + if expected.mutability != actual.mutability { + return Err(LinkError::GlobalMutabilityMismatch { + module: import.module.clone(), + name: import.name.clone(), + expected: expected.mutability, + actual: actual.mutability, + } + .into()); + } + Ok(()) +} + +fn check_limits( + import: &Import, + kind: ExternKind, + expected: Limits, + actual: Limits, +) -> Result<(), InstantiationError> { + let max_ok = match expected.max { + Some(expected_max) => matches!(actual.max, Some(actual_max) if actual_max <= expected_max), + None => true, + }; + if actual.min < expected.min || !max_ok { + return Err(LinkError::IncompatibleLimits { + module: import.module.clone(), + name: import.name.clone(), + kind, + expected_min: expected.min, + expected_max: expected.max, + actual_min: actual.min, + actual_max: actual.max, + } + .into()); + } + Ok(()) +} + +fn table_type(store: &Store, addr: TableAddr) -> TableType { + let table = store.table(addr); + TableType { + elem: table.elem_type(), + limits: Limits { + min: table.min(), + max: table.max(), + }, + } +} + +fn memory_type(store: &Store, addr: crate::runtime::store::MemoryAddr) -> MemoryType { + let memory = store.memory(addr); + MemoryType(Limits { + min: memory.min(), + max: memory.max(), + }) +} + +fn global_type(store: &Store, addr: crate::runtime::store::GlobalAddr) -> GlobalType { + let global = store.global(addr); + GlobalType { + valtype: global.val_type(), + mutability: global.mutability(), + } +} + +fn missing_import(import: &Import) -> LinkError { + LinkError::MissingImport { + module: import.module.clone(), + name: import.name.clone(), + } +} + +fn kind_for_import(desc: &ImportDesc) -> ExternKind { + match desc { + ImportDesc::Func(_) => ExternKind::Func, + ImportDesc::Table(_) => ExternKind::Table, + ImportDesc::Memory(_) => ExternKind::Memory, + ImportDesc::Global(_) => ExternKind::Global, + } +} + +fn kind_for_extern(value: ExternVal) -> ExternKind { + match value { + ExternVal::Func(_) => ExternKind::Func, + ExternVal::Table(_) => ExternKind::Table, + ExternVal::Memory(_) => ExternKind::Memory, + ExternVal::Global(_) => ExternKind::Global, + } +} diff --git a/crates/wasm/src/runtime/mod.rs b/crates/wasm/src/runtime/mod.rs index 7fad348..6cf3960 100644 --- a/crates/wasm/src/runtime/mod.rs +++ b/crates/wasm/src/runtime/mod.rs @@ -20,6 +20,7 @@ mod exec; mod function; mod global; mod instance; +mod instantiate; mod memory; mod store; mod table; @@ -33,6 +34,10 @@ pub use exec::{Interpreter, StepOutcome}; pub use function::{FunctionInstance, HostFunc, HostFuncTrampoline}; pub use global::Global; pub use instance::{ExternVal, ModuleInstance}; +pub use instantiate::{ + host_func, instantiate, Exports, ExternKind, ImportObject, Instance, InstantiationError, + LinkError, +}; pub use memory::{Memory, MAX_PAGES, PAGE_SIZE}; pub use store::{ DataAddr, ElementAddr, FuncAddr, GlobalAddr, MemoryAddr, ModuleAddr, Store, TableAddr, diff --git a/crates/wasm/tests/instantiation_tests.rs b/crates/wasm/tests/instantiation_tests.rs new file mode 100644 index 0000000..01922d3 --- /dev/null +++ b/crates/wasm/tests/instantiation_tests.rs @@ -0,0 +1,521 @@ +//! Integration tests for module instantiation, import linking, exports, active +//! segments, and start-function execution. + +use we_wasm::runtime::{ + host_func, instantiate, ExternVal, Global, ImportObject, InstantiationError, LinkError, Memory, + Ref, Store, Table, Trap, WasmValue, +}; +use we_wasm::{ + parse, ConstExpr, Data, DataMode, Element, ElementItems, ElementMode, Export, ExportDesc, + FuncType, GlobalType, Import, ImportDesc, Instruction, Limits, MemoryType, Module, Mutability, + RefType, TableType, ValType, +}; + +const PREAMBLE: [u8; 8] = [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]; + +fn ft(params: Vec, results: Vec) -> FuncType { + FuncType { params, results } +} + +fn section(id: u8, payload: &[u8]) -> Vec { + let mut out = vec![id]; + push_leb_u32(&mut out, payload.len() as u32); + out.extend_from_slice(payload); + out +} + +fn push_leb_u32(out: &mut Vec, mut value: u32) { + loop { + let byte = (value & 0x7f) as u8; + value >>= 7; + if value == 0 { + out.push(byte); + return; + } + out.push(byte | 0x80); + } +} + +fn module_with(sections: &[Vec]) -> Vec { + let mut bytes = PREAMBLE.to_vec(); + for section in sections { + bytes.extend_from_slice(section); + } + bytes +} + +fn empty_import_module(desc: ImportDesc, types: Vec) -> Module<'static> { + Module { + types, + imports: vec![Import { + module: "env".to_string(), + name: "x".to_string(), + desc, + }], + ..Module::default() + } +} + +fn i32_const(v: i32) -> ConstExpr { + vec![Instruction::I32Const(v), Instruction::End] +} + +#[test] +fn instantiate_links_host_import_and_calls_exported_function() { + // (import "env" "add" (func (param i32 i32) (result i32))) + // (func (export "run") (result i32) + // i32.const 20 + // i32.const 22 + // call 0) + let type_payload = vec![ + 0x02, // count + 0x60, 0x02, 0x7f, 0x7f, 0x01, 0x7f, // type 0: (i32 i32) -> i32 + 0x60, 0x00, 0x01, 0x7f, // type 1: () -> i32 + ]; + let import_payload = vec![ + 0x01, // count + 0x03, b'e', b'n', b'v', // module + 0x03, b'a', b'd', b'd', // name + 0x00, 0x00, // func, type 0 + ]; + let func_payload = vec![0x01, 0x01]; // one function, type 1 + let export_payload = vec![ + 0x01, // count + 0x03, b'r', b'u', b'n', // "run" + 0x00, 0x01, // func index 1 (imported func is index 0) + ]; + let code_payload = vec![ + 0x01, // count + 0x08, // body size + 0x00, // locals count + 0x41, 0x14, // i32.const 20 + 0x41, 0x16, // i32.const 22 + 0x10, 0x00, // call 0 + 0x0b, // end + ]; + let bytes = module_with(&[ + section(1, &type_payload), + section(2, &import_payload), + section(3, &func_payload), + section(7, &export_payload), + section(10, &code_payload), + ]); + let module = parse(&bytes).unwrap(); + + let mut store = Store::new(); + let add_ty = ft(vec![ValType::I32, ValType::I32], vec![ValType::I32]); + let add = host_func( + &mut store, + add_ty, + Box::new(|args: &[WasmValue]| match args { + [WasmValue::I32(a), WasmValue::I32(b)] => Ok(vec![WasmValue::I32(a + b)]), + _ => Err(Trap::IndirectCallTypeMismatch), + }), + ); + let imports = ImportObject::new().with("env", "add", add); + let instance = instantiate(&mut store, &module, &imports).unwrap(); + + let ExternVal::Func(run) = instance.export("run").unwrap() else { + panic!("run should export a function"); + }; + let out = we_wasm::runtime::Engine::new(&mut store) + .invoke(run, Vec::new()) + .unwrap(); + assert_eq!(out, vec![WasmValue::I32(42)]); +} + +#[test] +fn missing_import_is_a_link_error() { + let module = empty_import_module(ImportDesc::Func(0), vec![ft(vec![], vec![])]); + let mut store = Store::new(); + + let err = instantiate(&mut store, &module, &ImportObject::new()).unwrap_err(); + + assert!(matches!( + err, + InstantiationError::Link(e) + if matches!(e.as_ref(), LinkError::MissingImport { module, name } if module == "env" && name == "x") + )); +} + +#[test] +fn wrong_import_kind_is_a_link_error() { + let module = empty_import_module(ImportDesc::Func(0), vec![ft(vec![], vec![])]); + let mut store = Store::new(); + let memory = store.alloc_memory(Memory::from_limits(Limits { min: 1, max: None }).unwrap()); + let imports = ImportObject::new().with("env", "x", ExternVal::Memory(memory)); + + let err = instantiate(&mut store, &module, &imports).unwrap_err(); + + assert!(matches!( + err, + InstantiationError::Link(e) if matches!(e.as_ref(), LinkError::WrongImportKind { .. }) + )); +} + +#[test] +fn function_signature_mismatch_is_a_link_error() { + let module = empty_import_module( + ImportDesc::Func(0), + vec![ft(vec![ValType::I32], vec![ValType::I32])], + ); + let mut store = Store::new(); + let wrong = host_func( + &mut store, + ft(vec![ValType::I64], vec![ValType::I32]), + Box::new(|_: &[WasmValue]| Ok(vec![WasmValue::I32(0)])), + ); + let imports = ImportObject::new().with("env", "x", wrong); + + let err = instantiate(&mut store, &module, &imports).unwrap_err(); + + assert!(matches!( + err, + InstantiationError::Link(e) if matches!(e.as_ref(), LinkError::FunctionTypeMismatch { .. }) + )); +} + +#[test] +fn global_mutability_mismatch_is_a_link_error() { + let expected = GlobalType { + valtype: ValType::I32, + mutability: Mutability::Var, + }; + let actual = GlobalType { + valtype: ValType::I32, + mutability: Mutability::Const, + }; + let module = empty_import_module(ImportDesc::Global(expected), vec![]); + let mut store = Store::new(); + let global = store.alloc_global(Global::new(actual, WasmValue::I32(7)).unwrap()); + let imports = ImportObject::new().with("env", "x", ExternVal::Global(global)); + + let err = instantiate(&mut store, &module, &imports).unwrap_err(); + + assert!(matches!( + err, + InstantiationError::Link(e) if matches!(e.as_ref(), LinkError::GlobalMutabilityMismatch { .. }) + )); +} + +#[test] +fn global_value_type_mismatch_is_a_link_error() { + let expected = GlobalType { + valtype: ValType::I32, + mutability: Mutability::Const, + }; + let actual = GlobalType { + valtype: ValType::I64, + mutability: Mutability::Const, + }; + let module = empty_import_module(ImportDesc::Global(expected), vec![]); + let mut store = Store::new(); + let global = store.alloc_global(Global::new(actual, WasmValue::I64(7)).unwrap()); + let imports = ImportObject::new().with("env", "x", ExternVal::Global(global)); + + let err = instantiate(&mut store, &module, &imports).unwrap_err(); + + assert!(matches!( + err, + InstantiationError::Link(e) if matches!(e.as_ref(), LinkError::GlobalTypeMismatch { .. }) + )); +} + +#[test] +fn incompatible_memory_limits_are_a_link_error() { + let expected = MemoryType(Limits { + min: 2, + max: Some(3), + }); + let module = empty_import_module(ImportDesc::Memory(expected), vec![]); + let mut store = Store::new(); + let memory = store.alloc_memory(Memory::from_limits(Limits { min: 1, max: None }).unwrap()); + let imports = ImportObject::new().with("env", "x", ExternVal::Memory(memory)); + + let err = instantiate(&mut store, &module, &imports).unwrap_err(); + + assert!(matches!( + err, + InstantiationError::Link(e) if matches!(e.as_ref(), LinkError::IncompatibleLimits { .. }) + )); +} + +#[test] +fn table_element_type_mismatch_is_a_link_error() { + let expected = TableType { + elem: RefType::FuncRef, + limits: Limits { min: 1, max: None }, + }; + let actual = TableType { + elem: RefType::ExternRef, + limits: Limits { min: 1, max: None }, + }; + let module = empty_import_module(ImportDesc::Table(expected), vec![]); + let mut store = Store::new(); + let table = store.alloc_table(Table::new(actual).unwrap()); + let imports = ImportObject::new().with("env", "x", ExternVal::Table(table)); + + let err = instantiate(&mut store, &module, &imports).unwrap_err(); + + assert!(matches!( + err, + InstantiationError::Link(e) if matches!(e.as_ref(), LinkError::TableTypeMismatch { .. }) + )); +} + +#[test] +fn active_data_segment_initializes_memory_and_then_drops() { + let module = Module { + memories: vec![MemoryType(Limits { min: 1, max: None })], + data: vec![Data { + init: &[1, 2, 3], + mode: DataMode::Active { + memory: 0, + offset: i32_const(4), + }, + }], + exports: vec![Export { + name: "memory".to_string(), + desc: ExportDesc::Memory(0), + }], + ..Module::default() + }; + let mut store = Store::new(); + + let instance = instantiate(&mut store, &module, &ImportObject::new()).unwrap(); + + let ExternVal::Memory(memory) = instance.export("memory").unwrap() else { + panic!("memory export should be a memory"); + }; + assert_eq!(&store.memory(memory).bytes()[4..7], &[1, 2, 3]); + let data_addr = store + .module(instance.module_addr()) + .resolve_data(0) + .unwrap(); + assert!(store.data(data_addr).is_empty()); +} + +#[test] +fn active_data_out_of_bounds_traps() { + let module = Module { + memories: vec![MemoryType(Limits { min: 0, max: None })], + data: vec![Data { + init: &[1], + mode: DataMode::Active { + memory: 0, + offset: i32_const(0), + }, + }], + ..Module::default() + }; + let mut store = Store::new(); + + let err = instantiate(&mut store, &module, &ImportObject::new()).unwrap_err(); + + assert_eq!(err, InstantiationError::Trap(Trap::OutOfBoundsMemory)); +} + +#[test] +fn active_element_segment_initializes_table_and_then_drops() { + let module = Module { + types: vec![ft(vec![], vec![ValType::I32])], + functions: vec![0], + tables: vec![TableType { + elem: RefType::FuncRef, + limits: Limits { min: 2, max: None }, + }], + elements: vec![Element { + ref_type: RefType::FuncRef, + items: ElementItems::FunctionIndices(vec![0]), + mode: ElementMode::Active { + table: 0, + offset: i32_const(1), + }, + }], + code: vec![we_wasm::FunctionBody { + locals: vec![], + body: vec![Instruction::I32Const(9), Instruction::End], + }], + exports: vec![Export { + name: "table".to_string(), + desc: ExportDesc::Table(0), + }], + ..Module::default() + }; + let mut store = Store::new(); + + let instance = instantiate(&mut store, &module, &ImportObject::new()).unwrap(); + + let ExternVal::Table(table) = instance.export("table").unwrap() else { + panic!("table export should be a table"); + }; + let func = store + .module(instance.module_addr()) + .resolve_func(0) + .unwrap(); + assert_eq!(store.table(table).get(1).unwrap(), Ref::Func(func)); + let elem = store + .module(instance.module_addr()) + .resolve_element(0) + .unwrap(); + assert!(store.element(elem).is_empty()); +} + +#[test] +fn passive_segments_remain_available_after_instantiation() { + let module = Module { + types: vec![ft(vec![], vec![])], + functions: vec![0], + code: vec![we_wasm::FunctionBody { + locals: vec![], + body: vec![Instruction::End], + }], + elements: vec![Element { + ref_type: RefType::FuncRef, + items: ElementItems::Expressions(vec![vec![ + Instruction::RefNull(RefType::FuncRef), + Instruction::End, + ]]), + mode: ElementMode::Passive, + }], + data: vec![Data { + init: &[4, 5], + mode: DataMode::Passive, + }], + ..Module::default() + }; + let mut store = Store::new(); + + let instance = instantiate(&mut store, &module, &ImportObject::new()).unwrap(); + + let elem = store + .module(instance.module_addr()) + .resolve_element(0) + .unwrap(); + assert_eq!(store.element(elem).refs(), &[Ref::Null(RefType::FuncRef)]); + let data = store + .module(instance.module_addr()) + .resolve_data(0) + .unwrap(); + assert_eq!(store.data(data).bytes(), &[4, 5]); +} + +#[test] +fn start_function_trap_is_reported_as_instantiation_trap() { + let module = Module { + types: vec![ft(vec![], vec![])], + functions: vec![0], + start: Some(0), + code: vec![we_wasm::FunctionBody { + locals: vec![], + body: vec![Instruction::Unreachable, Instruction::End], + }], + ..Module::default() + }; + let mut store = Store::new(); + + let err = instantiate(&mut store, &module, &ImportObject::new()).unwrap_err(); + + assert_eq!(err, InstantiationError::Trap(Trap::Unreachable)); +} + +#[test] +fn imported_memory_table_and_global_can_be_exported() { + let memory_type = MemoryType(Limits { + min: 1, + max: Some(2), + }); + let table_type = TableType { + elem: RefType::FuncRef, + limits: Limits { + min: 1, + max: Some(2), + }, + }; + let global_type = GlobalType { + valtype: ValType::I32, + mutability: Mutability::Const, + }; + let module = Module { + imports: vec![ + Import { + module: "env".to_string(), + name: "memory".to_string(), + desc: ImportDesc::Memory(memory_type), + }, + Import { + module: "env".to_string(), + name: "table".to_string(), + desc: ImportDesc::Table(table_type), + }, + Import { + module: "env".to_string(), + name: "global".to_string(), + desc: ImportDesc::Global(global_type), + }, + ], + exports: vec![ + Export { + name: "memory".to_string(), + desc: ExportDesc::Memory(0), + }, + Export { + name: "table".to_string(), + desc: ExportDesc::Table(0), + }, + Export { + name: "global".to_string(), + desc: ExportDesc::Global(0), + }, + ], + ..Module::default() + }; + let mut store = Store::new(); + let memory = store.alloc_memory(Memory::new(memory_type).unwrap()); + let table = store.alloc_table(Table::new(table_type).unwrap()); + let global = store.alloc_global(Global::new(global_type, WasmValue::I32(11)).unwrap()); + let imports = ImportObject::new() + .with("env", "memory", ExternVal::Memory(memory)) + .with("env", "table", ExternVal::Table(table)) + .with("env", "global", ExternVal::Global(global)); + + let instance = instantiate(&mut store, &module, &imports).unwrap(); + + assert_eq!(instance.export("memory"), Some(ExternVal::Memory(memory))); + assert_eq!(instance.export("table"), Some(ExternVal::Table(table))); + assert_eq!(instance.export("global"), Some(ExternVal::Global(global))); +} + +#[test] +fn own_global_can_initialize_from_imported_const_global() { + let imported = GlobalType { + valtype: ValType::I32, + mutability: Mutability::Const, + }; + let module = Module { + imports: vec![Import { + module: "env".to_string(), + name: "g".to_string(), + desc: ImportDesc::Global(imported), + }], + globals: vec![we_wasm::Global { + ty: imported, + init: vec![Instruction::GlobalGet(0), Instruction::End], + }], + exports: vec![Export { + name: "copy".to_string(), + desc: ExportDesc::Global(1), + }], + ..Module::default() + }; + let mut store = Store::new(); + let source = store.alloc_global(Global::new(imported, WasmValue::I32(77)).unwrap()); + let imports = ImportObject::new().with("env", "g", ExternVal::Global(source)); + + let instance = instantiate(&mut store, &module, &imports).unwrap(); + + let ExternVal::Global(copy) = instance.export("copy").unwrap() else { + panic!("copy should export a global"); + }; + assert_eq!(store.global(copy).get(), WasmValue::I32(77)); +}