diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..e712117b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "external/testsuite"] + path = external/testsuite + url = https://github.com/WebAssembly/testsuite.git diff --git a/external/testsuite b/external/testsuite new file mode 160000 index 00000000..7e0b83ab --- /dev/null +++ b/external/testsuite @@ -0,0 +1 @@ +Subproject commit 7e0b83aba9dbbb6e0623c9334b0f73b3bb584b90 diff --git a/kiln-build-core/src/wast.rs b/kiln-build-core/src/wast.rs index 7dfd1512..2763550a 100644 --- a/kiln-build-core/src/wast.rs +++ b/kiln-build-core/src/wast.rs @@ -619,7 +619,7 @@ impl WastTestRunner { /// Handle assert_return directive fn handle_assert_return_directive( &mut self, - exec: &WastExecute, + exec: &mut WastExecute, results: &[WastRet], _file_path: &Path, ) -> Result { @@ -725,7 +725,7 @@ impl WastTestRunner { /// Handle assert_trap directive fn handle_assert_trap_directive( &mut self, - exec: &WastExecute, + exec: &mut WastExecute, expected_message: &str, _file_path: &Path, ) -> Result { @@ -821,7 +821,7 @@ impl WastTestRunner { /// This expects the execution to throw an uncaught exception fn handle_assert_exception_directive( &mut self, - exec: &WastExecute, + exec: &mut WastExecute, _file_path: &Path, ) -> Result { self.stats.assert_trap_count += 1; // Count with trap tests @@ -1415,7 +1415,7 @@ impl WastTestRunner { /// Handle invoke directive (standalone function call) fn handle_invoke_directive( &mut self, - exec: &WastExecute, + exec: &mut WastExecute, _file_path: &Path, ) -> Result { // Execute the function using the real engine diff --git a/kiln-build-core/src/wast_execution.rs b/kiln-build-core/src/wast_execution.rs index 77ee6010..5639faf0 100644 --- a/kiln-build-core/src/wast_execution.rs +++ b/kiln-build-core/src/wast_execution.rs @@ -12,8 +12,10 @@ use std::sync::Arc; use anyhow::{Context, Result}; use wast::{WastExecute, WastInvoke}; use kiln_decoder::decoder::decode_module; +use kiln_foundation::types::{GlobalType, Limits, MemoryType, RefType, TableType, ValueType}; use kiln_foundation::values::Value; -use kiln_runtime::{module::Module, stackless::StacklessEngine}; +use kiln_runtime::module::{ExportKind, Module, RuntimeImportDesc}; +use kiln_runtime::stackless::StacklessEngine; // Re-export value conversion utilities from wast_values module pub use crate::wast_values::{ @@ -73,10 +75,18 @@ impl WastEngine { *Module::from_kiln_module(&kiln_module).context("Failed to convert to runtime module")?, ); - // Create a module instance from the module - // Use Arc::clone to share the module reference without copying data + // Validate all imports can be satisfied BEFORE instantiation. + // Per WebAssembly spec, if an import cannot be satisfied (unknown module/field + // or incompatible type), instantiation must fail with a link error. + self.validate_imports(&module)?; + + // Create a module instance from the module. + // Use the engine's next instance ID so FuncRefs stored during element + // segment initialization have the correct instance_id for cross-instance + // call_indirect dispatch. + let engine_instance_id = self.engine.peek_next_instance_id(); let module_instance = Arc::new( - ModuleInstance::new(Arc::clone(&module), 0) + ModuleInstance::new(Arc::clone(&module), engine_instance_id) .context("Failed to create module instance")?, ); @@ -84,6 +94,9 @@ impl WastEngine { // This must happen before populate_memories_from_module() which adds defined memories Self::resolve_spectest_memory_imports(&module, &module_instance)?; + // Resolve memory imports from registered modules + self.resolve_registered_memory_imports(&module, &module_instance)?; + // Initialize module instance resources (memories, globals, tables, data segments, etc.) module_instance .populate_memories_from_module() @@ -121,18 +134,10 @@ impl WastEngine { .initialize_dropped_segments() .context("Failed to initialize dropped segments")?; - // Initialize active data segments (writes data to memory) - #[cfg(feature = "std")] - module_instance - .initialize_data_segments() - .context("Failed to initialize data segments")?; - - // Initialize element segments - module_instance - .initialize_element_segments() - .context("Failed to initialize element segments")?; - - // Set the current module in the engine + // Register the instance with the engine BEFORE data/element initialization. + // Per WebAssembly spec (v2+), active segments written before an out-of-bounds + // access persist after instantiation failure. The instance must be registered + // so that FuncRefs written to shared tables can resolve their target instance. let instance_idx = self .engine .set_current_module(module_instance) @@ -142,6 +147,39 @@ impl WastEngine { // Link function imports from registered modules self.link_function_imports(&module, instance_idx)?; + // Validate that all non-spectest imports are satisfied + // Per WebAssembly spec: if any import cannot be resolved, the module + // is unlinkable and instantiation must fail. + self.validate_imports(&module)?; + + // Initialize active data segments (writes data to memory). + // This happens after instance registration so shared memory writes persist + // even if a later segment causes an out-of-bounds error. + #[cfg(feature = "std")] + { + let inst = self.engine.get_instance(instance_idx) + .ok_or_else(|| anyhow::anyhow!("Instance not found after registration"))?; + inst.initialize_data_segments() + .context("Failed to initialize data segments")?; + } + + // Initialize element segments (writes to tables). + // Same persistence semantics as data segments. + { + let inst = self.engine.get_instance(instance_idx) + .ok_or_else(|| anyhow::anyhow!("Instance not found after registration"))?; + inst.initialize_element_segments() + .context("Failed to initialize element segments")?; + } + + // Execute the start function if one is defined + if let Some(start_func_idx) = module.start { + self.engine.reset_call_depth(); + self.engine + .execute(instance_idx, start_func_idx as usize, vec![]) + .map_err(|e| anyhow::Error::from(e))?; + } + // Store the module and instance ID for later reference let module_name = name.unwrap_or("current").to_string(); self.modules.insert(module_name.clone(), Arc::clone(&module)); @@ -204,10 +242,7 @@ impl WastEngine { // Search the module's export table for the function module .get_export(function_name) - .filter(|export| { - use kiln_runtime::module::ExportKind; - export.kind == ExportKind::Function - }) + .filter(|export| export.kind == ExportKind::Function) .map(|export| export.index) .ok_or_else(|| { anyhow::anyhow!("Function '{}' is not exported from module", function_name) @@ -216,8 +251,6 @@ impl WastEngine { /// Get a global variable value by name pub fn get_global(&self, module_name: Option<&str>, global_name: &str) -> Result { - use kiln_runtime::module::ExportKind; - // Get the module and instance_id let (module, instance_id) = if let Some(name) = module_name { let module = self @@ -312,9 +345,8 @@ impl WastEngine { module_instance: &Arc, ) -> Result<()> { use kiln_foundation::clean_core_types::CoreMemoryType; - use kiln_foundation::types::{Limits, MemoryType}; use kiln_runtime::memory::Memory; - use kiln_runtime::module::{MemoryWrapper, RuntimeImportDesc}; + use kiln_runtime::module::MemoryWrapper; // Look for memory imports from spectest for (i, (mod_name, field_name)) in module.import_order.iter().enumerate() { @@ -327,6 +359,7 @@ impl WastEngine { let core_mem_type = CoreMemoryType { limits: Limits { min: 1, max: Some(2) }, shared: false, + memory64: false, }; let memory = Memory::new(core_mem_type).map_err(|e| { @@ -361,20 +394,21 @@ impl WastEngine { use kiln_runtime::global::Global; use kiln_runtime::module::GlobalWrapper; - // The global_import_types stores global types in order of appearance - // We need to match them with the corresponding import names let mut global_import_idx = 0usize; - for (mod_name, field_name) in module.import_order.iter() { - // Check if this import is a global by seeing if we still have global types to process - // This assumes global imports are in the same order in import_order as in global_import_types - if global_import_idx < module.global_import_types.len() { - // Check if this is a global import by looking at the field name pattern - // Spectest globals are: global_i32, global_i64, global_f32, global_f64 - let is_global = field_name.starts_with("global_"); + for (i, (mod_name, field_name)) in module.import_order.iter().enumerate() { + // Use import_types to determine if this is a global import + let is_global = matches!(module.import_types.get(i), Some(RuntimeImportDesc::Global(_))); - if is_global && mod_name == "spectest" { - let global_type = &module.global_import_types[global_import_idx]; + if is_global { + if mod_name == "spectest" { + let global_type = match module.global_import_types.get(global_import_idx) { + Some(gt) => gt, + None => { + global_import_idx += 1; + continue; + }, + }; let value = match field_name.as_str() { "global_i32" => Value::I32(666), @@ -400,12 +434,9 @@ impl WastEngine { GlobalWrapper(StdArc::new(RwLock::new(global))), ) .map_err(|e| anyhow::anyhow!("Failed to set spectest global: {:?}", e))?; - - global_import_idx += 1; - } else if is_global { - // Non-spectest global import - global_import_idx += 1; } + + global_import_idx += 1; } } @@ -420,8 +451,7 @@ impl WastEngine { module: &Module, module_instance: &Arc, ) -> Result<()> { - use kiln_foundation::types::{Limits, RefType, TableType}; - use kiln_runtime::module::{RuntimeImportDesc, TableWrapper}; + use kiln_runtime::module::TableWrapper; use kiln_runtime::table::Table; // The spectest table type: (table 10 20 funcref) @@ -463,12 +493,10 @@ impl WastEngine { module: &Module, module_instance: &Arc, ) -> Result<()> { - use kiln_runtime::module::{RuntimeImportDesc, TableWrapper}; - let mut table_import_idx = 0usize; for (i, (mod_name, field_name)) in module.import_order.iter().enumerate() { - // Check if this is a table import + // Use import_types to determine if this is a table import if let Some(RuntimeImportDesc::Table(_)) = module.import_types.get(i) { // Skip spectest imports (handled separately) if mod_name == "spectest" { @@ -486,7 +514,7 @@ impl WastEngine { .map_err(|e| anyhow::anyhow!("Field name too long: {:?}", e))?; if let Some(export) = source_module_arc.exports.get(&bounded_field) { - if export.kind == kiln_runtime::module::ExportKind::Table { + if export.kind == ExportKind::Table { let source_table_idx = export.index as usize; // Get the source module instance to access its table @@ -518,6 +546,9 @@ impl WastEngine { } /// Resolve global imports from registered modules (like "G") + /// + /// Uses import_types to correctly identify which imports are globals, + /// rather than heuristic counting. fn resolve_registered_module_imports( &self, module: &Module, @@ -529,14 +560,9 @@ impl WastEngine { let mut global_import_idx = 0usize; - for (mod_name, field_name) in module.import_order.iter() { - // Count this if it's a global import - if global_import_idx >= module.global_import_types.len() { - break; - } - - // Check if this is a global import by checking if it matches our expected position - let is_global = module.global_import_types.get(global_import_idx).is_some(); + for (i, (mod_name, field_name)) in module.import_order.iter().enumerate() { + // Use import_types to determine if this is a global import + let is_global = matches!(module.import_types.get(i), Some(RuntimeImportDesc::Global(_))); if is_global { // Skip spectest imports (handled separately) @@ -555,38 +581,67 @@ impl WastEngine { .map_err(|e| anyhow::anyhow!("Field name too long: {:?}", e))?; if let Some(export) = source_module.exports.get(&bounded_field) { - if export.kind == kiln_runtime::module::ExportKind::Global { + if export.kind == ExportKind::Global { let source_global_idx = export.index as usize; - // Get the value from the source module's global - if let Ok(global_wrapper) = source_module.globals.get(source_global_idx) - { - if let Ok(guard) = global_wrapper.0.read() { - let value = guard.get(); - let global_type = - &module.global_import_types[global_import_idx]; - - // Create a new global with the resolved value - let global = Global::new( - global_type.value_type, - global_type.mutable, - value.clone(), - ) + // Share the GlobalWrapper from the source instance directly. + // This ensures mutable globals are shared (not copied) so that + // mutations in one module are visible in the other. + let shared = if let Some(&inst_id) = self.instance_ids.get(mod_name) { + if let Some(inst) = self.engine.get_instance(inst_id) { + inst.global(source_global_idx as u32).ok() + } else { + None + } + } else { + None + }; + + if let Some(wrapper) = shared { + module_instance + .set_global(global_import_idx, wrapper) .map_err(|e| { - anyhow::anyhow!("Failed to create imported global: {:?}", e) + anyhow::anyhow!( + "Failed to set imported global: {:?}", + e + ) })?; + } else { + // Fallback: create from value (only if instance not found) + let source_value = self.get_source_global_value( + mod_name, + source_module, + source_global_idx, + ); - module_instance - .set_global( - global_import_idx, - GlobalWrapper(StdArc::new(RwLock::new(global))), + if let Some(value) = source_value { + if let Some(global_type) = + module.global_import_types.get(global_import_idx) + { + let global = Global::new( + global_type.value_type, + global_type.mutable, + value, ) .map_err(|e| { anyhow::anyhow!( - "Failed to set imported global: {:?}", + "Failed to create imported global: {:?}", e ) })?; + + module_instance + .set_global( + global_import_idx, + GlobalWrapper(StdArc::new(RwLock::new(global))), + ) + .map_err(|e| { + anyhow::anyhow!( + "Failed to set imported global: {:?}", + e + ) + })?; + } } } } @@ -600,24 +655,662 @@ impl WastEngine { Ok(()) } + /// Get the value of a global from a source module, checking the runtime instance first + fn get_source_global_value( + &self, + mod_name: &str, + source_module: &Module, + source_global_idx: usize, + ) -> Option { + // Try to get from the runtime instance first (has up-to-date values) + if let Some(&instance_id) = self.instance_ids.get(mod_name) { + if let Some(instance) = self.engine.get_instance(instance_id) { + if let Ok(gw) = instance.global(source_global_idx as u32) { + if let Ok(val) = gw.get() { + return Some(val); + } + } + } + } + // Fall back to the module's globals array (static init values) + if let Some(global_wrapper) = source_module.globals.get(source_global_idx) { + if let Ok(guard) = global_wrapper.0.read() { + return Some(guard.get().clone()); + } + } + None + } + + /// Resolve memory imports from registered modules + /// + /// This handles cross-module memory sharing where a module imports a memory + /// exported by another registered module (e.g., `(import "Mm" "mem" (memory 1))`) + fn resolve_registered_memory_imports( + &self, + module: &Module, + module_instance: &Arc, + ) -> Result<()> { + let mut memory_import_idx = 0usize; + + for (i, (mod_name, field_name)) in module.import_order.iter().enumerate() { + // Use import_types to determine if this is a memory import + if let Some(RuntimeImportDesc::Memory(_)) = module.import_types.get(i) { + // Skip spectest imports (handled separately) + if mod_name == "spectest" { + memory_import_idx += 1; + continue; + } + + // Look up the registered module + if let Some(source_module_arc) = self.modules.get(mod_name) { + let bounded_field = + kiln_foundation::bounded::BoundedString::<256>::from_str_truncate( + field_name, + ) + .map_err(|e| anyhow::anyhow!("Field name too long: {:?}", e))?; + + if let Some(export) = source_module_arc.exports.get(&bounded_field) { + if export.kind == ExportKind::Memory { + let source_memory_idx = export.index as usize; + + // Get the memory from the source instance + if let Some(&source_instance_id) = self.instance_ids.get(mod_name) { + if let Some(source_instance) = + self.engine.get_instance(source_instance_id) + { + if let Ok(memory_wrapper) = + source_instance.memory(source_memory_idx as u32) + { + // Share the memory (clone the Arc, not the data) + module_instance + .set_memory(memory_import_idx, memory_wrapper) + .map_err(|e| { + anyhow::anyhow!( + "Failed to set imported memory: {:?}", + e + ) + })?; + } + } + } + } + } + } + + memory_import_idx += 1; + } + } + + Ok(()) + } + + /// Validate all imports can be satisfied before instantiation. + /// + /// Per WebAssembly spec section 4.5.4, instantiation validates that: + /// - Every import has a matching export in the registered modules + /// - Function types match structurally + /// - Global types match (value type and mutability) + /// - Memory limits are compatible (import min <= export min, etc.) + /// - Table element types match and limits are compatible + fn validate_imports(&self, module: &Module) -> Result<()> { + for (i, (mod_name, field_name)) in module.import_order.iter().enumerate() { + let import_desc = match module.import_types.get(i) { + Some(desc) => desc, + None => continue, + }; + + // Skip spectest imports (handled by spectest resolution) + if mod_name == "spectest" { + continue; + } + + // Look up the source module + let source_module = match self.modules.get(mod_name) { + Some(m) => m, + None => { + return Err(anyhow::anyhow!( + "unknown import: module '{}' field '{}' not found (no module '{}' registered)", + mod_name, + field_name, + mod_name + )); + }, + }; + + // Look up the export in the source module + let bounded_field = + kiln_foundation::bounded::BoundedString::<256>::from_str_truncate(field_name) + .map_err(|e| anyhow::anyhow!("Field name too long: {:?}", e))?; + + let export = match source_module.exports.get(&bounded_field) { + Some(e) => e, + None => { + return Err(anyhow::anyhow!( + "unknown import: module '{}' does not export '{}'", + mod_name, + field_name + )); + }, + }; + + // Validate import/export type compatibility + match import_desc { + RuntimeImportDesc::Function(type_idx) => { + if export.kind != ExportKind::Function { + return Err(anyhow::anyhow!( + "incompatible import type: '{}'::'{}' - expected function, got {:?}", + mod_name, + field_name, + export.kind + )); + } + // Validate function type signature matches + self.validate_function_import_type( + module, + *type_idx, + source_module, + export.index as usize, + mod_name, + field_name, + )?; + }, + RuntimeImportDesc::Global(import_global_type) => { + if export.kind != ExportKind::Global { + return Err(anyhow::anyhow!( + "incompatible import type: '{}'::'{}' - expected global, got {:?}", + mod_name, + field_name, + export.kind + )); + } + self.validate_global_import_type( + import_global_type, + source_module, + export.index as usize, + mod_name, + field_name, + )?; + }, + RuntimeImportDesc::Memory(import_mem_type) => { + if export.kind != ExportKind::Memory { + return Err(anyhow::anyhow!( + "incompatible import type: '{}'::'{}' - expected memory, got {:?}", + mod_name, + field_name, + export.kind + )); + } + self.validate_memory_import_type( + import_mem_type, + source_module, + export.index as usize, + mod_name, + field_name, + )?; + }, + RuntimeImportDesc::Table(import_table_type) => { + if export.kind != ExportKind::Table { + return Err(anyhow::anyhow!( + "incompatible import type: '{}'::'{}' - expected table, got {:?}", + mod_name, + field_name, + export.kind + )); + } + self.validate_table_import_type( + import_table_type, + source_module, + export.index as usize, + mod_name, + field_name, + )?; + }, + RuntimeImportDesc::Tag(_) => { + if export.kind != ExportKind::Tag { + return Err(anyhow::anyhow!( + "incompatible import type: '{}'::'{}' - expected tag, got {:?}", + mod_name, + field_name, + export.kind + )); + } + }, + // Component model import types not used in WAST core module tests + _ => {}, + } + } + Ok(()) + } + + /// Validate that a function import's type matches the exported function's type + fn validate_function_import_type( + &self, + importing_module: &Module, + import_type_idx: u32, + source_module: &Module, + export_func_idx: usize, + mod_name: &str, + field_name: &str, + ) -> Result<()> { + // Get the expected function type from the importing module + let import_func_type = match importing_module.types.get(import_type_idx as usize) { + Some(ft) => ft, + None => return Ok(()), // Type index out of bounds, skip validation + }; + + // Get the source function's type + let source_func = match source_module.functions.get(export_func_idx) { + Some(f) => f, + None => return Ok(()), // Function index out of bounds, skip + }; + let source_func_type = match source_module.types.get(source_func.type_idx as usize) { + Some(ft) => ft, + None => return Ok(()), // Type index out of bounds, skip + }; + + // Compare parameter and result types + if import_func_type.params.len() != source_func_type.params.len() + || import_func_type.results.len() != source_func_type.results.len() + { + return Err(anyhow::anyhow!( + "incompatible import type: function '{}'::'{}' signature mismatch - \ + expected ({} params, {} results), got ({} params, {} results)", + mod_name, + field_name, + import_func_type.params.len(), + import_func_type.results.len(), + source_func_type.params.len(), + source_func_type.results.len(), + )); + } + + // Check each parameter type + for idx in 0..import_func_type.params.len() { + let import_param = import_func_type.params.get(idx); + let source_param = source_func_type.params.get(idx); + match (import_param, source_param) { + (Some(ip), Some(sp)) if ip == sp => continue, + (Some(ip), Some(sp)) => { + return Err(anyhow::anyhow!( + "incompatible import type: function '{}'::'{}' param {} mismatch - \ + expected {:?}, got {:?}", + mod_name, + field_name, + idx, + ip, + sp, + )); + }, + _ => { + return Err(anyhow::anyhow!( + "incompatible import type: function '{}'::'{}' param {} - \ + failed to compare types", + mod_name, + field_name, + idx, + )); + }, + } + } + + // Check each result type + for idx in 0..import_func_type.results.len() { + let import_result = import_func_type.results.get(idx); + let source_result = source_func_type.results.get(idx); + match (import_result, source_result) { + (Some(ir), Some(sr)) if ir == sr => continue, + (Some(ir), Some(sr)) => { + return Err(anyhow::anyhow!( + "incompatible import type: function '{}'::'{}' result {} mismatch - \ + expected {:?}, got {:?}", + mod_name, + field_name, + idx, + ir, + sr, + )); + }, + _ => { + return Err(anyhow::anyhow!( + "incompatible import type: function '{}'::'{}' result {} - \ + failed to compare types", + mod_name, + field_name, + idx, + )); + }, + } + } + + Ok(()) + } + + /// Validate that a global import's type is compatible with the exported global + /// + /// Per WebAssembly spec: + /// - For immutable globals: import value type must be a subtype of export value type + /// - For mutable globals: import and export must have exactly the same type + /// - Mutability must match + fn validate_global_import_type( + &self, + import_global_type: &GlobalType, + source_module: &Module, + export_global_idx: usize, + mod_name: &str, + field_name: &str, + ) -> Result<()> { + // Get the source global's type + // The export index is into the combined (imports + defined) global index space + let source_global_type = + self.get_source_global_type(source_module, export_global_idx, mod_name); + + if let Some(source_type) = source_global_type { + // Mutability must match + if import_global_type.mutable != source_type.mutable { + return Err(anyhow::anyhow!( + "incompatible import type: global '{}'::'{}' mutability mismatch - \ + import is {}, export is {}", + mod_name, + field_name, + if import_global_type.mutable { "mutable" } else { "immutable" }, + if source_type.mutable { "mutable" } else { "immutable" }, + )); + } + + // For mutable globals, types must match exactly + // For immutable globals, import type must be a subtype of export type + if import_global_type.mutable { + // Mutable globals: types must match exactly + if !value_types_match_exact( + &import_global_type.value_type, + &source_type.value_type, + ) { + return Err(anyhow::anyhow!( + "incompatible import type: global '{}'::'{}' type mismatch - \ + import {:?}, export {:?}", + mod_name, + field_name, + import_global_type.value_type, + source_type.value_type, + )); + } + } else { + // Immutable globals: import type must match or be a supertype + if !value_type_is_subtype_of( + &source_type.value_type, + &import_global_type.value_type, + ) { + return Err(anyhow::anyhow!( + "incompatible import type: global '{}'::'{}' type mismatch - \ + import {:?}, export {:?}", + mod_name, + field_name, + import_global_type.value_type, + source_type.value_type, + )); + } + } + } + + Ok(()) + } + + /// Get the GlobalType for an exported global from a source module + fn get_source_global_type( + &self, + source_module: &Module, + export_global_idx: usize, + mod_name: &str, + ) -> Option { + // Check if the index is in the import range + let num_global_imports = source_module.num_global_imports; + if export_global_idx < num_global_imports { + // This is an imported global + return source_module + .global_import_types + .get(export_global_idx) + .cloned(); + } + + // This is a defined global - check deferred_global_inits + let defined_idx = export_global_idx - num_global_imports; + if let Some((global_type, _)) = source_module.deferred_global_inits.get(defined_idx) { + return Some(global_type.clone()); + } + + // Try to get from runtime instance + if let Some(&instance_id) = self.instance_ids.get(mod_name) { + if let Some(instance) = self.engine.get_instance(instance_id) { + if let Ok(gw) = instance.global(export_global_idx as u32) { + if let Ok(val) = gw.get() { + let value_type = match &val { + Value::I32(_) => ValueType::I32, + Value::I64(_) => ValueType::I64, + Value::F32(_) => ValueType::F32, + Value::F64(_) => ValueType::F64, + Value::V128(_) => ValueType::V128, + Value::FuncRef(_) => ValueType::FuncRef, + Value::ExternRef(_) => ValueType::ExternRef, + _ => ValueType::I32, + }; + // We cannot determine mutability from the value alone, + // but we can check if it's in global_import_types or deferred_global_inits + // For now, allow the check to pass by returning None + let _ = value_type; + } + } + } + } + + None + } + + /// Validate memory import type compatibility + /// + /// Per WebAssembly spec: + /// - Import min must be <= export min (the export provides at least what's needed) + /// - If import has max, export must also have max and export max <= import max + fn validate_memory_import_type( + &self, + import_mem_type: &MemoryType, + source_module: &Module, + export_memory_idx: usize, + mod_name: &str, + field_name: &str, + ) -> Result<()> { + // Get the source memory's actual limits from the runtime instance + let source_limits = self.get_source_memory_limits(source_module, export_memory_idx, mod_name); + + if let Some(source_lim) = source_limits { + // Import min must be <= export min + if import_mem_type.limits.min > source_lim.min { + return Err(anyhow::anyhow!( + "incompatible import type: memory '{}'::'{}' - \ + import min {} > export min {}", + mod_name, + field_name, + import_mem_type.limits.min, + source_lim.min, + )); + } + + // If import has max, export must also have max and export max <= import max + if let Some(import_max) = import_mem_type.limits.max { + match source_lim.max { + None => { + return Err(anyhow::anyhow!( + "incompatible import type: memory '{}'::'{}' - \ + import has max {} but export has no max", + mod_name, + field_name, + import_max, + )); + }, + Some(export_max) if export_max > import_max => { + return Err(anyhow::anyhow!( + "incompatible import type: memory '{}'::'{}' - \ + export max {} > import max {}", + mod_name, + field_name, + export_max, + import_max, + )); + }, + _ => {}, + } + } + } + + Ok(()) + } + + /// Get the limits for an exported memory from a source module + fn get_source_memory_limits( + &self, + source_module: &Module, + export_memory_idx: usize, + mod_name: &str, + ) -> Option { + // Try to get from the runtime instance + if let Some(&instance_id) = self.instance_ids.get(mod_name) { + if let Some(instance) = self.engine.get_instance(instance_id) { + if let Ok(mem_wrapper) = instance.memory(export_memory_idx as u32) { + let mem = mem_wrapper.0.as_ref(); + return Some(mem.ty.limits.clone()); + } + } + } + // Fall back to the module's memories array + if let Some(mem_wrapper) = source_module.memories.get(export_memory_idx) { + let mem = mem_wrapper.0.as_ref(); + return Some(mem.ty.limits.clone()); + } + None + } + + /// Validate table import type compatibility + /// + /// Per WebAssembly spec: + /// - Element types must match + /// - Import min must be <= export min + /// - If import has max, export must also have max and export max <= import max + fn validate_table_import_type( + &self, + import_table_type: &TableType, + source_module: &Module, + export_table_idx: usize, + mod_name: &str, + field_name: &str, + ) -> Result<()> { + // Get the source table's type from the runtime instance + let source_table_type = + self.get_source_table_type(source_module, export_table_idx, mod_name); + + if let Some(source_type) = source_table_type { + // Element types must match (or be subtypes for reference types) + if !ref_type_is_subtype_of(&source_type.element_type, &import_table_type.element_type) { + return Err(anyhow::anyhow!( + "incompatible import type: table '{}'::'{}' - \ + element type mismatch: import {:?}, export {:?}", + mod_name, + field_name, + import_table_type.element_type, + source_type.element_type, + )); + } + + // Import min must be <= export min + if import_table_type.limits.min > source_type.limits.min { + return Err(anyhow::anyhow!( + "incompatible import type: table '{}'::'{}' - \ + import min {} > export min {}", + mod_name, + field_name, + import_table_type.limits.min, + source_type.limits.min, + )); + } + + // If import has max, export must also have max and export max <= import max + if let Some(import_max) = import_table_type.limits.max { + match source_type.limits.max { + None => { + return Err(anyhow::anyhow!( + "incompatible import type: table '{}'::'{}' - \ + import has max {} but export has no max", + mod_name, + field_name, + import_max, + )); + }, + Some(export_max) if export_max > import_max => { + return Err(anyhow::anyhow!( + "incompatible import type: table '{}'::'{}' - \ + export max {} > import max {}", + mod_name, + field_name, + export_max, + import_max, + )); + }, + _ => {}, + } + } + } + + Ok(()) + } + + /// Get the TableType for an exported table from a source module + fn get_source_table_type( + &self, + source_module: &Module, + export_table_idx: usize, + mod_name: &str, + ) -> Option { + // Try to get from the runtime instance + if let Some(&instance_id) = self.instance_ids.get(mod_name) { + if let Some(instance) = self.engine.get_instance(instance_id) { + if let Ok(table_wrapper) = instance.table(export_table_idx as u32) { + let table = table_wrapper.0.as_ref(); + return Some(TableType { + element_type: table.ty.element_type.clone(), + limits: table.ty.limits.clone(), + table64: table.ty.table64, + }); + } + } + } + // Fall back to the module's tables array + if let Some(table_wrapper) = source_module.tables.get(export_table_idx) { + let table = table_wrapper.0.as_ref(); + return Some(TableType { + element_type: table.ty.element_type.clone(), + limits: table.ty.limits.clone(), + table64: table.ty.table64, + }); + } + None + } + /// Link function imports from registered modules /// /// This method sets up cross-instance function linking for imports /// from modules that have been registered (e.g., via `register "M"`). fn link_function_imports(&mut self, module: &Module, instance_id: usize) -> Result<()> { - use kiln_runtime::module::RuntimeImportDesc; - - // Use import_types Vec which parallels import_order (more reliable than BoundedMap) let import_types = &module.import_types; let import_order = &module.import_order; if import_types.len() != import_order.len() { - // Mismatch - this shouldn't happen but fall back gracefully - return Ok(()); + return Err(anyhow::anyhow!( + "import_types length ({}) does not match import_order length ({})", + import_types.len(), + import_order.len() + )); } for (i, (mod_name, field_name)) in import_order.iter().enumerate() { - // Skip spectest imports (handled by WASI stubs) + // Skip spectest imports (handled by spectest resolution) if mod_name == "spectest" { continue; } @@ -635,7 +1328,7 @@ impl WastEngine { .map_err(|e| anyhow::anyhow!("Field name too long: {:?}", e))?; if let Some(export) = source_module.exports.get(&bounded_field) { - if export.kind == kiln_runtime::module::ExportKind::Function { + if export.kind == ExportKind::Function { // Set up the import link self.engine.register_import_link( instance_id, @@ -655,6 +1348,87 @@ impl WastEngine { } } +/// Check if two ValueTypes match exactly (for mutable globals) +fn value_types_match_exact(a: &ValueType, b: &ValueType) -> bool { + a == b +} + +/// Check if `sub` is a subtype of `sup` for ValueType. +/// +/// Per WebAssembly spec, the subtyping rules for reference types are: +/// - funcref <: funcref +/// - externref <: externref +/// - (ref $t) <: (ref null func) when $t is a function type +/// - (ref null $t) <: (ref null func) when $t is a function type +/// - (ref $t) <: (ref $t) +/// - (ref $t) <: (ref null $t) +/// - (ref null $t) <: (ref null $t) +/// +/// For non-reference types, types must match exactly. +fn value_type_is_subtype_of(sub: &ValueType, sup: &ValueType) -> bool { + if sub == sup { + return true; + } + + match (sub, sup) { + // FuncRef is a subtype of FuncRef + (ValueType::FuncRef, ValueType::FuncRef) => true, + // ExternRef is a subtype of ExternRef + (ValueType::ExternRef, ValueType::ExternRef) => true, + // TypedFuncRef(idx, _) <: FuncRef (= ref null func) + // Any typed function reference is a subtype of the nullable abstract funcref. + // This covers: + // (ref null $t) <: (ref null func) when $t is a function type + // (ref $t) <: (ref null func) + // (ref func) <: (ref null func) + (ValueType::TypedFuncRef(_, _), ValueType::FuncRef) => true, + // TypedFuncRef(idx, false) <: TypedFuncRef(idx, true) + // Non-nullable is a subtype of nullable for the same type index + (ValueType::TypedFuncRef(idx1, false), ValueType::TypedFuncRef(idx2, true)) + if idx1 == idx2 => + { + true + } + // TypedFuncRef(concrete_idx, _) <: TypedFuncRef(u32::MAX, _) + // Any concrete func ref is a subtype of the abstract (ref func) or (ref null func). + // u32::MAX is a sentinel for abstract func heap type. + // Non-nullable concrete <: non-nullable abstract + (ValueType::TypedFuncRef(idx, false), ValueType::TypedFuncRef(u32::MAX, false)) + if *idx != u32::MAX => + { + true + } + // Any concrete (nullable or not) <: nullable abstract + (ValueType::TypedFuncRef(idx, _), ValueType::TypedFuncRef(u32::MAX, true)) + if *idx != u32::MAX => + { + true + } + // NullFuncRef (ref null nofunc) is the bottom type for the func hierarchy + // It is a subtype of any nullable funcref type + (ValueType::NullFuncRef, ValueType::FuncRef) => true, + (ValueType::NullFuncRef, ValueType::TypedFuncRef(_, true)) => true, + // Numeric types and all other cases must match exactly + _ => false, + } +} + +/// Check if reference type `sub` is a subtype of `sup`. +/// +/// For table element types, we need subtyping checks: +/// - funcref <: funcref +/// - externref <: externref +/// - (ref null $t) <: (ref null func) only when types structurally match +fn ref_type_is_subtype_of(sub: &RefType, sup: &RefType) -> bool { + if sub == sup { + return true; + } + + // RefType subtyping: same heap type and nullable match + // For basic types, only exact match (already handled above) + false +} + impl core::fmt::Debug for WastEngine { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_struct("WastEngine") @@ -677,12 +1451,23 @@ pub fn execute_wast_invoke(engine: &mut WastEngine, invoke: &WastInvoke) -> Resu } /// Helper function to execute a WAST execute directive -pub fn execute_wast_execute(engine: &mut WastEngine, execute: &WastExecute) -> Result> { +/// +/// Takes `&mut WastExecute` because `WastExecute::Wat` modules need to be +/// encoded via `encode()` which requires `&mut self`. +pub fn execute_wast_execute(engine: &mut WastEngine, execute: &mut WastExecute) -> Result> { match execute { WastExecute::Invoke(invoke) => execute_wast_invoke(engine, invoke), - WastExecute::Wat(_) => { - // WAT modules need to be compiled and executed - Err(anyhow::anyhow!("WAT execution not yet implemented")) + WastExecute::Wat(wat) => { + // WAT modules in assert_trap need to be compiled and instantiated. + // The trap occurs during instantiation (e.g., out-of-bounds table/memory + // access during element/data segment initialization, or start function trap). + let binary = wat.encode().map_err(|e| { + anyhow::anyhow!("Failed to encode WAT module: {}", e) + })?; + // Load the module - this triggers instantiation which may trap + engine.load_module(None, &binary)?; + // If we get here, the module loaded successfully (no trap) + Ok(vec![]) }, WastExecute::Get { module, global, .. } => { // Global variable access @@ -754,9 +1539,9 @@ pub fn run_simple_wast_test(wast_content: &str) -> Result<()> { )); }, }, - WastDirective::AssertTrap { exec, message, .. } => { + WastDirective::AssertTrap { mut exec, message, .. } => { // Test that execution traps with expected error - match execute_wast_execute(&mut engine, &exec) { + match execute_wast_execute(&mut engine, &mut exec) { Err(_) => { // Expected trap occurred }, @@ -1251,6 +2036,419 @@ mod tests { } } + #[test] + fn test_exception_basic_throw_catch() { + // Test basic throw + catch with i32 value passing + // Pattern: (block $h (result i32) (try_table (catch $e $h) (throw $e (i32.const 5))) (i32.const 0)) + // Expected: throw fires, catch matches, pushes 5 to block $h, returns 5 + let wast_content = r#" + (module + (tag $e0 (param i32)) + (func (export "catch-i32") (result i32) + (block $h (result i32) + (try_table (catch $e0 $h) + (throw $e0 (i32.const 5)) + ) + (i32.const 0) + ) + ) + ) + (assert_return (invoke "catch-i32") (i32.const 5)) + "#; + + let result = run_simple_wast_test(wast_content); + assert!(result.is_ok(), "Basic throw/catch failed: {:?}", result.err()); + } + + #[test] + fn test_exception_no_throw() { + // Test try_table where no exception is thrown (normal flow) + let wast_content = r#" + (module + (tag $e0 (param i32)) + (func (export "no-throw") (result i32) + (block $h (result i32) + (try_table (catch $e0 $h) + (i32.const 42) + (br 1) + ) + (i32.const 0) + ) + ) + ) + (assert_return (invoke "no-throw") (i32.const 42)) + "#; + + let result = run_simple_wast_test(wast_content); + assert!(result.is_ok(), "No-throw flow failed: {:?}", result.err()); + } + + #[test] + fn test_exception_catch_all() { + // Test catch_all handler + let wast_content = r#" + (module + (tag $e0 (param i32)) + (func (export "catch-all") (result i32) + (block $h + (try_table (catch_all $h) + (throw $e0 (i32.const 5)) + ) + (i32.const 0) + (return) + ) + (i32.const 1) + ) + ) + (assert_return (invoke "catch-all") (i32.const 1)) + "#; + + let result = run_simple_wast_test(wast_content); + assert!(result.is_ok(), "Catch-all failed: {:?}", result.err()); + } + + #[test] + fn test_exception_catch_ref() { + // Test catch_ref handler (pushes payload + exnref) + let wast_content = r#" + (module + (tag $e0 (param i32)) + (func (export "catch-ref") (result i32) + (block $h (result i32 exnref) + (try_table (catch_ref $e0 $h) + (throw $e0 (i32.const 7)) + ) + (i32.const 0) + (ref.null exn) + ) + (drop) + ) + ) + (assert_return (invoke "catch-ref") (i32.const 7)) + "#; + + let result = run_simple_wast_test(wast_content); + assert!(result.is_ok(), "Catch-ref failed: {:?}", result.err()); + } + + #[test] + #[ignore] // throw_ref stack management needs further runtime work + fn test_exception_throw_ref() { + // Test throw_ref: catch an exception with catch_all_ref, then re-throw via throw_ref + // Inner try_table catches with catch_all_ref, producing exnref on $h1 + // The exnref is then re-thrown with throw_ref + // Outer try_table catches with catch, producing i32 on $h2 + let wast_content = r#" + (module + (tag $e0 (param i32)) + (func (export "throw-ref") (result i32) + (block $h2 (result i32) + (try_table (catch $e0 $h2) + (block $h1 (result exnref) + (try_table (catch_all_ref $h1) + (throw $e0 (i32.const 9)) + ) + (ref.null exn) + ) + (throw_ref) + (unreachable) + ) + (unreachable) + ) + ) + ) + (assert_return (invoke "throw-ref") (i32.const 9)) + "#; + + let result = run_simple_wast_test(wast_content); + assert!(result.is_ok(), "Throw-ref failed: {:?}", result.err()); + } + + #[test] + fn test_exception_cross_function() { + // Test exception propagation across function calls + let wast_content = r#" + (module + (tag $e0 (param i32)) + (func $thrower (throw $e0 (i32.const 11))) + (func (export "cross-func") (result i32) + (block $h (result i32) + (try_table (catch $e0 $h) + (call $thrower) + ) + (i32.const 0) + ) + ) + ) + (assert_return (invoke "cross-func") (i32.const 11)) + "#; + + let result = run_simple_wast_test(wast_content); + assert!(result.is_ok(), "Cross-function exception failed: {:?}", result.err()); + } + + #[test] + fn test_exception_uncaught_trap() { + // Test that uncaught exception causes a trap + let wast_content = r#" + (module + (tag $e0 (param i32)) + (func (export "uncaught") (result i32) + (throw $e0 (i32.const 42)) + ) + ) + (assert_trap (invoke "uncaught") "unhandled exception") + "#; + + let result = run_simple_wast_test(wast_content); + assert!(result.is_ok(), "Uncaught exception trap failed: {:?}", result.err()); + } + + #[test] + fn test_exception_try_table_normal_end() { + // Test try_table where body completes normally (falls through to end) + // The catch handler branches to $h which expects no values (matching the empty tag) + let wast_content = r#" + (module + (tag $e0) + (func (export "normal-end") (result i32) + (block $h + (try_table (catch $e0 $h) + (nop) + ) + ) + (i32.const 99) + ) + ) + (assert_return (invoke "normal-end") (i32.const 99)) + "#; + + let result = run_simple_wast_test(wast_content); + assert!(result.is_ok(), "Normal try_table end failed: {:?}", result.err()); + } + + #[test] + #[ignore] // catch_all_ref exnref passing needs further runtime work + fn test_exception_catch_all_ref() { + // Test catch_all_ref handler + let wast_content = r#" + (module + (tag $e0 (param i32)) + (func (export "catch-all-ref") (result i32) + (block $h (result exnref) + (try_table (catch_all_ref $h) + (throw $e0 (i32.const 13)) + ) + (ref.null exn) + ) + (drop) + (i32.const 1) + ) + ) + (assert_return (invoke "catch-all-ref") (i32.const 1)) + "#; + + let result = run_simple_wast_test(wast_content); + assert!(result.is_ok(), "Catch-all-ref failed: {:?}", result.err()); + } + + #[test] + fn test_exception_null_throw_ref_trap() { + // Test that throw_ref with null exnref traps + let wast_content = r#" + (module + (func (export "null-throw-ref") + (throw_ref (ref.null exn)) + ) + ) + (assert_trap (invoke "null-throw-ref") "null exception reference") + "#; + + let result = run_simple_wast_test(wast_content); + assert!(result.is_ok(), "Null throw_ref trap failed: {:?}", result.err()); + } + + #[test] + fn test_exception_multi_param_tag() { + // Test throw + catch with multiple parameter tag + let wast_content = r#" + (module + (tag $e (param i32 i32)) + (func (export "multi-param") (result i32) + (block $h (result i32 i32) + (try_table (catch $e $h) + (throw $e (i32.const 3) (i32.const 4)) + ) + (i32.const 0) + (i32.const 0) + ) + (i32.add) + ) + ) + (assert_return (invoke "multi-param") (i32.const 7)) + "#; + + let result = run_simple_wast_test(wast_content); + assert!(result.is_ok(), "Multi-param tag failed: {:?}", result.err()); + } + + #[test] + fn test_exception_multiple_handlers() { + // Test try_table with multiple catch handlers + // First matching handler should be used + let wast_content = r#" + (module + (tag $e0 (param i32)) + (tag $e1 (param i32)) + (func (export "multi-handler") (result i32) + (block $h0 (result i32) + (block $h1 (result i32) + (try_table (catch $e0 $h0) (catch $e1 $h1) + (throw $e1 (i32.const 20)) + ) + (unreachable) + ) + ) + ) + ) + (assert_return (invoke "multi-handler") (i32.const 20)) + "#; + + let result = run_simple_wast_test(wast_content); + assert!(result.is_ok(), "Multiple handlers failed: {:?}", result.err()); + } + + #[test] + fn test_exception_nested_try_table() { + // Test nested try_table blocks - inner one catches, outer doesn't fire + let wast_content = r#" + (module + (tag $e (param i32)) + (func (export "nested") (result i32) + (block $outer (result i32) + (try_table (catch $e $outer) + (block $inner (result i32) + (try_table (catch $e $inner) + (throw $e (i32.const 30)) + ) + (unreachable) + ) + (i32.const 1) + (i32.add) + (return) + ) + (unreachable) + ) + ) + ) + (assert_return (invoke "nested") (i32.const 31)) + "#; + + let result = run_simple_wast_test(wast_content); + assert!(result.is_ok(), "Nested try_table failed: {:?}", result.err()); + } + + #[test] + fn test_exception_try_table_with_result() { + // Test try_table with non-empty result type (body completes normally) + let wast_content = r#" + (module + (tag $e (param i32)) + (func (export "try-result") (result i32) + (block $h (result i32) + (try_table (result i32) (catch $e $h) + (i32.const 42) + ) + ) + ) + ) + (assert_return (invoke "try-result") (i32.const 42)) + "#; + + let result = run_simple_wast_test(wast_content); + assert!(result.is_ok(), "try_table with result failed: {:?}", result.err()); + } + + #[test] + fn test_exception_deeply_nested_throw() { + // Test exception thrown from deep within nested blocks + let wast_content = r#" + (module + (tag $e (param i32)) + (func (export "deep") (result i32) + (block $h (result i32) + (try_table (catch $e $h) + (block + (block + (throw $e (i32.const 50)) + ) + ) + ) + (unreachable) + ) + ) + ) + (assert_return (invoke "deep") (i32.const 50)) + "#; + + let result = run_simple_wast_test(wast_content); + assert!(result.is_ok(), "Deeply nested throw failed: {:?}", result.err()); + } + + #[test] + fn test_exception_tag_mismatch() { + // Test that catch only catches matching tag, not a different one + // Inner try_table catches $e1 but we throw $e0 -- should propagate to outer + let wast_content = r#" + (module + (tag $e0 (param i32)) + (tag $e1 (param i32)) + (func (export "mismatch") (result i32) + (block $outer (result i32) + (try_table (catch $e0 $outer) + (block $inner (result i32) + (try_table (catch $e1 $inner) + (throw $e0 (i32.const 60)) + ) + (unreachable) + ) + (unreachable) + ) + (unreachable) + ) + ) + ) + (assert_return (invoke "mismatch") (i32.const 60)) + "#; + + let result = run_simple_wast_test(wast_content); + assert!(result.is_ok(), "Tag mismatch failed: {:?}", result.err()); + } + + #[test] + fn test_exception_empty_tag() { + // Test throw/catch with tag that has no parameters + let wast_content = r#" + (module + (tag $e) + (func (export "empty-tag") (result i32) + (block $h + (try_table (catch $e $h) + (throw $e) + ) + (i32.const 1) + (return) + ) + (i32.const 0) + ) + ) + (assert_return (invoke "empty-tag") (i32.const 0)) + "#; + + let result = run_simple_wast_test(wast_content); + assert!(result.is_ok(), "Empty tag failed: {:?}", result.err()); + } + #[test] fn test_try_table_gc_module_decode() { use wast::{ diff --git a/kiln-build-core/src/wast_validator.rs b/kiln-build-core/src/wast_validator.rs index b8d6f949..97ce55aa 100644 --- a/kiln-build-core/src/wast_validator.rs +++ b/kiln-build-core/src/wast_validator.rs @@ -12,7 +12,7 @@ use anyhow::{Context, Result, anyhow}; use std::collections::HashSet; -use kiln_format::module::{ExportKind, Function, Global, ImportDesc, Module}; +use kiln_format::module::{CompositeTypeKind, ExportKind, Function, Global, ImportDesc, Module}; use kiln_format::pure_format_types::{PureElementInit, PureElementMode, PureElementSegment}; use kiln_format::types::RefType; use kiln_foundation::ValueType; @@ -30,10 +30,27 @@ pub enum StackType { /// This is ref null nofunc - assignable to any nullable funcref type NullFuncRef, ExternRef, + /// Null extern reference - bottom type for extern hierarchy (noextern) + NullExternRef, ExnRef, + /// Null exception reference - bottom type for exn hierarchy (noexn) + NullExnRef, /// Typed function reference (ref null? $t) where t is a type index /// First field is type index, second is whether it's nullable TypedFuncRef(u32, bool), + /// GC abstract heap types + /// anyref = (ref null any) - top of internal reference hierarchy + AnyRef, + /// eqref = (ref null eq) - types supporting ref.eq + EqRef, + /// i31ref = (ref null i31) - unboxed 31-bit integers + I31Ref, + /// structref = (ref null struct) - supertype of all struct types + StructRef, + /// arrayref = (ref null array) - supertype of all array types + ArrayRef, + /// none - bottom type for any hierarchy + NullRef, Unknown, } @@ -52,13 +69,14 @@ impl StackType { ValueType::ExnRef => StackType::ExnRef, // Typed function reference - preserves type index and nullability ValueType::TypedFuncRef(idx, nullable) => StackType::TypedFuncRef(idx, nullable), - // WebAssembly 3.0 GC types - not yet fully supported, treat as unknown - ValueType::I16x8 - | ValueType::StructRef(_) - | ValueType::ArrayRef(_) - | ValueType::I31Ref - | ValueType::AnyRef - | ValueType::EqRef => StackType::Unknown, + // GC reference types + ValueType::AnyRef => StackType::AnyRef, + ValueType::EqRef => StackType::EqRef, + ValueType::I31Ref => StackType::I31Ref, + ValueType::StructRef(_) => StackType::StructRef, + ValueType::ArrayRef(_) => StackType::ArrayRef, + // i16x8 is a vector subtype, treat as V128 + ValueType::I16x8 => StackType::V128, } } @@ -79,21 +97,29 @@ impl StackType { StackType::FuncRef | StackType::NullFuncRef | StackType::ExternRef + | StackType::NullExternRef | StackType::ExnRef + | StackType::NullExnRef | StackType::TypedFuncRef(_, _) + | StackType::AnyRef + | StackType::EqRef + | StackType::I31Ref + | StackType::StructRef + | StackType::ArrayRef + | StackType::NullRef ) } /// Check if this type is a subtype of another type - /// GC subtyping rules: - /// - NullFuncRef <: FuncRef <: any (bottom <: top) - /// - NullFuncRef <: TypedFuncRef(t, true) (bottom is subtype of any nullable funcref) - /// - TypedFuncRef(t, _) <: FuncRef (specific is subtype of general) - /// - FuncRef bool { if self == other { return true; @@ -105,19 +131,73 @@ impl StackType { } match (self, other) { - // NullFuncRef (bottom) is subtype of all funcref-related types + // === Function reference hierarchy === + // nofunc <: (ref null? $t) <: funcref (StackType::NullFuncRef, StackType::FuncRef) => true, (StackType::NullFuncRef, StackType::TypedFuncRef(_, nullable)) => *nullable, - // TypedFuncRef is a subtype of FuncRef (StackType::TypedFuncRef(_, _), StackType::FuncRef) => true, - // Two TypedFuncRefs: type indices must match, and nullability must be compatible - // (ref $t) is a subtype of (ref null $t) - // (ref null $t) is NOT a subtype of (ref $t) (StackType::TypedFuncRef(t1, n1), StackType::TypedFuncRef(t2, n2)) => { - t1 == t2 && (*n1 == *n2 || (!*n1 && *n2)) + if t1 == t2 { + // Same type index: non-nullable <: nullable, or exact match + *n1 == *n2 || (!*n1 && *n2) + } else if *t2 == u32::MAX { + // u32::MAX is sentinel for abstract func heap type. + // Any concrete typed funcref is a subtype of abstract (ref func)/(ref null func). + // Check nullability: non-nullable <: nullable, or both match + *n1 == *n2 || (!*n1 && *n2) + } else { + false + } }, - // FuncRef is NOT a subtype of TypedFuncRef (general is not subtype of specific) (StackType::FuncRef, StackType::TypedFuncRef(_, _)) => false, + + // === External reference hierarchy === + // noextern <: externref + (StackType::NullExternRef, StackType::ExternRef) => true, + + // === Exception reference hierarchy === + // noexn <: exn + (StackType::NullExnRef, StackType::ExnRef) => true, + + // === Internal (any) reference hierarchy === + // none <: i31ref, structref, arrayref <: eqref <: anyref + // + // NullRef (none) is subtype of all nullable internal ref types + (StackType::NullRef, StackType::I31Ref) => true, + (StackType::NullRef, StackType::StructRef) => true, + (StackType::NullRef, StackType::ArrayRef) => true, + (StackType::NullRef, StackType::EqRef) => true, + (StackType::NullRef, StackType::AnyRef) => true, + // NullRef (none) is also subtype of concrete typed refs when nullable + (StackType::NullRef, StackType::TypedFuncRef(_, nullable)) => *nullable, + + // i31ref <: eqref <: anyref + (StackType::I31Ref, StackType::EqRef) => true, + (StackType::I31Ref, StackType::AnyRef) => true, + + // structref <: eqref <: anyref + (StackType::StructRef, StackType::EqRef) => true, + (StackType::StructRef, StackType::AnyRef) => true, + + // arrayref <: eqref <: anyref + (StackType::ArrayRef, StackType::EqRef) => true, + (StackType::ArrayRef, StackType::AnyRef) => true, + + // eqref <: anyref + (StackType::EqRef, StackType::AnyRef) => true, + + // Concrete typed references (TypedFuncRef) can be struct, array, or func types. + // TypedFuncRef is used for ALL concrete type references (ref $t). + // Concrete struct types are subtypes of structref, eqref, anyref. + // Concrete array types are subtypes of arrayref, eqref, anyref. + // Without module context we cannot distinguish, so we accept all + // concrete types as subtypes of the GC hierarchy. This is slightly + // overpermissive but prevents false "type mismatch" rejections. + (StackType::TypedFuncRef(_, _), StackType::StructRef) => true, + (StackType::TypedFuncRef(_, _), StackType::ArrayRef) => true, + (StackType::TypedFuncRef(_, _), StackType::EqRef) => true, + (StackType::TypedFuncRef(_, _), StackType::AnyRef) => true, + _ => false, } } @@ -134,9 +214,14 @@ impl StackType { HeapType::Extern => StackType::ExternRef, HeapType::Exn => StackType::ExnRef, HeapType::NoFunc => StackType::NullFuncRef, + HeapType::NoExtern => StackType::NullExternRef, + HeapType::None => StackType::NullRef, + HeapType::Any => StackType::AnyRef, + HeapType::Eq => StackType::EqRef, + HeapType::I31 => StackType::I31Ref, + HeapType::Struct => StackType::StructRef, + HeapType::Array => StackType::ArrayRef, HeapType::Concrete(idx) => StackType::TypedFuncRef(idx, gc.nullable), - // GC types not yet fully supported - _ => StackType::Unknown, } } } @@ -577,8 +662,14 @@ impl WastModuleValidator { /// Check if a ValueType contains a type index reference and validate it fn check_value_type_ref(vt: &ValueType, num_types: usize) -> Result<()> { match vt { - ValueType::TypedFuncRef(idx, _) - | ValueType::StructRef(idx) + ValueType::TypedFuncRef(idx, _) => { + // u32::MAX is a sentinel for abstract func heap type (ref func)/(ref null func) + // and does not reference a concrete type index + if *idx != u32::MAX && (*idx as usize) >= num_types { + return Err(anyhow!("unknown type")); + } + } + ValueType::StructRef(idx) | ValueType::ArrayRef(idx) => { if (*idx as usize) >= num_types { return Err(anyhow!("unknown type")); @@ -732,12 +823,13 @@ impl WastModuleValidator { match opcode { // Control flow 0x00 => { - // unreachable + // unreachable — per spec, stack becomes polymorphic. + // Always truncate stack to frame base, even if already + // unreachable, to discard concrete values pushed since + // the last terminating instruction. if let Some(frame) = frames.last_mut() { + stack.truncate(frame.stack_height); if frame.reachable { - // Truncate stack to frame base — per spec, stack becomes - // polymorphic after a terminating instruction - stack.truncate(frame.stack_height); frame.unreachable_height = Some(frame.stack_height); } frame.reachable = false; @@ -898,6 +990,20 @@ impl WastModuleValidator { } } } + } else { + // Unreachable then-branch: pop output types with + // polymorphic matching, then check for excess values. + let unreachable_height = + frame.unreachable_height.unwrap_or(frame.stack_height); + let output_types = frame.output_types.clone(); + for &expected in output_types.iter().rev() { + if !Self::pop_type(&mut stack, expected, unreachable_height, true) { + return Err(anyhow!("type mismatch")); + } + } + if stack.len() > frame.stack_height { + return Err(anyhow!("type mismatch")); + } } } @@ -914,6 +1020,7 @@ impl WastModuleValidator { if let Some(frame) = frames.last_mut() { frame.frame_type = FrameType::Else; frame.reachable = true; + frame.unreachable_height = None; } }, @@ -1022,8 +1129,8 @@ impl WastModuleValidator { // throw is a terminating instruction — stack becomes polymorphic if let Some(frame) = frames.last_mut() { + stack.truncate(frame.stack_height); if frame.reachable { - stack.truncate(frame.stack_height); frame.unreachable_height = Some(frame.stack_height); } frame.reachable = false; @@ -1045,8 +1152,8 @@ impl WastModuleValidator { // rethrow is a terminating instruction — stack becomes polymorphic if let Some(frame) = frames.last_mut() { + stack.truncate(frame.stack_height); if frame.reachable { - stack.truncate(frame.stack_height); frame.unreachable_height = Some(frame.stack_height); } frame.reachable = false; @@ -1068,8 +1175,8 @@ impl WastModuleValidator { // throw_ref is a terminating instruction — stack becomes polymorphic if let Some(frame) = frames.last_mut() { + stack.truncate(frame.stack_height); if frame.reachable { - stack.truncate(frame.stack_height); frame.unreachable_height = Some(frame.stack_height); } frame.reachable = false; @@ -1086,16 +1193,21 @@ impl WastModuleValidator { let unreachable = !frame.reachable; if unreachable { - // In unreachable code, the stack is polymorphic for underflow. - // Values pushed after unreachable are concrete and must type-check. + // In unreachable code, the stack is polymorphic. + // Pop expected output types with polymorphic matching. let unreachable_height = frame.unreachable_height.unwrap_or(frame_height); + for &expected in frame.output_types.iter().rev() { if !Self::pop_type(&mut stack, expected, unreachable_height, true) { return Err(anyhow!("type mismatch")); } } - stack.truncate(frame_height); + // After popping output types, reject excess concrete + // values above the frame base. + if stack.len() > frame_height { + return Err(anyhow!("type mismatch")); + } } else { // In reachable code, check exact stack height and types let expected_height = frame_height + frame.output_types.len(); @@ -1128,16 +1240,19 @@ impl WastModuleValidator { let unreachable = !frame.reachable; if unreachable { - // In unreachable code, the stack is polymorphic for underflow. - // Values pushed after unreachable are concrete and must type-check. + // In unreachable code, the stack is polymorphic. + // Pop expected output types with polymorphic matching. let unreachable_height = frame.unreachable_height.unwrap_or(frame_height); + for &expected in frame.output_types.iter().rev() { if !Self::pop_type(&mut stack, expected, unreachable_height, true) { return Err(anyhow!("type mismatch")); } } - // Truncate stack to frame height - stack.truncate(frame_height); + // After popping output types, reject excess concrete values + if stack.len() > frame_height { + return Err(anyhow!("type mismatch")); + } } else { // In reachable code, check exact stack height and types let expected_height = frame_height + frame.output_types.len(); @@ -1169,8 +1284,8 @@ impl WastModuleValidator { // Mark current frame as unreachable — stack becomes polymorphic if let Some(frame) = frames.last_mut() { + stack.truncate(frame.stack_height); if frame.reachable { - stack.truncate(frame.stack_height); frame.unreachable_height = Some(frame.stack_height); } frame.reachable = false; @@ -1293,8 +1408,8 @@ impl WastModuleValidator { // Mark current frame as unreachable — stack becomes polymorphic if let Some(frame) = frames.last_mut() { + stack.truncate(frame.stack_height); if frame.reachable { - stack.truncate(frame.stack_height); frame.unreachable_height = Some(frame.stack_height); } frame.reachable = false; @@ -1317,8 +1432,8 @@ impl WastModuleValidator { } if let Some(frame) = frames.last_mut() { + stack.truncate(frame.stack_height); if frame.reachable { - stack.truncate(frame.stack_height); frame.unreachable_height = Some(frame.stack_height); } frame.reachable = false; @@ -1383,10 +1498,11 @@ impl WastModuleValidator { let func_type = &module.types[type_idx as usize]; let frame_height = Self::current_frame_height(&frames); - // Pop table index (must be i32) + // Pop table index (i64 if table64, i32 otherwise) + let ci_it = if Self::is_table64(module, table_idx) { StackType::I64 } else { StackType::I32 }; if !Self::pop_type( &mut stack, - StackType::I32, + ci_it, frame_height, Self::is_unreachable(&frames), ) { @@ -1450,8 +1566,8 @@ impl WastModuleValidator { // return_call is a terminating instruction (like return) if let Some(frame) = frames.last_mut() { + stack.truncate(frame.stack_height); if frame.reachable { - stack.truncate(frame.stack_height); frame.unreachable_height = Some(frame.stack_height); } frame.reachable = false; @@ -1500,10 +1616,11 @@ impl WastModuleValidator { let frame_height = Self::current_frame_height(&frames); - // Pop table index (must be i32) + // Pop table index (i64 if table64, i32 otherwise) + let rci_it = if Self::is_table64(module, table_idx) { StackType::I64 } else { StackType::I32 }; if !Self::pop_type( &mut stack, - StackType::I32, + rci_it, frame_height, Self::is_unreachable(&frames), ) { @@ -1525,8 +1642,8 @@ impl WastModuleValidator { // return_call_indirect is a terminating instruction (like return) if let Some(frame) = frames.last_mut() { + stack.truncate(frame.stack_height); if frame.reachable { - stack.truncate(frame.stack_height); frame.unreachable_height = Some(frame.stack_height); } frame.reachable = false; @@ -2313,10 +2430,25 @@ impl WastModuleValidator { 0x70 => StackType::FuncRef, 0x6F => StackType::ExternRef, 0x69 => StackType::ExnRef, + // GC abstract heap types (shorthand reference types) + 0x6E => StackType::AnyRef, + 0x6D => StackType::EqRef, + 0x6C => StackType::I31Ref, + 0x6B => StackType::StructRef, + 0x6A => StackType::ArrayRef, + 0x73 => StackType::NullFuncRef, + 0x72 => StackType::NullExternRef, + 0x71 => StackType::NullRef, 0x63 | 0x64 => { // ref null heaptype / ref heaptype - let (heap_type, new_offset) = Self::parse_heap_type(code, offset)?; + let nullable = type_byte == 0x63; + let (heap_type, new_offset) = Self::parse_heap_type(code, offset, nullable)?; offset = new_offset; + // For concrete type indices, preserve nullability from the ref prefix + let heap_type = match heap_type { + ValueType::StructRef(idx) => ValueType::TypedFuncRef(idx, nullable), + other => other, + }; Self::check_value_type_ref(&heap_type, module.types.len())?; StackType::from_value_type(heap_type) }, @@ -2781,10 +2913,21 @@ impl WastModuleValidator { 0x70 | -16 => stack.push(StackType::NullFuncRef), // nofunc → NullFuncRef 0x73 | -13 => stack.push(StackType::NullFuncRef), - // extern, noextern → ExternRef - 0x6F | -17 | 0x72 | -14 => stack.push(StackType::ExternRef), + // extern → ExternRef (nullable) + 0x6F | -17 => stack.push(StackType::ExternRef), + // noextern → NullExternRef (bottom of extern hierarchy) + 0x72 | -14 => stack.push(StackType::NullExternRef), // exn → ExnRef 0x69 | -23 => stack.push(StackType::ExnRef), + // noexn → ExnRef (bottom of exn hierarchy) + 0x74 | -12 => stack.push(StackType::ExnRef), + // GC abstract heap types (all nullable since ref.null) + 0x6E | -18 => stack.push(StackType::AnyRef), // any + 0x6D | -19 => stack.push(StackType::EqRef), // eq + 0x6C | -20 => stack.push(StackType::I31Ref), // i31 + 0x6B | -21 => stack.push(StackType::StructRef), // struct + 0x6A | -22 => stack.push(StackType::ArrayRef), // array + 0x71 | -15 => stack.push(StackType::NullRef), // none (bottom of any) // Concrete type index → nullable typed reference _ if heap_type_val >= 0 => { stack.push(StackType::TypedFuncRef(heap_type_val as u32, true)); @@ -2798,7 +2941,16 @@ impl WastModuleValidator { // Pops a reference, pushes i32 let frame_height = Self::current_frame_height(&frames); let unreachable = Self::is_unreachable(&frames); - if !unreachable { + if unreachable { + // In unreachable code, pop with polymorphic underflow + if stack.len() > frame_height { + let ref_type = stack.pop().unwrap(); + if ref_type != StackType::Unknown && !ref_type.is_reference() { + return Err(anyhow!("type mismatch")); + } + } + // Underflow is OK in unreachable code + } else { if stack.len() <= frame_height { return Err(anyhow!("type mismatch")); } @@ -2831,6 +2983,76 @@ impl WastModuleValidator { let func_type_idx = Self::get_function_type_idx(module, func_idx)?; stack.push(StackType::TypedFuncRef(func_type_idx, false)); }, + // ref.eq (0xD3): [eqref eqref] -> [i32] + 0xD3 => { + let frame_height = Self::current_frame_height(&frames); + let unreachable = Self::is_unreachable(&frames); + if !Self::pop_type(&mut stack, StackType::EqRef, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + if !Self::pop_type(&mut stack, StackType::EqRef, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + stack.push(StackType::I32); + }, + // ref.as_non_null (0xD4) + 0xD4 => { + // Pop a reference, trap if null, push the non-null reference + let frame_height = Self::current_frame_height(&frames); + let unreachable = Self::is_unreachable(&frames); + if !unreachable { + if stack.len() <= frame_height { + return Err(anyhow!("type mismatch")); + } + let ref_type = stack.pop().unwrap(); + if !ref_type.is_reference() && ref_type != StackType::Unknown { + return Err(anyhow!("type mismatch")); + } + // Push back same type (now known to be non-null at runtime) + stack.push(ref_type); + } + }, + // br_on_null (0xD5) + 0xD5 => { + // Pop reference, branch if null, push non-null reference if not null + let (label, new_offset) = Self::parse_varuint32(code, offset)?; + offset = new_offset; + let frame_height = Self::current_frame_height(&frames); + let unreachable = Self::is_unreachable(&frames); + if !unreachable { + if stack.len() <= frame_height { + return Err(anyhow!("type mismatch")); + } + let ref_type = stack.pop().unwrap(); + if !ref_type.is_reference() && ref_type != StackType::Unknown { + return Err(anyhow!("type mismatch")); + } + // Branch validation (null case) + let _ = Self::validate_branch(&stack, label, &frames, false); + // Push back non-null reference (non-null case continues) + stack.push(ref_type); + } + }, + // br_on_non_null (0xD6) + 0xD6 => { + // Pop reference, branch if NOT null (with ref), continue if null + let (label, new_offset) = Self::parse_varuint32(code, offset)?; + offset = new_offset; + let frame_height = Self::current_frame_height(&frames); + let unreachable = Self::is_unreachable(&frames); + if !unreachable { + if stack.len() <= frame_height { + return Err(anyhow!("type mismatch")); + } + let ref_type = stack.pop().unwrap(); + if !ref_type.is_reference() && ref_type != StackType::Unknown { + return Err(anyhow!("type mismatch")); + } + // Validate branch target (non-null case branches with ref) + let _ = Self::validate_branch(&stack, label, &frames, false); + // Null case: reference is consumed, not pushed back + } + }, // Multi-byte prefix (0xFC) - saturating truncations, bulk memory, etc. 0xFC => { @@ -3495,6 +3717,20 @@ impl WastModuleValidator { } stack.push(StackType::V128); }, + // Relaxed SIMD ternary ops [v128, v128, v128] -> [v128]: + // f32x4.relaxed_madd(0x105), f32x4.relaxed_nmadd(0x106), + // f64x2.relaxed_madd(0x107), f64x2.relaxed_nmadd(0x108), + // i8x16.relaxed_laneselect(0x109), i16x8.relaxed_laneselect(0x10A), + // i32x4.relaxed_laneselect(0x10B), i64x2.relaxed_laneselect(0x10C), + // i32x4.relaxed_dot_i8x16_i7x16_add_s(0x113) + 0x105..=0x10C | 0x113 => { + for _ in 0..3 { + if !Self::pop_type(&mut stack, StackType::V128, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + } + stack.push(StackType::V128); + }, // All other ops: classify as unary [v128]->[v128] or binary [v128,v128]->[v128] _ => { let is_unary = matches!(simd_opcode, @@ -3503,7 +3739,11 @@ impl WastModuleValidator { 0x80 | 0x81 | 0x87..=0x8A | 0x94 | 0xA0 | 0xA1 | 0xA7..=0xAA | 0xC0 | 0xC1 | 0xC7..=0xCA | - 0xE0..=0xE3 | 0xEC..=0xEF | 0xF8..=0xFF + 0xE0..=0xE3 | 0xEC..=0xEF | 0xF8..=0xFF | + // Relaxed SIMD unary ops: + // i32x4.relaxed_trunc_f32x4_s/u (0x101-0x102), + // i32x4.relaxed_trunc_f64x2_s/u_zero (0x103-0x104) + 0x101..=0x104 ); if is_unary { if !Self::pop_type(&mut stack, StackType::V128, frame_height, unreachable) { @@ -3539,18 +3779,20 @@ impl WastModuleValidator { match sub_opcode { // struct.new $t: [field_types...] -> [(ref $t)] 0x00 => { - let (_type_idx, new_off) = Self::parse_varuint32(code, offset)?; + let (type_idx, new_off) = Self::parse_varuint32(code, offset)?; offset = new_off; - // Pop fields (we don't know how many without type info), push ref - // For now, just push the result - the type section would tell us field count - // but we approximate by not popping (avoids false negatives) - stack.push(StackType::Unknown); + // Pop field values based on struct field count from rec_groups + let field_count = Self::struct_field_count(module, type_idx); + for _ in 0..field_count { + Self::pop_type(&mut stack, StackType::Unknown, frame_height, unreachable); + } + stack.push(StackType::TypedFuncRef(type_idx, false)); }, // struct.new_default $t: [] -> [(ref $t)] 0x01 => { - let (_type_idx, new_off) = Self::parse_varuint32(code, offset)?; + let (type_idx, new_off) = Self::parse_varuint32(code, offset)?; offset = new_off; - stack.push(StackType::Unknown); + stack.push(StackType::TypedFuncRef(type_idx, false)); }, // struct.get $t $f: [(ref null $t)] -> [field_type] 0x02 => { @@ -3590,49 +3832,49 @@ impl WastModuleValidator { }, // array.new $t: [elem_type i32] -> [(ref $t)] 0x06 => { - let (_type_idx, new_off) = Self::parse_varuint32(code, offset)?; + let (type_idx, new_off) = Self::parse_varuint32(code, offset)?; offset = new_off; Self::pop_type(&mut stack, StackType::I32, frame_height, unreachable); Self::pop_type(&mut stack, StackType::Unknown, frame_height, unreachable); - stack.push(StackType::Unknown); + stack.push(StackType::TypedFuncRef(type_idx, false)); }, // array.new_default $t: [i32] -> [(ref $t)] 0x07 => { - let (_type_idx, new_off) = Self::parse_varuint32(code, offset)?; + let (type_idx, new_off) = Self::parse_varuint32(code, offset)?; offset = new_off; Self::pop_type(&mut stack, StackType::I32, frame_height, unreachable); - stack.push(StackType::Unknown); + stack.push(StackType::TypedFuncRef(type_idx, false)); }, // array.new_fixed $t $n: [elem_type * n] -> [(ref $t)] 0x08 => { - let (_type_idx, new_off) = Self::parse_varuint32(code, offset)?; + let (type_idx, new_off) = Self::parse_varuint32(code, offset)?; offset = new_off; let (count, new_off2) = Self::parse_varuint32(code, offset)?; offset = new_off2; for _ in 0..count { Self::pop_type(&mut stack, StackType::Unknown, frame_height, unreachable); } - stack.push(StackType::Unknown); + stack.push(StackType::TypedFuncRef(type_idx, false)); }, // array.new_data $t $d: [i32 i32] -> [(ref $t)] 0x09 => { - let (_type_idx, new_off) = Self::parse_varuint32(code, offset)?; + let (type_idx, new_off) = Self::parse_varuint32(code, offset)?; offset = new_off; let (_data_idx, new_off2) = Self::parse_varuint32(code, offset)?; offset = new_off2; Self::pop_type(&mut stack, StackType::I32, frame_height, unreachable); Self::pop_type(&mut stack, StackType::I32, frame_height, unreachable); - stack.push(StackType::Unknown); + stack.push(StackType::TypedFuncRef(type_idx, false)); }, // array.new_elem $t $e: [i32 i32] -> [(ref $t)] 0x0A => { - let (_type_idx, new_off) = Self::parse_varuint32(code, offset)?; + let (type_idx, new_off) = Self::parse_varuint32(code, offset)?; offset = new_off; let (_elem_idx, new_off2) = Self::parse_varuint32(code, offset)?; offset = new_off2; Self::pop_type(&mut stack, StackType::I32, frame_height, unreachable); Self::pop_type(&mut stack, StackType::I32, frame_height, unreachable); - stack.push(StackType::Unknown); + stack.push(StackType::TypedFuncRef(type_idx, false)); }, // array.get $t: [(ref null $t) i32] -> [elem_type] 0x0B => { @@ -3791,6 +4033,257 @@ impl WastModuleValidator { } }, + // Threads/Atomics instructions (0xFE prefix) - WebAssembly Threads Proposal + 0xFE => { + if offset >= code.len() { + return Err(anyhow!("unexpected end of code after 0xFE prefix")); + } + let (sub_opcode, new_offset) = Self::parse_varuint32(code, offset)?; + offset = new_offset; + + let frame_height = Self::current_frame_height(&frames); + let unreachable = Self::is_unreachable(&frames); + + match sub_opcode { + // atomic.fence (0x03) - reserved byte immediate, no stack effect + 0x03 => { + if offset >= code.len() { + return Err(anyhow!("unexpected end of code in atomic.fence")); + } + // Skip the reserved byte (must be 0x00) + offset += 1; + } + + // memory.atomic.notify (0x00): [i32 addr, i32 count] -> [i32] + 0x00 => { + if !Self::has_memory(module) { + return Err(anyhow!("unknown memory")); + } + let (_mem_idx, new_offset) = Self::parse_memarg(code, offset, module)?; + offset = new_offset; + // Pop count (i32) + if !Self::pop_type(&mut stack, StackType::I32, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + // Pop address (i32) + if !Self::pop_type(&mut stack, StackType::I32, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + stack.push(StackType::I32); + } + // memory.atomic.wait32 (0x01): [i32 addr, i32 expected, i64 timeout] -> [i32] + 0x01 => { + if !Self::has_memory(module) { + return Err(anyhow!("unknown memory")); + } + let (_mem_idx, new_offset) = Self::parse_memarg(code, offset, module)?; + offset = new_offset; + // Pop timeout (i64) + if !Self::pop_type(&mut stack, StackType::I64, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + // Pop expected (i32) + if !Self::pop_type(&mut stack, StackType::I32, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + // Pop address (i32) + if !Self::pop_type(&mut stack, StackType::I32, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + stack.push(StackType::I32); + } + // memory.atomic.wait64 (0x02): [i32 addr, i64 expected, i64 timeout] -> [i32] + 0x02 => { + if !Self::has_memory(module) { + return Err(anyhow!("unknown memory")); + } + let (_mem_idx, new_offset) = Self::parse_memarg(code, offset, module)?; + offset = new_offset; + // Pop timeout (i64) + if !Self::pop_type(&mut stack, StackType::I64, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + // Pop expected (i64) + if !Self::pop_type(&mut stack, StackType::I64, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + // Pop address (i32) + if !Self::pop_type(&mut stack, StackType::I32, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + stack.push(StackType::I32); + } + + // i32 atomic loads: [i32 addr] -> [i32] + // i32.atomic.load (0x10), i32.atomic.load8_u (0x12), i32.atomic.load16_u (0x13) + 0x10 | 0x12 | 0x13 => { + if !Self::has_memory(module) { + return Err(anyhow!("unknown memory")); + } + let (_mem_idx, new_offset) = Self::parse_memarg(code, offset, module)?; + offset = new_offset; + if !Self::pop_type(&mut stack, StackType::I32, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + stack.push(StackType::I32); + } + // i64 atomic loads: [i32 addr] -> [i64] + // i64.atomic.load (0x11), i64.atomic.load8_u (0x14), i64.atomic.load16_u (0x15), i64.atomic.load32_u (0x16) + 0x11 | 0x14 | 0x15 | 0x16 => { + if !Self::has_memory(module) { + return Err(anyhow!("unknown memory")); + } + let (_mem_idx, new_offset) = Self::parse_memarg(code, offset, module)?; + offset = new_offset; + if !Self::pop_type(&mut stack, StackType::I32, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + stack.push(StackType::I64); + } + + // i32 atomic stores: [i32 addr, i32 value] -> [] + // i32.atomic.store (0x17), i32.atomic.store8 (0x19), i32.atomic.store16 (0x1A) + 0x17 | 0x19 | 0x1A => { + if !Self::has_memory(module) { + return Err(anyhow!("unknown memory")); + } + let (_mem_idx, new_offset) = Self::parse_memarg(code, offset, module)?; + offset = new_offset; + // Pop value (i32) + if !Self::pop_type(&mut stack, StackType::I32, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + // Pop address (i32) + if !Self::pop_type(&mut stack, StackType::I32, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + } + // i64 atomic stores: [i32 addr, i64 value] -> [] + // i64.atomic.store (0x18), i64.atomic.store8 (0x1B), i64.atomic.store16 (0x1C), i64.atomic.store32 (0x1D) + 0x18 | 0x1B | 0x1C | 0x1D => { + if !Self::has_memory(module) { + return Err(anyhow!("unknown memory")); + } + let (_mem_idx, new_offset) = Self::parse_memarg(code, offset, module)?; + offset = new_offset; + // Pop value (i64) + if !Self::pop_type(&mut stack, StackType::I64, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + // Pop address (i32) + if !Self::pop_type(&mut stack, StackType::I32, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + } + + // i32 atomic RMW (add/sub/and/or/xor/xchg): [i32 addr, i32 value] -> [i32] + // i32.atomic.rmw.add (0x1E), i32.atomic.rmw8.add_u (0x20), i32.atomic.rmw16.add_u (0x21) + // i32.atomic.rmw.sub (0x25), i32.atomic.rmw8.sub_u (0x27), i32.atomic.rmw16.sub_u (0x28) + // i32.atomic.rmw.and (0x2C), i32.atomic.rmw8.and_u (0x2E), i32.atomic.rmw16.and_u (0x2F) + // i32.atomic.rmw.or (0x33), i32.atomic.rmw8.or_u (0x35), i32.atomic.rmw16.or_u (0x36) + // i32.atomic.rmw.xor (0x3A), i32.atomic.rmw8.xor_u (0x3C), i32.atomic.rmw16.xor_u (0x3D) + // i32.atomic.rmw.xchg (0x41), i32.atomic.rmw8.xchg_u (0x43), i32.atomic.rmw16.xchg_u (0x44) + 0x1E | 0x20 | 0x21 | + 0x25 | 0x27 | 0x28 | + 0x2C | 0x2E | 0x2F | + 0x33 | 0x35 | 0x36 | + 0x3A | 0x3C | 0x3D | + 0x41 | 0x43 | 0x44 => { + if !Self::has_memory(module) { + return Err(anyhow!("unknown memory")); + } + let (_mem_idx, new_offset) = Self::parse_memarg(code, offset, module)?; + offset = new_offset; + // Pop value (i32) + if !Self::pop_type(&mut stack, StackType::I32, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + // Pop address (i32) + if !Self::pop_type(&mut stack, StackType::I32, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + stack.push(StackType::I32); + } + + // i64 atomic RMW (add/sub/and/or/xor/xchg): [i32 addr, i64 value] -> [i64] + // i64.atomic.rmw.add (0x1F), i64.atomic.rmw8.add_u (0x22), i64.atomic.rmw16.add_u (0x23), i64.atomic.rmw32.add_u (0x24) + // i64.atomic.rmw.sub (0x26), i64.atomic.rmw8.sub_u (0x29), i64.atomic.rmw16.sub_u (0x2A), i64.atomic.rmw32.sub_u (0x2B) + // i64.atomic.rmw.and (0x2D), i64.atomic.rmw8.and_u (0x30), i64.atomic.rmw16.and_u (0x31), i64.atomic.rmw32.and_u (0x32) + // i64.atomic.rmw.or (0x34), i64.atomic.rmw8.or_u (0x37), i64.atomic.rmw16.or_u (0x38), i64.atomic.rmw32.or_u (0x39) + // i64.atomic.rmw.xor (0x3B), i64.atomic.rmw8.xor_u (0x3E), i64.atomic.rmw16.xor_u (0x3F), i64.atomic.rmw32.xor_u (0x40) + // i64.atomic.rmw.xchg (0x42), i64.atomic.rmw8.xchg_u (0x45), i64.atomic.rmw16.xchg_u (0x46), i64.atomic.rmw32.xchg_u (0x47) + 0x1F | 0x22 | 0x23 | 0x24 | + 0x26 | 0x29 | 0x2A | 0x2B | + 0x2D | 0x30 | 0x31 | 0x32 | + 0x34 | 0x37 | 0x38 | 0x39 | + 0x3B | 0x3E | 0x3F | 0x40 | + 0x42 | 0x45 | 0x46 | 0x47 => { + if !Self::has_memory(module) { + return Err(anyhow!("unknown memory")); + } + let (_mem_idx, new_offset) = Self::parse_memarg(code, offset, module)?; + offset = new_offset; + // Pop value (i64) + if !Self::pop_type(&mut stack, StackType::I64, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + // Pop address (i32) + if !Self::pop_type(&mut stack, StackType::I32, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + stack.push(StackType::I64); + } + + // i32.atomic.rmw.cmpxchg (0x48): [i32 addr, i32 expected, i32 replacement] -> [i32] + // i32.atomic.rmw8.cmpxchg_u (0x4A), i32.atomic.rmw16.cmpxchg_u (0x4B) + 0x48 | 0x4A | 0x4B => { + if !Self::has_memory(module) { + return Err(anyhow!("unknown memory")); + } + let (_mem_idx, new_offset) = Self::parse_memarg(code, offset, module)?; + offset = new_offset; + // Pop replacement (i32) + if !Self::pop_type(&mut stack, StackType::I32, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + // Pop expected (i32) + if !Self::pop_type(&mut stack, StackType::I32, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + // Pop address (i32) + if !Self::pop_type(&mut stack, StackType::I32, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + stack.push(StackType::I32); + } + // i64.atomic.rmw.cmpxchg (0x49): [i32 addr, i64 expected, i64 replacement] -> [i64] + // i64.atomic.rmw8.cmpxchg_u (0x4C), i64.atomic.rmw16.cmpxchg_u (0x4D), i64.atomic.rmw32.cmpxchg_u (0x4E) + 0x49 | 0x4C | 0x4D | 0x4E => { + if !Self::has_memory(module) { + return Err(anyhow!("unknown memory")); + } + let (_mem_idx, new_offset) = Self::parse_memarg(code, offset, module)?; + offset = new_offset; + // Pop replacement (i64) + if !Self::pop_type(&mut stack, StackType::I64, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + // Pop expected (i64) + if !Self::pop_type(&mut stack, StackType::I64, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + // Pop address (i32) + if !Self::pop_type(&mut stack, StackType::I32, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); + } + stack.push(StackType::I64); + } + + // Unknown atomic sub-opcode - skip + _ => {} + } + }, + // Skip other opcodes for now (will be handled by instruction executor) _ => { // For all other opcodes, try to skip variable-length immediates @@ -3921,10 +4414,19 @@ impl WastModuleValidator { 0x70 | -16 => stack.push(StackType::NullFuncRef), // nofunc → NullFuncRef 0x73 | -13 => stack.push(StackType::NullFuncRef), - // extern, noextern → ExternRef - 0x6F | -17 | 0x72 | -14 => stack.push(StackType::ExternRef), + // extern → ExternRef + 0x6F | -17 => stack.push(StackType::ExternRef), + // noextern → NullExternRef + 0x72 | -14 => stack.push(StackType::NullExternRef), // exn → ExnRef 0x69 | -23 => stack.push(StackType::ExnRef), + // GC abstract heap types + 0x6E | -18 => stack.push(StackType::AnyRef), + 0x6D | -19 => stack.push(StackType::EqRef), + 0x6C | -20 => stack.push(StackType::I31Ref), + 0x6B | -21 => stack.push(StackType::StructRef), + 0x6A | -22 => stack.push(StackType::ArrayRef), + 0x71 | -15 => stack.push(StackType::NullRef), // Concrete type index → nullable typed reference _ if heap_type >= 0 => { stack.push(StackType::TypedFuncRef(heap_type as u32, true)); @@ -4667,6 +5169,22 @@ impl WastModuleValidator { frames.last().map_or(0, |f| f.stack_height) } + /// Get the number of fields in a struct type by looking up rec_groups. + fn struct_field_count(module: &Module, type_idx: u32) -> usize { + for rec_group in &module.rec_groups { + for sub_type in &rec_group.types { + if sub_type.type_index == type_idx { + return match &sub_type.composite_kind { + CompositeTypeKind::StructWithFields(fields) => fields.len(), + CompositeTypeKind::Struct => 0, + _ => 0, + }; + } + } + } + 0 + } + /// Check if the current code path is unreachable fn is_unreachable(frames: &[ControlFrame]) -> bool { frames.last().map_or(false, |f| !f.reachable) @@ -4842,15 +5360,17 @@ impl WastModuleValidator { 0x6C => BlockType::ValueType(ValueType::I31Ref), 0x6B => BlockType::ValueType(ValueType::StructRef(0)), // abstract structref 0x6A => BlockType::ValueType(ValueType::ArrayRef(0)), // abstract arrayref - 0x73 => BlockType::ValueType(ValueType::FuncRef), // nofunc (bottom for func) + 0x74 => BlockType::ValueType(ValueType::ExnRef), // noexn (bottom for exn) + 0x73 => BlockType::ValueType(ValueType::NullFuncRef), // nofunc (bottom for func) 0x72 => BlockType::ValueType(ValueType::ExternRef), // noextern (bottom for extern) 0x71 => BlockType::ValueType(ValueType::AnyRef), // none (bottom for any) // GC typed references: (ref null? heaptype) 0x63 | 0x64 => { // 0x63 = ref null heaptype (nullable) // 0x64 = ref heaptype (non-nullable) + let nullable = byte == 0x63; // Parse the heap type following the prefix - let (heap_type, new_offset) = Self::parse_heap_type(code, offset + 1)?; + let (heap_type, new_offset) = Self::parse_heap_type(code, offset + 1, nullable)?; // Validate type index bounds for concrete references Self::check_value_type_ref(&heap_type, module.types.len())?; return Ok((BlockType::ValueType(heap_type), new_offset)); @@ -4884,7 +5404,7 @@ impl WastModuleValidator { } /// Parse a GC heap type and convert to ValueType - fn parse_heap_type(code: &[u8], offset: usize) -> Result<(ValueType, usize)> { + fn parse_heap_type(code: &[u8], offset: usize, nullable: bool) -> Result<(ValueType, usize)> { if offset >= code.len() { return Err(anyhow!("truncated heap type")); } @@ -4905,15 +5425,14 @@ impl WastModuleValidator { -21 => ValueType::StructRef(0), // struct (0x6B) - abstract -22 => ValueType::ArrayRef(0), // array (0x6A) - abstract -23 => ValueType::ExnRef, // exn (0x69) - -13 => ValueType::FuncRef, // nofunc (0x73) - bottom for func + -13 => ValueType::NullFuncRef, // nofunc (0x73) - bottom for func -14 => ValueType::ExternRef, // noextern (0x72) - bottom for extern -15 => ValueType::AnyRef, // none (0x71) - bottom for any _ => ValueType::AnyRef, // fallback } } else { // Concrete type index - reference to a defined type - // For now, map to StructRef with the type index - ValueType::StructRef(heap_type_val as u32) + ValueType::TypedFuncRef(heap_type_val as u32, nullable) }; Ok((value_type, new_offset)) @@ -5020,3 +5539,4 @@ pub enum BlockType { ValueType(ValueType), FuncType(u32), } + diff --git a/kiln-build-core/src/wast_values.rs b/kiln-build-core/src/wast_values.rs index f86966de..2fa44157 100644 --- a/kiln-build-core/src/wast_values.rs +++ b/kiln-build-core/src/wast_values.rs @@ -141,11 +141,40 @@ pub fn convert_wast_ret_core_to_value(ret: &WastRetCore) -> Result { }, } }, - WastRetCore::RefI31 => { + WastRetCore::RefI31 | WastRetCore::RefI31Shared => { // (ref.i31) - any non-null i31 reference // Use a sentinel value to indicate "any non-null i31ref" Ok(Value::I31Ref(Some(i32::MAX))) }, + WastRetCore::RefStruct => { + // (ref.struct) - any non-null struct reference + // Use a sentinel StructRef with alloc_id = u32::MAX + let sentinel = kiln_foundation::values::StructRef::new( + u32::MAX, + kiln_foundation::traits::DefaultMemoryProvider::default(), + ).map_err(|e| anyhow::anyhow!("Failed to create sentinel StructRef: {}", e))?; + Ok(Value::StructRef(Some(sentinel))) + }, + WastRetCore::RefArray => { + // (ref.array) - any non-null array reference + // Use a sentinel ArrayRef with alloc_id = u32::MAX + let mut sentinel = kiln_foundation::values::ArrayRef::new( + u32::MAX, + kiln_foundation::traits::DefaultMemoryProvider::default(), + ).map_err(|e| anyhow::anyhow!("Failed to create sentinel ArrayRef: {}", e))?; + sentinel.alloc_id = u32::MAX; + Ok(Value::ArrayRef(Some(sentinel))) + }, + WastRetCore::RefEq => { + // (ref.eq) - any non-null eqref (i31, struct, or array) + // Use I31Ref sentinel with i32::MAX - values_equal handles cross-type matching + Ok(Value::I31Ref(Some(i32::MAX))) + }, + WastRetCore::RefAny => { + // (ref.any) - any non-null anyref (i31, struct, array) + // Use I31Ref sentinel with i32::MAX - values_equal handles cross-type matching + Ok(Value::I31Ref(Some(i32::MAX))) + }, _ => { // Handle other reference types with default FuncRef Ok(Value::FuncRef(None)) @@ -332,10 +361,22 @@ pub fn values_equal(actual: &Value, expected: &Value) -> bool { // GC reference type comparisons (Value::ExnRef(a), Value::ExnRef(b)) => a == b, // I31Ref: i32::MAX sentinel means "any non-null i31ref" (from (ref.i31) in WAST) + // Also matches eqref/anyref sentinels for cross-type GC reference matching (Value::I31Ref(Some(_)), Value::I31Ref(Some(sentinel))) if *sentinel == i32::MAX => true, (Value::I31Ref(None), Value::I31Ref(Some(sentinel))) if *sentinel == i32::MAX => false, + // eqref/anyref sentinel (i32::MAX) matches any non-null struct/array/i31 + (Value::StructRef(Some(_)), Value::I31Ref(Some(sentinel))) if *sentinel == i32::MAX => true, + (Value::ArrayRef(Some(_)), Value::I31Ref(Some(sentinel))) if *sentinel == i32::MAX => true, + (Value::StructRef(None), Value::I31Ref(Some(sentinel))) if *sentinel == i32::MAX => false, + (Value::ArrayRef(None), Value::I31Ref(Some(sentinel))) if *sentinel == i32::MAX => false, (Value::I31Ref(a), Value::I31Ref(b)) => a == b, + // StructRef: type_index = u32::MAX sentinel means "any non-null structref" + (Value::StructRef(Some(_)), Value::StructRef(Some(sentinel))) if sentinel.type_index == u32::MAX => true, + (Value::StructRef(None), Value::StructRef(Some(sentinel))) if sentinel.type_index == u32::MAX => false, (Value::StructRef(a), Value::StructRef(b)) => a == b, + // ArrayRef: type_index = u32::MAX sentinel means "any non-null arrayref" + (Value::ArrayRef(Some(_)), Value::ArrayRef(Some(sentinel))) if sentinel.type_index == u32::MAX => true, + (Value::ArrayRef(None), Value::ArrayRef(Some(sentinel))) if sentinel.type_index == u32::MAX => false, (Value::ArrayRef(a), Value::ArrayRef(b)) => a == b, // Cross-type null reference comparisons for WAST testing // In GC spec, (ref.null) without type is polymorphic and matches any null reference diff --git a/kiln-component/src/components/component.rs b/kiln-component/src/components/component.rs index 7eaad8bb..89be9530 100644 --- a/kiln-component/src/components/component.rs +++ b/kiln-component/src/components/component.rs @@ -640,6 +640,7 @@ impl MemoryValue { let core_ty = kiln_runtime::CoreMemoryType { limits: ty.limits, shared: ty.shared, + memory64: ty.memory64, }; let memory = Memory::new(core_ty)?; Ok(Self { @@ -667,6 +668,7 @@ impl MemoryValue { let core_ty = kiln_runtime::CoreMemoryType { limits: ty.limits, shared: ty.shared, + memory64: ty.memory64, }; let memory = Memory::new_with_name(core_ty, name)?; Ok(Self { diff --git a/kiln-component/src/components/component_instantiation.rs b/kiln-component/src/components/component_instantiation.rs index 6fb0cd53..63117fb3 100644 --- a/kiln-component/src/components/component_instantiation.rs +++ b/kiln-component/src/components/component_instantiation.rs @@ -1342,7 +1342,9 @@ impl ComponentInstance { let module_idx = *module_idx as usize; if module_idx >= module_binaries.len() { - continue; + return Err(kiln_error::Error::component_linking_error( + "Core instance references module index out of bounds", + )); } let binary = &module_binaries[module_idx]; @@ -1444,7 +1446,14 @@ impl ComponentInstance { { // Resolve the provider handle for this specific export let provider_handle = if let Some(src_idx) = source_instance { - core_instances_map.get(src_idx).copied().unwrap_or(handle) + match core_instances_map.get(src_idx) { + Some(&h) => h, + None => { + return Err(kiln_error::Error::component_linking_error( + "InlineExports export references source instance which is not instantiated", + )); + } + } } else { handle }; @@ -1467,10 +1476,10 @@ impl ComponentInstance { ) { Ok(()) => { }, - Err(e) => println!( - " │ │ │ └─ Note: {:?}", - e - ), + Err(_e) => { + #[cfg(feature = "tracing")] + trace!(error = ?_e, "Import link note"); + }, } } // Skip the normal single-link below since we linked everything @@ -1686,12 +1695,19 @@ impl ComponentInstance { } }, - Err(e) => { - // Continue with other modules + Err(_e) => { + return Err(kiln_error::Error::component_linking_error( + "Failed to instantiate core module during component instantiation", + )); }, } }, - Err(e) => { + Err(_e) => { + #[cfg(feature = "tracing")] + tracing::error!(error = %_e, "Failed to load core module"); + return Err(kiln_error::Error::component_linking_error( + "Failed to load core module during component instantiation", + )); }, } }, @@ -2486,7 +2502,8 @@ impl ComponentInstance { for (idx, canon) in canonicals.iter().enumerate() { use kiln_format::component::CanonOperation; - print!(" Canon[{}]: ", idx); + #[cfg(feature = "tracing")] + trace!(idx = idx, "Processing canonical operation"); match &canon.operation { CanonOperation::Lift { @@ -3886,8 +3903,15 @@ impl ComponentInstance { // Try _initialize first (important for TinyGo components) match engine.execute(instance_handle, "_initialize", &[]) { - Ok(_) => println!("[CALL_NATIVE] ✓ _initialize completed"), - Err(e) => println!("[CALL_NATIVE] ⚠ _initialize skipped: {:?}", e), + Ok(_) => { + #[cfg(feature = "tracing")] + trace!("_initialize completed successfully"); + }, + Err(_) => { + // _initialize is optional - not all components have it + #[cfg(feature = "tracing")] + trace!("_initialize not found, skipping"); + }, } // Try entry point functions in order of preference: @@ -3910,7 +3934,7 @@ impl ComponentInstance { for entry_point in entry_points { match engine.execute(instance_handle, entry_point, &wasm_args) { - Ok(results) => { + Ok(_results) => { return Ok(vec![]); // wasi:cli/run returns nothing on success }, Err(e) => { @@ -3919,27 +3943,8 @@ impl ComponentInstance { } } - // All entry points failed - check available exports for debugging + // All entry points failed if let Some(e) = last_error { - // Debug: check for common exports (core module exports only) - let common_exports = [ - "_start", - "_initialize", - "run", - "main", - "memory", - "__heap_base", - "__data_end", - "cabi_realloc", - "__wasm_call_ctors", - ]; - for name in common_exports { - match engine.has_function(instance_handle, name) { - Ok(true) => println!(" ✓ {} - EXISTS", name), - Ok(false) => println!(" ✗ {} - not found", name), - Err(_) => println!(" ? {} - error checking", name), - } - } return Err(e); } } else { @@ -3987,6 +3992,9 @@ impl ComponentInstance { Error::runtime_execution_error("Failed to create tables") })?, memories: Vec::new(), + #[cfg(feature = "std")] + globals: Vec::new(), + #[cfg(not(feature = "std"))] globals: kiln_foundation::bounded::BoundedVec::new(provider.clone()).map_err(|e| { Error::runtime_execution_error("Failed to create globals") })?, @@ -4024,6 +4032,8 @@ impl ComponentInstance { #[cfg(feature = "std")] import_types: Vec::new(), num_import_functions: 0, + #[cfg(feature = "std")] + gc_types: Vec::new(), }; m.load_from_binary(&binary_clone) } diff --git a/kiln-component/src/linker/wasi_provider.rs b/kiln-component/src/linker/wasi_provider.rs index cdd0c232..c5bdfab9 100644 --- a/kiln-component/src/linker/wasi_provider.rs +++ b/kiln-component/src/linker/wasi_provider.rs @@ -1,7 +1,8 @@ //! WASI instance provider for Component Model //! //! Creates component instances that satisfy WASI Preview 2 imports. -//! This is a stub implementation - instances don't have actual WASI logic yet. +//! Each WASI interface is represented as a component instance with +//! function exports that map to the WIT-defined interface. use crate::bounded_component_infra::ComponentProvider; use crate::instantiation::{ExportValue, FunctionExport, InstanceImport}; @@ -13,6 +14,10 @@ use alloc::boxed::Box; #[cfg(feature = "std")] use std::boxed::Box; +// Tracing imports for structured logging +#[cfg(feature = "tracing")] +use kiln_foundation::tracing::{trace, warn}; + /// Provides WASI component instances /// /// Each WASI interface (e.g., wasi:cli/stdout, wasi:io/streams) is represented @@ -50,11 +55,8 @@ impl WasiInstanceProvider { let id = self.next_instance_id; self.next_instance_id += 1; - #[cfg(feature = "std")] - println!( - "[WASI-PROVIDER] Creating instance {} for {}", - id, interface_name - ); + #[cfg(feature = "tracing")] + trace!(id = id, interface = %interface_name, "Creating WASI instance"); // Create instance with exports based on interface let mut instance = InstanceImport { @@ -123,21 +125,37 @@ impl WasiInstanceProvider { // Resource-only interface - no function exports needed // The resource-drop is handled by canonical ABI }, + // WASI-NN interfaces (wasi:nn@0.2.0-rc-2024-10-28) + // These provide neural network inference for ML workloads + name if name.starts_with("wasi:nn/errors") => { + self.add_nn_errors_exports(&mut instance)?; + }, + name if name.starts_with("wasi:nn/tensor") => { + self.add_nn_tensor_exports(&mut instance)?; + }, + name if name.starts_with("wasi:nn/inference") => { + self.add_nn_inference_exports(&mut instance)?; + }, + name if name.starts_with("wasi:nn/graph") => { + self.add_nn_graph_exports(&mut instance)?; + }, + // WASI sockets interfaces (stub for components that import them) + name if name.starts_with("wasi:sockets/") => { + // Sockets are imported by some components but not yet implemented. + // Provide empty instance so linking succeeds; calls will error at runtime. + }, + // WASI random/insecure-seed (used by some components) + name if name.starts_with("wasi:random/insecure-seed") => { + self.add_simple_export(&mut instance, "insecure-seed")?; + }, _ => { - #[cfg(feature = "std")] - println!( - "[WASI-PROVIDER] Warning: No exports for unknown interface '{}'", - interface_name - ); + #[cfg(feature = "tracing")] + warn!(interface = %interface_name, "No exports for unknown WASI interface"); }, } - #[cfg(feature = "std")] - println!( - "[WASI-PROVIDER] Instance {} created with {} exports", - id, - instance.exports.len() - ); + #[cfg(feature = "tracing")] + trace!(id = id, export_count = instance.exports.len(), "WASI instance created"); Ok(instance) } @@ -148,11 +166,11 @@ impl WasiInstanceProvider { let get_stdout_index = self.next_function_index; self.next_function_index += 1; - let provider = ComponentProvider::default(); - let signature = KilnComponentType::Unit(provider)?; - + // Use Default::default() for signature since the WASI provider only needs the + // function index for dispatch - the full ComponentType is not used at runtime. + // ComponentType::Unit(provider) fails because BoundedVec::new rejects zero-sized items. let func_export = FunctionExport { - signature, + signature: KilnComponentType::::default(), index: get_stdout_index, }; @@ -172,11 +190,8 @@ impl WasiInstanceProvider { .map_err(|_| kiln_error::Error::resource_exhausted("Too many exports"))?; } - #[cfg(feature = "std")] - println!( - "[WASI-PROVIDER] Added export: get-stdout (index {})", - get_stdout_index - ); + #[cfg(feature = "tracing")] + trace!(index = get_stdout_index, "Added export: get-stdout"); Ok(()) } @@ -296,16 +311,57 @@ impl WasiInstanceProvider { self.add_simple_export(instance, "get-terminal-stderr") } + /// Add WASI-NN errors interface exports (wasi:nn/errors@0.2.0-rc-2024-10-28) + /// + /// The errors interface defines a resource `error` with a `code` method + /// that returns an `error-code` enum. + fn add_nn_errors_exports(&mut self, instance: &mut InstanceImport) -> Result<()> { + // Resource method: [method]error.code() -> error-code + self.add_simple_export(instance, "[method]error.code") + } + + /// Add WASI-NN tensor interface exports (wasi:nn/tensor@0.2.0-rc-2024-10-28) + /// + /// The tensor interface defines a resource `tensor` with constructor and + /// data accessor. Tensor types: FP16, FP32, FP64, BF16, U8, I32, I64. + fn add_nn_tensor_exports(&mut self, instance: &mut InstanceImport) -> Result<()> { + // Constructor: [constructor]tensor(dimensions, ty, data) -> tensor + self.add_simple_export(instance, "[constructor]tensor")?; + // Method: [method]tensor.data() -> tensor-data + self.add_simple_export(instance, "[method]tensor.data") + } + + /// Add WASI-NN inference interface exports (wasi:nn/inference@0.2.0-rc-2024-10-28) + /// + /// The inference interface provides graph-execution-context resource with a + /// compute method that takes named tensors and returns named tensors. + fn add_nn_inference_exports(&mut self, instance: &mut InstanceImport) -> Result<()> { + // Method: [method]graph-execution-context.compute(inputs) -> result, error> + self.add_simple_export(instance, "[method]graph-execution-context.compute") + } + + /// Add WASI-NN graph interface exports (wasi:nn/graph@0.2.0-rc-2024-10-28) + /// + /// The graph interface provides model loading and execution context creation. + /// Supports encodings: openvino, onnx, tensorflow, pytorch, tensorflowlite, ggml, autodetect. + /// Execution targets: cpu, gpu, tpu. + fn add_nn_graph_exports(&mut self, instance: &mut InstanceImport) -> Result<()> { + // Function: load(builder, encoding, target) -> result + self.add_simple_export(instance, "load")?; + // Method: [method]graph.init-execution-context() -> result + self.add_simple_export(instance, "[method]graph.init-execution-context") + } + /// Helper to add a simple function export fn add_simple_export(&mut self, instance: &mut InstanceImport, name: &str) -> Result<()> { let func_index = self.next_function_index; self.next_function_index += 1; - let provider = ComponentProvider::default(); - let signature = KilnComponentType::Unit(provider)?; - + // Use Default::default() for signature since the WASI provider only needs the + // function index for dispatch - the full ComponentType is not used at runtime. + // ComponentType::Unit(provider) fails because BoundedVec::new rejects zero-sized items. let func_export = FunctionExport { - signature, + signature: KilnComponentType::::default(), index: func_index, }; @@ -325,11 +381,8 @@ impl WasiInstanceProvider { .map_err(|_| kiln_error::Error::resource_exhausted("Too many exports"))?; } - #[cfg(feature = "std")] - println!( - "[WASI-PROVIDER] Added export: {} (index {})", - name, func_index - ); + #[cfg(feature = "tracing")] + trace!(name = %name, index = func_index, "Added WASI export"); Ok(()) } @@ -353,6 +406,10 @@ impl WasiInstanceProvider { "wasi:filesystem/types@0.2.0", "wasi:filesystem/preopens@0.2.0", "wasi:random/random@0.2.0", + "wasi:nn/errors@0.2.0-rc-2024-10-28", + "wasi:nn/tensor@0.2.0-rc-2024-10-28", + "wasi:nn/inference@0.2.0-rc-2024-10-28", + "wasi:nn/graph@0.2.0-rc-2024-10-28", ] } } diff --git a/kiln-decoder/src/decoder.rs b/kiln-decoder/src/decoder.rs index 3d1eac8b..c2fc1b84 100644 --- a/kiln-decoder/src/decoder.rs +++ b/kiln-decoder/src/decoder.rs @@ -121,6 +121,7 @@ fn build_module_from_sections(sections: Vec) -> Result core_version: kiln_format::types::CoreWasmVersion::default(), type_info_section: None, tags: Vec::new(), + rec_groups: Vec::new(), }; for section in sections { diff --git a/kiln-decoder/src/streaming_decoder.rs b/kiln-decoder/src/streaming_decoder.rs index 31b30baf..277af2c1 100644 --- a/kiln-decoder/src/streaming_decoder.rs +++ b/kiln-decoder/src/streaming_decoder.rs @@ -7,7 +7,7 @@ #[cfg(not(feature = "std"))] extern crate alloc; -use alloc::vec::Vec; +use alloc::{vec, vec::Vec}; #[cfg(feature = "tracing")] use kiln_foundation::tracing::trace; @@ -19,7 +19,7 @@ use kiln_foundation::limits; #[cfg(feature = "allocation-tracing")] use kiln_foundation::{AllocationPhase, trace_alloc}; -use kiln_format::module::{Function, Module as KilnModule}; +use kiln_format::module::{CompositeTypeKind, Function, GcFieldType, GcStorageType, Module as KilnModule, RecGroup, SubType}; use kiln_foundation::{bounded::BoundedVec, safe_memory::NoStdProvider, types::TagType}; use crate::{ @@ -45,7 +45,7 @@ fn decode_heap_type(val: i64) -> kiln_foundation::types::HeapType { -0x0E => HeapType::NoExtern, // 0x72 -0x0F => HeapType::None, // 0x71 -0x0C => HeapType::Exn, // 0x74 noexn (mapped to Exn for now) - _ => HeapType::Func, // fallback + _ => HeapType::Func, // unknown heap types default to func for forward compat } } @@ -183,6 +183,8 @@ pub struct StreamingDecoder<'a> { data_section_count: Option, /// Last non-custom section ID seen (for ordering validation) last_non_custom_section_id: u8, + /// Whether code section uses data.drop or memory.init (requires data count section) + uses_data_count_instructions: bool, /// The module being built (std version) #[cfg(feature = "std")] module: KilnModule, @@ -234,6 +236,7 @@ impl<'a> StreamingDecoder<'a> { data_count_value: None, data_section_count: None, last_non_custom_section_id: 0, + uses_data_count_instructions: false, module, }) } @@ -258,6 +261,7 @@ impl<'a> StreamingDecoder<'a> { data_count_value: None, data_section_count: None, last_non_custom_section_id: 0, + uses_data_count_instructions: false, module, }) } @@ -390,7 +394,10 @@ impl<'a> StreamingDecoder<'a> { // Process each type entry one at a time // Note: A type entry can be a single composite type, a subtype, or a rec group + // Track the current type index separately from the entry count, since rec groups + // can define multiple types with consecutive indices. let mut i = 0u32; + let mut type_index = self.module.types.len() as u32; while i < count { if offset >= data.len() { return Err(Error::parse_error("Unexpected end of type section")); @@ -408,26 +415,56 @@ impl<'a> StreamingDecoder<'a> { #[cfg(feature = "tracing")] trace!(rec_count = rec_count, "process_type_section: rec group"); + let start_type_index = type_index; + let mut rec_sub_types = Vec::with_capacity(rec_count as usize); + // Process each subtype in the recursive group for _j in 0..rec_count { - offset = self.parse_subtype_entry(data, offset)?; + let (new_offset, sub_type) = self.parse_subtype_entry(data, offset, type_index)?; + offset = new_offset; + rec_sub_types.push(sub_type); + type_index += 1; } - // A rec group with N types counts as N type entries - // But the loop already counted as 1, so we need to account for the rest - // Actually, for type indexing, the rec group entries each get their own index - // The outer count counts rec groups as single entries, but we need to adjust - // For now, treat rec as consuming one entry (the spec says rec is one type entry - // that defines multiple types with consecutive indices) + + self.module.rec_groups.push(RecGroup { + types: rec_sub_types, + start_type_index, + }); + + // A rec group counts as one entry in the type section count i += 1; }, COMPOSITE_TYPE_SUB | COMPOSITE_TYPE_SUB_FINAL => { // subtype: 0x50/0x4F supertype* comptype - offset = self.parse_subtype_entry(data, offset)?; + // A standalone subtype is an implicit single-element rec group + let (new_offset, sub_type) = self.parse_subtype_entry(data, offset, type_index)?; + offset = new_offset; + + self.module.rec_groups.push(RecGroup { + types: vec![sub_type], + start_type_index: type_index, + }); + + type_index += 1; i += 1; }, COMPOSITE_TYPE_FUNC | COMPOSITE_TYPE_STRUCT | COMPOSITE_TYPE_ARRAY => { // Direct composite type without subtype wrapper - offset = self.parse_composite_type(data, offset)?; + // Implicitly final with no supertypes, in its own implicit rec group + let (new_offset, composite_kind) = self.parse_composite_type(data, offset)?; + offset = new_offset; + + self.module.rec_groups.push(RecGroup { + types: vec![SubType { + is_final: true, + supertype_indices: Vec::new(), + composite_kind, + type_index, + }], + start_type_index: type_index, + }); + + type_index += 1; i += 1; }, _ => { @@ -436,7 +473,7 @@ impl<'a> StreamingDecoder<'a> { } #[cfg(feature = "tracing")] - trace!(type_index = i - 1, "process_type_section: parsed type"); + trace!(type_index = type_index - 1, "process_type_section: parsed type"); } #[cfg(feature = "tracing")] @@ -449,7 +486,9 @@ impl<'a> StreamingDecoder<'a> { } /// Parse a subtype entry (sub, sub final, or bare composite type) - fn parse_subtype_entry(&mut self, data: &[u8], mut offset: usize) -> Result { + /// Returns the new offset and the parsed SubType metadata. + /// The `type_index` parameter is the type index to assign to this entry. + fn parse_subtype_entry(&mut self, data: &[u8], mut offset: usize, type_index: u32) -> Result<(usize, SubType)> { use kiln_format::binary::{ COMPOSITE_TYPE_ARRAY, COMPOSITE_TYPE_FUNC, COMPOSITE_TYPE_STRUCT, COMPOSITE_TYPE_SUB, COMPOSITE_TYPE_SUB_FINAL, read_leb128_u32, @@ -461,36 +500,56 @@ impl<'a> StreamingDecoder<'a> { let marker = data[offset]; - match marker { + let sub_type = match marker { COMPOSITE_TYPE_SUB | COMPOSITE_TYPE_SUB_FINAL => { + let is_final = marker == COMPOSITE_TYPE_SUB_FINAL; // sub/sub_final: marker supertype_count supertype* comptype offset += 1; let (supertype_count, bytes_read) = read_leb128_u32(data, offset)?; offset += bytes_read; - // Skip supertype indices + // Collect supertype indices + let mut supertype_indices = Vec::with_capacity(supertype_count as usize); for _ in 0..supertype_count { - let (_supertype_idx, bytes_read) = read_leb128_u32(data, offset)?; + let (supertype_idx, bytes_read) = read_leb128_u32(data, offset)?; offset += bytes_read; + supertype_indices.push(supertype_idx); } // Parse the composite type - offset = self.parse_composite_type(data, offset)?; + let (new_offset, composite_kind) = self.parse_composite_type(data, offset)?; + offset = new_offset; + + SubType { + is_final, + supertype_indices, + composite_kind, + type_index, + } }, COMPOSITE_TYPE_FUNC | COMPOSITE_TYPE_STRUCT | COMPOSITE_TYPE_ARRAY => { // Direct composite type (implicitly final with no supertypes) - offset = self.parse_composite_type(data, offset)?; + let (new_offset, composite_kind) = self.parse_composite_type(data, offset)?; + offset = new_offset; + + SubType { + is_final: true, + supertype_indices: Vec::new(), + composite_kind, + type_index, + } }, _ => { return Err(Error::parse_error("Invalid subtype marker")); }, - } + }; - Ok(offset) + Ok((offset, sub_type)) } /// Parse a composite type (func, struct, or array) - fn parse_composite_type(&mut self, data: &[u8], mut offset: usize) -> Result { + /// Returns the new offset and the composite type kind parsed. + fn parse_composite_type(&mut self, data: &[u8], mut offset: usize) -> Result<(usize, CompositeTypeKind)> { use kiln_format::binary::{ COMPOSITE_TYPE_ARRAY, COMPOSITE_TYPE_FUNC, COMPOSITE_TYPE_STRUCT, read_leb128_u32, }; @@ -503,7 +562,7 @@ impl<'a> StreamingDecoder<'a> { let type_marker = data[offset]; offset += 1; - match type_marker { + let kind = match type_marker { COMPOSITE_TYPE_FUNC => { // Parse function type: param_count param* result_count result* let (param_count, bytes_read) = read_leb128_u32(data, offset)?; @@ -578,6 +637,8 @@ impl<'a> StreamingDecoder<'a> { let func_type = FuncType::new(params.into_iter(), results.into_iter())?; let _ = self.module.types.push(func_type); } + + CompositeTypeKind::Func }, COMPOSITE_TYPE_STRUCT => { // Parse struct type: field_count field* @@ -588,25 +649,66 @@ impl<'a> StreamingDecoder<'a> { #[cfg(feature = "tracing")] trace!(field_count = field_count, "parse_composite_type: struct"); + let mut gc_fields = Vec::with_capacity(field_count as usize); + // Collect field types so struct.new knows how many values to pop + #[cfg(feature = "std")] + let mut field_types = Vec::with_capacity(field_count as usize); + #[cfg(not(feature = "std"))] + let mut field_types = alloc::vec::Vec::with_capacity(field_count as usize); + for _ in 0..field_count { // Parse storage type (value type or packed type) - let (_, new_offset) = self.parse_storage_type(data, offset)?; + let (storage_byte, new_offset) = self.parse_storage_type(data, offset)?; offset = new_offset; + // Convert storage type to ValueType for the placeholder + // Packed types (i8=0x78, i16=0x77) are stored as I32 + let field_vt = match storage_byte { + 0x78 | 0x77 => ValueType::I32, // packed i8/i16 -> i32 + _ => { + // Re-parse as value type to get the proper type + // The parse_storage_type already advanced offset, so use the byte + match storage_byte { + 0x7F => ValueType::I32, + 0x7E => ValueType::I64, + 0x7D => ValueType::F32, + 0x7C => ValueType::F64, + 0x7B => ValueType::V128, + 0x70 => ValueType::FuncRef, + 0x6F => ValueType::ExternRef, + 0x6E => ValueType::AnyRef, + 0x6D => ValueType::EqRef, + 0x6C => ValueType::I31Ref, + 0x6B => ValueType::StructRef(0), + 0x6A => ValueType::ArrayRef(0), + _ => ValueType::I32, // default for complex ref types + } + } + }; + field_types.push(field_vt); + // Parse mutability flag if offset >= data.len() { return Err(Error::parse_error("Unexpected end of struct field")); } - offset += 1; // mut flag + let mutable = data[offset] != 0; + offset += 1; + + let storage_type = match storage_byte { + 0x78 => GcStorageType::I8, + 0x77 => GcStorageType::I16, + other => GcStorageType::Value(other), + }; + gc_fields.push(GcFieldType { storage_type, mutable }); } - // TODO: Store struct type when we have proper GC type storage - // For now, we add a placeholder func type to maintain index alignment + // Store struct type as a placeholder func type with fields in params + // This lets struct.new know the field count via params.len() #[cfg(feature = "std")] { use kiln_foundation::CleanCoreFuncType; let placeholder = CleanCoreFuncType { - params: Vec::new(), + params: field_types, results: Vec::new(), }; self.module.types.push(placeholder); @@ -615,31 +717,59 @@ impl<'a> StreamingDecoder<'a> { #[cfg(not(feature = "std"))] { use kiln_foundation::types::FuncType; - let placeholder = FuncType::new(core::iter::empty(), core::iter::empty())?; + let placeholder = FuncType::new(field_types.into_iter(), core::iter::empty())?; let _ = self.module.types.push(placeholder); } + + CompositeTypeKind::StructWithFields(gc_fields) }, COMPOSITE_TYPE_ARRAY => { // Parse array type: storage_type mutability - let (_, new_offset) = self.parse_storage_type(data, offset)?; + let (storage_byte, new_offset) = self.parse_storage_type(data, offset)?; offset = new_offset; // Parse mutability flag if offset >= data.len() { return Err(Error::parse_error("Unexpected end of array type")); } - offset += 1; // mut flag + let mutable = data[offset] != 0; + offset += 1; + + let storage_type = match storage_byte { + 0x78 => GcStorageType::I8, + 0x77 => GcStorageType::I16, + other => GcStorageType::Value(other), + }; + let gc_element = GcFieldType { storage_type, mutable }; #[cfg(feature = "tracing")] trace!("parse_composite_type: array"); - // TODO: Store array type when we have proper GC type storage - // For now, we add a placeholder func type to maintain index alignment + // Convert element storage type to ValueType + let elem_vt = match storage_byte { + 0x78 | 0x77 => ValueType::I32, + 0x7F => ValueType::I32, + 0x7E => ValueType::I64, + 0x7D => ValueType::F32, + 0x7C => ValueType::F64, + 0x7B => ValueType::V128, + 0x70 => ValueType::FuncRef, + 0x6F => ValueType::ExternRef, + 0x6E => ValueType::AnyRef, + 0x6D => ValueType::EqRef, + 0x6C => ValueType::I31Ref, + 0x6B => ValueType::StructRef(0), + 0x6A => ValueType::ArrayRef(0), + _ => ValueType::I32, + }; + + // Store array type as a placeholder func type with element type in params + // This lets array.new know the element type via params[0] #[cfg(feature = "std")] { use kiln_foundation::CleanCoreFuncType; let placeholder = CleanCoreFuncType { - params: Vec::new(), + params: vec![elem_vt], results: Vec::new(), }; self.module.types.push(placeholder); @@ -648,16 +778,19 @@ impl<'a> StreamingDecoder<'a> { #[cfg(not(feature = "std"))] { use kiln_foundation::types::FuncType; - let placeholder = FuncType::new(core::iter::empty(), core::iter::empty())?; + let elem_iter = core::iter::once(elem_vt); + let placeholder = FuncType::new(elem_iter, core::iter::empty())?; let _ = self.module.types.push(placeholder); } + + CompositeTypeKind::ArrayWithElement(gc_element) }, _ => { return Err(Error::parse_error("Invalid composite type marker")); }, - } + }; - Ok(offset) + Ok((offset, kind)) } /// Parse a storage type (value type or packed type) @@ -738,20 +871,24 @@ impl<'a> StreamingDecoder<'a> { // Concrete type indices are non-negative. if heap_type_idx < 0 { - // Abstract heap type - match heap_type_idx { - -16 => Ok((ValueType::FuncRef, new_offset)), // func (0x70) - -17 => Ok((ValueType::ExternRef, new_offset)), // extern (0x6F) - -18 => Ok((ValueType::AnyRef, new_offset)), // any (0x6E) - -19 => Ok((ValueType::EqRef, new_offset)), // eq (0x6D) - -20 => Ok((ValueType::I31Ref, new_offset)), // i31 (0x6C) - -21 => Ok((ValueType::StructRef(0), new_offset)), // struct (0x6B) - -22 => Ok((ValueType::ArrayRef(0), new_offset)), // array (0x6A) - -23 => Ok((ValueType::ExnRef, new_offset)), // exn (0x69) - -13 => Ok((ValueType::NullFuncRef, new_offset)), // nofunc (0x73) - bottom for func - -14 => Ok((ValueType::ExternRef, new_offset)), // noextern (0x72) - -15 => Ok((ValueType::AnyRef, new_offset)), // none (0x71) - bottom for any - -12 => Ok((ValueType::ExnRef, new_offset)), // noexn (0x74) - bottom for exn + // Abstract heap type - must respect nullability + // The shorthand forms (FuncRef, ExternRef, etc.) are nullable by definition. + // Non-nullable abstract refs use TypedFuncRef with a sentinel index (u32::MAX) + // to distinguish (ref func) from (ref null func) = FuncRef. + match (heap_type_idx, nullable) { + (-16, true) => Ok((ValueType::FuncRef, new_offset)), // (ref null func) = funcref + (-16, false) => Ok((ValueType::TypedFuncRef(u32::MAX, false), new_offset)), // (ref func) - non-nullable abstract funcref + (-17, _) => Ok((ValueType::ExternRef, new_offset)), // extern (0x6F) + (-18, _) => Ok((ValueType::AnyRef, new_offset)), // any (0x6E) + (-19, _) => Ok((ValueType::EqRef, new_offset)), // eq (0x6D) + (-20, _) => Ok((ValueType::I31Ref, new_offset)), // i31 (0x6C) + (-21, _) => Ok((ValueType::StructRef(0), new_offset)), // struct (0x6B) + (-22, _) => Ok((ValueType::ArrayRef(0), new_offset)), // array (0x6A) + (-23, _) => Ok((ValueType::ExnRef, new_offset)), // exn (0x69) + (-13, _) => Ok((ValueType::NullFuncRef, new_offset)), // nofunc (0x73) - bottom for func + (-14, _) => Ok((ValueType::ExternRef, new_offset)), // noextern (0x72) + (-15, _) => Ok((ValueType::AnyRef, new_offset)), // none (0x71) - bottom for any + (-12, _) => Ok((ValueType::ExnRef, new_offset)), // noexn (0x74) - bottom for exn _ => Ok((ValueType::AnyRef, new_offset)), // fallback for unknown } } else { @@ -899,6 +1036,12 @@ impl<'a> StreamingDecoder<'a> { } let flags = data[offset]; offset += 1; + + // Validate table limits flags: bit 0 (has max), bit 2 (table64) + if flags > 0x05 || (flags & 0x02) != 0 { + return Err(Error::parse_error("malformed limits flags")); + } + let (min, bytes_read) = read_leb128_u32(data, offset)?; offset += bytes_read; let max = if flags & 0x01 != 0 { @@ -965,11 +1108,20 @@ impl<'a> StreamingDecoder<'a> { let flags = data[offset]; offset += 1; + // Validate limits flags per WebAssembly spec: + // Maximum valid flag for memory is 0x07 (has_max | shared | memory64) + if flags > 0x07 { + return Err(Error::parse_error("malformed limits flags")); + } + // Check for memory64 flag (bit 2) let is_memory64 = (flags & 0x04) != 0; - // WebAssembly spec: memory size must be at most 65536 pages (4GB) - const MAX_MEMORY_PAGES: u32 = 65536; + // WebAssembly spec limits: + // - memory32: max 65536 pages (4 GiB) + // - memory64: max 0x1_0000_0000_0000 pages (2^48 pages) + const MAX_MEMORY32_PAGES: u64 = 65536; + const MAX_MEMORY64_PAGES: u64 = 0x1_0000_0000_0000; // Parse limits - memory64 uses u64, regular memory uses u32 let (min, max) = if is_memory64 { @@ -985,15 +1137,15 @@ impl<'a> StreamingDecoder<'a> { }; // Validate memory64 limits - if min64 > MAX_MEMORY_PAGES as u64 { + if min64 > MAX_MEMORY64_PAGES { return Err(Error::validation_error( - "memory size must be at most 65536 pages (4 GiB)", + "memory size must be at most 281474976710656 pages (16 EiB)", )); } if let Some(max64) = max64 { - if max64 > MAX_MEMORY_PAGES as u64 { + if max64 > MAX_MEMORY64_PAGES { return Err(Error::validation_error( - "memory size must be at most 65536 pages (4 GiB)", + "memory size must be at most 281474976710656 pages (16 EiB)", )); } } @@ -1011,13 +1163,13 @@ impl<'a> StreamingDecoder<'a> { }; // Validate regular memory limits - if min > MAX_MEMORY_PAGES { + if min as u64 > MAX_MEMORY32_PAGES { return Err(Error::validation_error( "memory size must be at most 65536 pages (4 GiB)", )); } if let Some(max_val) = max { - if max_val > MAX_MEMORY_PAGES { + if max_val as u64 > MAX_MEMORY32_PAGES { return Err(Error::validation_error( "memory size must be at most 65536 pages (4 GiB)", )); @@ -1057,12 +1209,19 @@ impl<'a> StreamingDecoder<'a> { }, 0x03 => { // Global import - need to parse global type - // value_type (1 byte) + mutability (1 byte) - if offset + 1 >= data.len() { + // value_type (variable length for ref types) + mutability (1 byte) + if offset >= data.len() { return Err(Error::parse_error("Unexpected end of global import")); } - let value_type_byte = data[offset]; - offset += 1; + + // Parse value type using full GC-aware parser (handles multi-byte ref types) + let (value_type, new_offset) = self.parse_value_type(data, offset)?; + offset = new_offset; + + // Parse mutability byte + if offset >= data.len() { + return Err(Error::parse_error("Unexpected end of global import mutability")); + } let mutability_byte = data[offset]; offset += 1; @@ -1071,19 +1230,6 @@ impl<'a> StreamingDecoder<'a> { return Err(Error::parse_error("malformed mutability")); } - // Parse value type - let value_type = match value_type_byte { - 0x7F => kiln_foundation::ValueType::I32, - 0x7E => kiln_foundation::ValueType::I64, - 0x7D => kiln_foundation::ValueType::F32, - 0x7C => kiln_foundation::ValueType::F64, - 0x7B => kiln_foundation::ValueType::V128, - 0x70 => kiln_foundation::ValueType::FuncRef, - 0x6F => kiln_foundation::ValueType::ExternRef, - 0x69 => kiln_foundation::ValueType::ExnRef, - _ => return Err(Error::parse_error("Invalid global import value type")), - }; - #[cfg(feature = "tracing")] trace!(import_index = i, value_type = ?value_type, mutable = (mutability_byte != 0), "import: global"); @@ -1284,6 +1430,14 @@ impl<'a> StreamingDecoder<'a> { let flags = data[offset]; offset += 1; + // Validate table limits flags per WebAssembly spec: + // - Bit 0: has max (0x01) + // - Bit 2: table64 (0x04) + // All other bits must be zero. Maximum valid flag is 0x05. + if flags > 0x05 || (flags & 0x02) != 0 { + return Err(Error::parse_error("malformed limits flags")); + } + let (min, bytes_read) = read_leb128_u32(data, offset)?; offset += bytes_read; @@ -1418,12 +1572,23 @@ impl<'a> StreamingDecoder<'a> { let flags = data[offset]; offset += 1; + // Validate limits flags per WebAssembly spec: + // - Bits 0: has max (0x01) + // - Bit 1: shared (0x02) - threads proposal + // - Bit 2: memory64 (0x04) + // All other bits must be zero. Maximum valid flag is 0x07. + if flags > 0x07 { + return Err(Error::parse_error("malformed limits flags")); + } + // Check for memory64 flag (bit 2) let is_memory64 = (flags & 0x04) != 0; - // WebAssembly spec: memory size must be at most 65536 pages (4GB) - // for non-memory64 memories (and memory64 has its own limit) - const MAX_MEMORY_PAGES: u32 = 65536; + // WebAssembly spec limits: + // - memory32: max 65536 pages (4 GiB) + // - memory64: max 0x1_0000_0000_0000 pages (2^48 pages) + const MAX_MEMORY32_PAGES: u64 = 65536; + const MAX_MEMORY64_PAGES: u64 = 0x1_0000_0000_0000; // Parse limits - memory64 uses u64, regular memory uses u32 let (min, max) = if is_memory64 { @@ -1438,21 +1603,21 @@ impl<'a> StreamingDecoder<'a> { None }; - // Validate memory64 limits (still have a limit, though higher) - // For non-memory64 tests, values > 65536 pages should fail - if min64 > MAX_MEMORY_PAGES as u64 { + // Validate memory64 limits + if min64 > MAX_MEMORY64_PAGES { return Err(Error::validation_error( - "memory size must be at most 65536 pages (4 GiB)", + "memory size must be at most 281474976710656 pages (16 EiB)", )); } if let Some(max64) = max64 { - if max64 > MAX_MEMORY_PAGES as u64 { + if max64 > MAX_MEMORY64_PAGES { return Err(Error::validation_error( - "memory size must be at most 65536 pages (4 GiB)", + "memory size must be at most 281474976710656 pages (16 EiB)", )); } } + // Safe to truncate to u32 for page counts within our runtime's capacity (min64 as u32, max64.map(|v| v as u32)) } else { let (min, bytes_read) = read_leb128_u32(data, offset)?; @@ -1475,17 +1640,20 @@ impl<'a> StreamingDecoder<'a> { return Err(Error::validation_error("shared memory must have maximum")); } - if min > MAX_MEMORY_PAGES { - return Err(Error::validation_error( - "memory size must be at most 65536 pages (4 GiB)", - )); - } - if let Some(max_val) = max { - if max_val > MAX_MEMORY_PAGES { + // Apply memory32 limits for non-memory64 memories + if !is_memory64 { + if min as u64 > MAX_MEMORY32_PAGES { return Err(Error::validation_error( "memory size must be at most 65536 pages (4 GiB)", )); } + if let Some(max_val) = max { + if max_val as u64 > MAX_MEMORY32_PAGES { + return Err(Error::validation_error( + "memory size must be at most 65536 pages (4 GiB)", + )); + } + } } // Create memory type @@ -2196,8 +2364,13 @@ impl<'a> StreamingDecoder<'a> { // Code section index i corresponds to module-defined function at index (num_imports + i) let func_index = num_imports + i as usize; - if let Some(func) = self.module.functions.get_mut(func_index) { - // Parse local variable declarations + + // First pass: parse all local declarations into a temporary vec. + // This avoids borrow conflicts between self.parse_value_type (immutable borrow) + // and self.module.functions.get_mut (mutable borrow). + let mut local_decls: Vec<(u32, kiln_foundation::types::ValueType)> = Vec::new(); + { + let mut total_locals: u64 = 0; for _ in 0..local_count { let (count, bytes) = read_leb128_u32(&data[body_start..body_end], body_offset)?; body_offset += bytes; @@ -2206,24 +2379,30 @@ impl<'a> StreamingDecoder<'a> { return Err(Error::parse_error("Unexpected end of function body")); } - let value_type = data[body_start + body_offset]; - body_offset += 1; - - // Convert to ValueType and add to locals - let vt = match value_type { - 0x7F => kiln_foundation::types::ValueType::I32, - 0x7E => kiln_foundation::types::ValueType::I64, - 0x7D => kiln_foundation::types::ValueType::F32, - 0x7C => kiln_foundation::types::ValueType::F64, - 0x7B => kiln_foundation::types::ValueType::V128, - 0x70 => kiln_foundation::types::ValueType::FuncRef, - 0x6F => kiln_foundation::types::ValueType::ExternRef, - 0x69 => kiln_foundation::types::ValueType::ExnRef, - _ => return Err(Error::parse_error("Invalid local type")), - }; + // Parse local value type using the full parse_value_type method, + // which handles GC reference types (0x63/0x64 prefixed) as well + // as standard value types. + let (vt, new_offset) = self.parse_value_type( + &data[body_start..body_end], + body_offset, + )?; + body_offset = new_offset; + + // Validate total locals: sum of all declared locals must fit in u32 + total_locals += count as u64; + if total_locals > u32::MAX as u64 { + return Err(Error::parse_error("too many locals")); + } + + local_decls.push((count, vt)); + } + } + if let Some(func) = self.module.functions.get_mut(func_index) { + // Apply parsed local declarations to the function + for (count, vt) in &local_decls { // Validate total locals against platform limits before allocation - let new_total = func.locals.len() + count as usize; + let new_total = func.locals.len() + *count as usize; if new_total > limits::MAX_FUNCTION_LOCALS { return Err(Error::parse_error( "Function exceeds maximum local count for platform", @@ -2235,12 +2414,12 @@ impl<'a> StreamingDecoder<'a> { AllocationPhase::Decode, "streaming_decoder:func_locals", "locals", - count as usize + *count as usize ); // Add 'count' locals of this type - for _ in 0..count { - func.locals.push(vt); + for _ in 0..*count { + func.locals.push(*vt); } } @@ -2248,6 +2427,16 @@ impl<'a> StreamingDecoder<'a> { let instructions_start = body_start + body_offset; let instructions_data = &data[instructions_start..body_end]; + // Validate function body ends with END opcode (0x0B) + if instructions_data.is_empty() || instructions_data[instructions_data.len() - 1] != 0x0B { + return Err(Error::parse_error("END opcode expected")); + } + + // Note: data count section validation (memory.init/data.drop require + // section 12) is handled at the validator level, not the decoder. + // Raw byte scanning here produced false positives from LEB128 immediates + // containing 0xFC 0x08/0x09 byte sequences. + #[cfg(feature = "allocation-tracing")] trace_alloc!( AllocationPhase::Decode, @@ -2608,6 +2797,9 @@ impl<'a> StreamingDecoder<'a> { } } + // Note: data count section requirement (when memory.init/data.drop are used) + // is validated at the instruction validation level, not during decoding. + Ok(()) } diff --git a/kiln-format/src/binary.rs b/kiln-format/src/binary.rs index e6cb89bb..5be26c1a 100644 --- a/kiln-format/src/binary.rs +++ b/kiln-format/src/binary.rs @@ -1945,8 +1945,12 @@ pub mod with_alloc { Ok((name_slice, pos + len_size + name_len as usize)) } - // STUB for parsing limits - to be fully implemented in kiln-format - // Should parse kiln_format::types::Limits + /// Parse WebAssembly limits encoding with proper handling of shared and memory64 flags. + /// + /// Limits flags byte encoding: + /// - bit 0 (0x01): has maximum + /// - bit 1 (0x02): shared memory + /// - bit 2 (0x04): memory64 (uses i64 for min/max) pub fn parse_limits( bytes: &[u8], offset: usize, @@ -1957,27 +1961,42 @@ pub mod with_alloc { } let flags = bytes[offset]; let mut current_offset = offset + 1; + let is_memory64 = (flags & 0x04) != 0; + let is_shared = (flags & 0x02) != 0; - let (min, new_offset) = read_leb128_u32(bytes, current_offset)?; - current_offset = new_offset; - - let max = if (flags & 0x01) != 0 { + // Parse min: memory64 uses u64, regular uses u32 + let min: u64 = if is_memory64 { + let (val, new_offset) = read_leb128_u64(bytes, current_offset)?; + current_offset = new_offset; + val + } else { let (val, new_offset) = read_leb128_u32(bytes, current_offset)?; current_offset = new_offset; - Some(val) + val as u64 + }; + + // Parse max if present + let max: Option = if (flags & 0x01) != 0 { + if is_memory64 { + let (val, new_offset) = read_leb128_u64(bytes, current_offset)?; + current_offset = new_offset; + Some(val) + } else { + let (val, new_offset) = read_leb128_u32(bytes, current_offset)?; + current_offset = new_offset; + Some(val as u64) + } } else { None }; - // Ignoring shared and memory64 flags for now as they are not in - // kiln_format::types::Limits directly Ok(( crate::types::Limits { - min: min.into(), - max: max.map(Into::into), - shared: false, - memory64: false, - }, // Assuming default shared/memory64 + min, + max, + shared: is_shared, + memory64: is_memory64, + }, current_offset, )) } diff --git a/kiln-format/src/module.rs b/kiln-format/src/module.rs index 67da99eb..d07930ce 100644 --- a/kiln-format/src/module.rs +++ b/kiln-format/src/module.rs @@ -1580,6 +1580,69 @@ impl kiln_foundation::traits::FromBytes } } +/// Storage type for GC fields (matches wasm binary encoding) +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GcStorageType { + /// Standard value type byte + Value(u8), + /// Packed i8 (0x78) + I8, + /// Packed i16 (0x77) + I16, +} + +/// A single field in a GC struct type +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GcFieldType { + /// The storage type of this field + pub storage_type: GcStorageType, + /// Whether this field is mutable + pub mutable: bool, +} + +/// GC proposal: Composite type kind (func, struct, or array) +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CompositeTypeKind { + /// Function type (0x60) + Func, + /// Struct type (0x5F) with field definitions + Struct, + /// Array type (0x5E) with element type + Array, + /// Struct type with parsed field info for runtime use + StructWithFields(Vec), + /// Array type with parsed element info for runtime use + ArrayWithElement(GcFieldType), +} + +/// GC proposal: Sub type declaration +/// +/// Represents a type with optional supertypes and a composite type. +/// `sub final` types cannot be further subtyped. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SubType { + /// Whether this is a final type (cannot be subtyped further) + pub is_final: bool, + /// Indices of supertypes (usually 0 or 1) + pub supertype_indices: Vec, + /// The composite type kind + pub composite_kind: CompositeTypeKind, + /// Type index in the module's type section + pub type_index: u32, +} + +/// GC proposal: Recursive type group +/// +/// Groups multiple sub types together for mutual recursion. +/// Each type in the group gets a consecutive type index. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RecGroup { + /// The sub types in this recursive group + pub types: Vec, + /// Starting type index for this group + pub start_type_index: u32, +} + /// Hypothetical Finding F5: Represents an entry in the TypeInformation section /// - With Allocation #[cfg(feature = "std")] @@ -1643,6 +1706,8 @@ pub struct Module< pub core_version: CoreWasmVersion, /// Type information section (if present) pub type_info_section: Option>, + /// GC proposal: recursive type groups with sub type declarations + pub rec_groups: Vec, } #[cfg(not(any(feature = "std")))] @@ -1673,6 +1738,7 @@ impl Module

{ binary: None, core_version: CoreWasmVersion::default(), type_info_section: None, + rec_groups: Vec::new(), }) } @@ -1717,6 +1783,8 @@ pub struct Module { pub core_version: CoreWasmVersion, /// Type information section (if present) pub type_info_section: Option, + /// GC proposal: recursive type groups with sub type declarations + pub rec_groups: Vec, } #[cfg(feature = "std")] @@ -1746,6 +1814,7 @@ impl Module { binary: None, core_version: CoreWasmVersion::default(), type_info_section: None, + rec_groups: Vec::new(), } } diff --git a/kiln-foundation/src/bounded.rs b/kiln-foundation/src/bounded.rs index 02f739f3..75e60a80 100644 --- a/kiln-foundation/src/bounded.rs +++ b/kiln-foundation/src/bounded.rs @@ -737,8 +737,12 @@ where write_stream.position() }; + // Write the full item_serialized_size bytes to ensure each slot is + // exactly item_serialized_size bytes in the provider. The buffer is + // zero-initialized, so any bytes beyond bytes_written are zero padding. + let write_len = core::cmp::max(bytes_written, self.item_serialized_size); self.handler - .write_data(offset, &item_bytes_buffer[..bytes_written]) + .write_data(offset, &item_bytes_buffer[..write_len]) .map_err(|e| BoundedError::runtime_execution_error("Operation failed"))?; self.length += 1; @@ -1216,8 +1220,15 @@ where write_stream.position() }; + // Write the full item_serialized_size bytes to ensure each slot is + // exactly item_serialized_size bytes in the provider. The buffer is + // zero-initialized, so any bytes beyond bytes_written are zero padding. + // This is critical because get() reads item_serialized_size bytes at + // offset = index * item_serialized_size, so the provider must have + // enough data for all slots. + let write_len = core::cmp::max(bytes_written, self.item_serialized_size); self.provider - .write_data(offset, &item_bytes_buffer[..bytes_written]) + .write_data(offset, &item_bytes_buffer[..write_len]) .map_err(|e| { BoundedError::new(BoundedErrorKind::SliceError, "Slice operation failed") })?; @@ -1751,9 +1762,11 @@ where write_stream.position() }; - // Write new value to memory + // Write the full item_serialized_size bytes to ensure the slot is + // fully written, matching what get() expects to read. + let write_len = core::cmp::max(bytes_written, self.item_serialized_size); self.provider - .write_data(offset, &item_bytes_buffer[..bytes_written]) + .write_data(offset, &item_bytes_buffer[..write_len]) .map_err(|e| BoundedError::runtime_execution_error("Operation failed"))?; // Update checksum if needed @@ -3766,6 +3779,10 @@ impl WasmName { // Trait implementations for WasmName impl ToBytes for WasmName { + fn serialized_size(&self) -> usize { + self.inner.serialized_size() + } + fn to_bytes_with_provider<'a, PStream: crate::MemoryProvider>( &self, writer: &mut WriteStream<'a>, diff --git a/kiln-foundation/src/capabilities/memory_factory.rs b/kiln-foundation/src/capabilities/memory_factory.rs index 172e6153..6faa6f3a 100644 --- a/kiln-foundation/src/capabilities/memory_factory.rs +++ b/kiln-foundation/src/capabilities/memory_factory.rs @@ -12,6 +12,7 @@ use super::{ use crate::{ budget_aware_provider::CrateId, memory_init::get_global_capability_context, + monitoring::MEMORY_MONITOR, safe_memory::{ NoStdProvider, Provider, @@ -108,6 +109,12 @@ impl MemoryFactory { } }); + // Record in global memory monitor + match &verification_result { + Ok(_) => MEMORY_MONITOR.record_allocation(N), + Err(_) => MEMORY_MONITOR.record_allocation_failure(), + } + // Return verification result verification_result?; @@ -174,6 +181,12 @@ impl MemoryFactory { } }); + // Record in global memory monitor + match &verification_result { + Ok(_) => MEMORY_MONITOR.record_allocation(N), + Err(_) => MEMORY_MONITOR.record_allocation_failure(), + } + // Return verification result let capability = capability_result?; verification_result?; @@ -269,6 +282,12 @@ impl MemoryFactory { } }); + // Record in global memory monitor + match &final_result { + Ok(_) => MEMORY_MONITOR.record_allocation(N), + Err(_) => MEMORY_MONITOR.record_allocation_failure(), + } + // Return verification result final_result?; @@ -341,6 +360,9 @@ impl MemoryFactory { 0, // No specific crate context for manual deallocation ); }); + + // Record in global memory monitor + MEMORY_MONITOR.record_deallocation(size); } /// Enter a module-level memory scope with budget tracking @@ -448,6 +470,8 @@ impl MemoryFactory { #[cfg(test)] mod tests { + use std::vec; + use super::*; #[test] @@ -580,4 +604,143 @@ mod tests { assert_eq!(report.failed_allocations, 1); assert_eq!(report.capability_violations, 1); } + + #[test] + #[serial_test::serial] + fn test_memory_monitor_allocation_tracking() { + use crate::monitoring::MEMORY_MONITOR; + + // Reset both monitors for clean test state + MEMORY_MONITOR.reset(); + with_safety_monitor(|monitor| { + #[cfg(test)] + monitor.reset(); + }); + + // Verify initial state + let stats = MEMORY_MONITOR.get_statistics(); + assert_eq!(stats.total_allocations, 0); + assert_eq!(stats.current_usage, 0); + + // Create a capability context for testing + use crate::{ + capabilities::MemoryCapabilityContext, + verification::VerificationLevel, + }; + let mut context = MemoryCapabilityContext::new(VerificationLevel::Standard, false); + let _ = context.register_dynamic_capability(CrateId::Foundation, 8192); + + // Allocate and verify MEMORY_MONITOR records it + let result = MemoryFactory::create_with_context::<2048>(&context, CrateId::Foundation); + assert!(result.is_ok()); + + let stats = MEMORY_MONITOR.get_statistics(); + assert_eq!(stats.total_allocations, 1); + assert_eq!(stats.current_usage, 2048); + assert_eq!(stats.peak_usage, 2048); + assert_eq!(stats.allocation_failures, 0); + } + + #[test] + #[serial_test::serial] + fn test_memory_monitor_deallocation_tracking() { + use crate::monitoring::MEMORY_MONITOR; + + // Reset both monitors for clean test state + MEMORY_MONITOR.reset(); + with_safety_monitor(|monitor| { + #[cfg(test)] + monitor.reset(); + }); + + // Create a capability context for testing + use crate::{ + capabilities::MemoryCapabilityContext, + verification::VerificationLevel, + }; + let mut context = MemoryCapabilityContext::new(VerificationLevel::Standard, false); + let _ = context.register_dynamic_capability(CrateId::Foundation, 8192); + + // Allocate + let result = MemoryFactory::create_with_context::<1024>(&context, CrateId::Foundation); + assert!(result.is_ok()); + + let stats = MEMORY_MONITOR.get_statistics(); + assert_eq!(stats.total_allocations, 1); + assert_eq!(stats.current_usage, 1024); + + // Deallocate and verify tracking + MemoryFactory::record_deallocation(1024); + + let stats = MEMORY_MONITOR.get_statistics(); + assert_eq!(stats.total_deallocations, 1); + assert_eq!(stats.current_usage, 0); + assert_eq!(stats.peak_usage, 1024); + } + + #[test] + #[serial_test::serial] + fn test_memory_monitor_failure_tracking() { + use crate::monitoring::MEMORY_MONITOR; + + // Reset both monitors for clean test state + MEMORY_MONITOR.reset(); + with_safety_monitor(|monitor| { + #[cfg(test)] + monitor.reset(); + }); + + // Create a capability context with no capabilities registered + use crate::{ + capabilities::MemoryCapabilityContext, + verification::VerificationLevel, + }; + let context = MemoryCapabilityContext::new(VerificationLevel::Standard, false); + + // Attempt allocation that should fail + let result = MemoryFactory::create_with_context::<1024>(&context, CrateId::Foundation); + assert!(result.is_err()); + + // Verify MEMORY_MONITOR records the failure + let stats = MEMORY_MONITOR.get_statistics(); + assert_eq!(stats.total_allocations, 0); + assert_eq!(stats.allocation_failures, 1); + assert_eq!(stats.current_usage, 0); + } + + #[test] + #[serial_test::serial] + fn test_memory_monitor_per_crate_budget() { + use crate::monitoring::MEMORY_MONITOR; + + // Reset both monitors for clean test state + MEMORY_MONITOR.reset(); + with_safety_monitor(|monitor| { + #[cfg(test)] + monitor.reset(); + }); + + // Create a capability context for testing + use crate::{ + capabilities::MemoryCapabilityContext, + verification::VerificationLevel, + }; + let mut context = MemoryCapabilityContext::new(VerificationLevel::Standard, false); + let _ = context.register_dynamic_capability(CrateId::Foundation, 8192); + + // Multiple allocations should accumulate + let _ = MemoryFactory::create_with_context::<1024>(&context, CrateId::Foundation); + let _ = MemoryFactory::create_with_context::<2048>(&context, CrateId::Foundation); + + let stats = MEMORY_MONITOR.get_statistics(); + assert_eq!(stats.total_allocations, 2); + assert_eq!(stats.current_usage, 3072); + assert_eq!(stats.peak_usage, 3072); + + // Deallocation reduces current but not peak + MemoryFactory::record_deallocation(1024); + let stats = MEMORY_MONITOR.get_statistics(); + assert_eq!(stats.current_usage, 2048); + assert_eq!(stats.peak_usage, 3072); + } } diff --git a/kiln-foundation/src/clean_core_types.rs b/kiln-foundation/src/clean_core_types.rs index 572e5770..1c68775c 100644 --- a/kiln-foundation/src/clean_core_types.rs +++ b/kiln-foundation/src/clean_core_types.rs @@ -34,6 +34,8 @@ mod types { pub limits: crate::types::Limits, /// Whether the memory is shared pub shared: bool, + /// Whether this memory uses 64-bit addressing (memory64 proposal) + pub memory64: bool, } /// Clean core WebAssembly table type without provider parameters diff --git a/kiln-foundation/src/component.rs b/kiln-foundation/src/component.rs index fdcae210..e50ca497 100644 --- a/kiln-foundation/src/component.rs +++ b/kiln-foundation/src/component.rs @@ -61,6 +61,10 @@ impl Checksummable for TypeRef { #[cfg(not(feature = "std"))] impl ToBytes for TypeRef { + fn serialized_size(&self) -> usize { + self.0.serialized_size() + } + fn to_bytes_with_provider( &self, writer: &mut WriteStream, @@ -307,6 +311,10 @@ impl Checksummable for ComponentAliasOuterKind { } impl ToBytes for ComponentAliasOuterKind { + fn serialized_size(&self) -> usize { + 1 // single byte discriminant + } + fn to_bytes_with_provider<'a, PStream: crate::MemoryProvider>( &self, writer: &mut WriteStream<'a>, @@ -440,6 +448,10 @@ impl Checksummable for ExternKind { } impl ToBytes for ExternKind { + fn serialized_size(&self) -> usize { + 1 // single byte discriminant + } + fn to_bytes_with_provider<'a, PStream: crate::MemoryProvider>( &self, writer: &mut WriteStream<'a>, @@ -700,6 +712,10 @@ macro_rules! impl_checksummable_struct { macro_rules! impl_tobytes_struct { ($type:ident < $( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),* >, P: $pbound:ident, $($field:ident),+) => { impl ToBytes for $type

{ + fn serialized_size(&self) -> usize { + 0 $( + self.$field.serialized_size() )+ + } + fn to_bytes_with_provider<'a, PStream: crate::MemoryProvider>( &self, writer: &mut WriteStream<'a>, @@ -713,6 +729,10 @@ macro_rules! impl_tobytes_struct { }; ($type:ident < $( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),* >, $($field:ident),+) => { impl< $( $lt $( : $clt $(+ $dlt )* )? ),* > ToBytes for $type< $( $lt),* > { + fn serialized_size(&self) -> usize { + 0 $( + self.$field.serialized_size() )+ + } + fn to_bytes_with_provider<'a, PStream: crate::MemoryProvider>( &self, writer: &mut WriteStream<'a>, @@ -726,6 +746,10 @@ macro_rules! impl_tobytes_struct { }; ($type:ident, $($field:ident),+) => { impl ToBytes for $type { + fn serialized_size(&self) -> usize { + 0 $( + self.$field.serialized_size() )+ + } + fn to_bytes_with_provider<'a, PStream: crate::MemoryProvider>( &self, writer: &mut WriteStream<'a>, @@ -806,6 +830,10 @@ impl

ToBytes for Export

where P: MemoryProvider + Clone + Default + Eq + core::fmt::Debug, { + fn serialized_size(&self) -> usize { + self.name.serialized_size() + self.ty.serialized_size() + self.desc.serialized_size() + } + fn to_bytes_with_provider<'a, PStream: crate::MemoryProvider>( &self, writer: &mut WriteStream<'a>, @@ -885,6 +913,22 @@ impl

ToBytes for ExternType

where P: MemoryProvider + Clone + Default + Eq + core::fmt::Debug, { + fn serialized_size(&self) -> usize { + // 1 byte for variant tag + inner type's serialized size + 1 + match self { + ExternType::Func(ft) => ft.serialized_size(), + ExternType::Table(tt) => tt.serialized_size(), + ExternType::Memory(mt) => mt.serialized_size(), + ExternType::Global(gt) => gt.serialized_size(), + ExternType::Tag(ty) => ty.serialized_size(), + ExternType::Component(ct) => ct.serialized_size(), + ExternType::Instance(it) => it.serialized_size(), + ExternType::CoreModule(cmt) => cmt.serialized_size(), + ExternType::TypeDef(tdt) => tdt.serialized_size(), + ExternType::Resource(rt) => rt.serialized_size(), + } + } + fn to_bytes_with_provider<'a, PStream: crate::MemoryProvider>( &self, writer: &mut WriteStream<'a>, @@ -1007,6 +1051,10 @@ impl Checksummable for ResourceType

{ } impl ToBytes for ResourceType

{ + fn serialized_size(&self) -> usize { + self.0.serialized_size() + } + fn to_bytes_with_provider<'a, PStream: crate::MemoryProvider>( &self, writer: &mut WriteStream<'a>, @@ -1036,6 +1084,10 @@ impl Checksummable for ComponentAliasExportKind { } impl ToBytes for ComponentAliasExportKind { + fn serialized_size(&self) -> usize { + 1 // single byte discriminant + } + fn to_bytes_with_provider<'a, PStream: crate::MemoryProvider>( &self, writer: &mut WriteStream<'a>, @@ -1111,6 +1163,15 @@ impl

ToBytes for ComponentAlias

where P: MemoryProvider + Clone + Default + Eq + core::fmt::Debug, { + fn serialized_size(&self) -> usize { + 1 + match self { + ComponentAlias::InstanceExport(e) => e.serialized_size(), + ComponentAlias::CoreInstanceExport(e) => e.serialized_size(), + ComponentAlias::Outer(e) => e.serialized_size(), + ComponentAlias::_Phantom(_) => 0, + } + } + fn to_bytes_with_provider<'a, PStream: crate::MemoryProvider>( &self, writer: &mut WriteStream<'a>, @@ -1206,6 +1267,17 @@ impl

ToBytes for ComponentInstanceKind

where P: MemoryProvider + Clone + Default + Eq + core::fmt::Debug, { + fn serialized_size(&self) -> usize { + 1 + match self { + ComponentInstanceKind::Unknown => 0, + ComponentInstanceKind::Instantiate { + component_idx, + args, + } => component_idx.serialized_size() + args.serialized_size(), + ComponentInstanceKind::FromExports { exports } => exports.serialized_size(), + } + } + fn to_bytes_with_provider<'a, PStream: crate::MemoryProvider>( &self, writer: &mut WriteStream<'a>, @@ -1311,6 +1383,16 @@ impl

ToBytes for CoreInstanceKind

where P: MemoryProvider + Clone + Default + Eq + core::fmt::Debug, { + fn serialized_size(&self) -> usize { + 1 + match self { + CoreInstanceKind::Unknown => 0, + CoreInstanceKind::Instantiate { module_idx, args } => { + module_idx.serialized_size() + args.serialized_size() + }, + CoreInstanceKind::FromExports { exports } => exports.serialized_size(), + } + } + fn to_bytes_with_provider<'a, PStream: crate::MemoryProvider>( &self, writer: &mut WriteStream<'a>, @@ -1393,6 +1475,17 @@ impl Checksummable for CoreType { } impl ToBytes for CoreType { + fn serialized_size(&self) -> usize { + 1 + match self { + CoreType::Unknown => 0, + CoreType::Func(ft) => ft.serialized_size(), + CoreType::Table(tt) => tt.serialized_size(), + CoreType::Memory(mt) => mt.serialized_size(), + CoreType::Global(gt) => gt.serialized_size(), + CoreType::Tag(tag_ft) => tag_ft.serialized_size(), + } + } + fn to_bytes_with_provider<'a, PStream: crate::MemoryProvider>( &self, writer: &mut WriteStream<'a>, @@ -1472,6 +1565,10 @@ impl Checksummable for ComponentAliasOuter { } } impl ToBytes for ComponentAliasOuter { + fn serialized_size(&self) -> usize { + self.count.serialized_size() + self.index.serialized_size() + self.kind.serialized_size() + } + fn to_bytes_with_provider<'a, PStream: crate::MemoryProvider>( &self, writer: &mut WriteStream<'a>, @@ -1508,6 +1605,10 @@ impl

Checksummable for ComponentInstantiationArg

{ } impl

ToBytes for ComponentInstantiationArg

{ + fn serialized_size(&self) -> usize { + self.name.serialized_size() + self.index.serialized_size() + self.kind.serialized_size() + } + fn to_bytes_with_provider<'a, PStream: crate::MemoryProvider>( &self, writer: &mut WriteStream<'a>, @@ -1548,6 +1649,10 @@ impl

Checksummable for CoreInstantiationArg

{ } impl

ToBytes for CoreInstantiationArg

{ + fn serialized_size(&self) -> usize { + self.name.serialized_size() + self.index.serialized_size() + self.kind.serialized_size() + } + fn to_bytes_with_provider<'a, PStream: crate::MemoryProvider>( &self, writer: &mut WriteStream<'a>, diff --git a/kiln-foundation/src/component_type_store.rs b/kiln-foundation/src/component_type_store.rs index d42ed475..595eafd2 100644 --- a/kiln-foundation/src/component_type_store.rs +++ b/kiln-foundation/src/component_type_store.rs @@ -62,6 +62,10 @@ impl crate::traits::Checksummable for TypeRef { } impl ToBytes for TypeRef { + fn serialized_size(&self) -> usize { + self.0.serialized_size() + } + fn to_bytes_with_provider<'a, PStream: crate::MemoryProvider>( &self, writer: &mut WriteStream<'a>, diff --git a/kiln-foundation/src/safe_memory.rs b/kiln-foundation/src/safe_memory.rs index 1f5064e0..6f411157 100644 --- a/kiln-foundation/src/safe_memory.rs +++ b/kiln-foundation/src/safe_memory.rs @@ -731,6 +731,14 @@ impl PartialEq for StdProvider { #[cfg(feature = "std")] impl Eq for StdProvider {} +#[cfg(feature = "std")] +impl core::hash::Hash for StdProvider { + fn hash(&self, state: &mut H) { + self.data.hash(state); + self.verification_level.hash(state); + } +} + #[cfg(feature = "std")] impl Default for StdProvider { fn default() -> Self { @@ -931,31 +939,17 @@ impl Provider for StdProvider { /// lengths match and slices are valid, which `verify_access` and slice /// creation ensure. fn write_data(&mut self, offset: usize, data_to_write: &[u8]) -> Result<()> { - self.verify_access(offset, data_to_write.len())?; - self.track_access(offset, data_to_write.len()); - debug_assert!( - offset - .checked_add(data_to_write.len()) - .map_or(false, |end| end <= self.data.len()), - "StdProvider::write_data: offset+len must be <= self.data.len() after verify_access. \ - Offset: {}, Len: {}, DataLen: {}", - offset, - data_to_write.len(), - self.data.len() - ); - - // Safety: verify_access ensures offset + data_to_write.len() is within - // self.data.capacity(). And also ensures offset + data_to_write.len() - // <= self.data.len() (current initialized part for Vec) + // StdProvider is heap-backed and growable, so we resize before access + // rather than failing with verify_access on an empty Vec. let required_len = offset .checked_add(data_to_write.len()) .ok_or_else(|| Error::memory_error("Write offset + length calculation overflow"))?; if required_len > self.data.len() { - // Binary std/no_std choice - self.data.resize(required_len, 0u8); // Or some other default byte + self.data.resize(required_len, 0u8); } + self.track_access(offset, data_to_write.len()); self.data[offset..required_len].copy_from_slice(data_to_write); // If StdProvider maintained its own checksum for the whole data Vec, it would // need updating. diff --git a/kiln-foundation/src/traits.rs b/kiln-foundation/src/traits.rs index a514881d..9336940f 100644 --- a/kiln-foundation/src/traits.rs +++ b/kiln-foundation/src/traits.rs @@ -814,39 +814,125 @@ impl FromBytes for char { // from_bytes is provided by the trait } -// NEW: DefaultMemoryProvider -/// A default memory provider for contexts where no specific provider is given. -/// Binary std/no_std choice -// const DEFAULT_NO_STD_PROVIDER_CAPACITY: usize = 0; // Capacity defined by NoStdProvider itself +// DefaultMemoryProvider: uses StdProvider (heap-backed, growable) in std mode, +// NoStdProvider with fixed buffer in no_std mode. + +#[cfg(feature = "std")] +use crate::safe_memory::StdProvider; -/// Default memory provider for no_std environments when no specific provider is -/// given. Wraps `NoStdProvider` with a fixed-size backing array. -#[derive(Debug, Clone, PartialEq, Eq, Hash)] // Removed Copy -pub struct DefaultMemoryProvider(NoStdProvider<0>); // Use 0 for default capacity of NoStdProvider +/// Default memory provider for GC struct/array values. +/// In std mode, wraps heap-backed `StdProvider` for dynamic growth. +/// In no_std mode, wraps `NoStdProvider` with a fixed-size backing array. +#[cfg(feature = "std")] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct DefaultMemoryProvider(StdProvider); + +#[cfg(not(feature = "std"))] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct DefaultMemoryProvider(NoStdProvider<1152>); +#[cfg(feature = "std")] impl Default for DefaultMemoryProvider { fn default() -> Self { - // Note: Using NoStdProvider::<0>::default() here is legitimate as this is - // the default memory provider implementation for trait-level fallbacks - Self(NoStdProvider::<0>::default()) + Self(StdProvider::default()) } } +#[cfg(not(feature = "std"))] +impl Default for DefaultMemoryProvider { + fn default() -> Self { + Self(NoStdProvider::<1152>::default()) + } +} + +#[cfg(feature = "std")] impl RootMemoryProvider for DefaultMemoryProvider { - type Allocator = NoStdProvider<0>; + type Allocator = StdProvider; + + fn acquire_memory(&self, layout: core::alloc::Layout) -> kiln_error::Result<*mut u8> { + self.0.acquire_memory(layout) + } + + fn release_memory(&self, ptr: *mut u8, layout: core::alloc::Layout) -> kiln_error::Result<()> { + self.0.release_memory(ptr, layout) + } + + fn get_allocator(&self) -> &Self::Allocator { + &self.0 + } + + fn new_handler(&self) -> kiln_error::Result> + where + Self: Sized, + { + Ok(SafeMemoryHandler::new(self.clone())) + } + + fn borrow_slice(&self, offset: usize, len: usize) -> kiln_error::Result> { + self.0.borrow_slice(offset, len) + } + + fn write_data(&mut self, offset: usize, data: &[u8]) -> kiln_error::Result<()> { + self.0.write_data(offset, data) + } + + fn verify_access(&self, offset: usize, len: usize) -> kiln_error::Result<()> { + self.0.verify_access(offset, len) + } + + fn size(&self) -> usize { + self.0.size() + } + + fn capacity(&self) -> usize { + self.0.capacity() + } + + fn verify_integrity(&self) -> kiln_error::Result<()> { + self.0.verify_integrity() + } + + fn set_verification_level(&mut self, level: VerificationLevel) { + self.0.set_verification_level(level) + } + + fn verification_level(&self) -> VerificationLevel { + self.0.verification_level() + } + + fn memory_stats(&self) -> Stats { + self.0.memory_stats() + } + + fn get_slice_mut(&mut self, offset: usize, len: usize) -> kiln_error::Result> { + self.0.get_slice_mut(offset, len) + } - // Binary std/no_std choice + fn copy_within( + &mut self, + src_offset: usize, + dst_offset: usize, + len: usize, + ) -> kiln_error::Result<()> { + self.0.copy_within(src_offset, dst_offset, len) + } + + fn ensure_used_up_to(&mut self, byte_offset: usize) -> kiln_error::Result<()> { + self.0.ensure_used_up_to(byte_offset) + } +} + +#[cfg(not(feature = "std"))] +impl RootMemoryProvider for DefaultMemoryProvider { + type Allocator = NoStdProvider<1152>; fn acquire_memory(&self, _layout: core::alloc::Layout) -> kiln_error::Result<*mut u8> { - // Binary std/no_std choice Err(KilnError::memory_error( - "DefaultMemoryProvider (NoStdProvider<0>) cannot dynamically allocate memory.", + "DefaultMemoryProvider (no_std) cannot dynamically allocate memory.", )) } fn release_memory(&self, _ptr: *mut u8, _layout: core::alloc::Layout) -> kiln_error::Result<()> { - // Binary std/no_std choice - // Safety: This encapsulates unsafe operations internally Ok(()) } @@ -861,9 +947,8 @@ impl RootMemoryProvider for DefaultMemoryProvider { Ok(SafeMemoryHandler::new(self.clone())) } - // Implement missing methods from crate::safe_memory::Provider fn borrow_slice(&self, offset: usize, len: usize) -> kiln_error::Result> { - self.0.borrow_slice(offset, len) // Delegate to inner NoStdProvider + self.0.borrow_slice(offset, len) } fn write_data(&mut self, offset: usize, data: &[u8]) -> kiln_error::Result<()> { diff --git a/kiln-foundation/src/types.rs b/kiln-foundation/src/types.rs index 0722b47d..35eb4923 100644 --- a/kiln-foundation/src/types.rs +++ b/kiln-foundation/src/types.rs @@ -572,6 +572,14 @@ impl RefType { ValueType::ArrayRef(idx) => Ok(RefType::Gc(GcRefType::new(true, HeapType::Concrete(idx)))), ValueType::NullFuncRef => Ok(RefType::Gc(GcRefType::NULLFUNCREF)), ValueType::ExnRef => Ok(RefType::Gc(GcRefType::EXNREF)), + ValueType::TypedFuncRef(idx, nullable) => { + if idx == u32::MAX { + // Sentinel for abstract func heap type + Ok(RefType::Gc(GcRefType::new(nullable, HeapType::Func))) + } else { + Ok(RefType::Gc(GcRefType::new(nullable, HeapType::Concrete(idx)))) + } + } _ => Err(Error::runtime_execution_error( "Invalid ValueType for RefType conversion", )), @@ -640,7 +648,14 @@ impl TryFrom for RefType { ValueType::ArrayRef(idx) => Ok(RefType::Gc(GcRefType::new(true, HeapType::Concrete(idx)))), ValueType::NullFuncRef => Ok(RefType::Gc(GcRefType::NULLFUNCREF)), ValueType::ExnRef => Ok(RefType::Gc(GcRefType::EXNREF)), - ValueType::TypedFuncRef(idx, nullable) => Ok(RefType::Gc(GcRefType::new(nullable, HeapType::Concrete(idx)))), + ValueType::TypedFuncRef(idx, nullable) => { + if idx == u32::MAX { + // Sentinel for abstract func heap type (ref func)/(ref null func) + Ok(RefType::Gc(GcRefType::new(nullable, HeapType::Func))) + } else { + Ok(RefType::Gc(GcRefType::new(nullable, HeapType::Concrete(idx)))) + } + } _ => Err(Error::runtime_execution_error( "Invalid ValueType for RefType try_from conversion", )), @@ -1072,16 +1087,21 @@ impl Checksummable for CatchHandler { impl Default for CatchHandler { fn default() -> Self { - Self::CatchAll { label: 0 } + // Use Catch variant as default since all variants now serialize to the + // same fixed size (9 bytes). This ensures BoundedVec's item_serialized_size + // computed from default().serialized_size() matches all variants. + Self::Catch { tag_idx: 0, label: 0 } } } impl ToBytes for CatchHandler { fn serialized_size(&self) -> usize { - match self { - Self::Catch { .. } | Self::CatchRef { .. } => 1 + 4 + 4, // discriminant + tag_idx + label - Self::CatchAll { .. } | Self::CatchAllRef { .. } => 1 + 4, // discriminant + label - } + // All variants use the same fixed size for BoundedVec compatibility. + // BoundedVec uses a fixed item_serialized_size computed from Default, + // so all variants must serialize to the same number of bytes. + // Format: discriminant(1) + tag_idx(4) + label(4) = 9 bytes + // CatchAll/CatchAllRef write 0 for the unused tag_idx field. + 1 + 4 + 4 } fn to_bytes_with_provider<'a, PStream: crate::MemoryProvider>( @@ -1102,10 +1122,12 @@ impl ToBytes for CatchHandler { } Self::CatchAll { label } => { writer.write_u8(0x02)?; + writer.write_u32_le(0)?; // padding for fixed-size layout writer.write_u32_le(*label)?; } Self::CatchAllRef { label } => { writer.write_u8(0x03)?; + writer.write_u32_le(0)?; // padding for fixed-size layout writer.write_u32_le(*label)?; } } @@ -1131,10 +1153,12 @@ impl FromBytes for CatchHandler { Ok(Self::CatchRef { tag_idx, label }) } 0x02 => { + let _padding = reader.read_u32_le()?; // skip fixed-size padding let label = reader.read_u32_le()?; Ok(Self::CatchAll { label }) } 0x03 => { + let _padding = reader.read_u32_le()?; // skip fixed-size padding let label = reader.read_u32_le()?; Ok(Self::CatchAllRef { label }) } @@ -1445,6 +1469,12 @@ pub enum Instruction { + checksum.update_slice(&[0x14]); // call_ref opcode + type_idx.update_checksum(checksum); + }, + Instruction::ReturnCallRef(type_idx) => { + checksum.update_slice(&[0x15]); // return_call_ref opcode + type_idx.update_checksum(checksum); + }, Instruction::BrOnNull(label_idx) => { checksum.update_slice(&[0xD5]); // br_on_null opcode label_idx.update_checksum(checksum); @@ -2101,10 +2139,10 @@ impl { - checksum.update_slice(&[0xD3]); // ref.as_non_null opcode + checksum.update_slice(&[0xD4]); // ref.as_non_null opcode }, Instruction::RefEq => { - checksum.update_slice(&[0xD2]); // ref.eq opcode + checksum.update_slice(&[0xD3]); // ref.eq opcode }, Instruction::LocalGet(idx) | Instruction::LocalSet(idx) @@ -2569,8 +2607,8 @@ impl writer.write_u8(0xD1)?, // ref.is_null opcode - Instruction::RefAsNonNull => writer.write_u8(0xD3)?, // ref.as_non_null opcode - Instruction::RefEq => writer.write_u8(0xD2)?, // ref.eq opcode + Instruction::RefAsNonNull => writer.write_u8(0xD4)?, // ref.as_non_null opcode + Instruction::RefEq => writer.write_u8(0xD3)?, // ref.eq opcode Instruction::LocalGet(idx) => { writer.write_u8(0x20)?; writer.write_u32_le(*idx)?; diff --git a/kiln-foundation/src/values.rs b/kiln-foundation/src/values.rs index 78ea97aa..818cec48 100644 --- a/kiln-foundation/src/values.rs +++ b/kiln-foundation/src/values.rs @@ -49,6 +49,8 @@ use crate::types::{ MAX_ARRAY_ELEMENTS, MAX_STRUCT_FIELDS, }; // Import ValueType and RefType +use core::sync::atomic::{AtomicU64, Ordering}; + use crate::{ bounded::BoundedVec, prelude::{ @@ -60,22 +62,48 @@ use crate::{ MemoryProvider, }; // Added for Checksummable +/// Global counter for GC allocation identity. +/// Each struct/array allocation gets a unique ID for ref.eq identity comparison. +static GC_ALLOC_COUNTER: AtomicU64 = AtomicU64::new(1); + +/// Generate a new unique allocation ID for GC objects. +/// Returns a u32 that fits within the serialized representation. +/// Wraps around at u32::MAX which is acceptable for identity comparison +/// (4 billion allocations before potential collision). +fn next_alloc_id() -> u32 { + (GC_ALLOC_COUNTER.fetch_add(1, Ordering::Relaxed) & 0xFFFF_FFFF) as u32 +} + /// GC-managed struct reference for WebAssembly 3.0 -#[derive(Debug, Clone, PartialEq, Eq, core::hash::Hash)] +#[derive(Debug, Clone, Eq)] pub struct StructRef< P: MemoryProvider + Default + Clone + core::fmt::Debug + PartialEq + Eq = DefaultMemoryProvider, > { + /// Unique allocation identity for ref.eq comparison + pub alloc_id: u32, /// Type index of the struct pub type_index: u32, /// Field values pub fields: BoundedVec, } +impl PartialEq for StructRef

{ + fn eq(&self, other: &Self) -> bool { + self.alloc_id == other.alloc_id + } +} + +impl core::hash::Hash for StructRef

{ + fn hash(&self, state: &mut H) { + self.alloc_id.hash(state); + } +} + impl StructRef

{ - /// Create a new struct reference + /// Create a new struct reference with a unique allocation ID pub fn new(type_index: u32, provider: P) -> kiln_error::Result { let fields = BoundedVec::new(provider).map_err(Error::from)?; - Ok(Self { type_index, fields }) + Ok(Self { alloc_id: next_alloc_id(), type_index, fields }) } /// Set a field value @@ -103,26 +131,42 @@ impl De { fn default() -> Self { let provider = P::default(); - Self::new(0, provider).expect("Default StructRef creation failed") + let fields = BoundedVec::new(provider).expect("Default StructRef creation failed"); + Self { alloc_id: 0, type_index: 0, fields } } } /// GC-managed array reference for WebAssembly 3.0 -#[derive(Debug, Clone, PartialEq, Eq, core::hash::Hash)] +#[derive(Debug, Clone, Eq)] pub struct ArrayRef< P: MemoryProvider + Default + Clone + core::fmt::Debug + PartialEq + Eq = DefaultMemoryProvider, > { + /// Unique allocation identity for ref.eq comparison + pub alloc_id: u32, /// Type index of the array pub type_index: u32, /// Array elements pub elements: BoundedVec, } +impl PartialEq for ArrayRef

{ + fn eq(&self, other: &Self) -> bool { + self.alloc_id == other.alloc_id + } +} + +impl core::hash::Hash for ArrayRef

{ + fn hash(&self, state: &mut H) { + self.alloc_id.hash(state); + } +} + impl ArrayRef

{ - /// Create a new array reference + /// Create a new array reference with a unique allocation ID pub fn new(type_index: u32, provider: P) -> kiln_error::Result { let elements = BoundedVec::new(provider).map_err(Error::from)?; Ok(Self { + alloc_id: next_alloc_id(), type_index, elements, }) @@ -140,6 +184,7 @@ impl Ar elements.push(init_value.clone()).map_err(Error::from)?; } Ok(Self { + alloc_id: next_alloc_id(), type_index, elements, }) @@ -180,7 +225,8 @@ impl De { fn default() -> Self { let provider = P::default(); - Self::new(0, provider).expect("Default ArrayRef creation failed") + let elements = BoundedVec::new(provider).expect("Default ArrayRef creation failed"); + Self { alloc_id: 0, type_index: 0, elements } } } @@ -280,12 +326,9 @@ impl Eq for Value {} impl Default for Value { fn default() -> Self { - // Return FuncRef(None) as default because: - // 1. Tables store Option and commonly use FuncRef values - // 2. Option::serialized_size() uses T::default().serialized_size() - // 3. FuncRef has size 6 (1 disc + 1 flag + 4 padding), larger than I32's size 5 - // 4. This ensures BoundedVec slots are large enough for all reference types - Value::FuncRef(None) + // Return I32(0) as default - Value::serialized_size() returns a fixed maximum + // across all variants, ensuring BoundedVec slots are always large enough. + Value::I32(0) } } @@ -1236,40 +1279,19 @@ impl Checksummable for Value { } } +/// Fixed serialized size for Value in BoundedVec. +/// Must be large enough for the largest Value variant serialization: +/// - V128: 1 disc + 16 bytes = 17 +/// - StructRef(Some(empty)): 1 disc + 1 flag + 4 alloc_id + 4 type_idx + 4 field_count = 14 +/// - FuncRef(Some): 1 disc + 1 flag + 8 bytes = 10 +/// Use 18 bytes to comfortably accommodate all variants. +const VALUE_SERIALIZED_SIZE: usize = 18; + impl ToBytes for Value { fn serialized_size(&self) -> usize { - // 1 byte for discriminant + variant-specific size - 1 + match self { - Value::I32(_) => 4, - Value::I64(_) => 8, - Value::F32(_) => 4, - Value::F64(_) => 8, - Value::V128(_) | Value::I16x8(_) => 16, - // Reference types with Option: always use max size for BoundedVec compatibility - // 1 byte for Some/None flag + 4 bytes for index (always reserved) - Value::FuncRef(_) => 1 + 8, // 1 flag + 4 index + 4 instance_id - Value::ExternRef(_) => 1 + 4, - Value::ExnRef(_) => 1 + 4, - Value::I31Ref(_) => 1 + 4, - Value::Ref(_) => 4, - Value::StructRef(_) => 1 + 4, - Value::ArrayRef(_) => 1 + 4, - Value::Bool(_) => 1, - Value::S8(_) | Value::U8(_) => 1, - Value::S16(_) | Value::U16(_) => 2, - Value::S32(_) | Value::U32(_) => 4, - Value::S64(_) | Value::U64(_) => 8, - Value::Char(_) => 4, - // String: length (4) + content (variable, use a reasonable max) - Value::String(s) => 4 + s.len(), - // Complex types - use conservative estimate - Value::List(_) | Value::Tuple(_) | Value::Record(_) => 64, - Value::Variant(_, _) | Value::Enum(_) => 8, - Value::Option(_) | Value::Result(_) => 16, - Value::Flags(_) => 8, - Value::Own(_) | Value::Borrow(_) | Value::Stream(_) | Value::Future(_) => 4, - Value::Void => 0, - } + // Return a fixed maximum size for all variants to ensure BoundedVec + // slots are always large enough regardless of which variant is stored. + VALUE_SERIALIZED_SIZE } fn to_bytes_with_provider<'a, PStream: crate::MemoryProvider>( @@ -1561,6 +1583,8 @@ impl To writer: &mut WriteStream<'a>, provider: &PStream, ) -> kiln_error::Result<()> { + // Write allocation identity for ref.eq + writer.write_u32_le(self.alloc_id)?; // Write type index self.type_index.to_bytes_with_provider(writer, provider)?; // Write field count @@ -1580,12 +1604,15 @@ impl Fr reader: &mut ReadStream<'a>, provider: &PStream, ) -> kiln_error::Result { + // Read allocation identity + let alloc_id = reader.read_u32_le()?; // Read type index let type_index = u32::from_bytes_with_provider(reader, provider)?; // Read field count let field_count = reader.read_u32_le()?; - // Create struct with default provider - let mut struct_ref = StructRef::new(type_index, P::default())?; + // Create struct with preserved alloc_id + let fields = BoundedVec::new(P::default()).map_err(Error::from)?; + let mut struct_ref = StructRef { alloc_id, type_index, fields }; // Read fields for _ in 0..field_count { let field = Value::from_bytes_with_provider(reader, provider)?; @@ -1612,6 +1639,8 @@ impl To writer: &mut WriteStream<'a>, provider: &PStream, ) -> kiln_error::Result<()> { + // Write allocation identity for ref.eq + writer.write_u32_le(self.alloc_id)?; // Write type index self.type_index.to_bytes_with_provider(writer, provider)?; // Write element count @@ -1631,12 +1660,15 @@ impl Fr reader: &mut ReadStream<'a>, provider: &PStream, ) -> kiln_error::Result { + // Read allocation identity + let alloc_id = reader.read_u32_le()?; // Read type index let type_index = u32::from_bytes_with_provider(reader, provider)?; // Read element count let element_count = reader.read_u32_le()?; - // Create array with default provider - let mut array_ref = ArrayRef::new(type_index, P::default())?; + // Create array with preserved alloc_id + let elements = BoundedVec::new(P::default()).map_err(Error::from)?; + let mut array_ref = ArrayRef { alloc_id, type_index, elements }; // Read elements for _ in 0..element_count { let element = Value::from_bytes_with_provider(reader, provider)?; diff --git a/kiln-foundation/src/verification.rs b/kiln-foundation/src/verification.rs index bcf5cff6..e5f389a1 100644 --- a/kiln-foundation/src/verification.rs +++ b/kiln-foundation/src/verification.rs @@ -231,6 +231,10 @@ impl fmt::Display for Checksum { } impl ToBytes for Checksum { + fn serialized_size(&self) -> usize { + core::mem::size_of::() // Checksum is stored as a single u32 + } + fn to_bytes_with_provider<'a, PStream: crate::MemoryProvider>( &self, writer: &mut WriteStream<'a>, diff --git a/kiln-runtime/src/instruction_parser.rs b/kiln-runtime/src/instruction_parser.rs index ed5411ba..8272076f 100644 --- a/kiln-runtime/src/instruction_parser.rs +++ b/kiln-runtime/src/instruction_parser.rs @@ -289,6 +289,18 @@ fn parse_instruction_with_provider( consumed += table_bytes; Instruction::ReturnCallIndirect(type_idx, table_idx) }, + 0x14 => { + // call_ref: type_idx (LEB128 u32) - typed function reference call + let (type_idx, bytes) = read_leb128_u32(bytecode, offset + 1)?; + consumed += bytes; + Instruction::CallRef(type_idx) + }, + 0x15 => { + // return_call_ref: type_idx (LEB128 u32) - typed function reference tail call + let (type_idx, bytes) = read_leb128_u32(bytecode, offset + 1)?; + consumed += bytes; + Instruction::ReturnCallRef(type_idx) + }, // Exception handling instructions (continued) 0x18 => { @@ -430,247 +442,136 @@ fn parse_instruction_with_provider( // Memory instructions 0x28 => { - let (align, bytes1) = read_leb128_u32(bytecode, offset + 1)?; - let (offset, bytes2) = read_leb128_u32(bytecode, offset + 1 + bytes1)?; - consumed += bytes1 + bytes2; - Instruction::I32Load(MemArg { - align_exponent: align, - offset, - memory_index: 0, - }) + let (memarg, bytes) = parse_memarg(bytecode, offset + 1)?; + consumed += bytes; + Instruction::I32Load(memarg) }, 0x29 => { - let (align, bytes1) = read_leb128_u32(bytecode, offset + 1)?; - let (offset, bytes2) = read_leb128_u32(bytecode, offset + 1 + bytes1)?; - consumed += bytes1 + bytes2; - Instruction::I64Load(MemArg { - align_exponent: align, - offset, - memory_index: 0, - }) + let (memarg, bytes) = parse_memarg(bytecode, offset + 1)?; + consumed += bytes; + Instruction::I64Load(memarg) }, 0x2A => { - let (align, bytes1) = read_leb128_u32(bytecode, offset + 1)?; - let (offset, bytes2) = read_leb128_u32(bytecode, offset + 1 + bytes1)?; - consumed += bytes1 + bytes2; - Instruction::F32Load(MemArg { - align_exponent: align, - offset, - memory_index: 0, - }) + let (memarg, bytes) = parse_memarg(bytecode, offset + 1)?; + consumed += bytes; + Instruction::F32Load(memarg) }, 0x2B => { - let (align, bytes1) = read_leb128_u32(bytecode, offset + 1)?; - let (offset, bytes2) = read_leb128_u32(bytecode, offset + 1 + bytes1)?; - consumed += bytes1 + bytes2; - Instruction::F64Load(MemArg { - align_exponent: align, - offset, - memory_index: 0, - }) + let (memarg, bytes) = parse_memarg(bytecode, offset + 1)?; + consumed += bytes; + Instruction::F64Load(memarg) }, 0x2C => { - let (align, bytes1) = read_leb128_u32(bytecode, offset + 1)?; - let (offset, bytes2) = read_leb128_u32(bytecode, offset + 1 + bytes1)?; - consumed += bytes1 + bytes2; - Instruction::I32Load8S(MemArg { - align_exponent: align, - offset, - memory_index: 0, - }) + let (memarg, bytes) = parse_memarg(bytecode, offset + 1)?; + consumed += bytes; + Instruction::I32Load8S(memarg) }, 0x2D => { - let (align, bytes1) = read_leb128_u32(bytecode, offset + 1)?; - let (offset, bytes2) = read_leb128_u32(bytecode, offset + 1 + bytes1)?; - consumed += bytes1 + bytes2; - Instruction::I32Load8U(MemArg { - align_exponent: align, - offset, - memory_index: 0, - }) + let (memarg, bytes) = parse_memarg(bytecode, offset + 1)?; + consumed += bytes; + Instruction::I32Load8U(memarg) }, 0x2E => { - let (align, bytes1) = read_leb128_u32(bytecode, offset + 1)?; - let (offset, bytes2) = read_leb128_u32(bytecode, offset + 1 + bytes1)?; - consumed += bytes1 + bytes2; - Instruction::I32Load16S(MemArg { - align_exponent: align, - offset, - memory_index: 0, - }) + let (memarg, bytes) = parse_memarg(bytecode, offset + 1)?; + consumed += bytes; + Instruction::I32Load16S(memarg) }, 0x2F => { - let (align, bytes1) = read_leb128_u32(bytecode, offset + 1)?; - let (offset, bytes2) = read_leb128_u32(bytecode, offset + 1 + bytes1)?; - consumed += bytes1 + bytes2; - Instruction::I32Load16U(MemArg { - align_exponent: align, - offset, - memory_index: 0, - }) + let (memarg, bytes) = parse_memarg(bytecode, offset + 1)?; + consumed += bytes; + Instruction::I32Load16U(memarg) }, 0x30 => { - let (align, bytes1) = read_leb128_u32(bytecode, offset + 1)?; - let (offset, bytes2) = read_leb128_u32(bytecode, offset + 1 + bytes1)?; - consumed += bytes1 + bytes2; - Instruction::I64Load8S(MemArg { - align_exponent: align, - offset, - memory_index: 0, - }) + let (memarg, bytes) = parse_memarg(bytecode, offset + 1)?; + consumed += bytes; + Instruction::I64Load8S(memarg) }, 0x31 => { - let (align, bytes1) = read_leb128_u32(bytecode, offset + 1)?; - let (offset, bytes2) = read_leb128_u32(bytecode, offset + 1 + bytes1)?; - consumed += bytes1 + bytes2; - Instruction::I64Load8U(MemArg { - align_exponent: align, - offset, - memory_index: 0, - }) + let (memarg, bytes) = parse_memarg(bytecode, offset + 1)?; + consumed += bytes; + Instruction::I64Load8U(memarg) }, 0x32 => { - let (align, bytes1) = read_leb128_u32(bytecode, offset + 1)?; - let (offset, bytes2) = read_leb128_u32(bytecode, offset + 1 + bytes1)?; - consumed += bytes1 + bytes2; - Instruction::I64Load16S(MemArg { - align_exponent: align, - offset, - memory_index: 0, - }) + let (memarg, bytes) = parse_memarg(bytecode, offset + 1)?; + consumed += bytes; + Instruction::I64Load16S(memarg) }, 0x33 => { - let (align, bytes1) = read_leb128_u32(bytecode, offset + 1)?; - let (offset, bytes2) = read_leb128_u32(bytecode, offset + 1 + bytes1)?; - consumed += bytes1 + bytes2; - Instruction::I64Load16U(MemArg { - align_exponent: align, - offset, - memory_index: 0, - }) + let (memarg, bytes) = parse_memarg(bytecode, offset + 1)?; + consumed += bytes; + Instruction::I64Load16U(memarg) }, 0x34 => { - let (align, bytes1) = read_leb128_u32(bytecode, offset + 1)?; - let (offset, bytes2) = read_leb128_u32(bytecode, offset + 1 + bytes1)?; - consumed += bytes1 + bytes2; - Instruction::I64Load32S(MemArg { - align_exponent: align, - offset, - memory_index: 0, - }) + let (memarg, bytes) = parse_memarg(bytecode, offset + 1)?; + consumed += bytes; + Instruction::I64Load32S(memarg) }, 0x35 => { - let (align, bytes1) = read_leb128_u32(bytecode, offset + 1)?; - let (offset, bytes2) = read_leb128_u32(bytecode, offset + 1 + bytes1)?; - consumed += bytes1 + bytes2; - Instruction::I64Load32U(MemArg { - align_exponent: align, - offset, - memory_index: 0, - }) + let (memarg, bytes) = parse_memarg(bytecode, offset + 1)?; + consumed += bytes; + Instruction::I64Load32U(memarg) }, 0x36 => { - let (align, bytes1) = read_leb128_u32(bytecode, offset + 1)?; - let (offset, bytes2) = read_leb128_u32(bytecode, offset + 1 + bytes1)?; - consumed += bytes1 + bytes2; - Instruction::I32Store(MemArg { - align_exponent: align, - offset, - memory_index: 0, - }) + let (memarg, bytes) = parse_memarg(bytecode, offset + 1)?; + consumed += bytes; + Instruction::I32Store(memarg) }, 0x37 => { - let (align, bytes1) = read_leb128_u32(bytecode, offset + 1)?; - let (offset, bytes2) = read_leb128_u32(bytecode, offset + 1 + bytes1)?; - consumed += bytes1 + bytes2; - Instruction::I64Store(MemArg { - align_exponent: align, - offset, - memory_index: 0, - }) + let (memarg, bytes) = parse_memarg(bytecode, offset + 1)?; + consumed += bytes; + Instruction::I64Store(memarg) }, 0x38 => { - let (align, bytes1) = read_leb128_u32(bytecode, offset + 1)?; - let (offset, bytes2) = read_leb128_u32(bytecode, offset + 1 + bytes1)?; - consumed += bytes1 + bytes2; - Instruction::F32Store(MemArg { - align_exponent: align, - offset, - memory_index: 0, - }) + let (memarg, bytes) = parse_memarg(bytecode, offset + 1)?; + consumed += bytes; + Instruction::F32Store(memarg) }, 0x39 => { - let (align, bytes1) = read_leb128_u32(bytecode, offset + 1)?; - let (offset, bytes2) = read_leb128_u32(bytecode, offset + 1 + bytes1)?; - consumed += bytes1 + bytes2; - Instruction::F64Store(MemArg { - align_exponent: align, - offset, - memory_index: 0, - }) + let (memarg, bytes) = parse_memarg(bytecode, offset + 1)?; + consumed += bytes; + Instruction::F64Store(memarg) }, 0x3A => { // i32.store8 - let (align, bytes1) = read_leb128_u32(bytecode, offset + 1)?; - let (offset, bytes2) = read_leb128_u32(bytecode, offset + 1 + bytes1)?; - consumed += bytes1 + bytes2; - Instruction::I32Store8(MemArg { - align_exponent: align, - offset, - memory_index: 0, - }) + let (memarg, bytes) = parse_memarg(bytecode, offset + 1)?; + consumed += bytes; + Instruction::I32Store8(memarg) }, 0x3B => { // i32.store16 - let (align, bytes1) = read_leb128_u32(bytecode, offset + 1)?; - let (offset, bytes2) = read_leb128_u32(bytecode, offset + 1 + bytes1)?; - consumed += bytes1 + bytes2; - Instruction::I32Store16(MemArg { - align_exponent: align, - offset, - memory_index: 0, - }) + let (memarg, bytes) = parse_memarg(bytecode, offset + 1)?; + consumed += bytes; + Instruction::I32Store16(memarg) }, 0x3C => { // i64.store8 - let (align, bytes1) = read_leb128_u32(bytecode, offset + 1)?; - let (offset, bytes2) = read_leb128_u32(bytecode, offset + 1 + bytes1)?; - consumed += bytes1 + bytes2; - Instruction::I64Store8(MemArg { - align_exponent: align, - offset, - memory_index: 0, - }) + let (memarg, bytes) = parse_memarg(bytecode, offset + 1)?; + consumed += bytes; + Instruction::I64Store8(memarg) }, 0x3D => { // i64.store16 - let (align, bytes1) = read_leb128_u32(bytecode, offset + 1)?; - let (offset, bytes2) = read_leb128_u32(bytecode, offset + 1 + bytes1)?; - consumed += bytes1 + bytes2; - Instruction::I64Store16(MemArg { - align_exponent: align, - offset, - memory_index: 0, - }) + let (memarg, bytes) = parse_memarg(bytecode, offset + 1)?; + consumed += bytes; + Instruction::I64Store16(memarg) }, 0x3E => { // i64.store32 - let (align, bytes1) = read_leb128_u32(bytecode, offset + 1)?; - let (offset, bytes2) = read_leb128_u32(bytecode, offset + 1 + bytes1)?; - consumed += bytes1 + bytes2; - Instruction::I64Store32(MemArg { - align_exponent: align, - offset, - memory_index: 0, - }) + let (memarg, bytes) = parse_memarg(bytecode, offset + 1)?; + consumed += bytes; + Instruction::I64Store32(memarg) }, 0x3F => { - consumed += 1; // Skip reserved byte - Instruction::MemorySize(0) + // memory.size: the byte is the memory index (0x00 for single memory) + let (mem_idx, bytes) = read_leb128_u32(bytecode, offset + 1)?; + consumed += bytes; + Instruction::MemorySize(mem_idx) }, 0x40 => { - consumed += 1; // Skip reserved byte - Instruction::MemoryGrow(0) + // memory.grow: the byte is the memory index (0x00 for single memory) + let (mem_idx, bytes) = read_leb128_u32(bytecode, offset + 1)?; + consumed += bytes; + Instruction::MemoryGrow(mem_idx) }, // Numeric instructions - Constants @@ -907,6 +808,20 @@ fn parse_instruction_with_provider( consumed += bytes; Instruction::RefFunc(func_idx) }, + 0xD3 => Instruction::RefEq, + 0xD4 => Instruction::RefAsNonNull, + 0xD5 => { + // br_on_null l - branch to label if reference is null + let (label_idx, bytes) = read_leb128_u32(bytecode, offset + 1)?; + consumed += bytes; + Instruction::BrOnNull(label_idx) + }, + 0xD6 => { + // br_on_non_null l - branch to label if reference is not null + let (label_idx, bytes) = read_leb128_u32(bytecode, offset + 1)?; + consumed += bytes; + Instruction::BrOnNonNull(label_idx) + }, // Multi-byte opcodes (bulk memory, SIMD, etc.) 0xFC => { @@ -1081,6 +996,134 @@ fn parse_instruction_with_provider( } } + // Threads/Atomics instructions (0xFE prefix) - WebAssembly Threads Proposal + 0xFE => { + // Atomic instructions use 0xFE prefix followed by LEB128-encoded sub-opcode + let (atomic_opcode, opcode_bytes) = read_leb128_u32(bytecode, offset + 1)?; + consumed += opcode_bytes; + + match atomic_opcode { + // atomic.fence (0x03) - has a reserved 0x00 byte immediate + 0x03 => { + if offset + consumed >= bytecode.len() { + return Err(Error::parse_error("Unexpected end in atomic.fence")); + } + let reserved = bytecode[offset + consumed]; + consumed += 1; + if reserved != 0x00 { + return Err(Error::parse_error("Invalid reserved byte in atomic.fence")); + } + Instruction::AtomicFence + } + + // All other atomic instructions take a memarg + _ => { + let (memarg, memarg_bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += memarg_bytes; + + match atomic_opcode { + // memory.atomic.notify (0x00) + 0x00 => Instruction::MemoryAtomicNotify { memarg }, + // memory.atomic.wait32 (0x01) + 0x01 => Instruction::MemoryAtomicWait32 { memarg }, + // memory.atomic.wait64 (0x02) + 0x02 => Instruction::MemoryAtomicWait64 { memarg }, + + // Atomic loads + 0x10 => Instruction::I32AtomicLoad { memarg }, + 0x11 => Instruction::I64AtomicLoad { memarg }, + 0x12 => Instruction::I32AtomicLoad8U { memarg }, + 0x13 => Instruction::I32AtomicLoad16U { memarg }, + 0x14 => Instruction::I64AtomicLoad8U { memarg }, + 0x15 => Instruction::I64AtomicLoad16U { memarg }, + 0x16 => Instruction::I64AtomicLoad32U { memarg }, + + // Atomic stores + 0x17 => Instruction::I32AtomicStore { memarg }, + 0x18 => Instruction::I64AtomicStore { memarg }, + 0x19 => Instruction::I32AtomicStore8 { memarg }, + 0x1A => Instruction::I32AtomicStore16 { memarg }, + 0x1B => Instruction::I64AtomicStore8 { memarg }, + 0x1C => Instruction::I64AtomicStore16 { memarg }, + 0x1D => Instruction::I64AtomicStore32 { memarg }, + + // Atomic RMW: add + 0x1E => Instruction::I32AtomicRmwAdd { memarg }, + 0x1F => Instruction::I64AtomicRmwAdd { memarg }, + 0x20 => Instruction::I32AtomicRmw8AddU { memarg }, + 0x21 => Instruction::I32AtomicRmw16AddU { memarg }, + 0x22 => Instruction::I64AtomicRmw8AddU { memarg }, + 0x23 => Instruction::I64AtomicRmw16AddU { memarg }, + 0x24 => Instruction::I64AtomicRmw32AddU { memarg }, + + // Atomic RMW: sub + 0x25 => Instruction::I32AtomicRmwSub { memarg }, + 0x26 => Instruction::I64AtomicRmwSub { memarg }, + 0x27 => Instruction::I32AtomicRmw8SubU { memarg }, + 0x28 => Instruction::I32AtomicRmw16SubU { memarg }, + 0x29 => Instruction::I64AtomicRmw8SubU { memarg }, + 0x2A => Instruction::I64AtomicRmw16SubU { memarg }, + 0x2B => Instruction::I64AtomicRmw32SubU { memarg }, + + // Atomic RMW: and + 0x2C => Instruction::I32AtomicRmwAnd { memarg }, + 0x2D => Instruction::I64AtomicRmwAnd { memarg }, + 0x2E => Instruction::I32AtomicRmw8AndU { memarg }, + 0x2F => Instruction::I32AtomicRmw16AndU { memarg }, + 0x30 => Instruction::I64AtomicRmw8AndU { memarg }, + 0x31 => Instruction::I64AtomicRmw16AndU { memarg }, + 0x32 => Instruction::I64AtomicRmw32AndU { memarg }, + + // Atomic RMW: or + 0x33 => Instruction::I32AtomicRmwOr { memarg }, + 0x34 => Instruction::I64AtomicRmwOr { memarg }, + 0x35 => Instruction::I32AtomicRmw8OrU { memarg }, + 0x36 => Instruction::I32AtomicRmw16OrU { memarg }, + 0x37 => Instruction::I64AtomicRmw8OrU { memarg }, + 0x38 => Instruction::I64AtomicRmw16OrU { memarg }, + 0x39 => Instruction::I64AtomicRmw32OrU { memarg }, + + // Atomic RMW: xor + 0x3A => Instruction::I32AtomicRmwXor { memarg }, + 0x3B => Instruction::I64AtomicRmwXor { memarg }, + 0x3C => Instruction::I32AtomicRmw8XorU { memarg }, + 0x3D => Instruction::I32AtomicRmw16XorU { memarg }, + 0x3E => Instruction::I64AtomicRmw8XorU { memarg }, + 0x3F => Instruction::I64AtomicRmw16XorU { memarg }, + 0x40 => Instruction::I64AtomicRmw32XorU { memarg }, + + // Atomic RMW: xchg + 0x41 => Instruction::I32AtomicRmwXchg { memarg }, + 0x42 => Instruction::I64AtomicRmwXchg { memarg }, + 0x43 => Instruction::I32AtomicRmw8XchgU { memarg }, + 0x44 => Instruction::I32AtomicRmw16XchgU { memarg }, + 0x45 => Instruction::I64AtomicRmw8XchgU { memarg }, + 0x46 => Instruction::I64AtomicRmw16XchgU { memarg }, + 0x47 => Instruction::I64AtomicRmw32XchgU { memarg }, + + // Atomic RMW: cmpxchg + 0x48 => Instruction::I32AtomicRmwCmpxchg { memarg }, + 0x49 => Instruction::I64AtomicRmwCmpxchg { memarg }, + 0x4A => Instruction::I32AtomicRmw8CmpxchgU { memarg }, + 0x4B => Instruction::I32AtomicRmw16CmpxchgU { memarg }, + 0x4C => Instruction::I64AtomicRmw8CmpxchgU { memarg }, + 0x4D => Instruction::I64AtomicRmw16CmpxchgU { memarg }, + 0x4E => Instruction::I64AtomicRmw32CmpxchgU { memarg }, + + _ => { + #[cfg(feature = "tracing")] + kiln_foundation::tracing::warn!( + subopcode = format!("0xFE 0x{:02X}", atomic_opcode), + offset = offset, + "Unknown atomic sub-opcode" + ); + return Err(Error::parse_error("Unknown atomic instruction opcode")); + } + } + } + } + } + // GC instructions (0xFB prefix) - WebAssembly GC Proposal 0xFB => { // GC instructions use 0xFB prefix followed by LEB128-encoded opcode @@ -1330,6 +1373,440 @@ fn parse_instruction_with_provider( } } + // Atomic instructions (0xFE prefix) - WebAssembly Threads Proposal + 0xFE => { + // Atomic instructions use 0xFE prefix followed by LEB128-encoded sub-opcode + let (atomic_opcode, opcode_bytes) = read_leb128_u32(bytecode, offset + 1)?; + consumed += opcode_bytes; + + match atomic_opcode { + // memory.atomic.notify + 0x00 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::MemoryAtomicNotify { memarg } + } + // memory.atomic.wait32 + 0x01 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::MemoryAtomicWait32 { memarg } + } + // memory.atomic.wait64 + 0x02 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::MemoryAtomicWait64 { memarg } + } + // atomic.fence + 0x03 => { + // atomic.fence has a single zero byte immediate + if offset + consumed >= bytecode.len() { + return Err(Error::parse_error("Unexpected end of bytecode in atomic.fence")); + } + let fence_byte = bytecode[offset + consumed]; + consumed += 1; + if fence_byte != 0x00 { + return Err(Error::parse_error("Invalid atomic.fence immediate (must be 0x00)")); + } + Instruction::AtomicFence + } + + // i32.atomic.load + 0x10 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicLoad { memarg } + } + // i64.atomic.load + 0x11 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicLoad { memarg } + } + // i32.atomic.load8_u + 0x12 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicLoad8U { memarg } + } + // i32.atomic.load16_u + 0x13 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicLoad16U { memarg } + } + // i64.atomic.load8_u + 0x14 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicLoad8U { memarg } + } + // i64.atomic.load16_u + 0x15 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicLoad16U { memarg } + } + // i64.atomic.load32_u + 0x16 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicLoad32U { memarg } + } + + // i32.atomic.store + 0x17 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicStore { memarg } + } + // i64.atomic.store + 0x18 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicStore { memarg } + } + // i32.atomic.store8 + 0x19 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicStore8 { memarg } + } + // i32.atomic.store16 + 0x1A => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicStore16 { memarg } + } + // i64.atomic.store8 + 0x1B => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicStore8 { memarg } + } + // i64.atomic.store16 + 0x1C => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicStore16 { memarg } + } + // i64.atomic.store32 + 0x1D => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicStore32 { memarg } + } + + // i32.atomic.rmw.add + 0x1E => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicRmwAdd { memarg } + } + // i64.atomic.rmw.add + 0x1F => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmwAdd { memarg } + } + // i32.atomic.rmw8.add_u + 0x20 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicRmw8AddU { memarg } + } + // i32.atomic.rmw16.add_u + 0x21 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicRmw16AddU { memarg } + } + // i64.atomic.rmw8.add_u + 0x22 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmw8AddU { memarg } + } + // i64.atomic.rmw16.add_u + 0x23 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmw16AddU { memarg } + } + // i64.atomic.rmw32.add_u + 0x24 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmw32AddU { memarg } + } + + // i32.atomic.rmw.sub + 0x25 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicRmwSub { memarg } + } + // i64.atomic.rmw.sub + 0x26 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmwSub { memarg } + } + // i32.atomic.rmw8.sub_u + 0x27 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicRmw8SubU { memarg } + } + // i32.atomic.rmw16.sub_u + 0x28 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicRmw16SubU { memarg } + } + // i64.atomic.rmw8.sub_u + 0x29 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmw8SubU { memarg } + } + // i64.atomic.rmw16.sub_u + 0x2A => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmw16SubU { memarg } + } + // i64.atomic.rmw32.sub_u + 0x2B => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmw32SubU { memarg } + } + + // i32.atomic.rmw.and + 0x2C => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicRmwAnd { memarg } + } + // i64.atomic.rmw.and + 0x2D => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmwAnd { memarg } + } + // i32.atomic.rmw8.and_u + 0x2E => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicRmw8AndU { memarg } + } + // i32.atomic.rmw16.and_u + 0x2F => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicRmw16AndU { memarg } + } + // i64.atomic.rmw8.and_u + 0x30 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmw8AndU { memarg } + } + // i64.atomic.rmw16.and_u + 0x31 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmw16AndU { memarg } + } + // i64.atomic.rmw32.and_u + 0x32 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmw32AndU { memarg } + } + + // i32.atomic.rmw.or + 0x33 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicRmwOr { memarg } + } + // i64.atomic.rmw.or + 0x34 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmwOr { memarg } + } + // i32.atomic.rmw8.or_u + 0x35 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicRmw8OrU { memarg } + } + // i32.atomic.rmw16.or_u + 0x36 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicRmw16OrU { memarg } + } + // i64.atomic.rmw8.or_u + 0x37 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmw8OrU { memarg } + } + // i64.atomic.rmw16.or_u + 0x38 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmw16OrU { memarg } + } + // i64.atomic.rmw32.or_u + 0x39 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmw32OrU { memarg } + } + + // i32.atomic.rmw.xor + 0x3A => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicRmwXor { memarg } + } + // i64.atomic.rmw.xor + 0x3B => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmwXor { memarg } + } + // i32.atomic.rmw8.xor_u + 0x3C => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicRmw8XorU { memarg } + } + // i32.atomic.rmw16.xor_u + 0x3D => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicRmw16XorU { memarg } + } + // i64.atomic.rmw8.xor_u + 0x3E => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmw8XorU { memarg } + } + // i64.atomic.rmw16.xor_u + 0x3F => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmw16XorU { memarg } + } + // i64.atomic.rmw32.xor_u + 0x40 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmw32XorU { memarg } + } + + // i32.atomic.rmw.xchg + 0x41 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicRmwXchg { memarg } + } + // i64.atomic.rmw.xchg + 0x42 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmwXchg { memarg } + } + // i32.atomic.rmw8.xchg_u + 0x43 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicRmw8XchgU { memarg } + } + // i32.atomic.rmw16.xchg_u + 0x44 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicRmw16XchgU { memarg } + } + // i64.atomic.rmw8.xchg_u + 0x45 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmw8XchgU { memarg } + } + // i64.atomic.rmw16.xchg_u + 0x46 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmw16XchgU { memarg } + } + // i64.atomic.rmw32.xchg_u + 0x47 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmw32XchgU { memarg } + } + + // i32.atomic.rmw.cmpxchg + 0x48 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicRmwCmpxchg { memarg } + } + // i64.atomic.rmw.cmpxchg + 0x49 => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmwCmpxchg { memarg } + } + // i32.atomic.rmw8.cmpxchg_u + 0x4A => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicRmw8CmpxchgU { memarg } + } + // i32.atomic.rmw16.cmpxchg_u + 0x4B => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I32AtomicRmw16CmpxchgU { memarg } + } + // i64.atomic.rmw8.cmpxchg_u + 0x4C => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmw8CmpxchgU { memarg } + } + // i64.atomic.rmw16.cmpxchg_u + 0x4D => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmw16CmpxchgU { memarg } + } + // i64.atomic.rmw32.cmpxchg_u + 0x4E => { + let (memarg, bytes) = parse_memarg(bytecode, offset + consumed)?; + consumed += bytes; + Instruction::I64AtomicRmw32CmpxchgU { memarg } + } + + _ => { + #[cfg(feature = "tracing")] + kiln_foundation::tracing::warn!(atomic_opcode = format!("0xFE 0x{:02X}", atomic_opcode), offset = offset, "Unknown atomic opcode"); + return Err(Error::parse_error("Unknown atomic instruction opcode")); + } + } + } + _ => { // Show context around the unknown opcode #[cfg(feature = "tracing")] @@ -1347,13 +1824,6 @@ fn parse_instruction_with_provider( Ok((instruction, consumed)) } -/// Parse a memory argument (align + offset) from bytecode -fn parse_memarg(bytecode: &[u8], offset: usize) -> Result<(MemArg, usize)> { - let (align, bytes1) = read_leb128_u32(bytecode, offset)?; - let (mem_offset, bytes2) = read_leb128_u32(bytecode, offset + bytes1)?; - Ok((MemArg { align_exponent: align, offset: mem_offset, memory_index: 0 }, bytes1 + bytes2)) -} - /// Decode a value type from its binary encoding /// /// Value types in WebAssembly are encoded as: @@ -1443,6 +1913,36 @@ fn parse_block_type(bytecode: &[u8], offset: usize) -> Result { } } +/// Parse a memarg from bytecode, supporting the multi-memory encoding. +/// +/// In the standard encoding, memarg is `align:u32 offset:u32`. +/// In the multi-memory proposal, when bit 6 (0x40) of the alignment byte is set, +/// a memory index follows: `(align | 0x40):u32 memory_index:u32 offset:u32`. +/// When bit 6 is not set, memory index defaults to 0. +fn parse_memarg(bytecode: &[u8], start: usize) -> Result<(MemArg, usize)> { + let (raw_align, bytes1) = read_leb128_u32(bytecode, start)?; + let has_mem_idx = (raw_align & 0x40) != 0; + let align_exponent = raw_align & !(0x40); + + let (memory_index, bytes_mem) = if has_mem_idx { + read_leb128_u32(bytecode, start + bytes1)? + } else { + (0u32, 0) + }; + + let (offset, bytes2) = read_leb128_u32(bytecode, start + bytes1 + bytes_mem)?; + + let total_bytes = bytes1 + bytes_mem + bytes2; + Ok(( + MemArg { + align_exponent, + offset, + memory_index, + }, + total_bytes, + )) +} + /// Read a LEB128 encoded u32 pub(crate) fn read_leb128_u32(data: &[u8], offset: usize) -> Result<(u32, usize)> { let mut result = 0u32; diff --git a/kiln-runtime/src/instruction_parser_tests.rs b/kiln-runtime/src/instruction_parser_tests.rs index 41949485..540f56a2 100644 --- a/kiln-runtime/src/instruction_parser_tests.rs +++ b/kiln-runtime/src/instruction_parser_tests.rs @@ -111,4 +111,111 @@ mod tests { // Empty bytecode should fail since we need at least an End instruction assert!(result.is_err()); } + + #[test] + fn test_parse_atomic_fence() { + // 0xFE 0x03 0x00 0x0B (atomic.fence + zero byte + end) + let bytecode = vec![0xFE, 0x03, 0x00, 0x0B]; + let instructions = parse_instructions(&bytecode).unwrap(); + assert_eq!(instructions.len(), 2); + assert!(matches!(instructions.get(0).unwrap(), Instruction::AtomicFence)); + } + + #[test] + fn test_parse_i32_atomic_load() { + // 0xFE 0x10 align=0x02 offset=0x00 0x0B (i32.atomic.load align=4 offset=0 + end) + let bytecode = vec![0xFE, 0x10, 0x02, 0x00, 0x0B]; + let instructions = parse_instructions(&bytecode).unwrap(); + assert_eq!(instructions.len(), 2); + match instructions.get(0).unwrap() { + Instruction::I32AtomicLoad { memarg } => { + assert_eq!(memarg.align_exponent, 2); + assert_eq!(memarg.offset, 0); + } + _ => panic!("Expected I32AtomicLoad instruction"), + } + } + + #[test] + fn test_parse_i32_atomic_store() { + // 0xFE 0x17 align=0x02 offset=0x04 0x0B (i32.atomic.store align=4 offset=4 + end) + let bytecode = vec![0xFE, 0x17, 0x02, 0x04, 0x0B]; + let instructions = parse_instructions(&bytecode).unwrap(); + assert_eq!(instructions.len(), 2); + match instructions.get(0).unwrap() { + Instruction::I32AtomicStore { memarg } => { + assert_eq!(memarg.align_exponent, 2); + assert_eq!(memarg.offset, 4); + } + _ => panic!("Expected I32AtomicStore instruction"), + } + } + + #[test] + fn test_parse_i32_atomic_rmw_add() { + // 0xFE 0x1E align=0x02 offset=0x00 0x0B (i32.atomic.rmw.add align=4 offset=0 + end) + let bytecode = vec![0xFE, 0x1E, 0x02, 0x00, 0x0B]; + let instructions = parse_instructions(&bytecode).unwrap(); + assert_eq!(instructions.len(), 2); + match instructions.get(0).unwrap() { + Instruction::I32AtomicRmwAdd { memarg } => { + assert_eq!(memarg.align_exponent, 2); + assert_eq!(memarg.offset, 0); + } + _ => panic!("Expected I32AtomicRmwAdd instruction"), + } + } + + #[test] + fn test_parse_i32_atomic_rmw_cmpxchg() { + // 0xFE 0x48 align=0x02 offset=0x00 0x0B (i32.atomic.rmw.cmpxchg align=4 offset=0 + end) + let bytecode = vec![0xFE, 0x48, 0x02, 0x00, 0x0B]; + let instructions = parse_instructions(&bytecode).unwrap(); + assert_eq!(instructions.len(), 2); + match instructions.get(0).unwrap() { + Instruction::I32AtomicRmwCmpxchg { memarg } => { + assert_eq!(memarg.align_exponent, 2); + assert_eq!(memarg.offset, 0); + } + _ => panic!("Expected I32AtomicRmwCmpxchg instruction"), + } + } + + #[test] + fn test_parse_memory_atomic_notify() { + // 0xFE 0x00 align=0x02 offset=0x00 0x0B (memory.atomic.notify align=4 offset=0 + end) + let bytecode = vec![0xFE, 0x00, 0x02, 0x00, 0x0B]; + let instructions = parse_instructions(&bytecode).unwrap(); + assert_eq!(instructions.len(), 2); + match instructions.get(0).unwrap() { + Instruction::MemoryAtomicNotify { memarg } => { + assert_eq!(memarg.align_exponent, 2); + assert_eq!(memarg.offset, 0); + } + _ => panic!("Expected MemoryAtomicNotify instruction"), + } + } + + #[test] + fn test_parse_i64_atomic_rmw_xchg() { + // 0xFE 0x42 align=0x03 offset=0x08 0x0B (i64.atomic.rmw.xchg align=8 offset=8 + end) + let bytecode = vec![0xFE, 0x42, 0x03, 0x08, 0x0B]; + let instructions = parse_instructions(&bytecode).unwrap(); + assert_eq!(instructions.len(), 2); + match instructions.get(0).unwrap() { + Instruction::I64AtomicRmwXchg { memarg } => { + assert_eq!(memarg.align_exponent, 3); + assert_eq!(memarg.offset, 8); + } + _ => panic!("Expected I64AtomicRmwXchg instruction"), + } + } + + #[test] + fn test_parse_unknown_atomic_opcode() { + // 0xFE with invalid sub-opcode + let bytecode = vec![0xFE, 0x7F, 0x0B]; + let result = parse_instructions(&bytecode); + assert!(result.is_err()); + } } diff --git a/kiln-runtime/src/memory.rs b/kiln-runtime/src/memory.rs index 09c20d32..4b29cfc1 100644 --- a/kiln-runtime/src/memory.rs +++ b/kiln-runtime/src/memory.rs @@ -105,7 +105,7 @@ extern crate alloc; // Core/std library imports -#[cfg(not(feature = "std"))] +#[cfg(all(not(feature = "std"), feature = "alloc"))] use alloc::vec; #[cfg(not(feature = "std"))] use core::borrow::BorrowMut; @@ -192,6 +192,7 @@ fn to_core_memory_type(memory_type: &MemoryType) -> CoreMemoryType { CoreMemoryType { limits: memory_type.limits, shared: memory_type.shared, + memory64: memory_type.memory64, } } diff --git a/kiln-runtime/src/module.rs b/kiln-runtime/src/module.rs index fe8c3df1..9a73fe73 100644 --- a/kiln-runtime/src/module.rs +++ b/kiln-runtime/src/module.rs @@ -153,6 +153,7 @@ fn to_core_memory_type(memory_type: KilnMemoryType) -> CoreMemoryType { CoreMemoryType { limits: memory_type.limits, shared: memory_type.shared, + memory64: memory_type.memory64, } } @@ -673,6 +674,59 @@ impl Data { } } +/// Storage type for a GC field (packed or full value type) +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GcFieldStorage { + /// Full value type (identified by wasm byte encoding) + Value(u8), + /// Packed i8 + I8, + /// Packed i16 + I16, +} + +/// A single field in a GC struct type +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GcField { + /// The storage type of this field + pub storage: GcFieldStorage, + /// Whether this field is mutable + pub mutable: bool, +} + +/// GC composite type information for struct and array types +/// +/// Stores the parsed field/element type information needed at runtime +/// for GC instructions like struct.new, array.new, etc. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum GcTypeInfo { + /// This type index is a function type (no GC info needed) + Func, + /// Struct type with field definitions + Struct(Vec), + /// Array type with element definition + Array(GcField), +} + +impl GcField { + /// Get the size of this field's storage type in bytes + pub fn size_in_bytes(&self) -> usize { + match self.storage { + GcFieldStorage::I8 => 1, + GcFieldStorage::I16 => 2, + GcFieldStorage::Value(byte) => match byte { + 0x7F => 4, // i32 + 0x7E => 8, // i64 + 0x7D => 4, // f32 + 0x7C => 8, // f64 + 0x7B => 16, // v128 + // Reference types are 4 bytes (ref index) + _ => 4, + }, + } + } +} + /// Represents a WebAssembly module in the runtime #[derive(Debug, Clone, PartialEq, Eq)] pub struct Module { @@ -709,6 +763,10 @@ pub struct Module { #[cfg(not(feature = "std"))] pub memories: BoundedMemoryVec, /// Global variable instances + /// In std mode, use Vec to avoid BoundedVec serialization which loses GC reference data + #[cfg(feature = "std")] + pub globals: Vec, + #[cfg(not(feature = "std"))] pub globals: BoundedGlobalVec, /// Exception tag definitions (exception handling proposal) #[cfg(feature = "std")] @@ -757,6 +815,10 @@ pub struct Module { /// Number of imported functions (set during decoding/loading) /// Used by the engine to distinguish import calls from local calls pub num_import_functions: usize, + /// GC type information indexed by type index + /// Stores struct field info and array element info needed for GC instructions + #[cfg(feature = "std")] + pub gc_types: Vec, } impl Module { @@ -813,8 +875,12 @@ impl Module { init_bytes: &[u8], num_global_imports: usize, global_import_types: &[kiln_foundation::types::GlobalType], + #[cfg(feature = "std")] + defined_globals: &Vec, + #[cfg(not(feature = "std"))] defined_globals: &BoundedGlobalVec, current_global_idx: usize, + gc_types: &[GcTypeInfo], ) -> Result { use kiln_foundation::values::{Value, FloatBits32, FloatBits64}; @@ -895,12 +961,25 @@ impl Module { } else { let defined_idx = ref_idx - num_global_imports; if defined_idx < current_global_idx && defined_idx < defined_globals.len() { - match defined_globals.get(defined_idx) { - Ok(ref_global) => { - let value = ref_global.get()?; - stack.push(value); - }, - Err(_) => return Err(Error::validation_error("global.get references non-existent global")), + #[cfg(feature = "std")] + { + match defined_globals.get(defined_idx) { + Some(ref_global) => { + let value = ref_global.get()?; + stack.push(value); + }, + None => return Err(Error::validation_error("global.get references non-existent global")), + } + } + #[cfg(not(feature = "std"))] + { + match defined_globals.get(defined_idx) { + Ok(ref_global) => { + let value = ref_global.get()?; + stack.push(value); + }, + Err(_) => return Err(Error::validation_error("global.get references non-existent global")), + } } } else { return Err(Error::validation_error("global.get forward reference")); @@ -1018,40 +1097,121 @@ impl Module { match sub_opcode { // struct.new $t: pops field values, pushes structref 0x00 => { - let (_type_idx, consumed2) = crate::instruction_parser::read_leb128_u32(init_bytes, pos)?; + let (type_idx, consumed2) = crate::instruction_parser::read_leb128_u32(init_bytes, pos)?; pos += consumed2; - // Pop field values and create struct (simplified: just create null for now) - stack.clear(); - stack.push(Value::StructRef(None)); + // Look up field count from gc_types + let field_count = match gc_types.get(type_idx as usize) { + Some(GcTypeInfo::Struct(fields)) => fields.len(), + _ => return Err(Error::parse_error( + "struct.new in const expr: type index is not a struct type" + )), + }; + // Pop field values in reverse order + let mut fields = Vec::with_capacity(field_count); + for _ in 0..field_count { + let val = stack.pop().ok_or_else(|| Error::parse_error( + "struct.new in const expr: stack underflow" + ))?; + fields.push(val); + } + fields.reverse(); + let mut struct_ref = kiln_foundation::values::StructRef::new( + type_idx, + kiln_foundation::traits::DefaultMemoryProvider::default(), + ).map_err(|_| Error::parse_error("Failed to create struct in const expr"))?; + for field in fields { + struct_ref.add_field(field).map_err(|_| + Error::parse_error("Failed to add struct field in const expr"))?; + } + stack.push(Value::StructRef(Some(struct_ref))); } // struct.new_default $t: pushes structref with default fields 0x01 => { - let (_type_idx, consumed2) = crate::instruction_parser::read_leb128_u32(init_bytes, pos)?; + let (type_idx, consumed2) = crate::instruction_parser::read_leb128_u32(init_bytes, pos)?; pos += consumed2; - stack.push(Value::StructRef(None)); + let mut struct_ref = kiln_foundation::values::StructRef::new( + type_idx, + kiln_foundation::traits::DefaultMemoryProvider::default(), + ).map_err(|_| Error::parse_error("Failed to create struct in const expr"))?; + if let Some(GcTypeInfo::Struct(gc_fields)) = gc_types.get(type_idx as usize) { + for field in gc_fields { + let default_val = Self::gc_field_default_value_const(field); + struct_ref.add_field(default_val).map_err(|_| + Error::parse_error("Failed to add default struct field in const expr"))?; + } + } + stack.push(Value::StructRef(Some(struct_ref))); } // array.new $t: [val i32] -> [(ref $t)] 0x06 => { - let (_type_idx, consumed2) = crate::instruction_parser::read_leb128_u32(init_bytes, pos)?; + let (type_idx, consumed2) = crate::instruction_parser::read_leb128_u32(init_bytes, pos)?; pos += consumed2; - stack.clear(); - stack.push(Value::ArrayRef(None)); + let length = match stack.pop() { + Some(Value::I32(n)) => n as u32, + _ => return Err(Error::parse_error( + "array.new in const expr: expected i32 length" + )), + }; + let init_value = stack.pop().ok_or_else(|| Error::parse_error( + "array.new in const expr: expected init value" + ))?; + let mut array_ref = kiln_foundation::values::ArrayRef::new( + type_idx, + kiln_foundation::traits::DefaultMemoryProvider::default(), + ).map_err(|_| Error::parse_error("Failed to create array in const expr"))?; + for _ in 0..length { + array_ref.push(init_value.clone()).map_err(|_| + Error::parse_error("Failed to push to array in const expr"))?; + } + stack.push(Value::ArrayRef(Some(array_ref))); } // array.new_default $t: [i32] -> [(ref $t)] 0x07 => { - let (_type_idx, consumed2) = crate::instruction_parser::read_leb128_u32(init_bytes, pos)?; + let (type_idx, consumed2) = crate::instruction_parser::read_leb128_u32(init_bytes, pos)?; pos += consumed2; - stack.clear(); - stack.push(Value::ArrayRef(None)); + let length = match stack.pop() { + Some(Value::I32(n)) => n as u32, + _ => return Err(Error::parse_error( + "array.new_default in const expr: expected i32 length" + )), + }; + let default_val = match gc_types.get(type_idx as usize) { + Some(GcTypeInfo::Array(field)) => Self::gc_field_default_value_const(field), + _ => Value::I32(0), + }; + let mut array_ref = kiln_foundation::values::ArrayRef::new( + type_idx, + kiln_foundation::traits::DefaultMemoryProvider::default(), + ).map_err(|_| Error::parse_error("Failed to create array in const expr"))?; + for _ in 0..length { + array_ref.push(default_val.clone()).map_err(|_| + Error::parse_error("Failed to push to array in const expr"))?; + } + stack.push(Value::ArrayRef(Some(array_ref))); } // array.new_fixed $t $n: [val*n] -> [(ref $t)] 0x08 => { - let (_type_idx, consumed2) = crate::instruction_parser::read_leb128_u32(init_bytes, pos)?; + let (type_idx, consumed2) = crate::instruction_parser::read_leb128_u32(init_bytes, pos)?; pos += consumed2; - let (_count, consumed3) = crate::instruction_parser::read_leb128_u32(init_bytes, pos)?; + let (count, consumed3) = crate::instruction_parser::read_leb128_u32(init_bytes, pos)?; pos += consumed3; - stack.clear(); - stack.push(Value::ArrayRef(None)); + let mut values = Vec::with_capacity(count as usize); + for _ in 0..count { + let val = stack.pop().ok_or_else(|| Error::parse_error( + "array.new_fixed in const expr: stack underflow" + ))?; + values.push(val); + } + values.reverse(); + let mut array_ref = kiln_foundation::values::ArrayRef::new( + type_idx, + kiln_foundation::traits::DefaultMemoryProvider::default(), + ).map_err(|_| Error::parse_error("Failed to create array in const expr"))?; + for val in values { + array_ref.push(val).map_err(|_| + Error::parse_error("Failed to push to array in const expr"))?; + } + stack.push(Value::ArrayRef(Some(array_ref))); } // ref.i31: [i32] -> [(ref i31)] 0x1C => { @@ -1108,6 +1268,30 @@ impl Module { Err(Error::parse_error("Constant expression missing end opcode")) } + /// Get the default value for a GC field type (for use in constant expressions). + fn gc_field_default_value_const(field: &GcField) -> kiln_foundation::values::Value { + use kiln_foundation::values::{Value, FloatBits32, FloatBits64}; + match &field.storage { + GcFieldStorage::I8 | GcFieldStorage::I16 => Value::I32(0), + GcFieldStorage::Value(byte) => match byte { + 0x7F => Value::I32(0), // i32 + 0x7E => Value::I64(0), // i64 + 0x7D => Value::F32(FloatBits32::from_f32(0.0)), // f32 + 0x7C => Value::F64(FloatBits64::from_f64(0.0)), // f64 + 0x7B => Value::V128(kiln_foundation::values::V128::zero()), // v128 + 0x70 | 0x6F => Value::FuncRef(None), // funcref / externref + 0x63 | 0x64 => Value::FuncRef(None), // ref null / ref + 0x6E => Value::I31Ref(None), // anyref + 0x6D => Value::I31Ref(None), // eqref + 0x6C => Value::I31Ref(None), // i31ref + 0x6B => Value::StructRef(None), // structref + 0x6A => Value::ArrayRef(None), // arrayref + 0x69 => Value::ExnRef(None), // exnref + _ => Value::I32(0), + }, + } + } + /// Re-evaluate globals that depend on imported globals. /// This should be called after import values have been set in the instance globals. /// @@ -1329,6 +1513,9 @@ impl Module { #[cfg(not(feature = "std"))] tables: kiln_foundation::bounded::BoundedVec::new(provider.clone())?, memories: Vec::new(), + #[cfg(feature = "std")] + globals: Vec::new(), + #[cfg(not(feature = "std"))] globals: kiln_foundation::bounded::BoundedVec::new(provider.clone())?, #[cfg(feature = "std")] tags: Vec::new(), @@ -1356,6 +1543,8 @@ impl Module { #[cfg(feature = "std")] import_types: Vec::new(), num_import_functions: 0, + #[cfg(feature = "std")] + gc_types: Vec::new(), }) } @@ -1371,7 +1560,6 @@ impl Module { // Create initial empty module with proper providers let imports_map = kiln_foundation::bounded_collections::BoundedMap::new(shared_provider.clone())?; - let globals_vec = kiln_foundation::bounded::BoundedVec::new(shared_provider.clone())?; let custom_sections_map = kiln_foundation::bounded_collections::BoundedMap::new(shared_provider.clone())?; let mut runtime_module = Self { types: Vec::new(), @@ -1380,7 +1568,7 @@ impl Module { functions: Vec::new(), tables: Vec::new(), // Vec in std mode to avoid serialization issues with Arc memories: Vec::new(), - globals: globals_vec, + globals: Vec::new(), tags: Vec::new(), // Exception tags (exception handling proposal) elements: Vec::new(), // Vec in std mode for variable-size Element items data: Vec::new(), // Vec in std mode for large data segments @@ -1395,8 +1583,56 @@ impl Module { deferred_global_inits: Vec::new(), // Will be populated when processing globals import_types: Vec::new(), // Will be populated when processing imports num_import_functions: 0, // Will be set after processing imports + gc_types: Vec::new(), // Will be populated from rec_groups }; + // Populate GC type info from rec_groups + { + use kiln_format::module::CompositeTypeKind; + // First pass: determine size + let total_types: usize = kiln_module.rec_groups.iter() + .map(|rg| rg.types.len()) + .sum(); + runtime_module.gc_types.reserve(total_types); + + // Collect types ordered by type_index + let mut gc_type_entries: Vec<(u32, GcTypeInfo)> = Vec::new(); + for rec_group in &kiln_module.rec_groups { + for sub_type in &rec_group.types { + let info = match &sub_type.composite_kind { + CompositeTypeKind::Func => GcTypeInfo::Func, + CompositeTypeKind::Struct => GcTypeInfo::Func, // Legacy variant, treat as func + CompositeTypeKind::Array => GcTypeInfo::Func, // Legacy variant, treat as func + CompositeTypeKind::StructWithFields(fields) => { + let gc_fields: Vec = fields.iter().map(|f| { + let storage = match &f.storage_type { + kiln_format::module::GcStorageType::I8 => GcFieldStorage::I8, + kiln_format::module::GcStorageType::I16 => GcFieldStorage::I16, + kiln_format::module::GcStorageType::Value(b) => GcFieldStorage::Value(*b), + }; + GcField { storage, mutable: f.mutable } + }).collect(); + GcTypeInfo::Struct(gc_fields) + } + CompositeTypeKind::ArrayWithElement(elem) => { + let storage = match &elem.storage_type { + kiln_format::module::GcStorageType::I8 => GcFieldStorage::I8, + kiln_format::module::GcStorageType::I16 => GcFieldStorage::I16, + kiln_format::module::GcStorageType::Value(b) => GcFieldStorage::Value(*b), + }; + GcTypeInfo::Array(GcField { storage, mutable: elem.mutable }) + } + }; + gc_type_entries.push((sub_type.type_index, info)); + } + } + // Sort by type index and fill gc_types + gc_type_entries.sort_by_key(|(idx, _)| *idx); + for (_, info) in gc_type_entries { + runtime_module.gc_types.push(info); + } + } + // Convert types #[cfg(feature = "tracing")] debug!(type_count = kiln_module.types.len(), "Converting types from kiln_module"); @@ -1776,6 +2012,7 @@ impl Module { &runtime_module.global_import_types, &runtime_module.globals, global_idx, + &runtime_module.gc_types, )? } else { // No init expression - this is an error, globals must be initialized @@ -1795,6 +2032,9 @@ impl Module { global.global_type.mutable, initial_value, )?; + #[cfg(feature = "std")] + runtime_module.globals.push(GlobalWrapper(Arc::new(RwLock::new(new_global)))); + #[cfg(not(feature = "std"))] runtime_module.globals.push(GlobalWrapper(Arc::new(RwLock::new(new_global))))?; } @@ -1823,6 +2063,10 @@ impl Module { // i32.const - parse LEB128 value let (value, _) = crate::instruction_parser::read_leb128_i32(offset_bytes, 1)?; value as u32 + } else if !offset_bytes.is_empty() && offset_bytes[0] == 0x42 { + // i64.const - parse LEB128 value (memory64 data segment offsets) + let (value, _) = crate::instruction_parser::read_leb128_i64(offset_bytes, 1)?; + value as u32 } else { 0 }; @@ -1921,9 +2165,13 @@ impl Module { // Parse the offset expression bytes let offset_bytes = &elem_seg.offset_expr_bytes; let offset: u32 = if !offset_bytes.is_empty() && offset_bytes[0] == 0x41 { - // i32.const - parse LEB128 value + // i32.const (0x41) - parse LEB128 value let (value, _) = crate::instruction_parser::read_leb128_i32(offset_bytes, 1)?; value as u32 + } else if !offset_bytes.is_empty() && offset_bytes[0] == 0x42 { + // i64.const (0x42) - parse LEB128 i64 value (table64 element offset) + let (value, _) = crate::instruction_parser::read_leb128_i64(offset_bytes, 1)?; + value as u32 } else { 0 }; @@ -2017,10 +2265,23 @@ impl Module { } } _ => { - // Unknown expression type - push null sentinel - #[cfg(feature = "tracing")] - trace!(elem_offset = offset_value + i as u32, opcode = format_args!("0x{:02X}", expr[0]), "Element item = unknown opcode (treating as null)"); - items.push(u32::MAX)?; // Default to null for unknown expressions + // Other const expressions (ref.i31, struct.new, etc.) + // Defer to item_exprs for evaluation during instantiation + #[cfg(feature = "std")] + { + #[cfg(feature = "tracing")] + trace!(elem_offset = offset_value + i as u32, opcode = format_args!("0x{:02X}", expr[0]), "Element item = const expr (deferred)"); + let expr_insts = crate::instruction_parser::parse_instructions_with_provider( + expr.as_slice(), + shared_provider.clone() + )?; + deferred_item_exprs.push((i as u32, KilnExpr { instructions: expr_insts })); + items.push(u32::MAX - 1)?; // Sentinel for deferred evaluation + } + #[cfg(not(feature = "std"))] + { + items.push(u32::MAX)?; + } } } } @@ -2256,6 +2517,9 @@ impl Module { global.global_type.mutable, initial_value, )?; + #[cfg(feature = "std")] + runtime_module.globals.push(GlobalWrapper(Arc::new(RwLock::new(new_global)))); + #[cfg(not(feature = "std"))] runtime_module.globals.push(GlobalWrapper(Arc::new(RwLock::new(new_global))))?; } @@ -2503,11 +2767,15 @@ impl Module { }, }; - runtime_module.globals.push(GlobalWrapper::new(Global::new( + let new_global = GlobalWrapper::new(Global::new( global_def.value_type, global_def.mutable, default_value, - )?))?; + )?); + #[cfg(feature = "std")] + runtime_module.globals.push(new_global); + #[cfg(not(feature = "std"))] + runtime_module.globals.push(new_global)?; } // Convert tags (exception handling proposal) @@ -2616,6 +2884,16 @@ impl Module { } /// Gets a global by index + #[cfg(feature = "std")] + pub fn get_global(&self, idx: usize) -> Result { + self.globals + .get(idx) + .cloned() + .ok_or_else(|| Error::runtime_execution_error("Global index out of bounds")) + } + + /// Gets a global by index + #[cfg(not(feature = "std"))] pub fn get_global(&self, idx: usize) -> Result { self.globals .get(idx) @@ -3024,6 +3302,9 @@ impl Module { /// Add a global to the module pub fn add_global(&mut self, global_type: KilnGlobalType, init: KilnValue) -> Result<()> { let global = Global::new(global_type.value_type, global_type.mutable, init)?; + #[cfg(feature = "std")] + self.globals.push(GlobalWrapper::new(global)); + #[cfg(not(feature = "std"))] self.globals.push(GlobalWrapper::new(global))?; Ok(()) } @@ -3538,6 +3819,9 @@ impl Module { #[cfg(not(feature = "std"))] tables: kiln_foundation::bounded::BoundedVec::new(provider.clone())?, memories: Vec::new(), + #[cfg(feature = "std")] + globals: Vec::new(), + #[cfg(not(feature = "std"))] globals: kiln_foundation::bounded::BoundedVec::new(provider.clone())?, #[cfg(feature = "std")] tags: Vec::new(), @@ -3565,6 +3849,8 @@ impl Module { #[cfg(feature = "std")] import_types: Vec::new(), num_import_functions: 0, + #[cfg(feature = "std")] + gc_types: Vec::new(), }; // Set start function if present @@ -4010,6 +4296,9 @@ impl kiln_foundation::traits::FromBytes for Module { #[cfg(not(feature = "std"))] tables: kiln_foundation::bounded::BoundedVec::new(provider.clone())?, memories: Vec::new(), + #[cfg(feature = "std")] + globals: Vec::new(), + #[cfg(not(feature = "std"))] globals: kiln_foundation::bounded::BoundedVec::new(provider.clone())?, #[cfg(feature = "std")] tags: Vec::new(), @@ -4037,6 +4326,8 @@ impl kiln_foundation::traits::FromBytes for Module { #[cfg(feature = "std")] import_types: Vec::new(), num_import_functions: 0, + #[cfg(feature = "std")] + gc_types: Vec::new(), }; Ok(module) @@ -4118,6 +4409,12 @@ impl TableWrapper { self.0.grow_shared(delta, init_value) } + /// Check if this table uses table64 (64-bit indices) + #[must_use] + pub fn is_table64(&self) -> bool { + self.0.ty.table64 + } + /// Initialize table using interior mutability pub fn init(&self, offset: u32, init_data: &[Option]) -> Result<()> { self.0.init_shared(offset, init_data) @@ -4648,7 +4945,7 @@ impl Checksummable for GlobalWrapper { impl ToBytes for GlobalWrapper { fn serialized_size(&self) -> usize { - 12 // value type (1) + mutable flag (1) + padding (2) + value (8) + 20 // value type (1) + mutable flag (1) + padding (2) + value (16 bytes for V128 compatibility) } fn to_bytes_with_provider( @@ -4673,80 +4970,79 @@ impl ToBytes for GlobalWrapper { writer.write_u8(0)?; writer.write_u8(0)?; - // Write value (8 bytes) + // Write value (16 bytes - supports V128 which needs all 16 bytes) let value = guard.get(); match value { KilnValue::I32(v) => { writer.write_all(&(*v as u32).to_le_bytes())?; - writer.write_all(&0u32.to_le_bytes())?; + writer.write_all(&[0u8; 12])?; // pad to 16 }, KilnValue::I64(v) => { writer.write_all(&(*v as u64).to_le_bytes())?; + writer.write_all(&[0u8; 8])?; // pad to 16 }, KilnValue::F32(kiln_foundation::values::FloatBits32(bits)) => { writer.write_all(&bits.to_le_bytes())?; - writer.write_all(&0u32.to_le_bytes())?; + writer.write_all(&[0u8; 12])?; // pad to 16 }, KilnValue::F64(kiln_foundation::values::FloatBits64(bits)) => { writer.write_all(&bits.to_le_bytes())?; + writer.write_all(&[0u8; 8])?; // pad to 16 + }, + KilnValue::V128(v128) => { + writer.write_all(&v128.bytes)?; // all 16 bytes }, KilnValue::FuncRef(ref_opt) => { - // FuncRef: store 0xFFFFFFFF for None, or the index for Some let value = match ref_opt { Some(func_ref) => func_ref.index, None => 0xFFFFFFFF, }; writer.write_all(&value.to_le_bytes())?; - writer.write_all(&0u32.to_le_bytes())?; + writer.write_all(&[0u8; 12])?; // pad to 16 }, KilnValue::ExternRef(ref_opt) => { - // ExternRef: store 0xFFFFFFFF for None, or the index for Some let value = match ref_opt { Some(extern_ref) => extern_ref.index, None => 0xFFFFFFFF, }; writer.write_all(&value.to_le_bytes())?; - writer.write_all(&0u32.to_le_bytes())?; + writer.write_all(&[0u8; 12])?; // pad to 16 }, KilnValue::ExnRef(ref_opt) => { - // ExnRef: store 0xFFFFFFFF for None, or the index for Some let value = match ref_opt { Some(exn_ref) => *exn_ref as u32, None => 0xFFFFFFFF, }; writer.write_all(&value.to_le_bytes())?; - writer.write_all(&0u32.to_le_bytes())?; + writer.write_all(&[0u8; 12])?; // pad to 16 }, KilnValue::I31Ref(ref_opt) => { - // I31Ref: store 0xFFFFFFFF for None, or the value for Some let value = match ref_opt { Some(i31_ref) => *i31_ref as u32, None => 0xFFFFFFFF, }; writer.write_all(&value.to_le_bytes())?; - writer.write_all(&0u32.to_le_bytes())?; + writer.write_all(&[0u8; 12])?; // pad to 16 }, KilnValue::StructRef(ref_opt) => { - // StructRef: store 0xFFFFFFFF for None, or the type_index for Some let value = match ref_opt { Some(struct_ref) => struct_ref.type_index, None => 0xFFFFFFFF, }; writer.write_all(&value.to_le_bytes())?; - writer.write_all(&0u32.to_le_bytes())?; + writer.write_all(&[0u8; 12])?; // pad to 16 }, KilnValue::ArrayRef(ref_opt) => { - // ArrayRef: store 0xFFFFFFFF for None, or the type_index for Some let value = match ref_opt { Some(array_ref) => array_ref.type_index, None => 0xFFFFFFFF, }; writer.write_all(&value.to_le_bytes())?; - writer.write_all(&0u32.to_le_bytes())?; + writer.write_all(&[0u8; 12])?; // pad to 16 }, _ => { // For other types, write zeros - writer.write_all(&0u64.to_le_bytes())?; + writer.write_all(&[0u8; 16])?; } } Ok(()) @@ -4790,9 +5086,14 @@ impl FromBytes for GlobalWrapper { let _ = reader.read_u8()?; let _ = reader.read_u8()?; - // Read value (8 bytes - i64/f64 size for maximum compatibility) - let value_low = reader.read_u32_le()?; - let value_high = reader.read_u32_le()?; + // Read value (16 bytes - supports V128 which needs all 16 bytes) + let mut value_bytes = [0u8; 16]; + for i in 0..16 { + value_bytes[i] = reader.read_u8()?; + } + + let value_low = u32::from_le_bytes([value_bytes[0], value_bytes[1], value_bytes[2], value_bytes[3]]); + let value_high = u32::from_le_bytes([value_bytes[4], value_bytes[5], value_bytes[6], value_bytes[7]]); let value = match value_type { ValueType::I32 => Value::I32(value_low as i32), @@ -4805,6 +5106,9 @@ impl FromBytes for GlobalWrapper { let v = ((value_high as u64) << 32) | (value_low as u64); Value::F64(kiln_foundation::values::FloatBits64(v)) }, + ValueType::V128 => { + Value::V128(kiln_foundation::values::V128 { bytes: value_bytes }) + }, ValueType::FuncRef => { // 0xFFFFFFFF means None, otherwise it's an index if value_low == 0xFFFFFFFF { diff --git a/kiln-runtime/src/module_instance.rs b/kiln-runtime/src/module_instance.rs index 7cfe843c..54d0f36b 100644 --- a/kiln-runtime/src/module_instance.rs +++ b/kiln-runtime/src/module_instance.rs @@ -363,6 +363,61 @@ impl ModuleInstance { } } + /// Evaluate a const expression from an element segment. + /// Supports: I32Const, I64Const, RefNull, RefFunc, RefI31, GlobalGet, End. + #[cfg(feature = "std")] + fn evaluate_elem_const_expr( + instructions: &[kiln_foundation::types::Instruction], + globals: &[crate::module::GlobalWrapper], + ) -> Result { + use kiln_foundation::values::{Value as ElemValue, FuncRef as ElemFuncRef}; + type Instr = kiln_foundation::types::Instruction; + let mut result = ElemValue::FuncRef(None); + + for instr in instructions { + match instr { + Instr::I32Const(v) => { + result = ElemValue::I32(*v); + } + Instr::I64Const(v) => { + result = ElemValue::I64(*v); + } + Instr::RefNull(_) => { + result = ElemValue::FuncRef(None); + } + Instr::RefFunc(func_idx) => { + let fref = ElemFuncRef::from_index(*func_idx); + result = ElemValue::FuncRef(Some(fref)); + } + Instr::RefI31 => { + if let ElemValue::I32(n) = result { + let i31_val = n & 0x7FFFFFFF; + result = ElemValue::I31Ref(Some(i31_val)); + } else { + return Err(Error::runtime_error("ref.i31: expected i32 operand in const expr")); + } + } + Instr::GlobalGet(global_idx) => { + if let Some(global_wrapper) = globals.iter().nth(*global_idx as usize) { + match global_wrapper.0.read() { + Ok(global) => { + result = global.get().clone(); + } + Err(_) => { + return Err(Error::runtime_error("Failed to read global in elem const expr")); + } + } + } else { + return Err(Error::runtime_error("Global index out of bounds in elem const expr")); + } + } + Instr::End => break, + _ => {} + } + } + Ok(result) + } + /// Re-evaluate globals that depend on imported globals after import values are set. /// This fixes the deferred initialization problem where globals using global.get /// of imported globals were evaluated before import values were known. @@ -638,7 +693,8 @@ impl ModuleInstance { // Now copy defined globals for idx in 0..self.module.globals.len() { - if let Ok(global_wrapper) = self.module.globals.get(idx) { + let global_opt = self.module.globals.get(idx); + if let Some(global_wrapper) = global_opt { #[cfg(feature = "tracing")] debug!( "Copying defined global {} (global index {}) to instance", @@ -956,6 +1012,12 @@ impl ModuleInstance { debug!("Data segment {} has I32Const offset: {}", idx, value); *value as u32 } + kiln_foundation::types::Instruction::I64Const(value) => { + // memory64: data segment offsets use i64.const + #[cfg(feature = "tracing")] + debug!("Data segment {} has I64Const offset: {}", idx, value); + *value as u32 + } kiln_foundation::types::Instruction::GlobalGet(global_idx) => { // Look up the global value for the offset #[cfg(feature = "tracing")] @@ -1117,6 +1179,12 @@ impl ModuleInstance { debug!("Element segment {} has I32Const offset: {}", idx, value); *value as u32 } + kiln_foundation::types::Instruction::I64Const(value) => { + // table64: element segment offset is i64 + #[cfg(feature = "tracing")] + debug!("Element segment {} has I64Const offset: {}", idx, value); + *value as u32 + } kiln_foundation::types::Instruction::GlobalGet(global_idx) => { // Look up the global value for the offset #[cfg(feature = "tracing")] @@ -1130,6 +1198,12 @@ impl ModuleInstance { debug!("Element segment {} global offset value: {}", idx, v); *v as u32 }, + kiln_foundation::values::Value::I64(v) => { + // table64: global offset is i64 + #[cfg(feature = "tracing")] + debug!("Element segment {} global i64 offset value: {}", idx, v); + *v as u32 + }, _ => *mode_offset } }, @@ -1170,6 +1244,15 @@ impl ModuleInstance { let table_wrapper = &tables[table_idx]; let table = table_wrapper.inner(); + // Per WebAssembly spec: check bounds for the ENTIRE segment before + // writing any elements. If the segment is out of bounds, none of its + // elements are written, but previously processed segments persist. + let segment_len = elem_segment.items.len() as u32; + let table_size = table.size(); + if actual_offset.checked_add(segment_len).map_or(true, |end| end > table_size) { + return Err(Error::runtime_trap("out of bounds table access")); + } + // Set each element in the table // Use the element segment's type to determine if we're dealing with // funcref or externref elements @@ -1207,42 +1290,12 @@ impl ModuleInstance { } } - // Evaluate and set deferred item expressions (e.g., global.get $gf) + // Evaluate and set deferred item expressions (ref.i31, global.get, etc.) #[cfg(feature = "std")] for (item_idx, expr) in elem_segment.item_exprs.iter() { let table_offset = actual_offset + *item_idx; - // Evaluate the expression to get the funcref - if let Some(instr) = expr.instructions.first() { - if let kiln_foundation::types::Instruction::GlobalGet(global_idx) = instr { - // Look up the global value - if let Some(global_wrapper) = globals.iter().nth(*global_idx as usize) { - match global_wrapper.0.read() { - Ok(global) => { - match global.get() { - KilnValue::FuncRef(func_ref_opt) => { - #[cfg(feature = "tracing")] - kiln_foundation::tracing::trace!( - table_offset = table_offset, - func_ref = ?func_ref_opt, - global_idx = global_idx, - "Set table element from global.get" - ); - table.set_shared(table_offset, Some(KilnValue::FuncRef(func_ref_opt.clone())))?; - }, - _ => { - #[cfg(feature = "tracing")] - kiln_foundation::tracing::warn!(table_offset = table_offset, global_idx = global_idx, "Global has non-funcref type"); - } - } - }, - Err(_) => { - #[cfg(feature = "tracing")] - kiln_foundation::tracing::warn!(table_offset = table_offset, global_idx = global_idx, "Failed to read global"); - } - } - } - } - } + let value = Self::evaluate_elem_const_expr(&expr.instructions, &globals)?; + table.set_shared(table_offset, Some(value))?; } #[cfg(feature = "tracing")] @@ -1705,6 +1758,9 @@ impl FromBytes for ModuleInstance { #[cfg(not(feature = "std"))] tables: kiln_foundation::bounded::BoundedVec::new(provider.clone())?, memories: Vec::new(), + #[cfg(feature = "std")] + globals: Vec::new(), + #[cfg(not(feature = "std"))] globals: kiln_foundation::bounded::BoundedVec::new(provider.clone())?, #[cfg(feature = "std")] tags: Vec::new(), @@ -1732,6 +1788,8 @@ impl FromBytes for ModuleInstance { #[cfg(feature = "std")] import_types: Vec::new(), num_import_functions: 0, + #[cfg(feature = "std")] + gc_types: Vec::new(), }; // Create the instance using the new method diff --git a/kiln-runtime/src/multi_memory.rs b/kiln-runtime/src/multi_memory.rs index 34a28b0a..39c196b6 100644 --- a/kiln-runtime/src/multi_memory.rs +++ b/kiln-runtime/src/multi_memory.rs @@ -204,6 +204,7 @@ impl MultiMemoryInstance { let core_mem_type = CoreMemoryType { limits: memory_type.limits, shared: memory_type.shared, + memory64: memory_type.memory64, }; let memory = Memory::new(core_mem_type) .map_err(|_| Error::runtime_execution_error("Failed to create memory instance"))?; diff --git a/kiln-runtime/src/shared_memory.rs b/kiln-runtime/src/shared_memory.rs index 00b536d8..b4ab8fa9 100644 --- a/kiln-runtime/src/shared_memory.rs +++ b/kiln-runtime/src/shared_memory.rs @@ -620,6 +620,7 @@ pub fn create_shared_memory( max: memory_type.max_pages(), }, shared: memory_type.is_shared(), + memory64: false, }; let memory_impl = Memory::new(core_mem_type) diff --git a/kiln-runtime/src/simd_additional_ops.rs b/kiln-runtime/src/simd_additional_ops.rs index e70ce77f..1be51b22 100644 --- a/kiln-runtime/src/simd_additional_ops.rs +++ b/kiln-runtime/src/simd_additional_ops.rs @@ -71,11 +71,11 @@ pub fn execute_i16x8_ext_mul_low_i8x16_u(inputs: &[Value]) -> Result { let b = extract_v128(&inputs[1])?; let mut result = [0u8; 16]; - // Multiply low 8 u8 values to produce 8 i16 values + // Multiply low 8 u8 values to produce 8 u16 values for i in 0..8 { - let a_val = a[i] as u8; - let b_val = b[i] as u8; - let product = (a_val as i16) * (b_val as i16); + let a_val = a[i] as u16; + let b_val = b[i] as u16; + let product = a_val * b_val; let product_bytes = product.to_le_bytes(); result[i * 2..i * 2 + 2].copy_from_slice(&product_bytes); } @@ -88,11 +88,11 @@ pub fn execute_i16x8_ext_mul_high_i8x16_u(inputs: &[Value]) -> Result { let b = extract_v128(&inputs[1])?; let mut result = [0u8; 16]; - // Multiply high 8 u8 values to produce 8 i16 values + // Multiply high 8 u8 values to produce 8 u16 values for i in 0..8 { - let a_val = a[i + 8] as u8; - let b_val = b[i + 8] as u8; - let product = (a_val as i16) * (b_val as i16); + let a_val = a[i + 8] as u16; + let b_val = b[i + 8] as u16; + let product = a_val * b_val; let product_bytes = product.to_le_bytes(); result[i * 2..i * 2 + 2].copy_from_slice(&product_bytes); } @@ -143,13 +143,13 @@ pub fn execute_i32x4_ext_mul_low_i16x8_u(inputs: &[Value]) -> Result { let b = extract_v128(&inputs[1])?; let mut result = [0u8; 16]; - // Multiply low 4 u16 values to produce 4 i32 values + // Multiply low 4 u16 values to produce 4 u32 values for i in 0..4 { let a_bytes = &a[i * 2..i * 2 + 2]; let b_bytes = &b[i * 2..i * 2 + 2]; - let a_val = u16::from_le_bytes([a_bytes[0], a_bytes[1]]); - let b_val = u16::from_le_bytes([b_bytes[0], b_bytes[1]]); - let product = (a_val as i32) * (b_val as i32); + let a_val = u16::from_le_bytes([a_bytes[0], a_bytes[1]]) as u32; + let b_val = u16::from_le_bytes([b_bytes[0], b_bytes[1]]) as u32; + let product = a_val * b_val; let product_bytes = product.to_le_bytes(); result[i * 4..i * 4 + 4].copy_from_slice(&product_bytes); } @@ -162,13 +162,13 @@ pub fn execute_i32x4_ext_mul_high_i16x8_u(inputs: &[Value]) -> Result { let b = extract_v128(&inputs[1])?; let mut result = [0u8; 16]; - // Multiply high 4 u16 values to produce 4 i32 values + // Multiply high 4 u16 values to produce 4 u32 values for i in 0..4 { let a_bytes = &a[(i + 4) * 2..(i + 4) * 2 + 2]; let b_bytes = &b[(i + 4) * 2..(i + 4) * 2 + 2]; - let a_val = u16::from_le_bytes([a_bytes[0], a_bytes[1]]); - let b_val = u16::from_le_bytes([b_bytes[0], b_bytes[1]]); - let product = (a_val as i32) * (b_val as i32); + let a_val = u16::from_le_bytes([a_bytes[0], a_bytes[1]]) as u32; + let b_val = u16::from_le_bytes([b_bytes[0], b_bytes[1]]) as u32; + let product = a_val * b_val; let product_bytes = product.to_le_bytes(); result[i * 4..i * 4 + 4].copy_from_slice(&product_bytes); } @@ -219,13 +219,13 @@ pub fn execute_i64x2_ext_mul_low_i32x4_u(inputs: &[Value]) -> Result { let b = extract_v128(&inputs[1])?; let mut result = [0u8; 16]; - // Multiply low 2 u32 values to produce 2 i64 values + // Multiply low 2 u32 values to produce 2 u64 values for i in 0..2 { let a_bytes = &a[i * 4..i * 4 + 4]; let b_bytes = &b[i * 4..i * 4 + 4]; - let a_val = u32::from_le_bytes([a_bytes[0], a_bytes[1], a_bytes[2], a_bytes[3]]); - let b_val = u32::from_le_bytes([b_bytes[0], b_bytes[1], b_bytes[2], b_bytes[3]]); - let product = (a_val as i64) * (b_val as i64); + let a_val = u32::from_le_bytes([a_bytes[0], a_bytes[1], a_bytes[2], a_bytes[3]]) as u64; + let b_val = u32::from_le_bytes([b_bytes[0], b_bytes[1], b_bytes[2], b_bytes[3]]) as u64; + let product = a_val * b_val; let product_bytes = product.to_le_bytes(); result[i * 8..i * 8 + 8].copy_from_slice(&product_bytes); } @@ -238,13 +238,13 @@ pub fn execute_i64x2_ext_mul_high_i32x4_u(inputs: &[Value]) -> Result { let b = extract_v128(&inputs[1])?; let mut result = [0u8; 16]; - // Multiply high 2 u32 values to produce 2 i64 values + // Multiply high 2 u32 values to produce 2 u64 values for i in 0..2 { let a_bytes = &a[(i + 2) * 4..(i + 2) * 4 + 4]; let b_bytes = &b[(i + 2) * 4..(i + 2) * 4 + 4]; - let a_val = u32::from_le_bytes([a_bytes[0], a_bytes[1], a_bytes[2], a_bytes[3]]); - let b_val = u32::from_le_bytes([b_bytes[0], b_bytes[1], b_bytes[2], b_bytes[3]]); - let product = (a_val as i64) * (b_val as i64); + let a_val = u32::from_le_bytes([a_bytes[0], a_bytes[1], a_bytes[2], a_bytes[3]]) as u64; + let b_val = u32::from_le_bytes([b_bytes[0], b_bytes[1], b_bytes[2], b_bytes[3]]) as u64; + let product = a_val * b_val; let product_bytes = product.to_le_bytes(); result[i * 8..i * 8 + 8].copy_from_slice(&product_bytes); } diff --git a/kiln-runtime/src/simd_runtime.rs b/kiln-runtime/src/simd_runtime.rs index e9d967fd..54089fa4 100644 --- a/kiln-runtime/src/simd_runtime.rs +++ b/kiln-runtime/src/simd_runtime.rs @@ -1524,7 +1524,16 @@ fn execute_f32x4_min(inputs: &[Value]) -> Result { for i in 0..4 { let a_val = f32::from_le_bytes([a[i * 4], a[i * 4 + 1], a[i * 4 + 2], a[i * 4 + 3]]); let b_val = f32::from_le_bytes([b[i * 4], b[i * 4 + 1], b[i * 4 + 2], b[i * 4 + 3]]); - let res_val = canonicalize_f32_nan(a_val.min(b_val)); + // WebAssembly uses IEEE 754-2019 minimum: propagates NaN, -0 < +0 + let res_val = if a_val.is_nan() || b_val.is_nan() { + canonicalize_f32_nan(f32::NAN) + } else if a_val == 0.0 && b_val == 0.0 { + if a_val.is_sign_negative() { a_val } else { b_val } + } else if a_val < b_val { + a_val + } else { + b_val + }; let res_bytes = res_val.to_le_bytes(); result[i * 4..i * 4 + 4].copy_from_slice(&res_bytes); } @@ -1540,7 +1549,16 @@ fn execute_f32x4_max(inputs: &[Value]) -> Result { for i in 0..4 { let a_val = f32::from_le_bytes([a[i * 4], a[i * 4 + 1], a[i * 4 + 2], a[i * 4 + 3]]); let b_val = f32::from_le_bytes([b[i * 4], b[i * 4 + 1], b[i * 4 + 2], b[i * 4 + 3]]); - let res_val = canonicalize_f32_nan(a_val.max(b_val)); + // WebAssembly uses IEEE 754-2019 maximum: propagates NaN, +0 > -0 + let res_val = if a_val.is_nan() || b_val.is_nan() { + canonicalize_f32_nan(f32::NAN) + } else if a_val == 0.0 && b_val == 0.0 { + if a_val.is_sign_positive() { a_val } else { b_val } + } else if a_val > b_val { + a_val + } else { + b_val + }; let res_bytes = res_val.to_le_bytes(); result[i * 4..i * 4 + 4].copy_from_slice(&res_bytes); } @@ -1556,18 +1574,8 @@ fn execute_f32x4_pmin(inputs: &[Value]) -> Result { for i in 0..4 { let a_val = f32::from_le_bytes([a[i * 4], a[i * 4 + 1], a[i * 4 + 2], a[i * 4 + 3]]); let b_val = f32::from_le_bytes([b[i * 4], b[i * 4 + 1], b[i * 4 + 2], b[i * 4 + 3]]); - // Pseudo-minimum: IEEE 754-2008 compliant - let res_val = if a_val.is_nan() || b_val.is_nan() { - f32::NAN - } else if a_val == 0.0 && b_val == 0.0 { - if a_val.is_sign_negative() { - a_val - } else { - b_val - } - } else { - a_val.min(b_val) - }; + // Pseudo-minimum: pmin(a, b) = b < a ? b : a (no special NaN/zero handling) + let res_val = if b_val < a_val { b_val } else { a_val }; let res_bytes = res_val.to_le_bytes(); result[i * 4..i * 4 + 4].copy_from_slice(&res_bytes); } @@ -1583,18 +1591,8 @@ fn execute_f32x4_pmax(inputs: &[Value]) -> Result { for i in 0..4 { let a_val = f32::from_le_bytes([a[i * 4], a[i * 4 + 1], a[i * 4 + 2], a[i * 4 + 3]]); let b_val = f32::from_le_bytes([b[i * 4], b[i * 4 + 1], b[i * 4 + 2], b[i * 4 + 3]]); - // Pseudo-maximum: IEEE 754-2008 compliant - let res_val = if a_val.is_nan() || b_val.is_nan() { - f32::NAN - } else if a_val == 0.0 && b_val == 0.0 { - if a_val.is_sign_positive() { - a_val - } else { - b_val - } - } else { - a_val.max(b_val) - }; + // Pseudo-maximum: pmax(a, b) = a < b ? b : a (no special NaN/zero handling) + let res_val = if a_val < b_val { b_val } else { a_val }; let res_bytes = res_val.to_le_bytes(); result[i * 4..i * 4 + 4].copy_from_slice(&res_bytes); } @@ -1772,7 +1770,16 @@ fn execute_f64x2_min(inputs: &[Value]) -> Result { b_bytes[0], b_bytes[1], b_bytes[2], b_bytes[3], b_bytes[4], b_bytes[5], b_bytes[6], b_bytes[7], ]); - let res_val = canonicalize_f64_nan(a_val.min(b_val)); + // WebAssembly uses IEEE 754-2019 minimum: propagates NaN, -0 < +0 + let res_val = if a_val.is_nan() || b_val.is_nan() { + canonicalize_f64_nan(f64::NAN) + } else if a_val == 0.0 && b_val == 0.0 { + if a_val.is_sign_negative() { a_val } else { b_val } + } else if a_val < b_val { + a_val + } else { + b_val + }; let res_bytes = res_val.to_le_bytes(); result[i * 8..i * 8 + 8].copy_from_slice(&res_bytes); } @@ -1796,7 +1803,16 @@ fn execute_f64x2_max(inputs: &[Value]) -> Result { b_bytes[0], b_bytes[1], b_bytes[2], b_bytes[3], b_bytes[4], b_bytes[5], b_bytes[6], b_bytes[7], ]); - let res_val = canonicalize_f64_nan(a_val.max(b_val)); + // WebAssembly uses IEEE 754-2019 maximum: propagates NaN, +0 > -0 + let res_val = if a_val.is_nan() || b_val.is_nan() { + canonicalize_f64_nan(f64::NAN) + } else if a_val == 0.0 && b_val == 0.0 { + if a_val.is_sign_positive() { a_val } else { b_val } + } else if a_val > b_val { + a_val + } else { + b_val + }; let res_bytes = res_val.to_le_bytes(); result[i * 8..i * 8 + 8].copy_from_slice(&res_bytes); } @@ -1820,18 +1836,8 @@ fn execute_f64x2_pmin(inputs: &[Value]) -> Result { b_bytes[0], b_bytes[1], b_bytes[2], b_bytes[3], b_bytes[4], b_bytes[5], b_bytes[6], b_bytes[7], ]); - // Pseudo-minimum: IEEE 754-2008 compliant - let res_val = if a_val.is_nan() || b_val.is_nan() { - f64::NAN - } else if a_val == 0.0 && b_val == 0.0 { - if a_val.is_sign_negative() { - a_val - } else { - b_val - } - } else { - a_val.min(b_val) - }; + // Pseudo-minimum: pmin(a, b) = b < a ? b : a (no special NaN/zero handling) + let res_val = if b_val < a_val { b_val } else { a_val }; let res_bytes = res_val.to_le_bytes(); result[i * 8..i * 8 + 8].copy_from_slice(&res_bytes); } @@ -1855,18 +1861,8 @@ fn execute_f64x2_pmax(inputs: &[Value]) -> Result { b_bytes[0], b_bytes[1], b_bytes[2], b_bytes[3], b_bytes[4], b_bytes[5], b_bytes[6], b_bytes[7], ]); - // Pseudo-maximum: IEEE 754-2008 compliant - let res_val = if a_val.is_nan() || b_val.is_nan() { - f64::NAN - } else if a_val == 0.0 && b_val == 0.0 { - if a_val.is_sign_positive() { - a_val - } else { - b_val - } - } else { - a_val.max(b_val) - }; + // Pseudo-maximum: pmax(a, b) = a < b ? b : a (no special NaN/zero handling) + let res_val = if a_val < b_val { b_val } else { a_val }; let res_bytes = res_val.to_le_bytes(); result[i * 8..i * 8 + 8].copy_from_slice(&res_bytes); } diff --git a/kiln-runtime/src/stackless/engine.rs b/kiln-runtime/src/stackless/engine.rs index e259cfa6..ffa6d86a 100644 --- a/kiln-runtime/src/stackless/engine.rs +++ b/kiln-runtime/src/stackless/engine.rs @@ -523,6 +523,43 @@ fn pop_atomic_address(operand_stack: &mut Vec, memarg_offset: u32) -> kil } } +/// Pop a memory operand from the operand stack for bulk memory operations. +/// For memory64 memories, the operand is i64; for memory32, it is i32. +/// Returns the value as u64 for uniform bounds checking. +#[inline] +fn pop_memory_operand(operand_stack: &mut Vec, is_memory64: bool) -> kiln_error::Result { + match operand_stack.pop() { + Some(Value::I64(v)) if is_memory64 => Ok(v as u64), + Some(Value::I32(v)) if !is_memory64 => Ok(v as u32 as u64), + Some(_) => Err(kiln_error::Error::runtime_trap("type mismatch")), + None => Err(kiln_error::Error::runtime_trap("operand stack underflow")), + } +} + +/// Pop a table operand from the operand stack for table operations. +/// For table64 tables, the operand is i64; for standard tables, it is i32. +/// Returns the value as u64 for uniform bounds checking. +#[inline] +fn pop_table_operand(operand_stack: &mut Vec, is_table64: bool, context: &'static str) -> kiln_error::Result { + match operand_stack.pop() { + Some(Value::I64(v)) if is_table64 => Ok(v as u64), + Some(Value::I32(v)) if !is_table64 => Ok(v as u32 as u64), + Some(_) => Err(kiln_error::Error::runtime_trap(context)), + None => Err(kiln_error::Error::runtime_trap(context)), + } +} + +/// Push a table result onto the operand stack. +/// For table64 tables, pushes i64; for standard tables, pushes i32. +#[inline] +fn push_table_result(operand_stack: &mut Vec, value: u64, is_table64: bool) { + if is_table64 { + operand_stack.push(Value::I64(value as i64)); + } else { + operand_stack.push(Value::I32(value as i32)); + } +} + impl StacklessEngine { /// Create a new stackless engine #[cfg(any(feature = "std", feature = "alloc"))] @@ -717,24 +754,26 @@ impl StacklessEngine { if let Some(ref mut dispatcher) = self.wasi_dispatcher { let wasi_results = dispatcher.dispatch(interface, function, &wasi_args)?; - let results: Vec = wasi_results.into_iter().map(|v| { + let results: Result> = wasi_results.into_iter().map(|v| { match v { - kiln_wasi::Value::S32(i) => Value::I32(i), - kiln_wasi::Value::U32(u) => Value::I32(u as i32), - kiln_wasi::Value::S64(i) => Value::I64(i), - kiln_wasi::Value::U64(u) => Value::I64(u as i64), - kiln_wasi::Value::F32(f) => Value::F32(FloatBits32::from_f32(f)), - kiln_wasi::Value::F64(f) => Value::F64(FloatBits64::from_f64(f)), - kiln_wasi::Value::Bool(b) => Value::I32(if b { 1 } else { 0 }), - kiln_wasi::Value::U8(u) => Value::I32(u as i32), - kiln_wasi::Value::S8(i) => Value::I32(i as i32), - kiln_wasi::Value::U16(u) => Value::I32(u as i32), - kiln_wasi::Value::S16(i) => Value::I32(i as i32), - _ => Value::I32(0), + kiln_wasi::Value::S32(i) => Ok(Value::I32(i)), + kiln_wasi::Value::U32(u) => Ok(Value::I32(u as i32)), + kiln_wasi::Value::S64(i) => Ok(Value::I64(i)), + kiln_wasi::Value::U64(u) => Ok(Value::I64(u as i64)), + kiln_wasi::Value::F32(f) => Ok(Value::F32(FloatBits32::from_f32(f))), + kiln_wasi::Value::F64(f) => Ok(Value::F64(FloatBits64::from_f64(f))), + kiln_wasi::Value::Bool(b) => Ok(Value::I32(if b { 1 } else { 0 })), + kiln_wasi::Value::U8(u) => Ok(Value::I32(u as i32)), + kiln_wasi::Value::S8(i) => Ok(Value::I32(i as i32)), + kiln_wasi::Value::U16(u) => Ok(Value::I32(u as i32)), + kiln_wasi::Value::S16(i) => Ok(Value::I32(i as i32)), + _other => Err(kiln_error::Error::runtime_execution_error( + "unsupported WASI value type in canon-lowered function result", + )), } }).collect(); - Ok(results) + results } else { Err(kiln_error::Error::runtime_error("WASI dispatcher not available for canon-lowered function")) } @@ -780,24 +819,26 @@ impl StacklessEngine { let wasi_results = dispatcher.dispatch(&lowered.interface, &lowered.function, &wasi_args)?; // Convert kiln_wasi::Value back to kiln_foundation::values::Value - let results: Vec = wasi_results.into_iter().map(|v| { + let results: Result> = wasi_results.into_iter().map(|v| { match v { - kiln_wasi::Value::S32(i) => Value::I32(i), - kiln_wasi::Value::U32(u) => Value::I32(u as i32), - kiln_wasi::Value::S64(i) => Value::I64(i), - kiln_wasi::Value::U64(u) => Value::I64(u as i64), - kiln_wasi::Value::F32(f) => Value::F32(FloatBits32::from_f32(f)), - kiln_wasi::Value::F64(f) => Value::F64(FloatBits64::from_f64(f)), - kiln_wasi::Value::Bool(b) => Value::I32(if b { 1 } else { 0 }), - kiln_wasi::Value::U8(u) => Value::I32(u as i32), - kiln_wasi::Value::S8(i) => Value::I32(i as i32), - kiln_wasi::Value::U16(u) => Value::I32(u as i32), - kiln_wasi::Value::S16(i) => Value::I32(i as i32), - _ => Value::I32(0), // Default for unsupported types + kiln_wasi::Value::S32(i) => Ok(Value::I32(i)), + kiln_wasi::Value::U32(u) => Ok(Value::I32(u as i32)), + kiln_wasi::Value::S64(i) => Ok(Value::I64(i)), + kiln_wasi::Value::U64(u) => Ok(Value::I64(u as i64)), + kiln_wasi::Value::F32(f) => Ok(Value::F32(FloatBits32::from_f32(f))), + kiln_wasi::Value::F64(f) => Ok(Value::F64(FloatBits64::from_f64(f))), + kiln_wasi::Value::Bool(b) => Ok(Value::I32(if b { 1 } else { 0 })), + kiln_wasi::Value::U8(u) => Ok(Value::I32(u as i32)), + kiln_wasi::Value::S8(i) => Ok(Value::I32(i as i32)), + kiln_wasi::Value::U16(u) => Ok(Value::I32(u as i32)), + kiln_wasi::Value::S16(i) => Ok(Value::I32(i as i32)), + _other => Err(kiln_error::Error::runtime_execution_error( + "unsupported WASI value type in lowered function result", + )), } }).collect(); - Ok(results) + results } else { Err(kiln_error::Error::runtime_error("WASI dispatcher not available for lowered function")) } @@ -1286,7 +1327,7 @@ impl StacklessEngine { #[cfg(feature = "tracing")] if !links_to_remap.is_empty() { - tracing::debug!( + debug!( old_id = old_id, new_id = new_id, count = links_to_remap.len(), @@ -1443,6 +1484,15 @@ impl StacklessEngine { } } + /// Peek at the next instance ID that will be assigned by `set_current_module`. + /// + /// This is useful when creating a `ModuleInstance` that needs to know its + /// future engine instance ID (e.g., for FuncRef storage during element + /// segment initialization). + pub fn peek_next_instance_id(&self) -> usize { + self.next_instance_id.load(Ordering::Relaxed) as usize + } + /// Set the current module for execution /// /// Returns the instance ID that can be used for execution @@ -1766,15 +1816,24 @@ impl StacklessEngine { if let Some((target_instance_id, export_name)) = linked { // Check if this is a canon-lowered function + // Route through call_wasi_function (which uses host_handler) + // to ensure consistent resource handle management. #[cfg(all(feature = "std", feature = "wasi"))] if export_name.starts_with("__canon_lower_") { let canon_suffix = &export_name["__canon_lower_".len()..]; if let Some(sep_pos) = canon_suffix.rfind("::") { let interface = &canon_suffix[..sep_pos]; let function = &canon_suffix[sep_pos + 2..]; - let results = self.dispatch_canon_lowered( - instance_id, interface, function, args, + // Use a temporary stack for call_wasi_function + let mut temp_stack = args; + let result = self.call_wasi_function( + interface, function, &mut temp_stack, &module, instance_id, )?; + let results = if let Some(value) = result { + vec![value] + } else { + vec![] + }; return Ok(ExecutionOutcome::Complete(results)); } } @@ -1798,31 +1857,16 @@ impl StacklessEngine { }); }, Err(_) => { - // Export not found - fall through to default results + return Err(kiln_error::Error::runtime_execution_error( + "unlinked import: export not found in target instance", + )); }, } } - // Import not linked or link unresolvable - return correct number of default results - // based on the imported function's type signature - // NOTE: Do NOT decrement here - execute() will decrement on Complete - if let Some(func) = module.functions.get(func_idx) { - if let Some(func_type) = module.types.get(func.type_idx as usize) { - let mut results = Vec::new(); - for result_type in &func_type.results { - let default_value = match result_type { - kiln_foundation::ValueType::I32 => Value::I32(0), - kiln_foundation::ValueType::I64 => Value::I64(0), - kiln_foundation::ValueType::F32 => Value::F32(FloatBits32(0)), - kiln_foundation::ValueType::F64 => Value::F64(FloatBits64(0)), - _ => Value::I32(0), - }; - results.push(default_value); - } - return Ok(ExecutionOutcome::Complete(results)); - } - } - // Fallback for corrupted module data - return Ok(ExecutionOutcome::Complete(Vec::new())); + // Import not linked - this is an error, not a fallback situation + return Err(kiln_error::Error::runtime_execution_error( + "unlinked import: import function is not linked to any target", + )); } } #[cfg(not(feature = "std"))] @@ -2054,8 +2098,8 @@ impl StacklessEngine { "[TRAP] Unreachable instruction executed" ); } - return Err(kiln_error::Error::runtime_execution_error( - "WebAssembly trap: unreachable instruction executed", + return Err(kiln_error::Error::runtime_trap( + "unreachable", )); } Instruction::Nop => { @@ -2189,26 +2233,28 @@ impl StacklessEngine { // dispatch to the canonical executor instead of using import_links. // This prevents infinite recursion when adapter modules import canon-lowered // functions that are backed by InlineExports with no real module. + // CHECK FOR LOWERED FUNCTION: If this import was created by canon.lower, + // dispatch through call_wasi_function (which uses host_handler) + // to ensure consistent resource handle management. #[cfg(all(feature = "std", feature = "wasi"))] { - let is_lowered = self.is_lowered_function(instance_id, func_idx as usize); - if is_lowered { + if let Some(lowered) = self.lowered_functions.get(&(instance_id, func_idx as usize)).cloned() { #[cfg(feature = "tracing")] trace!( instance_id = instance_id, func_idx = func_idx, + interface = %lowered.interface, + function = %lowered.function, "[CALL] Import is a canon.lower synthesized function - dispatching to WASI" ); - // Collect args from operand stack based on function signature - let args = Self::collect_function_args(&module, func_idx as usize, &mut operand_stack); - - // Execute the lowered function via WASI dispatcher - let results = self.execute_lowered_function(instance_id, func_idx as usize, args)?; - - // Push results back onto stack - for result in results { - operand_stack.push(result); + // Route through call_wasi_function for consistent resource management + let result = self.call_wasi_function( + &lowered.interface, &lowered.function, + &mut operand_stack, &module, instance_id, + )?; + if let Some(value) = result { + operand_stack.push(value); } // Skip the normal import handling @@ -2255,17 +2301,30 @@ impl StacklessEngine { #[cfg(all(feature = "std", feature = "wasi"))] if export_name.starts_with("__canon_lower_") { // Parse interface::function from __canon_lower_{interface}::{function} + // Route through call_wasi_function which handles + // canonical ABI lowering (memory-based returns for + // complex types like list) let canon_suffix = &export_name["__canon_lower_".len()..]; if let Some(sep_pos) = canon_suffix.rfind("::") { let interface = &canon_suffix[..sep_pos]; let function = &canon_suffix[sep_pos + 2..]; - // Collect args and dispatch to WASI - let args = Self::collect_function_args(&module, func_idx as usize, &mut operand_stack); - let results = self.dispatch_canon_lowered( - instance_id, interface, function, args, + + #[cfg(feature = "tracing")] + trace!( + interface = %interface, + function = %function, + "[CANON_LOWER] Routing to call_wasi_function" + ); + + let result = self.call_wasi_function( + interface, + function, + &mut operand_stack, + &module, + instance_id, )?; - for result in results { - operand_stack.push(result); + if let Some(value) = result { + operand_stack.push(value); } pc += 1; continue; @@ -2354,11 +2413,9 @@ impl StacklessEngine { operand_stack.push(value); } } else { - #[cfg(feature = "tracing")] - - trace!("Warning: Could not resolve import {}", func_idx); - // Push dummy return value to keep stack balanced - operand_stack.push(Value::I32(0)); + return Err(kiln_error::Error::runtime_execution_error( + "unlinked import: could not resolve import function", + )); } } else { // Regular function call - get function signature to know how many args to pop @@ -2474,11 +2531,15 @@ impl StacklessEngine { } Instruction::CallIndirect(type_idx, table_idx) => { // CallIndirect: call a function through an indirect table reference - // Pop the function index from the stack - let table_func_idx = if let Some(Value::I32(idx)) = operand_stack.pop() { - idx as u32 - } else { - return Err(kiln_error::Error::runtime_trap("CallIndirect: expected i32 function index on stack")); + // Pop the function index (i64 if table64, i32 otherwise) + let table_func_idx = { + let ci_table = instance.table(table_idx)?; + let ci_t64 = ci_table.is_table64(); + let idx_u64 = pop_table_operand(&mut operand_stack, ci_t64, "CallIndirect: type mismatch")?; + if idx_u64 > u32::MAX as u64 { + return Err(kiln_error::Error::runtime_trap("undefined element")); + } + idx_u64 as u32 }; #[cfg(feature = "tracing")] @@ -2604,12 +2665,13 @@ impl StacklessEngine { // Note: FuncRef.instance_id stores ModuleInstance.instance_id which uses a // different numbering scheme than the engine's instance_id. We must translate. if let Some(mod_target_id) = target_instance_id { - // Translate ModuleInstance.instance_id → engine instance_id - let engine_target_id = self.instances.iter() + // Translate ModuleInstance.instance_id -> engine instance_id + let target_id = self.instances.iter() .find(|(_, inst)| inst.instance_id() == mod_target_id) - .map(|(engine_id, _)| *engine_id); - - let target_id = engine_target_id.unwrap_or(mod_target_id); + .map(|(engine_id, _)| *engine_id) + .ok_or_else(|| kiln_error::Error::runtime_trap( + "cross-instance call_indirect: target instance not found" + ))?; if target_id != instance_id { // Get the target module to look up function type for arg count @@ -2617,13 +2679,28 @@ impl StacklessEngine { .ok_or_else(|| kiln_error::Error::runtime_trap("cross-instance target not found"))? .module().clone(); + // Validate function index + if func_idx >= target_module.functions.len() { + return Err(kiln_error::Error::runtime_trap( + "cross-instance call_indirect: function index out of bounds in target" + )); + } + + // Type check: validate function signature matches expected type + let expected_type = module.types.get(type_idx as usize) + .ok_or_else(|| kiln_error::Error::runtime_error("Invalid expected function type"))?; + let target_func = &target_module.functions[func_idx]; + let actual_type = target_module.types.get(target_func.type_idx as usize) + .ok_or_else(|| kiln_error::Error::runtime_error("Invalid function type in target"))?; + + if !func_types_match(expected_type, actual_type) { + return Err(kiln_error::Error::runtime_trap("indirect call type mismatch")); + } + // Check if this is a lowered function in the target instance #[cfg(all(feature = "std", feature = "wasi"))] if self.is_lowered_function(target_id, func_idx) { - let func = &target_module.functions[func_idx]; - let func_type = target_module.types.get(func.type_idx as usize) - .ok_or_else(|| kiln_error::Error::runtime_error("Invalid function type"))?; - let param_count = func_type.params.len(); + let param_count = actual_type.params.len(); let mut call_args = Vec::new(); for _ in 0..param_count { if let Some(arg) = operand_stack.pop() { @@ -2642,43 +2719,34 @@ impl StacklessEngine { } // Get the function type from the TARGET module for arg count - if func_idx < target_module.functions.len() { - let func = &target_module.functions[func_idx]; - let func_type = target_module.types.get(func.type_idx as usize) - .ok_or_else(|| kiln_error::Error::runtime_error("Invalid function type in cross-instance call"))?; - let param_count = func_type.params.len(); - let mut call_args = Vec::new(); - for _ in 0..param_count { - if let Some(arg) = operand_stack.pop() { - call_args.push(arg); - } else { - return Err(kiln_error::Error::runtime_error("Stack underflow on cross-instance call_indirect")); - } + let param_count = actual_type.params.len(); + let mut call_args = Vec::new(); + for _ in 0..param_count { + if let Some(arg) = operand_stack.pop() { + call_args.push(arg); + } else { + return Err(kiln_error::Error::runtime_error("Stack underflow on cross-instance call_indirect")); } - call_args.reverse(); - - // Save current frame and dispatch to target instance - let saved_state = SuspendedFrame { - instance_id, - func_idx: caller_func_idx, - pc: pc + 1, - locals, - operand_stack, - block_stack, - block_depth, - instruction_count, - }; - return Ok(ExecutionOutcome::Call { - instance_id: target_id, - func_idx, - args: call_args, - return_state: Some(saved_state), - }); - } else { - return Err(kiln_error::Error::runtime_trap( - "cross-instance call_indirect: function index out of bounds in target" - )); } + call_args.reverse(); + + // Save current frame and dispatch to target instance + let saved_state = SuspendedFrame { + instance_id, + func_idx: caller_func_idx, + pc: pc + 1, + locals, + operand_stack, + block_stack, + block_depth, + instruction_count, + }; + return Ok(ExecutionOutcome::Call { + instance_id: target_id, + func_idx, + args: call_args, + return_state: Some(saved_state), + }); } } @@ -2925,6 +2993,116 @@ impl StacklessEngine { }); } } + Instruction::CallRef(_type_idx) => { + // call_ref: call function via typed function reference + // Pop funcref from stack, trap if null, then call + let func_ref_val = operand_stack.pop() + .ok_or_else(|| kiln_error::Error::runtime_error("call_ref: stack underflow"))?; + + let func_idx = match &func_ref_val { + Value::FuncRef(Some(fref)) => fref.index as usize, + Value::FuncRef(None) => { + return Err(kiln_error::Error::runtime_trap("null function reference")); + } + _ => { + return Err(kiln_error::Error::runtime_trap("call_ref: expected funcref on stack")); + } + }; + + #[cfg(feature = "tracing")] + trace!("call_ref: type_idx={}, resolved func_idx={}", _type_idx, func_idx); + + // Validate function index + if func_idx >= module.functions.len() { + return Err(kiln_error::Error::runtime_trap("call_ref: function index out of bounds")); + } + + // Get function type to determine parameter count + let func = &module.functions[func_idx]; + let func_type = module.types.get(func.type_idx as usize) + .ok_or_else(|| kiln_error::Error::runtime_error("call_ref: invalid function type"))?; + let param_count = func_type.params.len(); + + // Pop arguments from the stack + let mut call_args = Vec::new(); + for _ in 0..param_count { + if let Some(arg) = operand_stack.pop() { + call_args.push(arg); + } else { + return Err(kiln_error::Error::runtime_error("call_ref: stack underflow for args")); + } + } + call_args.reverse(); + + // Dispatch via trampoline + let saved_state = SuspendedFrame { + instance_id, + func_idx: caller_func_idx, + pc: pc + 1, + locals, + operand_stack, + block_stack, + block_depth, + instruction_count, + }; + return Ok(ExecutionOutcome::Call { + instance_id, + func_idx, + args: call_args, + return_state: Some(saved_state), + }); + } + Instruction::ReturnCallRef(_type_idx) => { + // return_call_ref: tail call function via typed function reference + let func_ref_val = operand_stack.pop() + .ok_or_else(|| kiln_error::Error::runtime_error("return_call_ref: stack underflow"))?; + + let func_idx = match &func_ref_val { + Value::FuncRef(Some(fref)) => fref.index as usize, + Value::FuncRef(None) => { + return Err(kiln_error::Error::runtime_trap("null function reference")); + } + _ => { + return Err(kiln_error::Error::runtime_trap("return_call_ref: expected funcref on stack")); + } + }; + + #[cfg(feature = "tracing")] + trace!("return_call_ref: type_idx={}, resolved func_idx={}", _type_idx, func_idx); + + // Validate function index + if func_idx >= module.functions.len() { + return Err(kiln_error::Error::runtime_trap("return_call_ref: function index out of bounds")); + } + + // Get function type to determine parameter count + let func = &module.functions[func_idx]; + let func_type = module.types.get(func.type_idx as usize) + .ok_or_else(|| kiln_error::Error::runtime_error("return_call_ref: invalid function type"))?; + let param_count = func_type.params.len(); + + // Pop arguments from the stack + let mut call_args = Vec::new(); + for _ in 0..param_count { + if let Some(arg) = operand_stack.pop() { + call_args.push(arg); + } else { + return Err(kiln_error::Error::runtime_error("return_call_ref: stack underflow for args")); + } + } + call_args.reverse(); + + // Restore debugger before returning for tail call + #[cfg(all(feature = "std", feature = "debugger"))] + { + self.debugger = debugger_opt; + } + + return Ok(ExecutionOutcome::TailCall { + func_idx, + args: call_args, + }); + } Instruction::ReturnCall(func_idx) => { // ReturnCall: tail call to another function // Similar to Call, but the results become the current function's return value @@ -2966,11 +3144,15 @@ impl StacklessEngine { } Instruction::ReturnCallIndirect(type_idx, table_idx) => { // ReturnCallIndirect: tail call through indirect table reference - // Pop the function index from the stack - let table_func_idx = if let Some(Value::I32(idx)) = operand_stack.pop() { - idx as u32 - } else { - return Err(kiln_error::Error::runtime_trap("return_call_indirect: expected i32 function index on stack")); + // Pop the function index (i64 if table64, i32 otherwise) + let table_func_idx = { + let rci_table = instance.table(table_idx)?; + let rci_t64 = rci_table.is_table64(); + let idx_u64 = pop_table_operand(&mut operand_stack, rci_t64, "return_call_indirect: type mismatch")?; + if idx_u64 > u32::MAX as u64 { + return Err(kiln_error::Error::runtime_trap("undefined element")); + } + idx_u64 as u32 }; #[cfg(feature = "tracing")] @@ -4378,7 +4560,8 @@ impl StacklessEngine { } } Instruction::I32Store(mem_arg) => { - if let Some(Value::I32(value)) = operand_stack.pop() { + let store_val = operand_stack.pop(); + if let Some(Value::I32(value)) = store_val { let eff_addr = pop_memory_address(&mut operand_stack, mem_arg.offset, 4)?; #[cfg(feature = "tracing")] trace!("I32Store: writing value {} to address {} (offset={})", value, eff_addr, mem_arg.offset); @@ -4401,6 +4584,10 @@ impl StacklessEngine { return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); } } + } else { + return Err(kiln_error::Error::runtime_execution_error( + "type mismatch in i32.store: expected i32 value on stack", + )); } } Instruction::I32Load8S(mem_arg) => { @@ -4498,7 +4685,8 @@ impl StacklessEngine { } } Instruction::I32Store8(mem_arg) => { - if let Some(Value::I32(value)) = operand_stack.pop() { + let store_val = operand_stack.pop(); + if let Some(Value::I32(value)) = store_val { let eff_addr = pop_memory_address(&mut operand_stack, mem_arg.offset, 1)?; #[cfg(feature = "tracing")] @@ -4519,10 +4707,15 @@ impl StacklessEngine { return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); } } + } else { + return Err(kiln_error::Error::runtime_execution_error( + "type mismatch in i32.store8: expected i32 value on stack", + )); } } Instruction::I32Store16(mem_arg) => { - if let Some(Value::I32(value)) = operand_stack.pop() { + let store_val = operand_stack.pop(); + if let Some(Value::I32(value)) = store_val { let eff_addr = pop_memory_address(&mut operand_stack, mem_arg.offset, 2)?; match instance.memory(mem_arg.memory_index as u32) { Ok(memory_wrapper) => { @@ -4542,6 +4735,10 @@ impl StacklessEngine { return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); } } + } else { + return Err(kiln_error::Error::runtime_execution_error( + "type mismatch in i32.store16: expected i32 value on stack", + )); } } Instruction::I64Load(mem_arg) => { @@ -4587,7 +4784,8 @@ impl StacklessEngine { } } Instruction::I64Store(mem_arg) => { - if let Some(Value::I64(value)) = operand_stack.pop() { + let store_val = operand_stack.pop(); + if let Some(Value::I64(value)) = store_val { let eff_addr = pop_memory_address(&mut operand_stack, mem_arg.offset, 8)?; match instance.memory(mem_arg.memory_index as u32) { Ok(memory_wrapper) => { @@ -4609,6 +4807,10 @@ impl StacklessEngine { return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); } } + } else { + return Err(kiln_error::Error::runtime_execution_error( + "type mismatch in i64.store: expected i64 value on stack", + )); } } // ======================================== @@ -4780,13 +4982,12 @@ impl StacklessEngine { // I64 Partial Store Instructions (store lower bits of i64) // ======================================== Instruction::I64Store8(mem_arg) => { - if let (Some(Value::I64(value)), Some(Value::I32(addr))) = (operand_stack.pop(), operand_stack.pop()) { - let offset = calculate_effective_address(addr, mem_arg.offset, 1)? as u32; + if let Some(Value::I64(value)) = operand_stack.pop() { + let eff_addr = pop_memory_address(&mut operand_stack, mem_arg.offset, 1)?; #[cfg(feature = "tracing")] trace!( instance_id = instance_id, - addr = addr, - offset = format_args!("{:#x}", offset), + offset = format_args!("{:#x}", eff_addr), value = value & 0xFF, "[I64Store8] Store low 8 bits of i64" ); @@ -4794,11 +4995,10 @@ impl StacklessEngine { Ok(memory_wrapper) => { let memory = &memory_wrapper.0; let bytes = [(value & 0xFF) as u8]; - // ASIL-B COMPLIANT: Use write_shared for thread-safe writes - match memory.write_shared(offset, &bytes) { + match memory.write_shared(eff_addr, &bytes) { Ok(()) => { #[cfg(feature = "tracing")] - trace!("I64Store8: successfully wrote value {} to address {}", value & 0xFF, offset); + trace!("I64Store8: successfully wrote value {} to address {}", value & 0xFF, eff_addr); } Err(_e) => { return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); @@ -4811,16 +5011,19 @@ impl StacklessEngine { return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); } } + } else { + return Err(kiln_error::Error::runtime_execution_error( + "type mismatch in i64.store8: expected i64 value and i32 address on stack", + )); } } Instruction::I64Store16(mem_arg) => { - if let (Some(Value::I64(value)), Some(Value::I32(addr))) = (operand_stack.pop(), operand_stack.pop()) { - let offset = calculate_effective_address(addr, mem_arg.offset, 2)? as u32; + if let Some(Value::I64(value)) = operand_stack.pop() { + let eff_addr = pop_memory_address(&mut operand_stack, mem_arg.offset, 2)?; #[cfg(feature = "tracing")] trace!( instance_id = instance_id, - addr = addr, - offset = format_args!("{:#x}", offset), + offset = format_args!("{:#x}", eff_addr), value = value & 0xFFFF, "[I64Store16] Store low 16 bits of i64" ); @@ -4828,11 +5031,10 @@ impl StacklessEngine { Ok(memory_wrapper) => { let memory = &memory_wrapper.0; let bytes = (value as u16).to_le_bytes(); - // ASIL-B COMPLIANT: Use write_shared for thread-safe writes - match memory.write_shared(offset, &bytes) { + match memory.write_shared(eff_addr, &bytes) { Ok(()) => { #[cfg(feature = "tracing")] - trace!("I64Store16: successfully wrote value {} to address {}", value & 0xFFFF, offset); + trace!("I64Store16: successfully wrote value {} to address {}", value & 0xFFFF, eff_addr); } Err(_e) => { return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); @@ -4845,16 +5047,19 @@ impl StacklessEngine { return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); } } + } else { + return Err(kiln_error::Error::runtime_execution_error( + "type mismatch in i64.store16: expected i64 value and i32 address on stack", + )); } } Instruction::I64Store32(mem_arg) => { - if let (Some(Value::I64(value)), Some(Value::I32(addr))) = (operand_stack.pop(), operand_stack.pop()) { - let offset = calculate_effective_address(addr, mem_arg.offset, 4)? as u32; + if let Some(Value::I64(value)) = operand_stack.pop() { + let eff_addr = pop_memory_address(&mut operand_stack, mem_arg.offset, 4)?; #[cfg(feature = "tracing")] trace!( instance_id = instance_id, - addr = addr, - offset = format_args!("{:#x}", offset), + offset = format_args!("{:#x}", eff_addr), value = value & 0xFFFFFFFF, "[I64Store32] Store low 32 bits of i64" ); @@ -4862,11 +5067,10 @@ impl StacklessEngine { Ok(memory_wrapper) => { let memory = &memory_wrapper.0; let bytes = (value as u32).to_le_bytes(); - // ASIL-B COMPLIANT: Use write_shared for thread-safe writes - match memory.write_shared(offset, &bytes) { + match memory.write_shared(eff_addr, &bytes) { Ok(()) => { #[cfg(feature = "tracing")] - trace!("I64Store32: successfully wrote value {} to address {}", value & 0xFFFFFFFF, offset); + trace!("I64Store32: successfully wrote value {} to address {}", value & 0xFFFFFFFF, eff_addr); } Err(_e) => { return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); @@ -4879,6 +5083,10 @@ impl StacklessEngine { return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); } } + } else { + return Err(kiln_error::Error::runtime_execution_error( + "type mismatch in i64.store32: expected i64 value and i32 address on stack", + )); } } // ======================================== @@ -4890,51 +5098,38 @@ impl StacklessEngine { operand_stack.push(Value::F64(FloatBits64(bits))); } Instruction::F32Load(mem_arg) => { + let eff_addr = pop_memory_address(&mut operand_stack, mem_arg.offset, 4)?; #[cfg(feature = "tracing")] - trace!("F32Load: stack before pop has {} elements", operand_stack.len()); - if let Some(Value::I32(addr)) = operand_stack.pop() { - let offset = calculate_effective_address(addr, mem_arg.offset, 4)? as u32; - #[cfg(feature = "tracing")] - trace!("F32Load: addr={}, offset={}, mem_idx={}", addr, offset, mem_arg.memory_index); - match instance.memory(mem_arg.memory_index as u32) { - Ok(memory_wrapper) => { - let memory = &memory_wrapper.0; - let mut buffer = [0u8; 4]; - match memory.read(offset, &mut buffer) { - Ok(()) => { - let bits = u32::from_le_bytes(buffer); - #[cfg(feature = "tracing")] - trace!("F32Load: read bytes {:?}, bits={:#x}, pushing F32", buffer, bits); - operand_stack.push(Value::F32(FloatBits32(bits))); - #[cfg(feature = "tracing")] - trace!("F32Load: stack after push has {} elements", operand_stack.len()); - } - Err(e) => { - #[cfg(feature = "tracing")] - error!("F32Load: memory read error: {:?}", e); - return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); - } + trace!("F32Load: eff_addr={}, mem_idx={}", eff_addr, mem_arg.memory_index); + match instance.memory(mem_arg.memory_index as u32) { + Ok(memory_wrapper) => { + let memory = &memory_wrapper.0; + let mut buffer = [0u8; 4]; + match memory.read(eff_addr, &mut buffer) { + Ok(()) => { + let bits = u32::from_le_bytes(buffer); + #[cfg(feature = "tracing")] + trace!("F32Load: read bytes {:?}, bits={:#x}, pushing F32", buffer, bits); + operand_stack.push(Value::F32(FloatBits32(bits))); + } + Err(_) => { + return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); } - } - Err(e) => { - #[cfg(feature = "tracing")] - error!("F32Load: memory access error: {:?}", e); - return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); } } - } else { - #[cfg(feature = "tracing")] - error!("F32Load: stack was empty or top was not I32!"); + Err(_) => { + return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); + } } } Instruction::F32Store(mem_arg) => { - if let (Some(Value::F32(bits)), Some(Value::I32(addr))) = (operand_stack.pop(), operand_stack.pop()) { - let offset = calculate_effective_address(addr, mem_arg.offset, 4)? as u32; + if let Some(Value::F32(bits)) = operand_stack.pop() { + let eff_addr = pop_memory_address(&mut operand_stack, mem_arg.offset, 4)?; match instance.memory(mem_arg.memory_index as u32) { Ok(memory_wrapper) => { let memory = &memory_wrapper.0; let bytes = bits.0.to_le_bytes(); - if memory.write_shared(offset, &bytes).is_err() { + if memory.write_shared(eff_addr, &bytes).is_err() { return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); } } @@ -4942,39 +5137,41 @@ impl StacklessEngine { return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); } } + } else { + return Err(kiln_error::Error::runtime_execution_error( + "type mismatch in f32.store: expected f32 value and i32 address on stack", + )); } } Instruction::F64Load(mem_arg) => { - if let Some(Value::I32(addr)) = operand_stack.pop() { - let offset = calculate_effective_address(addr, mem_arg.offset, 8)? as u32; - match instance.memory(mem_arg.memory_index as u32) { - Ok(memory_wrapper) => { - let memory = &memory_wrapper.0; - let mut buffer = [0u8; 8]; - match memory.read(offset, &mut buffer) { - Ok(()) => { - let bits = u64::from_le_bytes(buffer); - operand_stack.push(Value::F64(FloatBits64(bits))); - } - Err(_) => { - return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); - } + let eff_addr = pop_memory_address(&mut operand_stack, mem_arg.offset, 8)?; + match instance.memory(mem_arg.memory_index as u32) { + Ok(memory_wrapper) => { + let memory = &memory_wrapper.0; + let mut buffer = [0u8; 8]; + match memory.read(eff_addr, &mut buffer) { + Ok(()) => { + let bits = u64::from_le_bytes(buffer); + operand_stack.push(Value::F64(FloatBits64(bits))); + } + Err(_) => { + return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); } } - Err(_) => { - return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); - } + } + Err(_) => { + return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); } } } Instruction::F64Store(mem_arg) => { - if let (Some(Value::F64(bits)), Some(Value::I32(addr))) = (operand_stack.pop(), operand_stack.pop()) { - let offset = calculate_effective_address(addr, mem_arg.offset, 8)? as u32; + if let Some(Value::F64(bits)) = operand_stack.pop() { + let eff_addr = pop_memory_address(&mut operand_stack, mem_arg.offset, 8)?; match instance.memory(mem_arg.memory_index as u32) { Ok(memory_wrapper) => { let memory = &memory_wrapper.0; let bytes = bits.0.to_le_bytes(); - if memory.write_shared(offset, &bytes).is_err() { + if memory.write_shared(eff_addr, &bytes).is_err() { return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); } } @@ -4982,30 +5179,38 @@ impl StacklessEngine { return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); } } + } else { + return Err(kiln_error::Error::runtime_execution_error( + "type mismatch in f64.store: expected f64 value and i32 address on stack", + )); } } // F32 Arithmetic operations Instruction::F32Add => { if let (Some(Value::F32(b)), Some(Value::F32(a))) = (operand_stack.pop(), operand_stack.pop()) { let result = a.value() + b.value(); + let result = if result.is_nan() { f32::from_bits(0x7FC0_0000) } else { result }; operand_stack.push(Value::F32(FloatBits32(result.to_bits()))); } } Instruction::F32Sub => { if let (Some(Value::F32(b)), Some(Value::F32(a))) = (operand_stack.pop(), operand_stack.pop()) { let result = a.value() - b.value(); + let result = if result.is_nan() { f32::from_bits(0x7FC0_0000) } else { result }; operand_stack.push(Value::F32(FloatBits32(result.to_bits()))); } } Instruction::F32Mul => { if let (Some(Value::F32(b)), Some(Value::F32(a))) = (operand_stack.pop(), operand_stack.pop()) { let result = a.value() * b.value(); + let result = if result.is_nan() { f32::from_bits(0x7FC0_0000) } else { result }; operand_stack.push(Value::F32(FloatBits32(result.to_bits()))); } } Instruction::F32Div => { if let (Some(Value::F32(b)), Some(Value::F32(a))) = (operand_stack.pop(), operand_stack.pop()) { let result = a.value() / b.value(); + let result = if result.is_nan() { f32::from_bits(0x7FC0_0000) } else { result }; operand_stack.push(Value::F32(FloatBits32(result.to_bits()))); } } @@ -5025,18 +5230,21 @@ impl StacklessEngine { Instruction::F32Ceil => { if let Some(Value::F32(a)) = operand_stack.pop() { let result = a.value().ceil(); + let result = if result.is_nan() { f32::from_bits(0x7FC0_0000) } else { result }; operand_stack.push(Value::F32(FloatBits32(result.to_bits()))); } } Instruction::F32Floor => { if let Some(Value::F32(a)) = operand_stack.pop() { let result = a.value().floor(); + let result = if result.is_nan() { f32::from_bits(0x7FC0_0000) } else { result }; operand_stack.push(Value::F32(FloatBits32(result.to_bits()))); } } Instruction::F32Trunc => { if let Some(Value::F32(a)) = operand_stack.pop() { let result = a.value().trunc(); + let result = if result.is_nan() { f32::from_bits(0x7FC0_0000) } else { result }; operand_stack.push(Value::F32(FloatBits32(result.to_bits()))); } } @@ -5044,7 +5252,9 @@ impl StacklessEngine { if let Some(Value::F32(a)) = operand_stack.pop() { let f = a.value(); // Round to nearest even (banker's rounding) - let result = if f.fract().abs() == 0.5 { + let result = if f.is_nan() { + f32::from_bits(0x7FC0_0000) + } else if f.fract().abs() == 0.5 { let floor = f.floor(); if floor as i32 % 2 == 0 { floor } else { f.ceil() } } else { @@ -5056,6 +5266,7 @@ impl StacklessEngine { Instruction::F32Sqrt => { if let Some(Value::F32(a)) = operand_stack.pop() { let result = a.value().sqrt(); + let result = if result.is_nan() { f32::from_bits(0x7FC0_0000) } else { result }; operand_stack.push(Value::F32(FloatBits32(result.to_bits()))); } } @@ -5063,10 +5274,10 @@ impl StacklessEngine { if let (Some(Value::F32(b)), Some(Value::F32(a))) = (operand_stack.pop(), operand_stack.pop()) { let fa = a.value(); let fb = b.value(); - // WebAssembly spec: If either operand is NaN, return NaN + // WebAssembly spec: If either operand is NaN, return canonical NaN // If both are zero with different signs, return -0.0 let result = if fa.is_nan() || fb.is_nan() { - f32::NAN + f32::from_bits(0x7FC0_0000) } else if fa == 0.0 && fb == 0.0 { if fa.is_sign_negative() || fb.is_sign_negative() { -0.0 } else { 0.0 } } else { @@ -5079,10 +5290,10 @@ impl StacklessEngine { if let (Some(Value::F32(b)), Some(Value::F32(a))) = (operand_stack.pop(), operand_stack.pop()) { let fa = a.value(); let fb = b.value(); - // WebAssembly spec: If either operand is NaN, return NaN + // WebAssembly spec: If either operand is NaN, return canonical NaN // If both are zero with different signs, return +0.0 let result = if fa.is_nan() || fb.is_nan() { - f32::NAN + f32::from_bits(0x7FC0_0000) } else if fa == 0.0 && fb == 0.0 { if fa.is_sign_positive() || fb.is_sign_positive() { 0.0 } else { -0.0 } } else { @@ -5101,24 +5312,28 @@ impl StacklessEngine { Instruction::F64Add => { if let (Some(Value::F64(b)), Some(Value::F64(a))) = (operand_stack.pop(), operand_stack.pop()) { let result = f64::from_bits(a.0) + f64::from_bits(b.0); + let result = if result.is_nan() { f64::from_bits(0x7FF8_0000_0000_0000) } else { result }; operand_stack.push(Value::F64(FloatBits64(result.to_bits()))); } } Instruction::F64Sub => { if let (Some(Value::F64(b)), Some(Value::F64(a))) = (operand_stack.pop(), operand_stack.pop()) { let result = f64::from_bits(a.0) - f64::from_bits(b.0); + let result = if result.is_nan() { f64::from_bits(0x7FF8_0000_0000_0000) } else { result }; operand_stack.push(Value::F64(FloatBits64(result.to_bits()))); } } Instruction::F64Mul => { if let (Some(Value::F64(b)), Some(Value::F64(a))) = (operand_stack.pop(), operand_stack.pop()) { let result = f64::from_bits(a.0) * f64::from_bits(b.0); + let result = if result.is_nan() { f64::from_bits(0x7FF8_0000_0000_0000) } else { result }; operand_stack.push(Value::F64(FloatBits64(result.to_bits()))); } } Instruction::F64Div => { if let (Some(Value::F64(b)), Some(Value::F64(a))) = (operand_stack.pop(), operand_stack.pop()) { let result = f64::from_bits(a.0) / f64::from_bits(b.0); + let result = if result.is_nan() { f64::from_bits(0x7FF8_0000_0000_0000) } else { result }; operand_stack.push(Value::F64(FloatBits64(result.to_bits()))); } } @@ -5175,25 +5390,30 @@ impl StacklessEngine { Instruction::F64Ceil => { if let Some(Value::F64(a)) = operand_stack.pop() { let result = f64::from_bits(a.0).ceil(); + let result = if result.is_nan() { f64::from_bits(0x7FF8_0000_0000_0000) } else { result }; operand_stack.push(Value::F64(FloatBits64(result.to_bits()))); } } Instruction::F64Floor => { if let Some(Value::F64(a)) = operand_stack.pop() { let result = f64::from_bits(a.0).floor(); + let result = if result.is_nan() { f64::from_bits(0x7FF8_0000_0000_0000) } else { result }; operand_stack.push(Value::F64(FloatBits64(result.to_bits()))); } } Instruction::F64Trunc => { if let Some(Value::F64(a)) = operand_stack.pop() { let result = f64::from_bits(a.0).trunc(); + let result = if result.is_nan() { f64::from_bits(0x7FF8_0000_0000_0000) } else { result }; operand_stack.push(Value::F64(FloatBits64(result.to_bits()))); } } Instruction::F64Nearest => { if let Some(Value::F64(a)) = operand_stack.pop() { let f = f64::from_bits(a.0); - let result = if f.fract().abs() == 0.5 { + let result = if f.is_nan() { + f64::from_bits(0x7FF8_0000_0000_0000) + } else if f.fract().abs() == 0.5 { let floor = f.floor(); if floor as i64 % 2 == 0 { floor } else { f.ceil() } } else { @@ -5205,6 +5425,7 @@ impl StacklessEngine { Instruction::F64Sqrt => { if let Some(Value::F64(a)) = operand_stack.pop() { let result = f64::from_bits(a.0).sqrt(); + let result = if result.is_nan() { f64::from_bits(0x7FF8_0000_0000_0000) } else { result }; operand_stack.push(Value::F64(FloatBits64(result.to_bits()))); } } @@ -5212,8 +5433,9 @@ impl StacklessEngine { if let (Some(Value::F64(b)), Some(Value::F64(a))) = (operand_stack.pop(), operand_stack.pop()) { let fa = f64::from_bits(a.0); let fb = f64::from_bits(b.0); + // WebAssembly spec: If either operand is NaN, return canonical NaN let result = if fa.is_nan() || fb.is_nan() { - f64::NAN + f64::from_bits(0x7FF8_0000_0000_0000) } else if fa == 0.0 && fb == 0.0 { if fa.is_sign_negative() || fb.is_sign_negative() { -0.0 } else { 0.0 } } else { @@ -5226,8 +5448,9 @@ impl StacklessEngine { if let (Some(Value::F64(b)), Some(Value::F64(a))) = (operand_stack.pop(), operand_stack.pop()) { let fa = f64::from_bits(a.0); let fb = f64::from_bits(b.0); + // WebAssembly spec: If either operand is NaN, return canonical NaN let result = if fa.is_nan() || fb.is_nan() { - f64::NAN + f64::from_bits(0x7FF8_0000_0000_0000) } else if fa == 0.0 && fb == 0.0 { if fa.is_sign_positive() || fb.is_sign_positive() { 0.0 } else { -0.0 } } else { @@ -5295,12 +5518,14 @@ impl StacklessEngine { Instruction::F64PromoteF32 => { if let Some(Value::F32(a)) = operand_stack.pop() { let result = f32::from_bits(a.0) as f64; + let result = if result.is_nan() { f64::from_bits(0x7FF8_0000_0000_0000) } else { result }; operand_stack.push(Value::F64(FloatBits64(result.to_bits()))); } } Instruction::F32DemoteF64 => { if let Some(Value::F64(a)) = operand_stack.pop() { let result = f64::from_bits(a.0) as f32; + let result = if result.is_nan() { f32::from_bits(0x7FC0_0000) } else { result }; operand_stack.push(Value::F32(FloatBits32(result.to_bits()))); } } @@ -5505,6 +5730,8 @@ impl StacklessEngine { 0x40 => 0, // empty type - no params 0x7F | 0x7E | 0x7D | 0x7C | 0x7B => 0, // inline value types: 0 params 0x70 | 0x6F => 0, // funcref, externref: 0 params + // GC reference types: single value type, 0 params + 0x6E | 0x6D | 0x6C | 0x6B | 0x6A | 0x73 | 0x72 | 0x71 | 0x69 => 0, _ => { // Type index - look up actual param count from module types if let Some(func_type) = module.types.get(block_type_idx as usize) { @@ -5519,12 +5746,16 @@ impl StacklessEngine { 0x40 => 0, // empty type - no return 0x7F | 0x7E | 0x7D | 0x7C | 0x7B => 1, // i32, i64, f32, f64, v128 0x70 | 0x6F => 1, // funcref, externref + // GC reference types: single value type, 1 result + 0x6E | 0x6D | 0x6C | 0x6B | 0x6A | 0x73 | 0x72 | 0x71 | 0x69 => 1, _ => { // Type index - look up actual result count from module types if let Some(func_type) = module.types.get(block_type_idx as usize) { func_type.results.len() } else { - 1 // Fallback if type not found + return Err(kiln_error::Error::runtime_error( + "Br: block type index not found in module types" + )); } } } @@ -5712,6 +5943,8 @@ impl StacklessEngine { 0x40 => 0, // empty type - no params 0x7F | 0x7E | 0x7D | 0x7C | 0x7B => 0, // inline value types: 0 params 0x70 | 0x6F => 0, // funcref, externref: 0 params + // GC reference types: single value type, 0 params + 0x6E | 0x6D | 0x6C | 0x6B | 0x6A | 0x73 | 0x72 | 0x71 | 0x69 => 0, _ => { // Type index - look up actual param count from module types if let Some(func_type) = module.types.get(block_type_idx as usize) { @@ -5726,12 +5959,16 @@ impl StacklessEngine { 0x40 => 0, // empty type - no return 0x7F | 0x7E | 0x7D | 0x7C | 0x7B => 1, // i32, i64, f32, f64, v128 0x70 | 0x6F => 1, // funcref, externref + // GC reference types: single value type, 1 result + 0x6E | 0x6D | 0x6C | 0x6B | 0x6A | 0x73 | 0x72 | 0x71 | 0x69 => 1, _ => { // Type index - look up actual result count if let Some(func_type) = module.types.get(block_type_idx as usize) { func_type.results.len() } else { - 1 + return Err(kiln_error::Error::runtime_error( + "BrIf: block type index not found in module types" + )); } } } @@ -5859,13 +6096,19 @@ impl StacklessEngine { Ok(memory_wrapper) => { let memory = &memory_wrapper.0; let size_in_pages = memory.size(); + let is_memory64 = memory.ty.memory64; #[cfg(feature = "tracing")] trace!( memory_idx = memory_idx, size_in_pages = size_in_pages, + is_memory64 = is_memory64, "[MemorySize] Retrieved memory size" ); - operand_stack.push(Value::I32(size_in_pages as i32)); + if is_memory64 { + operand_stack.push(Value::I64(size_in_pages as i64)); + } else { + operand_stack.push(Value::I32(size_in_pages as i32)); + } } Err(e) => { #[cfg(feature = "tracing")] @@ -5875,51 +6118,94 @@ impl StacklessEngine { } } Instruction::MemoryGrow(memory_idx) => { - // Pop the number of pages to grow - if let Some(Value::I32(delta)) = operand_stack.pop() { - if delta < 0 { - // Negative delta is invalid, return -1 (failure) - #[cfg(feature = "tracing")] - trace!("MemoryGrow: negative delta {}, pushing -1", delta); - operand_stack.push(Value::I32(-1)); - } else { - // Use instance memory for grow (has initialized data segments) - match instance.memory(memory_idx as u32) { - Ok(memory_wrapper) => { - let memory = &memory_wrapper.0; - let current_size = memory.size(); + // Determine if memory is 64-bit to know the operand type + let is_memory64 = instance.memory(memory_idx as u32) + .map(|mw| mw.0.ty.memory64) + .unwrap_or(false); + + // Pop the number of pages to grow (i64 for memory64, i32 for memory32) + let mut _grow_failed_early = false; + let delta_u32 = if is_memory64 { + match operand_stack.pop() { + Some(Value::I64(delta)) => { + if delta < 0 || delta as u64 > u32::MAX as u64 { #[cfg(feature = "tracing")] - trace!( - memory_idx = memory_idx, - delta = delta, - current_size = current_size, - "[MemoryGrow] Attempting to grow memory" - ); - match memory.grow_shared(delta as u32) { - Ok(prev_pages) => { - #[cfg(feature = "tracing")] - trace!( - memory_idx = memory_idx, - prev_pages = prev_pages, - new_pages = prev_pages + delta as u32, - "[MemoryGrow] Success" - ); + trace!("MemoryGrow: invalid delta {}, pushing -1", delta); + operand_stack.push(Value::I64(-1)); + _grow_failed_early = true; + None + } else { + Some(delta as u32) + } + } + _ => None, + } + } else { + match operand_stack.pop() { + Some(Value::I32(delta)) => { + if delta < 0 { + #[cfg(feature = "tracing")] + trace!("MemoryGrow: negative delta {}, pushing -1", delta); + operand_stack.push(Value::I32(-1)); + _grow_failed_early = true; + None + } else { + Some(delta as u32) + } + } + _ => None, + } + }; + + if let Some(delta) = delta_u32 { + // Use instance memory for grow (has initialized data segments) + match instance.memory(memory_idx as u32) { + Ok(memory_wrapper) => { + let memory = &memory_wrapper.0; + let current_size = memory.size(); + #[cfg(feature = "tracing")] + trace!( + memory_idx = memory_idx, + delta = delta, + current_size = current_size, + "[MemoryGrow] Attempting to grow memory" + ); + match memory.grow_shared(delta) { + Ok(prev_pages) => { + #[cfg(feature = "tracing")] + trace!( + memory_idx = memory_idx, + prev_pages = prev_pages, + new_pages = prev_pages + delta, + "[MemoryGrow] Success" + ); + if is_memory64 { + operand_stack.push(Value::I64(prev_pages as i64)); + } else { operand_stack.push(Value::I32(prev_pages as i32)); } - Err(e) => { - #[cfg(feature = "tracing")] - warn!( - memory_idx = memory_idx, - error = ?e, - "[MemoryGrow] Failed" - ); + } + Err(e) => { + #[cfg(feature = "tracing")] + warn!( + memory_idx = memory_idx, + error = ?e, + "[MemoryGrow] Failed" + ); + if is_memory64 { + operand_stack.push(Value::I64(-1)); + } else { operand_stack.push(Value::I32(-1)); } } } - Err(e) => { - #[cfg(feature = "tracing")] - trace!("MemoryGrow: memory[{}] not found: {:?}", memory_idx, e); + } + Err(e) => { + #[cfg(feature = "tracing")] + trace!("MemoryGrow: memory[{}] not found: {:?}", memory_idx, e); + if is_memory64 { + operand_stack.push(Value::I64(-1)); + } else { operand_stack.push(Value::I32(-1)); } } @@ -5927,60 +6213,63 @@ impl StacklessEngine { } } Instruction::MemoryCopy(dst_mem_idx, src_mem_idx) => { + // Determine memory64 status for both memories + let dst_is_64 = instance.memory(dst_mem_idx) + .map(|mw| mw.0.ty.memory64).unwrap_or(false); + let src_is_64 = instance.memory(src_mem_idx as u32) + .map(|mw| mw.0.ty.memory64).unwrap_or(false); + // Pop size, src, dest from stack (in that order per wasm spec) - if let (Some(Value::I32(size)), Some(Value::I32(src)), Some(Value::I32(dest))) = - (operand_stack.pop(), operand_stack.pop(), operand_stack.pop()) - { - #[cfg(feature = "tracing")] - trace!( - dest = format_args!("{:#x}", dest), - src = format_args!("{:#x}", src), - size = size, - dst_mem_idx = dst_mem_idx, - src_mem_idx = src_mem_idx, - "[MemoryCopy] Starting copy operation" - ); + // Per spec: n type = dst index type, s type = src index type, d type = dst index type + let size = pop_memory_operand(&mut operand_stack, dst_is_64)?; + let src = pop_memory_operand(&mut operand_stack, src_is_64)?; + let dest = pop_memory_operand(&mut operand_stack, dst_is_64)?; - // For now, only support same-memory copy (most common case) - // Multi-memory support can be added later - if dst_mem_idx != src_mem_idx { - #[cfg(feature = "tracing")] - trace!("MemoryCopy: cross-memory copy not yet implemented"); - return Err(kiln_error::Error::runtime_error("Cross-memory copy not yet implemented")); - } + #[cfg(feature = "tracing")] + trace!( + dest = format_args!("{:#x}", dest), + src = format_args!("{:#x}", src), + size = size, + dst_mem_idx = dst_mem_idx, + src_mem_idx = src_mem_idx, + "[MemoryCopy] Starting copy operation" + ); - // Per WebAssembly spec: bounds check MUST happen before checking size==0 - // If size == 0 AND (dest > memory.size OR src > memory.size): TRAP - // If size > 0 AND ((dest + size) > memory.size OR (src + size) > memory.size): TRAP - #[cfg(any(feature = "std", feature = "alloc"))] - { - let memory_wrapper = instance.memory(dst_mem_idx)?; - let memory = &memory_wrapper.0; - let memory_size = memory.size_in_bytes() as u32; - let dest_u32 = dest as u32; - let src_u32 = src as u32; - let size_u32 = size as u32; + // For now, only support same-memory copy (most common case) + // Multi-memory support can be added later + if dst_mem_idx != src_mem_idx as u32 { + #[cfg(feature = "tracing")] + trace!("MemoryCopy: cross-memory copy not yet implemented"); + return Err(kiln_error::Error::runtime_error("Cross-memory copy not yet implemented")); + } - if size_u32 == 0 { - // For size 0, check if offsets are within bounds (can be equal to size) - if dest_u32 > memory_size || src_u32 > memory_size { - return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); - } - // No-op for zero size copy after bounds check passes - continue; - } + // Per WebAssembly spec: bounds check MUST happen before checking size==0 + #[cfg(any(feature = "std", feature = "alloc"))] + { + let memory_wrapper = instance.memory(dst_mem_idx)?; + let memory = &memory_wrapper.0; + let memory_size = memory.size_in_bytes() as u64; + if size == 0 { + // For size 0, check if offsets are within bounds (can be equal to size) + if dest > memory_size || src > memory_size { + return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); + } + // No-op for zero size copy after bounds check passes + } else { // For size > 0, check if (offset + size) overflows or exceeds memory size - let dest_end = dest_u32.checked_add(size_u32) + let dest_end = dest.checked_add(size) .ok_or_else(|| kiln_error::Error::runtime_trap("out of bounds memory access"))?; - let src_end = src_u32.checked_add(size_u32) + let src_end = src.checked_add(size) .ok_or_else(|| kiln_error::Error::runtime_trap("out of bounds memory access"))?; if dest_end > memory_size || src_end > memory_size { return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); } - let size_usize = size_u32 as usize; + let size_usize = size as usize; + let src_u32 = src as u32; + let dest_u32 = dest as u32; // Read source data into temp buffer (handles overlapping regions) let mut buffer = vec![0u8; size_usize]; @@ -6013,60 +6302,61 @@ impl StacklessEngine { } } } - #[cfg(not(any(feature = "std", feature = "alloc")))] - return Err(kiln_error::Error::runtime_error("MemoryCopy requires std or alloc feature")); - } else { - #[cfg(feature = "tracing")] - trace!("MemoryCopy: insufficient values on stack"); } + #[cfg(not(any(feature = "std", feature = "alloc")))] + return Err(kiln_error::Error::runtime_error("MemoryCopy requires std or alloc feature")); } Instruction::MemoryFill(mem_idx) => { + // Determine memory64 status + let is_memory64 = instance.memory(mem_idx) + .map(|mw| mw.0.ty.memory64).unwrap_or(false); + // Pop size, value, dest from stack (in that order per wasm spec) - if let (Some(Value::I32(size)), Some(Value::I32(value)), Some(Value::I32(dest))) = - (operand_stack.pop(), operand_stack.pop(), operand_stack.pop()) - { - #[cfg(feature = "tracing")] - trace!( - dest = format_args!("{:#x}", dest), - value = format_args!("{:#x}", value), - size = size, - mem_idx = mem_idx, - "[MemoryFill] Starting fill operation" - ); + // Per spec: n (size) = index type, val = i32, d (dest) = index type + let size = pop_memory_operand(&mut operand_stack, is_memory64)?; + let value = match operand_stack.pop() { + Some(Value::I32(v)) => v, + _ => return Err(kiln_error::Error::runtime_trap("type mismatch")), + }; + let dest = pop_memory_operand(&mut operand_stack, is_memory64)?; - // Per WebAssembly spec: bounds check MUST happen before checking size==0 - // If size == 0 AND dest > memory.size: TRAP - // If size > 0 AND (dest + size) > memory.size: TRAP - let memory_wrapper = instance.memory(mem_idx)?; - let memory = &memory_wrapper.0; - let memory_size = memory.size_in_bytes() as u32; - let dest_u32 = dest as u32; - let size_u32 = size as u32; + #[cfg(feature = "tracing")] + trace!( + dest = format_args!("{:#x}", dest), + value = format_args!("{:#x}", value), + size = size, + mem_idx = mem_idx, + "[MemoryFill] Starting fill operation" + ); - if size_u32 == 0 { - // For size 0, check if offset is within bounds (can be equal to size) - if dest_u32 > memory_size { - return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); - } - // No-op for zero size fill after bounds check passes - continue; - } + // Per WebAssembly spec: bounds check MUST happen before checking size==0 + let memory_wrapper = instance.memory(mem_idx)?; + let memory = &memory_wrapper.0; + let memory_size = memory.size_in_bytes() as u64; + if size == 0 { + // For size 0, check if offset is within bounds (can be equal to size) + if dest > memory_size { + return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); + } + // No-op for zero size fill after bounds check passes + } else { // For size > 0, check if (offset + size) overflows or exceeds memory size - let dest_end = dest_u32.checked_add(size_u32) + let dest_end = dest.checked_add(size) .ok_or_else(|| kiln_error::Error::runtime_trap("out of bounds memory access"))?; if dest_end > memory_size { return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); } - let size_usize = size_u32 as usize; + let size_usize = size as usize; let fill_byte = (value & 0xFF) as u8; // Create buffer filled with the value let buffer = vec![fill_byte; size_usize]; // Write to destination using write_shared (thread-safe) + let dest_u32 = dest as u32; if let Err(e) = memory.write_shared(dest_u32, &buffer) { #[cfg(feature = "tracing")] trace!("MemoryFill: write failed: {:?}", e); @@ -6080,63 +6370,67 @@ impl StacklessEngine { fill_byte = format_args!("{:#x}", fill_byte), "[MemoryFill] SUCCESS" ); - } else { - #[cfg(feature = "tracing")] - trace!("MemoryFill: insufficient values on stack"); } } Instruction::MemoryInit(data_idx, mem_idx) => { - // Pop n (length), s (source offset in data), d (dest offset in memory) - if let (Some(Value::I32(n)), Some(Value::I32(s)), Some(Value::I32(d))) = - (operand_stack.pop(), operand_stack.pop(), operand_stack.pop()) - { - #[cfg(feature = "tracing")] - trace!( - dest = format_args!("{:#x}", d), - src = format_args!("{:#x}", s), - len = n, - data_idx = data_idx, - mem_idx = mem_idx, - "[MemoryInit] Starting memory init operation" - ); - - // Check if this data segment has been dropped - // Per WebAssembly spec, a dropped segment behaves as if it has zero length - let is_dropped = self.dropped_data_segments - .get(&instance_id) - .and_then(|v| v.get(data_idx as usize)) - .copied() - .unwrap_or(false); + // Determine memory64 status for destination memory + let is_memory64 = instance.memory(mem_idx) + .map(|mw| mw.0.ty.memory64).unwrap_or(false); - // Get data segment from module (for length calculation) - let data_segment = module.data.get(data_idx as usize) - .ok_or_else(|| kiln_error::Error::runtime_trap("out of bounds memory access"))?; - - // If dropped, treat as zero-length segment - let data_len = if is_dropped { 0u32 } else { data_segment.init.len() as u32 }; - let s_u32 = s as u32; - let d_u32 = d as u32; - let n_u32 = n as u32; + // Pop n (length), s (source offset in data), d (dest offset in memory) + // Per spec: n = i32, s = i32, d = index type of memory + let n = match operand_stack.pop() { + Some(Value::I32(v)) => v as u32, + _ => return Err(kiln_error::Error::runtime_trap("type mismatch")), + }; + let s = match operand_stack.pop() { + Some(Value::I32(v)) => v as u32, + _ => return Err(kiln_error::Error::runtime_trap("type mismatch")), + }; + let d = pop_memory_operand(&mut operand_stack, is_memory64)?; - // Per WebAssembly spec: bounds check MUST happen before checking n==0 - // Get memory for bounds checking - let memory_wrapper = instance.memory(mem_idx)?; - let memory = &memory_wrapper.0; - let memory_size = memory.size_in_bytes() as u32; + #[cfg(feature = "tracing")] + trace!( + dest = format_args!("{:#x}", d), + src = format_args!("{:#x}", s), + len = n, + data_idx = data_idx, + mem_idx = mem_idx, + "[MemoryInit] Starting memory init operation" + ); - if n_u32 == 0 { - // For n == 0, check if offsets are within bounds (can be equal to size) - if s_u32 > data_len || d_u32 > memory_size { - return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); - } - // No-op for zero size init after bounds check passes - continue; + // Check if this data segment has been dropped + // Per WebAssembly spec, a dropped segment behaves as if it has zero length + let is_dropped = self.dropped_data_segments + .get(&instance_id) + .and_then(|v| v.get(data_idx as usize)) + .copied() + .unwrap_or(false); + + // Get data segment from module (for length calculation) + let data_segment = module.data.get(data_idx as usize) + .ok_or_else(|| kiln_error::Error::runtime_trap("out of bounds memory access"))?; + + // If dropped, treat as zero-length segment + let data_len = if is_dropped { 0u32 } else { data_segment.init.len() as u32 }; + + // Per WebAssembly spec: bounds check MUST happen before checking n==0 + // Get memory for bounds checking + let memory_wrapper = instance.memory(mem_idx)?; + let memory = &memory_wrapper.0; + let memory_size = memory.size_in_bytes() as u64; + + if n == 0 { + // For n == 0, check if offsets are within bounds (can be equal to size) + if s > data_len || d > memory_size { + return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); } - + // No-op for zero size init after bounds check passes + } else { // For n > 0, check if (offset + n) overflows or exceeds bounds - let src_end = s_u32.checked_add(n_u32) + let src_end = s.checked_add(n) .ok_or_else(|| kiln_error::Error::runtime_trap("out of bounds memory access"))?; - let dest_end = d_u32.checked_add(n_u32) + let dest_end = d.checked_add(n as u64) .ok_or_else(|| kiln_error::Error::runtime_trap("out of bounds memory access"))?; if src_end > data_len { @@ -6149,7 +6443,8 @@ impl StacklessEngine { // Copy data from segment to memory #[cfg(any(feature = "std", feature = "alloc"))] { - let src_slice = &data_segment.init[s_u32 as usize..src_end as usize]; + let src_slice = &data_segment.init[s as usize..src_end as usize]; + let d_u32 = d as u32; if let Err(e) = memory.write_shared(d_u32, src_slice) { #[cfg(feature = "tracing")] trace!("MemoryInit: write failed: {:?}", e); @@ -6164,9 +6459,6 @@ impl StacklessEngine { len = n, "[MemoryInit] SUCCESS" ); - } else { - #[cfg(feature = "tracing")] - trace!("MemoryInit: insufficient values on stack"); } } Instruction::DataDrop(data_idx) => { @@ -6250,6 +6542,8 @@ impl StacklessEngine { 0x40 => 0, // empty type - no params 0x7F | 0x7E | 0x7D | 0x7C | 0x7B => 0, // inline value types: 0 params 0x70 | 0x6F => 0, // funcref, externref: 0 params + // GC reference types: single value type, 0 params + 0x6E | 0x6D | 0x6C | 0x6B | 0x6A | 0x73 | 0x72 | 0x71 | 0x69 => 0, _ => { // Type index - look up actual param count from module types if let Some(func_type) = module.types.get(block_type_idx as usize) { @@ -6264,12 +6558,16 @@ impl StacklessEngine { 0x40 => 0, // empty type - no return 0x7F | 0x7E | 0x7D | 0x7C | 0x7B => 1, // i32, i64, f32, f64, v128 0x70 | 0x6F => 1, // funcref, externref + // GC reference types: single value type, 1 result + 0x6E | 0x6D | 0x6C | 0x6B | 0x6A | 0x73 | 0x72 | 0x71 | 0x69 => 1, _ => { // Type index - look up actual result count if let Some(func_type) = module.types.get(block_type_idx as usize) { func_type.results.len() } else { - 1 + return Err(kiln_error::Error::runtime_error( + "BrTable: block type index not found in module types" + )); } } } @@ -6385,21 +6683,37 @@ impl StacklessEngine { } Instruction::Return => { #[cfg(feature = "tracing")] - trace!("🔙 Return at pc={}", pc); + trace!("Return at pc={}", pc); #[cfg(feature = "tracing")] trace!(" Operand stack size: {}", operand_stack.len()); #[cfg(feature = "tracing")] trace!(" Instructions executed: {}", instruction_count); - // Trace return from specific functions for debugging - #[cfg(feature = "tracing")] - if func_idx == 76 { - trace!( - func_idx = func_idx, - pc = pc, - stack_size = operand_stack.len(), - "[RETURN] Function returning" - ); + + // Determine how many values to preserve based on function's return type + let values_to_preserve = if let Some(func_type) = module.types.get(func.type_idx as usize) { + func_type.results.len() + } else { + return Err(kiln_error::Error::runtime_error( + "Return: function type not found in module types" + )); + }; + + // Save the return values from top of stack + let mut preserved_values = Vec::new(); + for _ in 0..values_to_preserve { + if let Some(v) = operand_stack.pop() { + preserved_values.push(v); + } + } + + // Clear the rest of the stack + operand_stack.clear(); + + // Restore preserved values (in reverse order) + for v in preserved_values.into_iter().rev() { + operand_stack.push(v); } + break; // Exit function } Instruction::End => { @@ -6423,23 +6737,56 @@ impl StacklessEngine { break; // Exit function } else { // This ends a block/loop/if - continue execution + // Per WebAssembly spec: save result values, pop stack to + // entry height, push result values back. if !block_stack.is_empty() { - let (block_type, start_pc, _, _) = block_stack.pop().unwrap(); - #[cfg(feature = "tracing")] - trace!("End at pc={} (closes {} from pc={}, depth now {})", pc, block_type, start_pc, block_depth); - // Trace End in specific functions for debugging + let (block_type, start_pc, block_type_idx, entry_stack_height) = block_stack.pop().unwrap(); #[cfg(feature = "tracing")] - if func_idx == 76 || func_idx == 222 { - trace!( - func_idx = func_idx, - pc = pc, - block_type = %block_type, - start_pc = start_pc, - block_depth = block_depth, - block_stack_len = block_stack.len(), - "[END] Block closing" - ); + trace!("End at pc={} (closes {} from pc={}, depth now {}, entry_height={})", pc, block_type, start_pc, block_depth, entry_stack_height); + + // Determine number of result values for this block + let result_count = match block_type_idx { + 0x40 => 0, // empty type - no results + 0x7F | 0x7E | 0x7D | 0x7C | 0x7B => 1, // i32, i64, f32, f64, v128 + 0x70 | 0x6F => 1, // funcref, externref + // GC abstract reference types (all produce 1 result) + 0x6E | 0x6D | 0x6C | 0x6B | 0x6A | 0x69 => 1, // anyref, eqref, i31ref, structref, arrayref, exnref + 0x73 | 0x72 | 0x71 | 0x74 => 1, // nofunc, noextern, none, noexn + _ => { + // Type index - look up actual result count + if let Some(ft) = module.types.get(block_type_idx as usize) { + ft.results.len() + } else if (block_type_idx as i32) < 0 { + // Negative values might be abstract types encoded differently + 0 + } else { + // Positive type index that isn't in types table + // Could be a GC type - assume 1 result + 1 + } + } + }; + + // Save result values from top of stack + let mut result_values = Vec::new(); + for _ in 0..result_count { + if let Some(v) = operand_stack.pop() { + result_values.push(v); + } + } + + // Pop stack back to entry height (removes intermediate values) + while operand_stack.len() > entry_stack_height { + let _ = operand_stack.pop(); + } + + // Push result values back (in original order) + for v in result_values.into_iter().rev() { + operand_stack.push(v); } + + #[cfg(feature = "tracing")] + trace!("End: stack after cleanup: len={}", operand_stack.len()); } else { #[cfg(feature = "tracing")] trace!("End at pc={} (closes block, depth now {})", pc, block_depth); @@ -6458,14 +6805,17 @@ impl StacklessEngine { let null_value = match value_type { // Standard reference types ValueType::FuncRef => Value::FuncRef(None), + ValueType::NullFuncRef => Value::FuncRef(None), ValueType::ExternRef => Value::ExternRef(None), - // GC abstract heap types (using their Value representations) - ValueType::AnyRef => Value::ExternRef(None), // anyref uses externref repr - ValueType::EqRef => Value::I31Ref(None), // eqref uses i31ref repr + // GC abstract heap types + ValueType::AnyRef => Value::I31Ref(None), + ValueType::EqRef => Value::I31Ref(None), ValueType::I31Ref => Value::I31Ref(None), ValueType::StructRef(_) => Value::StructRef(None), ValueType::ArrayRef(_) => Value::ArrayRef(None), ValueType::ExnRef => Value::ExnRef(None), + // Typed func ref: null typed function reference + ValueType::TypedFuncRef(_, _) => Value::FuncRef(None), // Non-reference types shouldn't reach here, default to externref _ => Value::ExternRef(None), }; @@ -6485,6 +6835,15 @@ impl StacklessEngine { Value::FuncRef(Some(_)) => 0i32, Value::ExternRef(None) => 1i32, Value::ExternRef(Some(_)) => 0i32, + // GC reference types + Value::I31Ref(None) => 1i32, + Value::I31Ref(Some(_)) => 0i32, + Value::StructRef(None) => 1i32, + Value::StructRef(Some(_)) => 0i32, + Value::ArrayRef(None) => 1i32, + Value::ArrayRef(Some(_)) => 0i32, + Value::ExnRef(None) => 1i32, + Value::ExnRef(Some(_)) => 0i32, _ => { #[cfg(feature = "tracing")] error!("RefIsNull: expected reference type, got {:?}", ref_val); @@ -6502,14 +6861,18 @@ impl StacklessEngine { // Pop reference, trap if null, push back if not null if let Some(ref_val) = operand_stack.pop() { match &ref_val { - Value::FuncRef(None) | Value::ExternRef(None) => { + Value::FuncRef(None) | Value::ExternRef(None) + | Value::I31Ref(None) | Value::StructRef(None) + | Value::ArrayRef(None) | Value::ExnRef(None) => { #[cfg(feature = "tracing")] error!("RefAsNonNull: null reference"); return Err(kiln_error::Error::runtime_trap( "null reference in ref.as_non_null", )); } - Value::FuncRef(Some(_)) | Value::ExternRef(Some(_)) => { + Value::FuncRef(Some(_)) | Value::ExternRef(Some(_)) + | Value::I31Ref(Some(_)) | Value::StructRef(Some(_)) + | Value::ArrayRef(Some(_)) | Value::ExnRef(Some(_)) => { #[cfg(feature = "tracing")] trace!("RefAsNonNull: non-null reference"); operand_stack.push(ref_val); @@ -6525,22 +6888,37 @@ impl StacklessEngine { } } Instruction::RefEq => { - // Pop two references, push 1 if equal, 0 if not + // Pop two eqref values, push 1 if equal, 0 if not + // Per spec: ref.eq compares two eqref values for identity if let (Some(ref2), Some(ref1)) = (operand_stack.pop(), operand_stack.pop()) { - let result = match (&ref1, &ref2) { - // Two null funcref/externref are equal - (Value::FuncRef(None), Value::FuncRef(None)) => 1i32, - (Value::ExternRef(None), Value::ExternRef(None)) => 1i32, - // Two non-null funcrefs are equal if they reference the same function - (Value::FuncRef(Some(f1)), Value::FuncRef(Some(f2))) => { - if f1.index == f2.index { 1i32 } else { 0i32 } - } - // Two non-null externrefs are equal if they're the same object - (Value::ExternRef(Some(e1)), Value::ExternRef(Some(e2))) => { - if e1 == e2 { 1i32 } else { 0i32 } - } - // Different types or null vs non-null are not equal - _ => 0i32, + // Helper: check if a value is a null reference + let is_null = |v: &Value| -> bool { + matches!(v, + Value::FuncRef(None) | Value::ExternRef(None) + | Value::I31Ref(None) | Value::StructRef(None) + | Value::ArrayRef(None) | Value::ExnRef(None) + ) + }; + let result = if is_null(&ref1) && is_null(&ref2) { + // Two null references are equal regardless of type + 1i32 + } else { + match (&ref1, &ref2) { + // i31ref equality: equal if they contain the same value + (Value::I31Ref(Some(a)), Value::I31Ref(Some(b))) => { + if a == b { 1i32 } else { 0i32 } + } + // Struct references: equal only if same allocation (alloc_id identity) + (Value::StructRef(Some(s1)), Value::StructRef(Some(s2))) => { + if s1.alloc_id == s2.alloc_id { 1i32 } else { 0i32 } + } + // Array references: equal only if same allocation (alloc_id identity) + (Value::ArrayRef(Some(a1)), Value::ArrayRef(Some(a2))) => { + if a1.alloc_id == a2.alloc_id { 1i32 } else { 0i32 } + } + // Different types or null vs non-null are not equal + _ => 0i32, + } }; #[cfg(feature = "tracing")] trace!("RefEq: {:?} == {:?} => {}", ref1, ref2, result); @@ -6550,7 +6928,7 @@ impl StacklessEngine { Instruction::BrOnNull(br_label_idx) => { // Pop reference, branch if null, push back if not null if let Some(ref_val) = operand_stack.pop() { - let is_null = matches!(&ref_val, Value::FuncRef(None) | Value::ExternRef(None)); + let is_null = is_null_ref(&ref_val); #[cfg(feature = "tracing")] trace!("BrOnNull: label={}, is_null={}", br_label_idx, is_null); if is_null { @@ -6605,27 +6983,88 @@ impl StacklessEngine { } } + Instruction::BrOnNonNull(br_label_idx) => { + // Pop reference, branch if NOT null (pushing ref on branch path), + // consume if null + if let Some(ref_val) = operand_stack.pop() { + let is_null = is_null_ref(&ref_val); + #[cfg(feature = "tracing")] + trace!("BrOnNonNull: label={}, is_null={}", br_label_idx, is_null); + if !is_null { + // Not null - push reference and branch + operand_stack.push(ref_val); + if br_label_idx as usize >= block_stack.len() { + #[cfg(feature = "tracing")] + trace!("BrOnNonNull: branching out of function"); + break; + } + let target_depth = block_stack.len() - 1 - br_label_idx as usize; + if let Some((block_type, start_pc, _block_type_idx, entry_stack_height)) = block_stack.get(target_depth).copied() { + if block_type == "loop" { + pc = start_pc; + } else { + // Skip to end of block + let mut depth = 1; + let mut search_pc = pc + 1; + while depth > 0 && search_pc < instructions.len() { + #[cfg(feature = "std")] + if let Some(search_instr) = instructions.get(search_pc) { + match search_instr { + Instruction::Block { .. } | Instruction::Loop { .. } | Instruction::If { .. } | Instruction::Try { .. } | Instruction::TryTable { .. } => depth += 1, + Instruction::End => depth -= 1, + _ => {} + } + } + #[cfg(not(feature = "std"))] + if let Ok(search_instr) = instructions.get(search_pc) { + match search_instr { + Instruction::Block { .. } | Instruction::Loop { .. } | Instruction::If { .. } | Instruction::Try { .. } | Instruction::TryTable { .. } => depth += 1, + Instruction::End => depth -= 1, + _ => {} + } + } + if depth > 0 { search_pc += 1; } + } + pc = search_pc; + } + // Keep branch value, restore stack below + let branch_val = operand_stack.pop(); + while operand_stack.len() > entry_stack_height { + operand_stack.pop(); + } + if let Some(bv) = branch_val { + operand_stack.push(bv); + } + } + continue; + } + // Null - consume the reference (don't push back) + } + } + // ==================== TABLE OPERATIONS ==================== Instruction::TableGet(table_idx) => { - // table.get: [i32] -> [ref] - // Pop index from stack, get element from table at that index - if let Some(Value::I32(elem_idx)) = operand_stack.pop() { + // table.get: [it] -> [ref] (it = i64 if table64, i32 otherwise) + // Get the table first to check table64 flag + let table = instance.table(table_idx)?; + let is_t64 = table.is_table64(); + let elem_idx_u64 = pop_table_operand(&mut operand_stack, is_t64, "table.get: type mismatch")?; + + { #[cfg(feature = "tracing")] trace!( table_idx = table_idx, - elem_idx = elem_idx, + elem_idx = elem_idx_u64, "[TableGet] Getting element from table" ); - if elem_idx < 0 { + if elem_idx_u64 > u32::MAX as u64 { return Err(kiln_error::Error::runtime_trap( - "table.get: index cannot be negative", + "out of bounds table access", )); } - // Get the table from the instance - let table = instance.table(table_idx)?; - let elem = table.get(elem_idx as u32)?; + let elem = table.get(elem_idx_u64 as u32)?; // Push the element (or null ref) onto the stack let value = match elem { @@ -6635,7 +7074,17 @@ impl StacklessEngine { match table.element_type() { kiln_foundation::types::RefType::Funcref => Value::FuncRef(None), kiln_foundation::types::RefType::Externref => Value::ExternRef(None), - kiln_foundation::types::RefType::Gc(_) => Value::ExternRef(None), + kiln_foundation::types::RefType::Gc(gc) => { + match gc.heap_type { + kiln_foundation::types::HeapType::I31 => Value::I31Ref(None), + kiln_foundation::types::HeapType::Struct | kiln_foundation::types::HeapType::Concrete(_) => Value::StructRef(None), + kiln_foundation::types::HeapType::Array => Value::ArrayRef(None), + kiln_foundation::types::HeapType::Func | kiln_foundation::types::HeapType::NoFunc => Value::FuncRef(None), + kiln_foundation::types::HeapType::Extern | kiln_foundation::types::HeapType::NoExtern => Value::ExternRef(None), + // eq, any, none, exn: use I31Ref(None) as canonical null for eqref hierarchy + _ => Value::I31Ref(None), + } + } } } }; @@ -6644,72 +7093,62 @@ impl StacklessEngine { #[cfg(feature = "tracing")] trace!( table_idx = table_idx, - elem_idx = elem_idx, + elem_idx = elem_idx_u64, "[TableGet] SUCCESS" ); - } else { - return Err(kiln_error::Error::runtime_trap( - "table.get: expected i32 index on stack", - )); } } Instruction::TableSet(table_idx) => { - // table.set: [i32 ref] -> [] - // Pop value, then index from stack; set element in table + // table.set: [it ref] -> [] (it = i64 if table64, i32 otherwise) let value = operand_stack.pop().ok_or_else(|| { kiln_error::Error::runtime_trap("table.set: expected value on stack") })?; - let idx = operand_stack.pop().ok_or_else(|| { - kiln_error::Error::runtime_trap("table.set: expected index on stack") - })?; - if let Value::I32(elem_idx) = idx { - #[cfg(feature = "tracing")] - trace!( - table_idx = table_idx, - elem_idx = elem_idx, - value = ?value, - "[TableSet] Setting element in table" - ); + let table = instance.table(table_idx)?; + let is_t64 = table.is_table64(); + let elem_idx_u64 = pop_table_operand(&mut operand_stack, is_t64, "table.set: type mismatch")?; + + #[cfg(feature = "tracing")] + trace!( + table_idx = table_idx, + elem_idx = elem_idx_u64, + value = ?value, + "[TableSet] Setting element in table" + ); + + if elem_idx_u64 > u32::MAX as u64 { + return Err(kiln_error::Error::runtime_trap( + "out of bounds table access", + )); + } - if elem_idx < 0 { + // Validate value is a reference type + let table_value = match &value { + Value::FuncRef(fr) => Some(Value::FuncRef(fr.clone())), + Value::ExternRef(er) => Some(Value::ExternRef(er.clone())), + Value::I31Ref(_) => Some(value.clone()), + Value::StructRef(_) => Some(value.clone()), + Value::ArrayRef(_) => Some(value.clone()), + _ => { return Err(kiln_error::Error::runtime_trap( - "table.set: index cannot be negative", + "table.set: value must be a reference type", )); } + }; - // Validate value is a reference type - let table_value = match &value { - Value::FuncRef(fr) => Some(Value::FuncRef(fr.clone())), - Value::ExternRef(er) => Some(Value::ExternRef(er.clone())), - _ => { - return Err(kiln_error::Error::runtime_trap( - "table.set: value must be a reference type", - )); - } - }; - - // Get the table and set the element - let table = instance.table(table_idx)?; - table.set(elem_idx as u32, table_value)?; + table.set(elem_idx_u64 as u32, table_value)?; - #[cfg(feature = "tracing")] - trace!( - table_idx = table_idx, - elem_idx = elem_idx, - "[TableSet] SUCCESS" - ); - } else { - return Err(kiln_error::Error::runtime_trap( - "table.set: expected i32 index", - )); - } + #[cfg(feature = "tracing")] + trace!( + table_idx = table_idx, + elem_idx = elem_idx_u64, + "[TableSet] SUCCESS" + ); } Instruction::TableSize(table_idx) => { - // table.size: [] -> [i32] - // Push current table size onto stack + // table.size: [] -> [it] (it = i64 if table64, i32 otherwise) #[cfg(feature = "tracing")] trace!( table_idx = table_idx, @@ -6717,8 +7156,9 @@ impl StacklessEngine { ); let table = instance.table(table_idx)?; + let is_t64 = table.is_table64(); let size = table.size(); - operand_stack.push(Value::I32(size as i32)); + push_table_result(&mut operand_stack, size as u64, is_t64); #[cfg(feature = "tracing")] trace!( @@ -6729,325 +7169,296 @@ impl StacklessEngine { } Instruction::TableGrow(table_idx) => { - // table.grow: [ref i32] -> [i32] - // Pop delta (i32), pop init value (ref), grow table, push old size or -1 - let delta = operand_stack.pop().ok_or_else(|| { - kiln_error::Error::runtime_trap("table.grow: expected delta on stack") - })?; + // table.grow: [ref it] -> [it] (it = i64 if table64, i32 otherwise) + let table = instance.table(table_idx)?; + let is_t64 = table.is_table64(); + let delta_u64 = pop_table_operand(&mut operand_stack, is_t64, "table.grow: type mismatch")?; let init_value = operand_stack.pop().ok_or_else(|| { kiln_error::Error::runtime_trap("table.grow: expected init value on stack") })?; - if let Value::I32(delta_val) = delta { - #[cfg(feature = "tracing")] - trace!( - table_idx = table_idx, - delta = delta_val, - init_value = ?init_value, - "[TableGrow] Growing table" - ); + #[cfg(feature = "tracing")] + trace!( + table_idx = table_idx, + delta = delta_u64, + init_value = ?init_value, + "[TableGrow] Growing table" + ); - // Negative delta should return -1 (failure) - if delta_val < 0 { - operand_stack.push(Value::I32(-1)); - } else { - // Validate init value is a reference type - match &init_value { - Value::FuncRef(_) | Value::ExternRef(_) => {} - _ => { - return Err(kiln_error::Error::runtime_trap( - "table.grow: init value must be a reference type", - )); - } - } + // Validate init value is a reference type + match &init_value { + Value::FuncRef(_) | Value::ExternRef(_) + | Value::I31Ref(_) | Value::StructRef(_) | Value::ArrayRef(_) => {} + _ => { + return Err(kiln_error::Error::runtime_trap( + "table.grow: init value must be a reference type", + )); + } + } - let table = instance.table(table_idx)?; - match table.grow(delta_val as u32, init_value) { - Ok(old_size) => { - operand_stack.push(Value::I32(old_size as i32)); - #[cfg(feature = "tracing")] - trace!( - table_idx = table_idx, - old_size = old_size, - "[TableGrow] SUCCESS" - ); - } - Err(_) => { - // Growth failed (e.g., exceeded max size) - operand_stack.push(Value::I32(-1)); - #[cfg(feature = "tracing")] - trace!( - table_idx = table_idx, - "[TableGrow] Failed, returning -1" - ); - } + if delta_u64 > u32::MAX as u64 { + // Cannot grow beyond u32 range, return failure (-1) + push_table_result(&mut operand_stack, u64::MAX, is_t64); + } else { + match table.grow(delta_u64 as u32, init_value) { + Ok(old_size) => { + push_table_result(&mut operand_stack, old_size as u64, is_t64); + #[cfg(feature = "tracing")] + trace!( + table_idx = table_idx, + old_size = old_size, + "[TableGrow] SUCCESS" + ); + } + Err(_) => { + // Growth failed (e.g., exceeded max size) + push_table_result(&mut operand_stack, u64::MAX, is_t64); + #[cfg(feature = "tracing")] + trace!( + table_idx = table_idx, + "[TableGrow] Failed, returning -1" + ); } } - } else { - return Err(kiln_error::Error::runtime_trap( - "table.grow: expected i32 delta", - )); } } Instruction::TableFill(table_idx) => { - // table.fill: [i32 ref i32] -> [] - // Pop size, value, dest; fill table region with value - let size = operand_stack.pop().ok_or_else(|| { - kiln_error::Error::runtime_trap("table.fill: expected size on stack") - })?; + // table.fill: [it ref it] -> [] (it = i64 if table64, i32 otherwise) + let table = instance.table(table_idx)?; + let is_t64 = table.is_table64(); + let fill_size_u64 = pop_table_operand(&mut operand_stack, is_t64, "table.fill: type mismatch")?; let value = operand_stack.pop().ok_or_else(|| { kiln_error::Error::runtime_trap("table.fill: expected value on stack") })?; - let dest = operand_stack.pop().ok_or_else(|| { - kiln_error::Error::runtime_trap("table.fill: expected dest on stack") - })?; - - if let (Value::I32(dest_idx), Value::I32(fill_size)) = (&dest, &size) { - #[cfg(feature = "tracing")] - trace!( - table_idx = table_idx, - dest = dest_idx, - size = fill_size, - value = ?value, - "[TableFill] Filling table region" - ); - - if *dest_idx < 0 || *fill_size < 0 { - return Err(kiln_error::Error::runtime_trap( - "table.fill: negative dest or size", - )); - } - - // Validate value is a reference type - let fill_value = match &value { - Value::FuncRef(fr) => Some(Value::FuncRef(fr.clone())), - Value::ExternRef(er) => Some(Value::ExternRef(er.clone())), - _ => { - return Err(kiln_error::Error::runtime_trap( - "table.fill: value must be a reference type", - )); - } - }; + let dest_u64 = pop_table_operand(&mut operand_stack, is_t64, "table.fill: type mismatch")?; - let table = instance.table(table_idx)?; - table.fill(*dest_idx as u32, *fill_size as u32, fill_value)?; + #[cfg(feature = "tracing")] + trace!( + table_idx = table_idx, + dest = dest_u64, + size = fill_size_u64, + value = ?value, + "[TableFill] Filling table region" + ); - #[cfg(feature = "tracing")] - trace!( - table_idx = table_idx, - dest = dest_idx, - size = fill_size, - "[TableFill] SUCCESS" - ); - } else { + if dest_u64 > u32::MAX as u64 || fill_size_u64 > u32::MAX as u64 { return Err(kiln_error::Error::runtime_trap( - "table.fill: expected i32 values for dest and size", + "out of bounds table access", )); } - } - - Instruction::TableCopy(dst_table_idx, src_table_idx) => { - // table.copy: [i32 i32 i32] -> [] - // Pop size, src_offset, dst_offset; copy elements between tables - let size = operand_stack.pop().ok_or_else(|| { - kiln_error::Error::runtime_trap("table.copy: expected size on stack") - })?; - let src_offset = operand_stack.pop().ok_or_else(|| { - kiln_error::Error::runtime_trap("table.copy: expected src offset on stack") - })?; - let dst_offset = operand_stack.pop().ok_or_else(|| { - kiln_error::Error::runtime_trap("table.copy: expected dst offset on stack") - })?; - - if let (Value::I32(dst_idx), Value::I32(src_idx), Value::I32(copy_size)) = - (&dst_offset, &src_offset, &size) - { - #[cfg(feature = "tracing")] - trace!( - dst_table = dst_table_idx, - src_table = src_table_idx, - dst_offset = dst_idx, - src_offset = src_idx, - size = copy_size, - "[TableCopy] Copying table elements" - ); - if *dst_idx < 0 || *src_idx < 0 || *copy_size < 0 { + // Validate value is a reference type + let fill_value = match &value { + Value::FuncRef(fr) => Some(Value::FuncRef(fr.clone())), + Value::ExternRef(er) => Some(Value::ExternRef(er.clone())), + Value::I31Ref(_) => Some(value.clone()), + Value::StructRef(_) => Some(value.clone()), + Value::ArrayRef(_) => Some(value.clone()), + _ => { return Err(kiln_error::Error::runtime_trap( - "out of bounds table access", + "table.fill: value must be a reference type", )); } + }; - // Handle same-table and cross-table copy - if dst_table_idx == src_table_idx { - // Same table copy - use the table's copy method - let table = instance.table(dst_table_idx)?; - table.copy(*dst_idx as u32, *src_idx as u32, *copy_size as u32)?; - } else { - // Cross-table copy - read from src, write to dst - let src_table = instance.table(src_table_idx)?; - let dst_table = instance.table(dst_table_idx)?; - - // Bounds check BEFORE zero-length check (per WebAssembly spec) - let src_end = (*src_idx as u32).checked_add(*copy_size as u32) - .ok_or_else(|| kiln_error::Error::runtime_trap( - "out of bounds table access" - ))?; - let dst_end = (*dst_idx as u32).checked_add(*copy_size as u32) - .ok_or_else(|| kiln_error::Error::runtime_trap( - "out of bounds table access" - ))?; - if src_end > src_table.size() || dst_end > dst_table.size() { - return Err(kiln_error::Error::runtime_trap( - "out of bounds table access" - )); - } + table.fill(dest_u64 as u32, fill_size_u64 as u32, fill_value)?; - // Zero-length copy is a no-op (after bounds check) - if *copy_size == 0 { - // Continue to next instruction - } else { - // Read all source elements first (to handle any overlap scenarios) - let mut temp_elements = Vec::new(); - for i in 0..*copy_size as u32 { - let elem = src_table.get(*src_idx as u32 + i)?; - temp_elements.push(elem); - } + #[cfg(feature = "tracing")] + trace!( + table_idx = table_idx, + dest = dest_u64, + size = fill_size_u64, + "[TableFill] SUCCESS" + ); + } - // Write to destination table - for (i, elem) in temp_elements.into_iter().enumerate() { - dst_table.set(*dst_idx as u32 + i as u32, elem)?; - } - } - } + Instruction::TableCopy(dst_table_idx, src_table_idx) => { + // table.copy: [it_d it_s it_n] -> [] + // dst uses dst table's index type, src uses src table's index type + // length is i64 if either table is table64 + let dst_table_w = instance.table(dst_table_idx)?; + let src_table_w = instance.table(src_table_idx)?; + let dst64 = dst_table_w.is_table64(); + let src64 = src_table_w.is_table64(); + let len64 = dst64 || src64; + + let copy_size_u64 = pop_table_operand(&mut operand_stack, len64, "table.copy: type mismatch")?; + let src_idx_u64 = pop_table_operand(&mut operand_stack, src64, "table.copy: type mismatch")?; + let dst_idx_u64 = pop_table_operand(&mut operand_stack, dst64, "table.copy: type mismatch")?; - #[cfg(feature = "tracing")] - trace!( - dst_table = dst_table_idx, - src_table = src_table_idx, - "[TableCopy] SUCCESS" - ); - } else { + #[cfg(feature = "tracing")] + trace!( + dst_table = dst_table_idx, + src_table = src_table_idx, + dst_offset = dst_idx_u64, + src_offset = src_idx_u64, + size = copy_size_u64, + "[TableCopy] Copying table elements" + ); + + if dst_idx_u64 > u32::MAX as u64 || src_idx_u64 > u32::MAX as u64 || copy_size_u64 > u32::MAX as u64 { return Err(kiln_error::Error::runtime_trap( - "table.copy: expected i32 values for offsets and size", + "out of bounds table access", )); } - } - - Instruction::TableInit(elem_seg_idx, table_idx) => { - // table.init: [i32 i32 i32] -> [] - // Pop size, src_offset (in elem segment), dst_offset (in table) - // Initialize table elements from element segment - let size = operand_stack.pop().ok_or_else(|| { - kiln_error::Error::runtime_trap("table.init: expected size on stack") - })?; - let src_offset = operand_stack.pop().ok_or_else(|| { - kiln_error::Error::runtime_trap("table.init: expected src offset on stack") - })?; - let dst_offset = operand_stack.pop().ok_or_else(|| { - kiln_error::Error::runtime_trap("table.init: expected dst offset on stack") - })?; - - if let (Value::I32(dst_idx), Value::I32(src_idx), Value::I32(init_size)) = - (&dst_offset, &src_offset, &size) - { - #[cfg(feature = "tracing")] - trace!( - elem_seg_idx = elem_seg_idx, - table_idx = table_idx, - dst_offset = dst_idx, - src_offset = src_idx, - size = init_size, - "[TableInit] Initializing table from element segment" - ); - - if *dst_idx < 0 || *src_idx < 0 || *init_size < 0 { - return Err(kiln_error::Error::runtime_trap( - "out of bounds table access", - )); - } - // Get the element segment from the module (needed for bounds check) - #[cfg(feature = "std")] - let elem_segment = module.elements.get(elem_seg_idx as usize) - .ok_or_else(|| kiln_error::Error::runtime_trap( - "table.init: invalid element segment index" - ))?; - #[cfg(not(feature = "std"))] - let elem_segment = module.elements.get(elem_seg_idx as usize) - .map_err(|_| kiln_error::Error::runtime_trap( - "table.init: invalid element segment index" - ))?; + let dst_idx = dst_idx_u64 as u32; + let src_idx = src_idx_u64 as u32; + let copy_size = copy_size_u64 as u32; - // Check if the element segment has been dropped - // Dropped segments have effective length 0 - let effective_elem_len = if instance.is_element_segment_dropped(elem_seg_idx) { - 0 - } else { - elem_segment.items.len() - }; + // Handle same-table and cross-table copy + if dst_table_idx == src_table_idx { + let table = instance.table(dst_table_idx)?; + table.copy(dst_idx, src_idx, copy_size)?; + } else { + let src_table = instance.table(src_table_idx)?; + let dst_table = instance.table(dst_table_idx)?; - // Check bounds in element segment (must happen BEFORE zero-size check per spec) - let src_end = (*src_idx as usize).checked_add(*init_size as usize) + // Bounds check BEFORE zero-length check (per WebAssembly spec) + let src_end = src_idx.checked_add(copy_size) .ok_or_else(|| kiln_error::Error::runtime_trap( "out of bounds table access" ))?; - if src_end > effective_elem_len { - return Err(kiln_error::Error::runtime_trap( - "out of bounds table access", - )); - } - - // Get table and check bounds (must happen BEFORE zero-size check per spec) - let table = instance.table(table_idx)?; - let dst_end = (*dst_idx as u32).checked_add(*init_size as u32) + let dst_end = dst_idx.checked_add(copy_size) .ok_or_else(|| kiln_error::Error::runtime_trap( "out of bounds table access" ))?; - if dst_end > table.size() { + if src_end > src_table.size() || dst_end > dst_table.size() { return Err(kiln_error::Error::runtime_trap( - "out of bounds table access", + "out of bounds table access" )); } - // Handle zero-size init (valid no-op) AFTER bounds checks - if *init_size == 0 { - #[cfg(feature = "tracing")] - trace!("[TableInit] Zero size, no-op"); - // Continue to next instruction + if copy_size == 0 { + // Zero-length copy is a no-op (after bounds check) } else { - // Copy elements from segment to table - for i in 0..*init_size as usize { - let item_idx = *src_idx as usize + i; - let func_idx = elem_segment.items.get(item_idx) - .map_err(|_| kiln_error::Error::runtime_trap( - "table.init: element segment access error" - ))?; - - // u32::MAX is sentinel for null reference - let value = if func_idx == u32::MAX { - Some(Value::FuncRef(None)) // null funcref - } else { - Some(Value::FuncRef(Some( - kiln_foundation::values::FuncRef::from_index(func_idx) - ))) - }; - table.set(*dst_idx as u32 + i as u32, value)?; + let mut temp_elements = Vec::new(); + for i in 0..copy_size { + let elem = src_table.get(src_idx + i)?; + temp_elements.push(elem); + } + for (i, elem) in temp_elements.into_iter().enumerate() { + dst_table.set(dst_idx + i as u32, elem)?; } - - #[cfg(feature = "tracing")] - trace!( - elem_seg_idx = elem_seg_idx, - table_idx = table_idx, - "[TableInit] SUCCESS" - ); } + } + + #[cfg(feature = "tracing")] + trace!( + dst_table = dst_table_idx, + src_table = src_table_idx, + "[TableCopy] SUCCESS" + ); + } + + Instruction::TableInit(elem_seg_idx, table_idx) => { + // table.init: [it i32 i32] -> [] + // dst offset uses table's index type (i64 if table64) + // src offset and length are always i32 (element segment indices) + let table = instance.table(table_idx)?; + let is_t64 = table.is_table64(); + + // Pop length (always i32) + let init_size = match operand_stack.pop() { + Some(Value::I32(v)) => v as u32, + _ => return Err(kiln_error::Error::runtime_trap("table.init: expected i32 length")), + }; + // Pop src offset (always i32 - indexes element segment) + let src_idx = match operand_stack.pop() { + Some(Value::I32(v)) => v as u32, + _ => return Err(kiln_error::Error::runtime_trap("table.init: expected i32 src offset")), + }; + // Pop dst offset (uses table's index type) + let dst_idx_u64 = pop_table_operand(&mut operand_stack, is_t64, "table.init: type mismatch")?; + + #[cfg(feature = "tracing")] + trace!( + elem_seg_idx = elem_seg_idx, + table_idx = table_idx, + dst_offset = dst_idx_u64, + src_offset = src_idx, + size = init_size, + "[TableInit] Initializing table from element segment" + ); + + if dst_idx_u64 > u32::MAX as u64 { + return Err(kiln_error::Error::runtime_trap( + "out of bounds table access", + )); + } + let dst_idx = dst_idx_u64 as u32; + + // Get the element segment from the module (needed for bounds check) + #[cfg(feature = "std")] + let elem_segment = module.elements.get(elem_seg_idx as usize) + .ok_or_else(|| kiln_error::Error::runtime_trap( + "table.init: invalid element segment index" + ))?; + #[cfg(not(feature = "std"))] + let elem_segment = module.elements.get(elem_seg_idx as usize) + .map_err(|_| kiln_error::Error::runtime_trap( + "table.init: invalid element segment index" + ))?; + + // Check if the element segment has been dropped + let effective_elem_len = if instance.is_element_segment_dropped(elem_seg_idx) { + 0 } else { + elem_segment.items.len() + }; + + // Check bounds in element segment (must happen BEFORE zero-size check per spec) + let src_end = (src_idx as usize).checked_add(init_size as usize) + .ok_or_else(|| kiln_error::Error::runtime_trap( + "out of bounds table access" + ))?; + if src_end > effective_elem_len { + return Err(kiln_error::Error::runtime_trap( + "out of bounds table access", + )); + } + + // Check table bounds (must happen BEFORE zero-size check per spec) + let dst_end = dst_idx.checked_add(init_size) + .ok_or_else(|| kiln_error::Error::runtime_trap( + "out of bounds table access" + ))?; + if dst_end > table.size() { return Err(kiln_error::Error::runtime_trap( - "table.init: expected i32 values for offsets and size", + "out of bounds table access", )); } + + // Handle zero-size init (valid no-op) AFTER bounds checks + if init_size == 0 { + #[cfg(feature = "tracing")] + trace!("[TableInit] Zero size, no-op"); + } else { + for i in 0..init_size as usize { + let item_idx = src_idx as usize + i; + let func_idx = elem_segment.items.get(item_idx) + .map_err(|_| kiln_error::Error::runtime_trap( + "table.init: element segment access error" + ))?; + + let value = if func_idx == u32::MAX { + Some(Value::FuncRef(None)) + } else { + Some(Value::FuncRef(Some( + kiln_foundation::values::FuncRef::from_index(func_idx) + ))) + }; + table.set(dst_idx + i as u32, value)?; + } + + #[cfg(feature = "tracing")] + trace!( + elem_seg_idx = elem_seg_idx, + table_idx = table_idx, + "[TableInit] SUCCESS" + ); + } } Instruction::ElemDrop(elem_seg_idx) => { @@ -7102,6 +7513,22 @@ impl StacklessEngine { if effective_addr % 4 != 0 { return Err(kiln_error::Error::runtime_trap("unaligned atomic access")); } + // Validate memory bounds (address + 4 bytes must be within memory) + match instance.memory(memarg.memory_index as u32) { + Ok(memory_wrapper) => { + let memory = &memory_wrapper.0; + let mut buffer = [0u8; 4]; + match memory.read(effective_addr, &mut buffer) { + Ok(()) => {} + Err(_) => { + return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); + } + } + } + Err(_) => { + return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); + } + } #[cfg(feature = "tracing")] trace!( addr = format_args!("0x{:x}", effective_addr), @@ -7109,7 +7536,6 @@ impl StacklessEngine { "[AtomicNotify] Notify operation" ); // For single-threaded runtime, notify always returns 0 (no waiters) - // A full implementation would use futex-like mechanisms operand_stack.push(Value::I32(0)); } } @@ -7117,7 +7543,7 @@ impl StacklessEngine { Instruction::MemoryAtomicWait32 { memarg } => { // memory.atomic.wait32: [i32, i32, i64] -> [i32] // Wait for i32 value at address to change, with timeout - if let (Some(Value::I64(timeout)), Some(Value::I32(expected)), Some(Value::I32(addr))) = + if let (Some(Value::I64(_timeout)), Some(Value::I32(expected)), Some(Value::I32(addr))) = (operand_stack.pop(), operand_stack.pop(), operand_stack.pop()) { let effective_addr = (addr as u32).wrapping_add(memarg.offset); @@ -7125,33 +7551,33 @@ impl StacklessEngine { if effective_addr % 4 != 0 { return Err(kiln_error::Error::runtime_trap("unaligned atomic access")); } - #[cfg(feature = "tracing")] - trace!( - addr = format_args!("0x{:x}", effective_addr), - expected = expected, - timeout = timeout, - "[AtomicWait32] Wait operation" - ); - // For single-threaded runtime, return 1 (not equal) or 2 (timed out) - // We'll read the current value and return immediately match instance.memory(memarg.memory_index as u32) { Ok(memory_wrapper) => { let memory = &memory_wrapper.0; + // Per the spec, memory.atomic.wait on non-shared memory traps + if !memory.ty.shared { + return Err(kiln_error::Error::runtime_trap("expected shared memory")); + } let mut buffer = [0u8; 4]; match memory.read(effective_addr, &mut buffer) { Ok(()) => { let current = i32::from_le_bytes(buffer); - // Return 1 if value differs, 2 if would timeout (single-threaded) - let result = if current != expected { 1 } else { 2 }; - operand_stack.push(Value::I32(result)); + if current != expected { + // Value differs from expected: return 1 (not-equal) + operand_stack.push(Value::I32(1)); + } else { + // In single-threaded runtime, no other thread will notify, + // so wait always times out: return 2 (timed-out) + operand_stack.push(Value::I32(2)); + } } Err(_) => { - return Err(kiln_error::Error::runtime_trap("Memory read out of bounds")); + return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); } } } Err(_) => { - return Err(kiln_error::Error::runtime_trap("Memory access error")); + return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); } } } @@ -7160,7 +7586,7 @@ impl StacklessEngine { Instruction::MemoryAtomicWait64 { memarg } => { // memory.atomic.wait64: [i32, i64, i64] -> [i32] // Wait for i64 value at address to change, with timeout - if let (Some(Value::I64(timeout)), Some(Value::I64(expected)), Some(Value::I32(addr))) = + if let (Some(Value::I64(_timeout)), Some(Value::I64(expected)), Some(Value::I32(addr))) = (operand_stack.pop(), operand_stack.pop(), operand_stack.pop()) { let effective_addr = (addr as u32).wrapping_add(memarg.offset); @@ -7168,30 +7594,33 @@ impl StacklessEngine { if effective_addr % 8 != 0 { return Err(kiln_error::Error::runtime_trap("unaligned atomic access")); } - #[cfg(feature = "tracing")] - trace!( - addr = format_args!("0x{:x}", effective_addr), - expected = expected, - timeout = timeout, - "[AtomicWait64] Wait operation" - ); match instance.memory(memarg.memory_index as u32) { Ok(memory_wrapper) => { let memory = &memory_wrapper.0; + // Per the spec, memory.atomic.wait on non-shared memory traps + if !memory.ty.shared { + return Err(kiln_error::Error::runtime_trap("expected shared memory")); + } let mut buffer = [0u8; 8]; match memory.read(effective_addr, &mut buffer) { Ok(()) => { let current = i64::from_le_bytes(buffer); - let result = if current != expected { 1 } else { 2 }; - operand_stack.push(Value::I32(result)); + if current != expected { + // Value differs from expected: return 1 (not-equal) + operand_stack.push(Value::I32(1)); + } else { + // In single-threaded runtime, no other thread will notify, + // so wait always times out: return 2 (timed-out) + operand_stack.push(Value::I32(2)); + } } Err(_) => { - return Err(kiln_error::Error::runtime_trap("Memory read out of bounds")); + return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); } } } Err(_) => { - return Err(kiln_error::Error::runtime_trap("Memory access error")); + return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); } } } @@ -9926,27 +10355,68 @@ impl StacklessEngine { Instruction::StructNew(type_idx) => { // struct.new: [field_values...] -> [structref] - // Pop field values, create struct, push reference + // Pop field values in reverse order, create struct, push reference #[cfg(feature = "tracing")] trace!("StructNew: type_idx={}", type_idx); - // For now, create an empty struct reference - // Full implementation requires type info to pop correct number of fields - let struct_ref = kiln_foundation::values::StructRef::new( + // Look up field count from gc_types (struct field definitions) + let field_count = { + #[cfg(feature = "std")] + { + match module.gc_types.get(type_idx as usize) { + Some(crate::module::GcTypeInfo::Struct(fields)) => fields.len(), + _ => return Err(kiln_error::Error::runtime_trap( + "struct.new: type index is not a struct type" + )), + } + } + #[cfg(not(feature = "std"))] + { 0usize } + }; + // Pop field values (they're on the stack in order, so pop in reverse) + let mut fields = Vec::with_capacity(field_count); + for _ in 0..field_count { + let val = operand_stack.pop().ok_or_else(|| + kiln_error::Error::runtime_trap("struct.new: stack underflow"))?; + fields.push(val); + } + fields.reverse(); // Restore original field order + let mut struct_ref = kiln_foundation::values::StructRef::new( type_idx, kiln_foundation::traits::DefaultMemoryProvider::default() ).map_err(|_| kiln_error::Error::runtime_error("Failed to create struct"))?; + for field in fields { + struct_ref.add_field(field).map_err(|_| + kiln_error::Error::runtime_error("Failed to add struct field"))?; + } operand_stack.push(Value::StructRef(Some(struct_ref))); } Instruction::StructNewDefault(type_idx) => { // struct.new_default: [] -> [structref] - // Create struct with default field values + // Create struct with default field values based on gc_types #[cfg(feature = "tracing")] trace!("StructNewDefault: type_idx={}", type_idx); - let struct_ref = kiln_foundation::values::StructRef::new( + let mut struct_ref = kiln_foundation::values::StructRef::new( type_idx, kiln_foundation::traits::DefaultMemoryProvider::default() ).map_err(|_| kiln_error::Error::runtime_error("Failed to create struct"))?; + #[cfg(feature = "std")] + { + match module.gc_types.get(type_idx as usize) { + Some(crate::module::GcTypeInfo::Struct(fields)) => { + for field in fields { + let default_val = gc_field_default_value(field); + struct_ref.add_field(default_val).map_err(|_| + kiln_error::Error::runtime_error( + "Failed to add default struct field" + ))?; + } + } + _ => return Err(kiln_error::Error::runtime_trap( + "struct.new_default: type index is not a struct type" + )), + } + } operand_stack.push(Value::StructRef(Some(struct_ref))); } @@ -9961,7 +10431,7 @@ impl StacklessEngine { return Err(kiln_error::Error::runtime_trap("struct.get: field index out of bounds")); } } else { - return Err(kiln_error::Error::runtime_trap("struct.get: null reference")); + return Err(kiln_error::Error::runtime_trap("null structure reference")); } } @@ -9971,12 +10441,43 @@ impl StacklessEngine { trace!("StructGetS: type_idx={}, field_idx={}", type_idx, field_idx); if let Some(Value::StructRef(Some(s))) = operand_stack.pop() { if let Ok(field) = s.get_field(field_idx as usize) { - operand_stack.push(field.clone()); + // Sign-extend packed field based on storage type + let result = match field { + Value::I32(v) => { + #[cfg(feature = "std")] + { + match module.gc_types.get(type_idx as usize) { + Some(crate::module::GcTypeInfo::Struct(fields)) => { + if let Some(gc_field) = fields.get(field_idx as usize) { + match gc_field.storage { + crate::module::GcFieldStorage::I8 => { + // Sign-extend from 8 bits + Value::I32(((v as u8) as i8) as i32) + } + crate::module::GcFieldStorage::I16 => { + // Sign-extend from 16 bits + Value::I32(((v as u16) as i16) as i32) + } + _ => field.clone(), + } + } else { + field.clone() + } + } + _ => field.clone(), + } + } + #[cfg(not(feature = "std"))] + { field.clone() } + } + _ => field.clone(), + }; + operand_stack.push(result); } else { return Err(kiln_error::Error::runtime_trap("struct.get_s: field index out of bounds")); } } else { - return Err(kiln_error::Error::runtime_trap("struct.get_s: null reference")); + return Err(kiln_error::Error::runtime_trap("null structure reference")); } } @@ -9986,12 +10487,43 @@ impl StacklessEngine { trace!("StructGetU: type_idx={}, field_idx={}", type_idx, field_idx); if let Some(Value::StructRef(Some(s))) = operand_stack.pop() { if let Ok(field) = s.get_field(field_idx as usize) { - operand_stack.push(field.clone()); + // Zero-extend packed field based on storage type + let result = match field { + Value::I32(v) => { + #[cfg(feature = "std")] + { + match module.gc_types.get(type_idx as usize) { + Some(crate::module::GcTypeInfo::Struct(fields)) => { + if let Some(gc_field) = fields.get(field_idx as usize) { + match gc_field.storage { + crate::module::GcFieldStorage::I8 => { + // Zero-extend from 8 bits + Value::I32((v as u8) as i32) + } + crate::module::GcFieldStorage::I16 => { + // Zero-extend from 16 bits + Value::I32((v as u16) as i32) + } + _ => field.clone(), + } + } else { + field.clone() + } + } + _ => field.clone(), + } + } + #[cfg(not(feature = "std"))] + { field.clone() } + } + _ => field.clone(), + }; + operand_stack.push(result); } else { return Err(kiln_error::Error::runtime_trap("struct.get_u: field index out of bounds")); } } else { - return Err(kiln_error::Error::runtime_trap("struct.get_u: null reference")); + return Err(kiln_error::Error::runtime_trap("null structure reference")); } } @@ -10005,7 +10537,7 @@ impl StacklessEngine { s.set_field(field_idx as usize, value).map_err(|_| kiln_error::Error::runtime_trap("struct.set: field index out of bounds"))?; } else { - return Err(kiln_error::Error::runtime_trap("struct.set: null reference")); + return Err(kiln_error::Error::runtime_trap("null structure reference")); } } @@ -10038,12 +10570,26 @@ impl StacklessEngine { Some(Value::I32(n)) => n as u32, _ => return Err(kiln_error::Error::runtime_trap("array.new_default: expected i32 length")), }; + // Determine default value based on element type from gc_types + let default_val = { + #[cfg(feature = "std")] + { + match module.gc_types.get(type_idx as usize) { + Some(crate::module::GcTypeInfo::Array(field)) => gc_field_default_value(field), + _ => return Err(kiln_error::Error::runtime_trap( + "array.new_default: type index is not an array type" + )), + } + } + #[cfg(not(feature = "std"))] + { Value::I32(0) } + }; let mut array_ref = kiln_foundation::values::ArrayRef::new( type_idx, kiln_foundation::traits::DefaultMemoryProvider::default() ).map_err(|_| kiln_error::Error::runtime_error("Failed to create array"))?; for _ in 0..length { - array_ref.push(Value::I32(0)).map_err(|_| + array_ref.push(default_val.clone()).map_err(|_| kiln_error::Error::runtime_error("Failed to push to array"))?; } operand_stack.push(Value::ArrayRef(Some(array_ref))); @@ -10086,7 +10632,7 @@ impl StacklessEngine { return Err(kiln_error::Error::runtime_trap("array.get: index out of bounds")); } } else { - return Err(kiln_error::Error::runtime_trap("array.get: null reference")); + return Err(kiln_error::Error::runtime_trap("null array reference")); } } @@ -10105,7 +10651,7 @@ impl StacklessEngine { return Err(kiln_error::Error::runtime_trap("array.get_s: index out of bounds")); } } else { - return Err(kiln_error::Error::runtime_trap("array.get_s: null reference")); + return Err(kiln_error::Error::runtime_trap("null array reference")); } } @@ -10124,7 +10670,7 @@ impl StacklessEngine { return Err(kiln_error::Error::runtime_trap("array.get_u: index out of bounds")); } } else { - return Err(kiln_error::Error::runtime_trap("array.get_u: null reference")); + return Err(kiln_error::Error::runtime_trap("null array reference")); } } @@ -10142,92 +10688,654 @@ impl StacklessEngine { a.set(index, value).map_err(|_| kiln_error::Error::runtime_trap("array.set: index out of bounds"))?; } else { - return Err(kiln_error::Error::runtime_trap("array.set: null reference")); + return Err(kiln_error::Error::runtime_trap("null array reference")); + } + } + + Instruction::ArrayLen => { + // array.len: [arrayref] -> [i32] + #[cfg(feature = "tracing")] + trace!("ArrayLen"); + if let Some(Value::ArrayRef(Some(a))) = operand_stack.pop() { + operand_stack.push(Value::I32(a.len() as i32)); + } else { + return Err(kiln_error::Error::runtime_trap("null array reference")); + } + } + + Instruction::RefI31 => { + // ref.i31: [i32] -> [i31ref] + // Store the lower 31 bits of the i32 value + #[cfg(feature = "tracing")] + trace!("RefI31"); + if let Some(Value::I32(n)) = operand_stack.pop() { + // Mask to lower 31 bits (unsigned representation) + let i31_val = n & 0x7FFFFFFF; + operand_stack.push(Value::I31Ref(Some(i31_val))); + } else { + return Err(kiln_error::Error::runtime_trap("ref.i31: expected i32")); + } + } + + Instruction::I31GetS => { + // i31.get_s: [i31ref] -> [i32] (sign-extended from 31 bits) + #[cfg(feature = "tracing")] + trace!("I31GetS"); + match operand_stack.pop() { + Some(Value::I31Ref(Some(n))) => { + // Sign-extend from bit 30: if bit 30 is set, the value is negative + let sign_extended = if n & 0x40000000 != 0 { + // Set the upper bit (sign extend) + n | !0x7FFFFFFF_u32 as i32 + } else { + n + }; + operand_stack.push(Value::I32(sign_extended)); + } + Some(Value::I31Ref(None)) => { + return Err(kiln_error::Error::runtime_trap("null i31 reference")); + } + _ => { + return Err(kiln_error::Error::runtime_trap("i31.get_s: expected i31ref")); + } + } + } + + Instruction::I31GetU => { + // i31.get_u: [i31ref] -> [i32] (zero-extended) + #[cfg(feature = "tracing")] + trace!("I31GetU"); + match operand_stack.pop() { + Some(Value::I31Ref(Some(n))) => { + // Zero-extend: mask to 31 bits + operand_stack.push(Value::I32(n & 0x7FFFFFFF)); + } + Some(Value::I31Ref(None)) => { + return Err(kiln_error::Error::runtime_trap("null i31 reference")); + } + _ => { + return Err(kiln_error::Error::runtime_trap("i31.get_u: expected i31ref")); + } + } + } + + Instruction::ArrayNewData(type_idx, data_idx) => { + // array.new_data: [offset i32, size i32] -> [arrayref] + // Create array from data segment + #[cfg(feature = "tracing")] + trace!("ArrayNewData: type_idx={}, data_idx={}", type_idx, data_idx); + let size = match operand_stack.pop() { + Some(Value::I32(n)) => n as u32, + _ => return Err(kiln_error::Error::runtime_trap("array.new_data: expected i32 size")), + }; + let offset_val = match operand_stack.pop() { + Some(Value::I32(n)) => n as u32, + _ => return Err(kiln_error::Error::runtime_trap("array.new_data: expected i32 offset")), + }; + // Get element size from GC type info + let elem_size = { + #[cfg(feature = "std")] + { + match module.gc_types.get(type_idx as usize) { + Some(crate::module::GcTypeInfo::Array(field)) => field.size_in_bytes(), + _ => 4, // default to 4 bytes if type info unavailable + } + } + #[cfg(not(feature = "std"))] + { 4usize } + }; + // Get data segment + let data_segment = module.data.get(data_idx as usize) + .ok_or_else(|| kiln_error::Error::runtime_trap("array.new_data: invalid data index"))?; + let data_bytes = data_segment.data()?; + let byte_offset = offset_val as usize; + let byte_length = size as usize * elem_size; + if byte_offset + byte_length > data_bytes.len() { + return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); + } + let mut array_ref = kiln_foundation::values::ArrayRef::new( + type_idx, + kiln_foundation::traits::DefaultMemoryProvider::default() + ).map_err(|_| kiln_error::Error::runtime_error("Failed to create array"))?; + for i in 0..size as usize { + let start = byte_offset + i * elem_size; + let val = match elem_size { + 1 => Value::I32(data_bytes[start] as i32), + 2 => Value::I32(i16::from_le_bytes([data_bytes[start], data_bytes[start+1]]) as i32), + 4 => Value::I32(i32::from_le_bytes([data_bytes[start], data_bytes[start+1], data_bytes[start+2], data_bytes[start+3]])), + 8 => Value::I64(i64::from_le_bytes([ + data_bytes[start], data_bytes[start+1], data_bytes[start+2], data_bytes[start+3], + data_bytes[start+4], data_bytes[start+5], data_bytes[start+6], data_bytes[start+7], + ])), + _ => Value::I32(0), + }; + array_ref.push(val).map_err(|_| + kiln_error::Error::runtime_error("Failed to push to array"))?; + } + operand_stack.push(Value::ArrayRef(Some(array_ref))); + } + + Instruction::ArrayNewElem(type_idx, elem_idx) => { + // array.new_elem: [offset i32, size i32] -> [arrayref] + #[cfg(feature = "tracing")] + trace!("ArrayNewElem: type_idx={}, elem_idx={}", type_idx, elem_idx); + let size = match operand_stack.pop() { + Some(Value::I32(n)) => n as u32, + _ => return Err(kiln_error::Error::runtime_trap("array.new_elem: expected i32 size")), + }; + let offset_val = match operand_stack.pop() { + Some(Value::I32(n)) => n as u32, + _ => return Err(kiln_error::Error::runtime_trap("array.new_elem: expected i32 offset")), + }; + let elem_segment = module.elements.get(elem_idx as usize) + .ok_or_else(|| kiln_error::Error::runtime_trap("array.new_elem: invalid elem index"))?; + if offset_val as usize + size as usize > elem_segment.items.len() { + return Err(kiln_error::Error::runtime_trap("out of bounds table access")); + } + let mut array_ref = kiln_foundation::values::ArrayRef::new( + type_idx, + kiln_foundation::traits::DefaultMemoryProvider::default() + ).map_err(|_| kiln_error::Error::runtime_error("Failed to create array"))?; + for i in 0..size as usize { + let item_idx = elem_segment.items.get(offset_val as usize + i) + .map_err(|_| kiln_error::Error::runtime_trap("array.new_elem: element access failed"))?; + let item_val = Value::FuncRef(Some(kiln_foundation::values::FuncRef::from_index(item_idx))); + array_ref.push(item_val).map_err(|_| + kiln_error::Error::runtime_error("Failed to push to array"))?; + } + operand_stack.push(Value::ArrayRef(Some(array_ref))); + } + + Instruction::ArrayFill(type_idx) => { + // array.fill: [arrayref i32 value i32] -> [] + #[cfg(feature = "tracing")] + trace!("ArrayFill: type_idx={}", type_idx); + let size = match operand_stack.pop() { + Some(Value::I32(n)) => n as u32, + _ => return Err(kiln_error::Error::runtime_trap("array.fill: expected i32 size")), + }; + let fill_value = operand_stack.pop().ok_or_else(|| + kiln_error::Error::runtime_trap("array.fill: expected fill value"))?; + let offset_val = match operand_stack.pop() { + Some(Value::I32(n)) => n as u32, + _ => return Err(kiln_error::Error::runtime_trap("array.fill: expected i32 offset")), + }; + if let Some(Value::ArrayRef(Some(mut a))) = operand_stack.pop() { + let len = a.len() as u32; + if offset_val + size > len { + return Err(kiln_error::Error::runtime_trap("out of bounds array access")); + } + for i in 0..size { + a.set((offset_val + i) as usize, fill_value.clone()).map_err(|_| + kiln_error::Error::runtime_trap("array.fill: set failed"))?; + } + } else { + return Err(kiln_error::Error::runtime_trap("null array reference")); + } + } + + Instruction::ArrayCopy(dst_type_idx, src_type_idx) => { + // array.copy: [dst_arrayref dst_offset src_arrayref src_offset len] -> [] + #[cfg(feature = "tracing")] + trace!("ArrayCopy: dst={}, src={}", dst_type_idx, src_type_idx); + let len = match operand_stack.pop() { + Some(Value::I32(n)) => n as u32, + _ => return Err(kiln_error::Error::runtime_trap("array.copy: expected i32 len")), + }; + let src_offset = match operand_stack.pop() { + Some(Value::I32(n)) => n as u32, + _ => return Err(kiln_error::Error::runtime_trap("array.copy: expected i32 src_offset")), + }; + let src_array = match operand_stack.pop() { + Some(Value::ArrayRef(Some(a))) => a, + _ => return Err(kiln_error::Error::runtime_trap("null array reference")), + }; + let dst_offset = match operand_stack.pop() { + Some(Value::I32(n)) => n as u32, + _ => return Err(kiln_error::Error::runtime_trap("array.copy: expected i32 dst_offset")), + }; + if let Some(Value::ArrayRef(Some(mut dst_array))) = operand_stack.pop() { + if src_offset + len > src_array.len() as u32 || dst_offset + len > dst_array.len() as u32 { + return Err(kiln_error::Error::runtime_trap("out of bounds array access")); + } + // Copy elements (handle overlap correctly) + let mut elems = Vec::new(); + for i in 0..len { + let elem = src_array.get((src_offset + i) as usize).map_err(|_| + kiln_error::Error::runtime_trap("array.copy: src get failed"))?; + elems.push(elem); + } + for (i, elem) in elems.into_iter().enumerate() { + dst_array.set((dst_offset as usize) + i, elem).map_err(|_| + kiln_error::Error::runtime_trap("array.copy: dst set failed"))?; + } + } else { + return Err(kiln_error::Error::runtime_trap("null array reference")); + } + } + + Instruction::ArrayInitData(type_idx, data_idx) => { + // array.init_data: [arrayref dst_offset src_offset len] -> [] + #[cfg(feature = "tracing")] + trace!("ArrayInitData: type_idx={}, data_idx={}", type_idx, data_idx); + let len = match operand_stack.pop() { + Some(Value::I32(n)) => n as u32, + _ => return Err(kiln_error::Error::runtime_trap("array.init_data: expected i32 len")), + }; + let src_offset = match operand_stack.pop() { + Some(Value::I32(n)) => n as u32, + _ => return Err(kiln_error::Error::runtime_trap("array.init_data: expected i32 src_offset")), + }; + let dst_offset = match operand_stack.pop() { + Some(Value::I32(n)) => n as u32, + _ => return Err(kiln_error::Error::runtime_trap("array.init_data: expected i32 dst_offset")), + }; + if let Some(Value::ArrayRef(Some(mut a))) = operand_stack.pop() { + let elem_size = { + #[cfg(feature = "std")] + { + match module.gc_types.get(type_idx as usize) { + Some(crate::module::GcTypeInfo::Array(field)) => field.size_in_bytes(), + _ => 4, + } + } + #[cfg(not(feature = "std"))] + { 4usize } + }; + let data_segment = module.data.get(data_idx as usize) + .ok_or_else(|| kiln_error::Error::runtime_trap("array.init_data: invalid data index"))?; + let data_bytes = data_segment.data()?; + let byte_offset = src_offset as usize; + let byte_length = len as usize * elem_size; + if byte_offset + byte_length > data_bytes.len() { + return Err(kiln_error::Error::runtime_trap("out of bounds memory access")); + } + if dst_offset + len > a.len() as u32 { + return Err(kiln_error::Error::runtime_trap("out of bounds array access")); + } + for i in 0..len as usize { + let start = byte_offset + i * elem_size; + let val = match elem_size { + 1 => Value::I32(data_bytes[start] as i32), + 2 => Value::I32(i16::from_le_bytes([data_bytes[start], data_bytes[start+1]]) as i32), + 4 => Value::I32(i32::from_le_bytes([data_bytes[start], data_bytes[start+1], data_bytes[start+2], data_bytes[start+3]])), + 8 => Value::I64(i64::from_le_bytes([ + data_bytes[start], data_bytes[start+1], data_bytes[start+2], data_bytes[start+3], + data_bytes[start+4], data_bytes[start+5], data_bytes[start+6], data_bytes[start+7], + ])), + _ => Value::I32(0), + }; + a.set((dst_offset as usize) + i, val).map_err(|_| + kiln_error::Error::runtime_trap("array.init_data: set failed"))?; + } + } else { + return Err(kiln_error::Error::runtime_trap("null array reference")); + } + } + + Instruction::ArrayInitElem(type_idx, elem_idx) => { + // array.init_elem: [arrayref dst_offset src_offset len] -> [] + #[cfg(feature = "tracing")] + trace!("ArrayInitElem: type_idx={}, elem_idx={}", type_idx, elem_idx); + let len = match operand_stack.pop() { + Some(Value::I32(n)) => n as u32, + _ => return Err(kiln_error::Error::runtime_trap("array.init_elem: expected i32 len")), + }; + let src_offset = match operand_stack.pop() { + Some(Value::I32(n)) => n as u32, + _ => return Err(kiln_error::Error::runtime_trap("array.init_elem: expected i32 src_offset")), + }; + let dst_offset = match operand_stack.pop() { + Some(Value::I32(n)) => n as u32, + _ => return Err(kiln_error::Error::runtime_trap("array.init_elem: expected i32 dst_offset")), + }; + if let Some(Value::ArrayRef(Some(mut a))) = operand_stack.pop() { + let elem_segment = module.elements.get(elem_idx as usize) + .ok_or_else(|| kiln_error::Error::runtime_trap("array.init_elem: invalid elem index"))?; + if src_offset as usize + len as usize > elem_segment.items.len() { + return Err(kiln_error::Error::runtime_trap("out of bounds table access")); + } + if dst_offset + len > a.len() as u32 { + return Err(kiln_error::Error::runtime_trap("out of bounds array access")); + } + for i in 0..len as usize { + let item_idx = elem_segment.items.get(src_offset as usize + i) + .map_err(|_| kiln_error::Error::runtime_trap("array.init_elem: element access failed"))?; + let item_val = Value::FuncRef(Some(kiln_foundation::values::FuncRef::from_index(item_idx))); + a.set((dst_offset as usize) + i, item_val).map_err(|_| + kiln_error::Error::runtime_trap("array.init_elem: set failed"))?; + } + } else { + return Err(kiln_error::Error::runtime_trap("null array reference")); + } + } + + Instruction::RefTest(heap_type) => { + // ref.test: [ref] -> [i32] + // Test if reference matches heap type (non-null) + #[cfg(feature = "tracing")] + trace!("RefTest: {:?}", heap_type); + let val = operand_stack.pop().ok_or_else(|| + kiln_error::Error::runtime_trap("ref.test: expected reference"))?; + let result = ref_test_value(&val, &heap_type, false); + operand_stack.push(Value::I32(if result { 1 } else { 0 })); + } + + Instruction::RefTestNull(heap_type) => { + // ref.test (nullable): [ref] -> [i32] + // Test if reference matches heap type (nullable variant) + #[cfg(feature = "tracing")] + trace!("RefTestNull: {:?}", heap_type); + let val = operand_stack.pop().ok_or_else(|| + kiln_error::Error::runtime_trap("ref.test: expected reference"))?; + let result = ref_test_value(&val, &heap_type, true); + operand_stack.push(Value::I32(if result { 1 } else { 0 })); + } + + Instruction::RefCast(heap_type) => { + // ref.cast: [ref] -> [ref] + // Cast reference to heap type, trap if fails (non-null) + #[cfg(feature = "tracing")] + trace!("RefCast: {:?}", heap_type); + let val = operand_stack.pop().ok_or_else(|| + kiln_error::Error::runtime_trap("ref.cast: expected reference"))?; + if !ref_test_value(&val, &heap_type, false) { + return Err(kiln_error::Error::runtime_trap("cast failure")); + } + operand_stack.push(val); + } + + Instruction::RefCastNull(heap_type) => { + // ref.cast (nullable): [ref] -> [ref] + // Cast reference, allow null + #[cfg(feature = "tracing")] + trace!("RefCastNull: {:?}", heap_type); + let val = operand_stack.pop().ok_or_else(|| + kiln_error::Error::runtime_trap("ref.cast: expected reference"))?; + if !ref_test_value(&val, &heap_type, true) { + return Err(kiln_error::Error::runtime_trap("cast failure")); + } + operand_stack.push(val); + } + + Instruction::BrOnCast { flags, label: label_idx, from_type: _, to_type } => { + // br_on_cast: [ref] -> [ref] + // Branch if cast succeeds, consuming the reference on the branch path + // flags bit 1: target nullable + let target_nullable = (flags & 0x02) != 0; + #[cfg(feature = "tracing")] + trace!("BrOnCast: label={}, to_type={:?}, target_nullable={}", label_idx, to_type, target_nullable); + let val = operand_stack.pop().ok_or_else(|| + kiln_error::Error::runtime_trap("br_on_cast: expected reference"))?; + // Test if cast would succeed + if ref_test_value(&val, &to_type, target_nullable) { + // Cast succeeds - push value and branch + operand_stack.push(val); + // Branch logic (same as Br instruction) + if (label_idx as usize) < block_stack.len() { + let stack_idx = block_stack.len() - 1 - label_idx as usize; + let (block_type, start_pc, block_type_idx, entry_stack_height) = block_stack[stack_idx]; + + // Determine how many values to preserve based on block type + let values_to_preserve = if block_type == "loop" { + match block_type_idx { + 0x40 => 0, + 0x7F | 0x7E | 0x7D | 0x7C | 0x7B => 0, + 0x70 | 0x6F => 0, + 0x6E | 0x6D | 0x6C | 0x6B | 0x6A | 0x73 | 0x72 | 0x71 | 0x69 => 0, + _ => { + if let Some(func_type) = module.types.get(block_type_idx as usize) { + func_type.params.len() + } else { + 0 + } + } + } + } else { + match block_type_idx { + 0x40 => 0, + 0x7F | 0x7E | 0x7D | 0x7C | 0x7B => 1, + 0x70 | 0x6F => 1, + 0x6E | 0x6D | 0x6C | 0x6B | 0x6A | 0x73 | 0x72 | 0x71 | 0x69 => 1, + _ => { + if let Some(func_type) = module.types.get(block_type_idx as usize) { + func_type.results.len() + } else { + 1 // br_on_cast always has at least the ref value + } + } + } + }; + + // Save the values to preserve from top of stack + let mut preserved_values = Vec::new(); + for _ in 0..values_to_preserve { + if let Some(v) = operand_stack.pop() { + preserved_values.push(v); + } + } + + // Clear stack down to the entry height + while operand_stack.len() > entry_stack_height { + let _ = operand_stack.pop(); + } + + if block_type == "loop" { + // Pop inner blocks from block stack + let blocks_to_pop = label_idx as usize; + for _ in 0..blocks_to_pop { + if !block_stack.is_empty() { + block_stack.pop(); + block_depth -= 1; + } + } + pc = start_pc; + } else { + // Pop inner blocks from block stack + let blocks_to_pop = label_idx as usize; + for _ in 0..blocks_to_pop { + if !block_stack.is_empty() { + block_stack.pop(); + block_depth -= 1; + } + } + + // Scan forward to find the target block's End + let mut target_depth = label_idx as i32 + 1; + let mut new_pc = pc + 1; + let mut depth = 0; + + while new_pc < instructions.len() && target_depth > 0 { + if let Some(instr) = instructions.get(new_pc) { + match instr { + Instruction::Block { .. } | + Instruction::Loop { .. } | + Instruction::If { .. } | + Instruction::Try { .. } | + Instruction::TryTable { .. } => { + depth += 1; + } + Instruction::End => { + if depth == 0 { + target_depth -= 1; + if target_depth == 0 { + pc = new_pc; + break; + } + } else { + depth -= 1; + } + } + _ => {} + } + } + new_pc += 1; + } + } + + // Restore preserved values back to stack (in reverse order) + for v in preserved_values.into_iter().rev() { + operand_stack.push(v); + } + } else { + break; // Branch out of function + } + continue; + } else { + // Cast fails - push original value back, don't branch + operand_stack.push(val); } } - Instruction::ArrayLen => { - // array.len: [arrayref] -> [i32] + Instruction::BrOnCastFail { flags, label: label_idx, from_type: _, to_type } => { + // br_on_cast_fail: [ref] -> [ref] + // Branch if cast FAILS + // flags bit 1: target nullable + let target_nullable = (flags & 0x02) != 0; #[cfg(feature = "tracing")] - trace!("ArrayLen"); - if let Some(Value::ArrayRef(Some(a))) = operand_stack.pop() { - operand_stack.push(Value::I32(a.len() as i32)); - } else { - return Err(kiln_error::Error::runtime_trap("array.len: null reference")); - } - } + trace!("BrOnCastFail: label={}, to_type={:?}, target_nullable={}", label_idx, to_type, target_nullable); + let val = operand_stack.pop().ok_or_else(|| + kiln_error::Error::runtime_trap("br_on_cast_fail: expected reference"))?; + if !ref_test_value(&val, &to_type, target_nullable) { + // Cast fails - push value and branch + operand_stack.push(val); + // Branch logic (same as Br instruction) + if (label_idx as usize) < block_stack.len() { + let stack_idx = block_stack.len() - 1 - label_idx as usize; + let (block_type, start_pc, block_type_idx, entry_stack_height) = block_stack[stack_idx]; - Instruction::RefI31 => { - // ref.i31: [i32] -> [i31ref] - #[cfg(feature = "tracing")] - trace!("RefI31"); - if let Some(Value::I32(n)) = operand_stack.pop() { - // Truncate to 31 bits (sign-extend from 31 bits) - let i31_val = (n << 1) >> 1; - operand_stack.push(Value::I31Ref(Some(i31_val))); - } else { - return Err(kiln_error::Error::runtime_trap("ref.i31: expected i32")); - } - } + // Determine how many values to preserve based on block type + let values_to_preserve = if block_type == "loop" { + match block_type_idx { + 0x40 => 0, + 0x7F | 0x7E | 0x7D | 0x7C | 0x7B => 0, + 0x70 | 0x6F => 0, + 0x6E | 0x6D | 0x6C | 0x6B | 0x6A | 0x73 | 0x72 | 0x71 | 0x69 => 0, + _ => { + if let Some(func_type) = module.types.get(block_type_idx as usize) { + func_type.params.len() + } else { + 0 + } + } + } + } else { + match block_type_idx { + 0x40 => 0, + 0x7F | 0x7E | 0x7D | 0x7C | 0x7B => 1, + 0x70 | 0x6F => 1, + 0x6E | 0x6D | 0x6C | 0x6B | 0x6A | 0x73 | 0x72 | 0x71 | 0x69 => 1, + _ => { + if let Some(func_type) = module.types.get(block_type_idx as usize) { + func_type.results.len() + } else { + 1 // br_on_cast_fail always has at least the ref value + } + } + } + }; - Instruction::I31GetS => { - // i31.get_s: [i31ref] -> [i32] (sign-extended) - #[cfg(feature = "tracing")] - trace!("I31GetS"); - match operand_stack.pop() { - Some(Value::I31Ref(Some(n))) => { - operand_stack.push(Value::I32(n)); - } - Some(Value::I31Ref(None)) => { - return Err(kiln_error::Error::runtime_trap("null i31 reference")); - } - _ => { - return Err(kiln_error::Error::runtime_trap("i31.get_s: expected i31ref")); + // Save the values to preserve from top of stack + let mut preserved_values = Vec::new(); + for _ in 0..values_to_preserve { + if let Some(v) = operand_stack.pop() { + preserved_values.push(v); + } + } + + // Clear stack down to the entry height + while operand_stack.len() > entry_stack_height { + let _ = operand_stack.pop(); + } + + if block_type == "loop" { + // Pop inner blocks from block stack + let blocks_to_pop = label_idx as usize; + for _ in 0..blocks_to_pop { + if !block_stack.is_empty() { + block_stack.pop(); + block_depth -= 1; + } + } + pc = start_pc; + } else { + // Pop inner blocks from block stack + let blocks_to_pop = label_idx as usize; + for _ in 0..blocks_to_pop { + if !block_stack.is_empty() { + block_stack.pop(); + block_depth -= 1; + } + } + + // Scan forward to find the target block's End + let mut target_depth = label_idx as i32 + 1; + let mut new_pc = pc + 1; + let mut depth = 0; + + while new_pc < instructions.len() && target_depth > 0 { + if let Some(instr) = instructions.get(new_pc) { + match instr { + Instruction::Block { .. } | + Instruction::Loop { .. } | + Instruction::If { .. } | + Instruction::Try { .. } | + Instruction::TryTable { .. } => { + depth += 1; + } + Instruction::End => { + if depth == 0 { + target_depth -= 1; + if target_depth == 0 { + pc = new_pc; + break; + } + } else { + depth -= 1; + } + } + _ => {} + } + } + new_pc += 1; + } + } + + // Restore preserved values back to stack (in reverse order) + for v in preserved_values.into_iter().rev() { + operand_stack.push(v); + } + } else { + break; } + continue; + } else { + // Cast succeeds - push value back, don't branch + operand_stack.push(val); } } - Instruction::I31GetU => { - // i31.get_u: [i31ref] -> [i32] (zero-extended) + Instruction::AnyConvertExtern => { + // any.convert_extern: [externref] -> [anyref] + // Convert an externref to an anyref (internalize) #[cfg(feature = "tracing")] - trace!("I31GetU"); - match operand_stack.pop() { - Some(Value::I31Ref(Some(n))) => { - // Zero-extend: mask to 31 bits - operand_stack.push(Value::I32(n & 0x7FFFFFFF)); - } - Some(Value::I31Ref(None)) => { - return Err(kiln_error::Error::runtime_trap("null i31 reference")); - } - _ => { - return Err(kiln_error::Error::runtime_trap("i31.get_u: expected i31ref")); - } - } + trace!("AnyConvertExtern"); + let val = operand_stack.pop().ok_or_else(|| + kiln_error::Error::runtime_trap("any.convert_extern: expected reference"))?; + // In our representation, externref and anyref share Value::ExternRef + // The spec says null stays null, non-null wraps + operand_stack.push(val); } - // GC instructions that need more context (type info, etc.) - // These return stubs for now - full implementation requires type section data - Instruction::ArrayNewData(_, _) | - Instruction::ArrayNewElem(_, _) | - Instruction::ArrayFill(_) | - Instruction::ArrayCopy(_, _) | - Instruction::ArrayInitData(_, _) | - Instruction::ArrayInitElem(_, _) | - Instruction::RefTest(_) | - Instruction::RefTestNull(_) | - Instruction::RefCast(_) | - Instruction::RefCastNull(_) | - Instruction::BrOnCast { .. } | - Instruction::BrOnCastFail { .. } | - Instruction::AnyConvertExtern | Instruction::ExternConvertAny => { + // extern.convert_any: [anyref] -> [externref] + // Convert an anyref to an externref (externalize) #[cfg(feature = "tracing")] - trace!("GC instruction (stub): {:?}", instruction); - // These instructions require more complex type system integration - // For now, return an error indicating incomplete implementation - return Err(kiln_error::Error::runtime_error( - "GC instruction not yet fully implemented", - )); + trace!("ExternConvertAny"); + let val = operand_stack.pop().ok_or_else(|| + kiln_error::Error::runtime_trap("extern.convert_any: expected reference"))?; + // In our representation, just pass through + operand_stack.push(val); } // ======================================== @@ -10287,10 +11395,9 @@ impl StacklessEngine { trace!("Result {}: {:?}", i, value); results.insert(0, value); } else { - #[cfg(feature = "tracing")] - - trace!("Result {}: missing, using default", i); - results.insert(0, Value::I32(0)); + return Err(kiln_error::Error::runtime_execution_error( + "function expected to return a value but operand stack was empty", + )); } } @@ -10309,119 +11416,26 @@ impl StacklessEngine { #[cfg(not(feature = "std"))] { - // Fallback for no_std - return default values - let mut results = { - use kiln_foundation::{ - budget_aware_provider::CrateId, - safe_managed_alloc, - }; - use crate::bounded_runtime_infra::RUNTIME_MEMORY_SIZE; - let provider = safe_managed_alloc!(RUNTIME_MEMORY_SIZE, CrateId::Runtime)?; - BoundedVec::new(provider)? - }; - for result_type in &func_type.results { - let default_value = match result_type { - kiln_foundation::ValueType::I32 => Value::I32(0), - kiln_foundation::ValueType::I64 => Value::I64(0), - kiln_foundation::ValueType::F32 => Value::F32(FloatBits32(0)), - kiln_foundation::ValueType::F64 => Value::F64(FloatBits64(0)), - _ => Value::I32(0), - }; - results.push(default_value)?; - } - // Return completed execution - call depth is handled by the trampoline wrapper - Ok(ExecutionOutcome::Complete(results)) + // no_std: execution should have produced results on the stack + // If we reach here without having executed, that's an error + return Err(kiln_error::Error::runtime_execution_error( + "function execution not supported in no_std without stack results", + )); } } #[cfg(not(any(feature = "std", feature = "alloc")))] pub fn execute( &self, - instance_id: usize, - func_idx: usize, - args: Vec, + _instance_id: usize, + _func_idx: usize, + _args: Vec, ) -> Result> { - #[cfg(feature = "std")] - #[cfg(feature = "tracing")] - - trace!("DEBUG StacklessEngine::execute: instance_id={}, func_idx={}", instance_id, func_idx); - - let instance = self - .instances - .get(&instance_id)? - .ok_or_else(|| kiln_error::Error::runtime_execution_error("Instance not found"))?; - - // For now, implement a basic execution that validates the function exists - // and returns appropriate results - let module = instance.module(); - - #[cfg(feature = "std")] - #[cfg(feature = "tracing")] - - debug!("Got module, functions.len()={}", module.functions.len()); - - // Validate function index - if func_idx >= module.functions.len() { - return Err(kiln_error::Error::runtime_function_not_found( - "Function index out of bounds", - )); - } - - let func = module - .functions - .get(func_idx) - .map_err(|_| kiln_error::Error::runtime_error("Failed to get function"))?; - - #[cfg(feature = "std")] - #[cfg(feature = "tracing")] - - debug!("Retrieved func, body.instructions.len()={}", func.body.instructions.len()); - - #[cfg(feature = "std")] - #[cfg(feature = "tracing")] - - trace!("DEBUG execute: func.type_idx={}, module.types.len()={}", func.type_idx, module.types.len()); - - // In std mode, types is Vec so get() returns Option<&T> - #[cfg(feature = "std")] - let func_type = module - .types - .get(func.type_idx as usize) - .ok_or_else(|| kiln_error::Error::runtime_error("Failed to get function type"))?; - - // In no_std mode, types is BoundedVec so get() returns Result - #[cfg(not(feature = "std"))] - let func_type = &module - .types - .get(func.type_idx as usize) - .map_err(|_| kiln_error::Error::runtime_error("Failed to get function type"))?; - - // Return appropriate default values based on function signature - let mut results = { - use kiln_foundation::{ - budget_aware_provider::CrateId, - safe_managed_alloc, - }; - - let provider = safe_managed_alloc!(4096, CrateId::Runtime)?; - BoundedVec::new(provider) - .map_err(|_| kiln_error::Error::runtime_error("Failed to create results vector"))? - }; - for result_type in &func_type.results { - let default_value = match result_type { - kiln_foundation::ValueType::I32 => Value::I32(0), - kiln_foundation::ValueType::I64 => Value::I64(0), - kiln_foundation::ValueType::F32 => Value::F32(FloatBits32(0.0f32.to_bits())), - kiln_foundation::ValueType::F64 => Value::F64(FloatBits64(0.0f64.to_bits())), - // Add other types as needed - _ => Value::I32(0), // Default fallback - }; - results - .push(default_value) - .map_err(|_| kiln_error::Error::runtime_error("Failed to push result value"))?; - } - - Ok(results) + // Execution in no_std/no_alloc mode is not yet implemented. + // Returning default values would silently mask this gap. + Err(kiln_error::Error::runtime_execution_error( + "function execution not implemented in no_std/no_alloc configuration", + )) } /// Get the remaining fuel for execution @@ -10437,77 +11451,16 @@ impl StacklessEngine { /// Execute a single step of function execution with instruction limit pub fn execute_function_step( &mut self, - instance: &ModuleInstance, - func_idx: usize, - params: &[Value], - max_instructions: u32, + _instance: &ModuleInstance, + _func_idx: usize, + _params: &[Value], + _max_instructions: u32, ) -> Result { - use kiln_foundation::{ - budget_aware_provider::CrateId, - safe_managed_alloc, - }; - - // Validate function exists - let module = instance.module(); - if func_idx >= module.functions.len() { - return Err(kiln_error::Error::runtime_function_not_found( - "Function index out of bounds", - )); - } - - // Get function type - let func = module - .functions - .get(func_idx) - .ok_or_else(|| kiln_error::Error::runtime_function_not_found("Failed to get function"))?; - // In std mode, types is Vec so get() returns Option<&T> - #[cfg(feature = "std")] - let func_type = module - .types - .get(func.type_idx as usize) - .ok_or_else(|| kiln_error::Error::runtime_error("Failed to get function type"))?; - - // In no_std mode, types is BoundedVec so get() returns Result - #[cfg(not(feature = "std"))] - let func_type = &module - .types - .get(func.type_idx as usize) - .map_err(|_| kiln_error::Error::runtime_error("Failed to get function type"))?; - - // Simulate step execution - in real implementation would execute instructions - // For now, return completed with default values - let provider = safe_managed_alloc!(1024, CrateId::Runtime)?; - let mut results = kiln_foundation::bounded::BoundedVec::new(provider) - .map_err(|_| kiln_error::Error::runtime_error("Failed to create results vector"))?; - - for result_type in &func_type.results { - let default_value = match result_type { - kiln_foundation::ValueType::I32 => Value::I32(0), - kiln_foundation::ValueType::I64 => Value::I64(0), - kiln_foundation::ValueType::F32 => Value::F32(FloatBits32(0.0f32.to_bits())), - kiln_foundation::ValueType::F64 => Value::F64(FloatBits64(0.0f64.to_bits())), - _ => Value::I32(0), - }; - results - .push(default_value) - .map_err(|_| kiln_error::Error::runtime_error("Failed to push result value"))?; - } - - // Update instruction pointer - self.instruction_pointer - .fetch_add(max_instructions as u64, Ordering::Relaxed); - - // Consume some fuel - let fuel_to_consume = max_instructions.min(100) as u64; - let current_fuel = self.fuel.load(Ordering::Relaxed); - if current_fuel < fuel_to_consume { - self.fuel.store(0, Ordering::Relaxed); - return Ok(crate::stackless::ExecutionResult::FuelExhausted); - } - self.fuel - .fetch_sub(fuel_to_consume, Ordering::Relaxed); - - Ok(crate::stackless::ExecutionResult::Completed(results)) + // Step execution is not yet implemented - returning default values + // would silently mask this gap + Err(kiln_error::Error::runtime_execution_error( + "execute_function_step is not yet implemented", + )) } /// Restore engine state from a saved state @@ -10803,9 +11756,11 @@ impl StacklessEngine { #[cfg(feature = "tracing")] trace!(args = ?args, "[CABI_REALLOC] Arguments prepared"); - // Use execute_leaf_function instead of execute() to avoid nested trampolines. - // cabi_realloc is guaranteed by canonical ABI to be a leaf function (no calls). - let results = self.execute_leaf_function(instance_id, func_idx, args)?; + // Use full execute() trampoline - cabi_realloc in real components (e.g., Rust + // components using dlmalloc) makes internal calls, so the leaf function restriction + // is too strict. The canonical ABI only specifies the signature, not that it must + // be a leaf function. + let results = self.execute(instance_id, func_idx, args)?; if let Some(Value::I32(ptr)) = results.first() { Ok(*ptr as u32) @@ -11241,10 +12196,86 @@ impl StacklessEngine { // All WASI dispatch is now handled by WasiDispatcher via HostImportHandler trait // See kiln-wasi/src/dispatcher.rs for the implementation +// ============================================================ +// GC helper functions +// ============================================================ + +/// Returns the default value for a GC field based on its storage type. +#[cfg(feature = "std")] +fn gc_field_default_value(field: &crate::module::GcField) -> Value { + use crate::module::GcFieldStorage; + match &field.storage { + GcFieldStorage::I8 | GcFieldStorage::I16 => Value::I32(0), + GcFieldStorage::Value(byte) => match byte { + 0x7F => Value::I32(0), // i32 + 0x7E => Value::I64(0), // i64 + 0x7D => Value::F32(kiln_foundation::values::FloatBits32::from_f32(0.0)), // f32 + 0x7C => Value::F64(kiln_foundation::values::FloatBits64::from_f64(0.0)), // f64 + 0x7B => Value::V128(kiln_foundation::values::V128::zero()), // v128 + 0x70 | 0x6F => Value::FuncRef(None), // funcref / externref + 0x63 | 0x64 => Value::FuncRef(None), // ref null / ref + 0x6E => Value::I31Ref(None), // anyref + 0x6D => Value::I31Ref(None), // eqref + 0x6C => Value::I31Ref(None), // i31ref + 0x6B => Value::StructRef(None), // structref + 0x6A => Value::ArrayRef(None), // arrayref + 0x69 => Value::ExnRef(None), // exnref + _ => Value::I32(0), + }, + } +} + // ============================================================ // SIMD helper functions // ============================================================ +/// Test if a value matches a given heap type for ref.test/ref.cast +/// +/// The `allow_null` parameter controls whether null references pass the test: +/// - `false`: ref.test / ref.cast (non-nullable) - null fails +/// - `true`: ref.test null / ref.cast null (nullable) - null passes +fn ref_test_value(val: &Value, heap_type: &kiln_foundation::types::HeapType, allow_null: bool) -> bool { + use kiln_foundation::types::HeapType; + + // Check if the value is null + let is_null = is_null_ref(val); + + if is_null { + return allow_null; + } + + match heap_type { + HeapType::Func => matches!(val, Value::FuncRef(Some(_))), + HeapType::Extern => matches!(val, Value::ExternRef(Some(_))), + HeapType::Any => matches!(val, + Value::StructRef(Some(_)) | Value::ArrayRef(Some(_)) | Value::I31Ref(Some(_)) + ), + HeapType::Eq => matches!(val, + Value::StructRef(Some(_)) | Value::ArrayRef(Some(_)) | Value::I31Ref(Some(_)) + ), + HeapType::I31 => matches!(val, Value::I31Ref(Some(_))), + HeapType::Struct => matches!(val, Value::StructRef(Some(_))), + HeapType::Array => matches!(val, Value::ArrayRef(Some(_))), + HeapType::Exn => matches!(val, Value::ExnRef(Some(_))), + HeapType::None | HeapType::NoFunc | HeapType::NoExtern => false, + // Concrete type index - check if the value's type index matches + HeapType::Concrete(type_idx) => match val { + Value::StructRef(Some(sref)) => sref.type_index == *type_idx, + Value::ArrayRef(Some(aref)) => aref.type_index == *type_idx, + Value::FuncRef(Some(_fref)) => true, // FuncRef doesn't carry type index + _ => false, + }, + } +} + +/// Test if a value is a null reference +fn is_null_ref(val: &Value) -> bool { + matches!(val, + Value::FuncRef(None) | Value::ExternRef(None) | Value::StructRef(None) | + Value::ArrayRef(None) | Value::I31Ref(None) | Value::ExnRef(None) + ) +} + /// Pop a v128 value from the stack fn pop_v128(stack: &mut Vec) -> Result<[u8; 16]> { match stack.pop() { @@ -11594,6 +12625,51 @@ fn execute_simd_op(opcode: u32, stack: &mut Vec) -> Result<()> { 0x5E => { let a = pop_v128(stack)?; push_v128(stack, simd_ops::f32x4_demote_f64x2_zero(&a)); } 0x5F => { let a = pop_v128(stack)?; push_v128(stack, simd_ops::f64x2_promote_low_f32x4(&a)); } + // ============================================================ + // Relaxed SIMD operations (opcodes >= 0x100) + // ============================================================ + + // i8x16.relaxed_swizzle (0x100): [v128, v128] -> [v128] + 0x100 => { let b = pop_v128(stack)?; let a = pop_v128(stack)?; push_v128(stack, simd_ops::i8x16_relaxed_swizzle(&a, &b)); } + // i32x4.relaxed_trunc_f32x4_s (0x101): [v128] -> [v128] + 0x101 => { let a = pop_v128(stack)?; push_v128(stack, simd_ops::i32x4_relaxed_trunc_f32x4_s(&a)); } + // i32x4.relaxed_trunc_f32x4_u (0x102): [v128] -> [v128] + 0x102 => { let a = pop_v128(stack)?; push_v128(stack, simd_ops::i32x4_relaxed_trunc_f32x4_u(&a)); } + // i32x4.relaxed_trunc_f64x2_s_zero (0x103): [v128] -> [v128] + 0x103 => { let a = pop_v128(stack)?; push_v128(stack, simd_ops::i32x4_relaxed_trunc_f64x2_s_zero(&a)); } + // i32x4.relaxed_trunc_f64x2_u_zero (0x104): [v128] -> [v128] + 0x104 => { let a = pop_v128(stack)?; push_v128(stack, simd_ops::i32x4_relaxed_trunc_f64x2_u_zero(&a)); } + // f32x4.relaxed_madd (0x105): [v128, v128, v128] -> [v128] + 0x105 => { let c = pop_v128(stack)?; let b = pop_v128(stack)?; let a = pop_v128(stack)?; push_v128(stack, simd_ops::f32x4_relaxed_madd(&a, &b, &c)); } + // f32x4.relaxed_nmadd (0x106): [v128, v128, v128] -> [v128] + 0x106 => { let c = pop_v128(stack)?; let b = pop_v128(stack)?; let a = pop_v128(stack)?; push_v128(stack, simd_ops::f32x4_relaxed_nmadd(&a, &b, &c)); } + // f64x2.relaxed_madd (0x107): [v128, v128, v128] -> [v128] + 0x107 => { let c = pop_v128(stack)?; let b = pop_v128(stack)?; let a = pop_v128(stack)?; push_v128(stack, simd_ops::f64x2_relaxed_madd(&a, &b, &c)); } + // f64x2.relaxed_nmadd (0x108): [v128, v128, v128] -> [v128] + 0x108 => { let c = pop_v128(stack)?; let b = pop_v128(stack)?; let a = pop_v128(stack)?; push_v128(stack, simd_ops::f64x2_relaxed_nmadd(&a, &b, &c)); } + // i8x16.relaxed_laneselect (0x109): [v128, v128, v128] -> [v128] + 0x109 => { let c = pop_v128(stack)?; let b = pop_v128(stack)?; let a = pop_v128(stack)?; push_v128(stack, simd_ops::relaxed_laneselect(&a, &b, &c)); } + // i16x8.relaxed_laneselect (0x10A): [v128, v128, v128] -> [v128] + 0x10A => { let c = pop_v128(stack)?; let b = pop_v128(stack)?; let a = pop_v128(stack)?; push_v128(stack, simd_ops::relaxed_laneselect(&a, &b, &c)); } + // i32x4.relaxed_laneselect (0x10B): [v128, v128, v128] -> [v128] + 0x10B => { let c = pop_v128(stack)?; let b = pop_v128(stack)?; let a = pop_v128(stack)?; push_v128(stack, simd_ops::relaxed_laneselect(&a, &b, &c)); } + // i64x2.relaxed_laneselect (0x10C): [v128, v128, v128] -> [v128] + 0x10C => { let c = pop_v128(stack)?; let b = pop_v128(stack)?; let a = pop_v128(stack)?; push_v128(stack, simd_ops::relaxed_laneselect(&a, &b, &c)); } + // f32x4.relaxed_min (0x10D): [v128, v128] -> [v128] + 0x10D => { let b = pop_v128(stack)?; let a = pop_v128(stack)?; push_v128(stack, simd_ops::f32x4_relaxed_min(&a, &b)); } + // f32x4.relaxed_max (0x10E): [v128, v128] -> [v128] + 0x10E => { let b = pop_v128(stack)?; let a = pop_v128(stack)?; push_v128(stack, simd_ops::f32x4_relaxed_max(&a, &b)); } + // f64x2.relaxed_min (0x10F): [v128, v128] -> [v128] + 0x10F => { let b = pop_v128(stack)?; let a = pop_v128(stack)?; push_v128(stack, simd_ops::f64x2_relaxed_min(&a, &b)); } + // f64x2.relaxed_max (0x110): [v128, v128] -> [v128] + 0x110 => { let b = pop_v128(stack)?; let a = pop_v128(stack)?; push_v128(stack, simd_ops::f64x2_relaxed_max(&a, &b)); } + // i16x8.relaxed_q15mulr_s (0x111): [v128, v128] -> [v128] + 0x111 => { let b = pop_v128(stack)?; let a = pop_v128(stack)?; push_v128(stack, simd_ops::i16x8_relaxed_q15mulr_s(&a, &b)); } + // i16x8.relaxed_dot_i8x16_i7x16_s (0x112): [v128, v128] -> [v128] + 0x112 => { let b = pop_v128(stack)?; let a = pop_v128(stack)?; push_v128(stack, simd_ops::i16x8_relaxed_dot_i8x16_i7x16_s(&a, &b)); } + // i32x4.relaxed_dot_i8x16_i7x16_add_s (0x113): [v128, v128, v128] -> [v128] + 0x113 => { let c = pop_v128(stack)?; let b = pop_v128(stack)?; let a = pop_v128(stack)?; push_v128(stack, simd_ops::i32x4_relaxed_dot_i8x16_i7x16_add_s(&a, &b, &c)); } + _ => { return Err(kiln_error::Error::runtime_execution_error("Unimplemented SIMD opcode")); } diff --git a/kiln-runtime/src/stackless/simd_ops.rs b/kiln-runtime/src/stackless/simd_ops.rs index eeda9739..f7e588c8 100644 --- a/kiln-runtime/src/stackless/simd_ops.rs +++ b/kiln-runtime/src/stackless/simd_ops.rs @@ -1534,3 +1534,224 @@ fn wasm_nearest_f64(f: f64) -> f64 { result } } + +// ============================================================ +// Relaxed SIMD operations +// ============================================================ +// These implement deterministic behavior for relaxed SIMD operations. +// The relaxed SIMD proposal allows implementation-defined behavior, +// but we choose specific deterministic semantics. + +/// `i8x16.relaxed_swizzle` — same as regular swizzle (out-of-range indices yield 0). +#[inline] +pub fn i8x16_relaxed_swizzle(a: &[u8; 16], s: &[u8; 16]) -> [u8; 16] { + i8x16_swizzle(a, s) +} + +/// `i32x4.relaxed_trunc_f32x4_s` — saturating truncation, NaN yields 0. +#[inline] +pub fn i32x4_relaxed_trunc_f32x4_s(v: &[u8; 16]) -> [u8; 16] { + i32x4_trunc_sat_f32x4_s(v) +} + +/// `i32x4.relaxed_trunc_f32x4_u` — saturating truncation, NaN yields 0. +#[inline] +pub fn i32x4_relaxed_trunc_f32x4_u(v: &[u8; 16]) -> [u8; 16] { + i32x4_trunc_sat_f32x4_u(v) +} + +/// `i32x4.relaxed_trunc_f64x2_s_zero` — saturating truncation, NaN yields 0, high lanes zero. +#[inline] +pub fn i32x4_relaxed_trunc_f64x2_s_zero(v: &[u8; 16]) -> [u8; 16] { + i32x4_trunc_sat_f64x2_s_zero(v) +} + +/// `i32x4.relaxed_trunc_f64x2_u_zero` — saturating truncation, NaN yields 0, high lanes zero. +#[inline] +pub fn i32x4_relaxed_trunc_f64x2_u_zero(v: &[u8; 16]) -> [u8; 16] { + i32x4_trunc_sat_f64x2_u_zero(v) +} + +/// `f32x4.relaxed_madd(a, b, c)` — fused multiply-add: `a * b + c`. +#[inline] +pub fn f32x4_relaxed_madd(a: &[u8; 16], b: &[u8; 16], c: &[u8; 16]) -> [u8; 16] { + let mut r = [0u8; 16]; + for i in 0..4 { + let va = get_f32(a, i); + let vb = get_f32(b, i); + let vc = get_f32(c, i); + set_f32(&mut r, i, canonicalize_f32(va.mul_add(vb, vc))); + } + r +} + +/// `f32x4.relaxed_nmadd(a, b, c)` — fused negative multiply-add: `-(a * b) + c`. +#[inline] +pub fn f32x4_relaxed_nmadd(a: &[u8; 16], b: &[u8; 16], c: &[u8; 16]) -> [u8; 16] { + let mut r = [0u8; 16]; + for i in 0..4 { + let va = get_f32(a, i); + let vb = get_f32(b, i); + let vc = get_f32(c, i); + set_f32(&mut r, i, canonicalize_f32((-va).mul_add(vb, vc))); + } + r +} + +/// `f64x2.relaxed_madd(a, b, c)` — fused multiply-add: `a * b + c`. +#[inline] +pub fn f64x2_relaxed_madd(a: &[u8; 16], b: &[u8; 16], c: &[u8; 16]) -> [u8; 16] { + let mut r = [0u8; 16]; + for i in 0..2 { + let va = get_f64(a, i); + let vb = get_f64(b, i); + let vc = get_f64(c, i); + set_f64(&mut r, i, canonicalize_f64(va.mul_add(vb, vc))); + } + r +} + +/// `f64x2.relaxed_nmadd(a, b, c)` — fused negative multiply-add: `-(a * b) + c`. +#[inline] +pub fn f64x2_relaxed_nmadd(a: &[u8; 16], b: &[u8; 16], c: &[u8; 16]) -> [u8; 16] { + let mut r = [0u8; 16]; + for i in 0..2 { + let va = get_f64(a, i); + let vb = get_f64(b, i); + let vc = get_f64(c, i); + set_f64(&mut r, i, canonicalize_f64((-va).mul_add(vb, vc))); + } + r +} + +/// `iNxM.relaxed_laneselect(a, b, c)` — bitselect: for each bit, if c bit is 1 pick a, else b. +/// This is the same as v128.bitselect(a, b, c). +#[inline] +pub fn relaxed_laneselect(a: &[u8; 16], b: &[u8; 16], c: &[u8; 16]) -> [u8; 16] { + v128_bitselect(a, b, c) +} + +/// `f32x4.relaxed_min` — IEEE 754 minimum: propagate NaN, handle -0 < +0. +#[inline] +pub fn f32x4_relaxed_min(a: &[u8; 16], b: &[u8; 16]) -> [u8; 16] { + let mut r = [0u8; 16]; + for i in 0..4 { + let va = get_f32(a, i); + let vb = get_f32(b, i); + let result = if va.is_nan() || vb.is_nan() { + canonicalize_f32(f32::NAN) + } else if va == 0.0 && vb == 0.0 { + if va.is_sign_negative() { va } else { vb } + } else if va < vb { + va + } else { + vb + }; + set_f32(&mut r, i, result); + } + r +} + +/// `f32x4.relaxed_max` — IEEE 754 maximum: propagate NaN, handle +0 > -0. +#[inline] +pub fn f32x4_relaxed_max(a: &[u8; 16], b: &[u8; 16]) -> [u8; 16] { + let mut r = [0u8; 16]; + for i in 0..4 { + let va = get_f32(a, i); + let vb = get_f32(b, i); + let result = if va.is_nan() || vb.is_nan() { + canonicalize_f32(f32::NAN) + } else if va == 0.0 && vb == 0.0 { + if va.is_sign_positive() { va } else { vb } + } else if va > vb { + va + } else { + vb + }; + set_f32(&mut r, i, result); + } + r +} + +/// `f64x2.relaxed_min` — IEEE 754 minimum. +#[inline] +pub fn f64x2_relaxed_min(a: &[u8; 16], b: &[u8; 16]) -> [u8; 16] { + let mut r = [0u8; 16]; + for i in 0..2 { + let va = get_f64(a, i); + let vb = get_f64(b, i); + let result = if va.is_nan() || vb.is_nan() { + canonicalize_f64(f64::NAN) + } else if va == 0.0 && vb == 0.0 { + if va.is_sign_negative() { va } else { vb } + } else if va < vb { + va + } else { + vb + }; + set_f64(&mut r, i, result); + } + r +} + +/// `f64x2.relaxed_max` — IEEE 754 maximum. +#[inline] +pub fn f64x2_relaxed_max(a: &[u8; 16], b: &[u8; 16]) -> [u8; 16] { + let mut r = [0u8; 16]; + for i in 0..2 { + let va = get_f64(a, i); + let vb = get_f64(b, i); + let result = if va.is_nan() || vb.is_nan() { + canonicalize_f64(f64::NAN) + } else if va == 0.0 && vb == 0.0 { + if va.is_sign_positive() { va } else { vb } + } else if va > vb { + va + } else { + vb + }; + set_f64(&mut r, i, result); + } + r +} + +/// `i16x8.relaxed_q15mulr_s` — Q15 fixed-point multiply (same as saturating). +#[inline] +pub fn i16x8_relaxed_q15mulr_s(a: &[u8; 16], b: &[u8; 16]) -> [u8; 16] { + i16x8_q15mulr_sat_s(a, b) +} + +/// `i16x8.relaxed_dot_i8x16_i7x16_s` — dot product of signed i8x16 and "i7" (treated as signed) i8x16. +/// For each pair of adjacent i8 lanes, computes a_i * b_i (signed) and sums adjacent pairs into i16. +#[inline] +pub fn i16x8_relaxed_dot_i8x16_i7x16_s(a: &[u8; 16], b: &[u8; 16]) -> [u8; 16] { + let mut r = [0u8; 16]; + for i in 0..8 { + let a0 = a[i * 2] as i8 as i16; + let a1 = a[i * 2 + 1] as i8 as i16; + let b0 = b[i * 2] as i8 as i16; + let b1 = b[i * 2 + 1] as i8 as i16; + let sum = a0.wrapping_mul(b0).wrapping_add(a1.wrapping_mul(b1)); + set_i16(&mut r, i, sum); + } + r +} + +/// `i32x4.relaxed_dot_i8x16_i7x16_add_s` — dot product with accumulate. +/// Groups of 4 i8 lanes are multiplied (signed), summed, and added to c lanes. +#[inline] +pub fn i32x4_relaxed_dot_i8x16_i7x16_add_s(a: &[u8; 16], b: &[u8; 16], c: &[u8; 16]) -> [u8; 16] { + let mut r = [0u8; 16]; + for i in 0..4 { + let mut sum = 0i32; + for j in 0..4 { + let idx = i * 4 + j; + let av = a[idx] as i8 as i32; + let bv = b[idx] as i8 as i32; + sum = sum.wrapping_add(av.wrapping_mul(bv)); + } + let cv = get_i32(c, i); + set_i32(&mut r, i, sum.wrapping_add(cv)); + } + r +} diff --git a/kiln-runtime/src/table.rs b/kiln-runtime/src/table.rs index 0005389e..017c7c9b 100644 --- a/kiln-runtime/src/table.rs +++ b/kiln-runtime/src/table.rs @@ -92,7 +92,13 @@ fn usize_to_wasm_u32(size: usize) -> Result { }) } -/// Type alias for the inner elements storage +/// Type alias for the inner elements storage. +/// In std mode, use Vec to avoid BoundedVec serialization issues with +/// variable-size GC types (StructRef, ArrayRef) that exceed the fixed +/// VALUE_SERIALIZED_SIZE when serialized. +#[cfg(feature = "std")] +type TableElements = Vec>; +#[cfg(not(feature = "std"))] type TableElements = kiln_foundation::bounded::BoundedVec, 1024, TableProvider>; /// A WebAssembly table is a vector of opaque values of a single type. @@ -130,10 +136,6 @@ impl Debug for Table { impl Clone for Table { fn clone(&self) -> Self { - let mut new_elements: TableElements = - kiln_foundation::bounded::BoundedVec::new(TableProvider::default()) - .expect("Failed to allocate table elements during clone"); - // Lock the source elements for reading #[cfg(feature = "std")] let source_elements = self.elements.lock() @@ -141,15 +143,22 @@ impl Clone for Table { #[cfg(not(feature = "std"))] let source_elements = self.elements.lock(); - for i in 0..source_elements.len() { - // Use BoundedVec get method for safe access - if let Ok(elem) = source_elements.get(i) { - assert!( - new_elements.push(elem.clone()).is_ok(), - "Failed to clone table: out of memory" - ); + #[cfg(feature = "std")] + let new_elements: TableElements = source_elements.clone(); + #[cfg(not(feature = "std"))] + let new_elements: TableElements = { + let mut ne = kiln_foundation::bounded::BoundedVec::new(TableProvider::default()) + .expect("Failed to allocate table elements during clone"); + for i in 0..source_elements.len() { + if let Ok(elem) = source_elements.get(i) { + assert!( + ne.push(elem.clone()).is_ok(), + "Failed to clone table: out of memory" + ); + } } - } + ne + }; Self { ty: self.ty.clone(), @@ -185,18 +194,27 @@ impl PartialEq for Table { #[cfg(not(feature = "std"))] let other_elements = other.elements.lock(); - // Compare elements manually since BoundedStack doesn't have to_vec() if self_elements.len() != other_elements.len() { return false; } - for i in 0..self_elements.len() { - // Use get() method instead of direct indexing for BoundedVec - let (self_elem, other_elem) = match (self_elements.get(i), other_elements.get(i)) { - (Ok(a), Ok(b)) => (a, b), - _ => return false, - }; - if self_elem != other_elem { - return false; + #[cfg(feature = "std")] + { + for i in 0..self_elements.len() { + if self_elements[i] != other_elements[i] { + return false; + } + } + } + #[cfg(not(feature = "std"))] + { + for i in 0..self_elements.len() { + let (self_elem, other_elem) = match (self_elements.get(i), other_elements.get(i)) { + (Ok(a), Ok(b)) => (a, b), + _ => return false, + }; + if self_elem != other_elem { + return false; + } } } true @@ -306,24 +324,31 @@ impl Table { #[cfg(feature = "tracing")] kiln_foundation::tracing::trace!(capacity = 1024, elements = initial_size, "Creating Table BoundedVec"); - let mut elements: TableElements = - kiln_foundation::bounded::BoundedVec::new(TableProvider::default()).map_err(|e| { - #[cfg(feature = "tracing")] - kiln_foundation::tracing::error!(error = ?e, "BoundedVec::new failed"); - e - })?; - // Note: BoundedVec doesn't have set_verification_level method - - #[cfg(feature = "tracing")] - kiln_foundation::tracing::trace!(elements = initial_size, "Pushing elements to table"); - - for i in 0..initial_size { - if let Err(e) = elements.push(init_val.clone()) { - #[cfg(feature = "tracing")] - kiln_foundation::tracing::error!(index = i, error = ?e, "Failed to push element"); - return Err(e.into()); + #[cfg(feature = "std")] + let elements: TableElements = { + let mut elems = Vec::with_capacity(initial_size); + for _ in 0..initial_size { + elems.push(init_val.clone()); } - } + elems + }; + #[cfg(not(feature = "std"))] + let elements: TableElements = { + let mut elems: TableElements = + kiln_foundation::bounded::BoundedVec::new(TableProvider::default()).map_err(|e| { + #[cfg(feature = "tracing")] + kiln_foundation::tracing::error!(error = ?e, "BoundedVec::new failed"); + e + })?; + for i in 0..initial_size { + if let Err(e) = elems.push(init_val.clone()) { + #[cfg(feature = "tracing")] + kiln_foundation::tracing::error!(index = i, error = ?e, "Failed to push element"); + return Err(e.into()); + } + } + elems + }; Ok(Self { ty, @@ -410,10 +435,16 @@ impl Table { } } - // Use BoundedVec's get method for direct access - elements - .get(idx_usize) - .map_err(|_| Error::invalid_function_index("Table index out of bounds")) + #[cfg(feature = "std")] + { + Ok(elements[idx_usize].clone()) + } + #[cfg(not(feature = "std"))] + { + elements + .get(idx_usize) + .map_err(|_| Error::invalid_function_index("Table index out of bounds")) + } } /// Sets an element at the specified index @@ -461,7 +492,14 @@ impl Table { )); } } - elements.set(idx_usize, value)?; + #[cfg(feature = "std")] + { + elements[idx_usize] = value; + } + #[cfg(not(feature = "std"))] + { + elements.set(idx_usize, value)?; + } Ok(()) } @@ -512,7 +550,14 @@ impl Table { )); } } - elements.set(idx_usize, value)?; + #[cfg(feature = "std")] + { + elements[idx_usize] = value; + } + #[cfg(not(feature = "std"))] + { + elements.set(idx_usize, value)?; + } Ok(()) } @@ -572,6 +617,9 @@ impl Table { let mut elements = self.elements.lock(); for _ in 0..delta { + #[cfg(feature = "std")] + elements.push(Some(init_value_from_arg.clone())); + #[cfg(not(feature = "std"))] elements.push(Some(init_value_from_arg.clone()))?; } @@ -605,23 +653,25 @@ impl Table { return Ok(()); } - // Create a new stack with the filled elements - let mut result_vec: TableElements = - kiln_foundation::bounded::BoundedVec::new(TableProvider::default()).unwrap(); - - // Copy elements with fill applied - for i in 0..elements.len() { - if i >= offset && i < offset + len { - // This is in the fill range - result_vec.push(value.clone())?; - } else { - // Outside fill range, use original value - result_vec.push(elements.get(i)?)?; + #[cfg(feature = "std")] + { + for i in offset..offset + len { + elements[i] = value.clone(); } } - - // Replace the elements stack - *elements = result_vec; + #[cfg(not(feature = "std"))] + { + let mut result_vec: TableElements = + kiln_foundation::bounded::BoundedVec::new(TableProvider::default()).unwrap(); + for i in 0..elements.len() { + if i >= offset && i < offset + len { + result_vec.push(value.clone())?; + } else { + result_vec.push(elements.get(i)?)?; + } + } + *elements = result_vec; + } Ok(()) } @@ -650,33 +700,33 @@ impl Table { return Ok(()); } - // Create temporary stack to store elements during copy - let mut temp_vec: TableElements = - kiln_foundation::bounded::BoundedVec::new(TableProvider::default()).unwrap(); - - // Read source elements into temporary stack - for i in 0..len { - temp_vec.push(elements.get(src + i)?)?; + #[cfg(feature = "std")] + { + // Copy source elements to temp buffer, then write to destination + let temp: Vec<_> = (0..len).map(|i| elements[src + i].clone()).collect(); + for i in 0..len { + elements[dst + i] = temp[i].clone(); + } } - - // Create a new stack for the full result - let mut result_vec: TableElements = - kiln_foundation::bounded::BoundedVec::new(TableProvider::default()).unwrap(); - - // Copy elements with the updated values - for i in 0..elements.len() { - if i >= dst && i < dst + len { - // This is in the destination range, use value from temp_vec - result_vec.push(temp_vec.get(i - dst)?)?; - } else { - // Outside destination range, use original value - result_vec.push(elements.get(i)?)?; + #[cfg(not(feature = "std"))] + { + let mut temp_vec: TableElements = + kiln_foundation::bounded::BoundedVec::new(TableProvider::default()).unwrap(); + for i in 0..len { + temp_vec.push(elements.get(src + i)?)?; } + let mut result_vec: TableElements = + kiln_foundation::bounded::BoundedVec::new(TableProvider::default()).unwrap(); + for i in 0..elements.len() { + if i >= dst && i < dst + len { + result_vec.push(temp_vec.get(i - dst)?)?; + } else { + result_vec.push(elements.get(i)?)?; + } + } + *elements = result_vec; } - // Replace the elements stack - *elements = result_vec; - Ok(()) } @@ -702,6 +752,9 @@ impl Table { return Err(Error::validation_error("Table init value type mismatch")); } } + #[cfg(feature = "std")] + { elements[(offset as usize) + i] = val_opt.clone(); } + #[cfg(not(feature = "std"))] elements.set((offset as usize) + i, val_opt.clone())?; } Ok(()) @@ -762,11 +815,11 @@ impl Table { let mut elements = self.elements.lock(); for _ in 0..delta { + #[cfg(feature = "std")] + elements.push(Some(init_value_from_arg.clone())); + #[cfg(not(feature = "std"))] elements.push(Some(init_value_from_arg.clone()))?; } - // Update the min limit in the table type if it changes due to growth (spec is a - // bit unclear if ty should reflect current size) For now, ty.limits.min - // reflects the *initial* min. Current size is self.size(). Ok(old_size) } @@ -831,6 +884,9 @@ impl Table { return Err(Error::validation_error("Table init value type mismatch")); } } + #[cfg(feature = "std")] + { elements[(offset as usize) + i] = val_opt.clone(); } + #[cfg(not(feature = "std"))] elements.set((offset as usize) + i, val_opt.clone())?; } Ok(()) @@ -858,35 +914,32 @@ impl Table { return Ok(()); } - // Create temporary stack to store elements during copy - let mut temp_vec: TableElements = - kiln_foundation::bounded::BoundedVec::new(TableProvider::default()).unwrap(); - // Note: verification level handled by provider - - // Read source elements into temporary stack - for i in 0..len { - temp_vec.push(elements.get(src + i)?)?; + #[cfg(feature = "std")] + { + let temp: Vec<_> = (0..len).map(|i| elements[src + i].clone()).collect(); + for i in 0..len { + elements[dst + i] = temp[i].clone(); + } } - - // Create a new stack for the full result - let mut result_vec: TableElements = - kiln_foundation::bounded::BoundedVec::new(TableProvider::default()).unwrap(); - // Note: verification level handled by provider - - // Copy elements with the updated values - for i in 0..elements.len() { - if i >= dst && i < dst + len { - // This is in the destination range, use value from temp_vec - result_vec.push(temp_vec.get(i - dst)?)?; - } else { - // Outside destination range, use original value - result_vec.push(elements.get(i)?)?; + #[cfg(not(feature = "std"))] + { + let mut temp_vec: TableElements = + kiln_foundation::bounded::BoundedVec::new(TableProvider::default()).unwrap(); + for i in 0..len { + temp_vec.push(elements.get(src + i)?)?; + } + let mut result_vec: TableElements = + kiln_foundation::bounded::BoundedVec::new(TableProvider::default()).unwrap(); + for i in 0..elements.len() { + if i >= dst && i < dst + len { + result_vec.push(temp_vec.get(i - dst)?)?; + } else { + result_vec.push(elements.get(i)?)?; + } } + *elements = result_vec; } - // Replace the elements stack - *elements = result_vec; - Ok(()) } @@ -915,23 +968,25 @@ impl Table { return Ok(()); } - // Create a new stack with the filled elements - let mut result_vec: TableElements = - kiln_foundation::bounded::BoundedVec::new(TableProvider::default()).unwrap(); - - // Copy elements with fill applied - for i in 0..elements.len() { - if i >= offset && i < offset + len { - // This is in the fill range - result_vec.push(value.clone())?; - } else { - // Outside fill range, use original value - result_vec.push(elements.get(i)?)?; + #[cfg(feature = "std")] + { + for i in offset..offset + len { + elements[i] = value.clone(); } } - - // Replace the elements stack - *elements = result_vec; + #[cfg(not(feature = "std"))] + { + let mut result_vec: TableElements = + kiln_foundation::bounded::BoundedVec::new(TableProvider::default()).unwrap(); + for i in 0..elements.len() { + if i >= offset && i < offset + len { + result_vec.push(value.clone())?; + } else { + result_vec.push(elements.get(i)?)?; + } + } + *elements = result_vec; + } Ok(()) } @@ -970,8 +1025,14 @@ impl Table { return Err(Error::runtime_trap("out of bounds table access")); } - // Set the element directly using BoundedVec's set method - elements.set(idx, value)?; + #[cfg(feature = "std")] + { + elements[idx] = value; + } + #[cfg(not(feature = "std"))] + { + elements.set(idx, value)?; + } Ok(()) } diff --git a/kiln-wasi/src/dispatcher.rs b/kiln-wasi/src/dispatcher.rs index 701091f1..81bb26b3 100644 --- a/kiln-wasi/src/dispatcher.rs +++ b/kiln-wasi/src/dispatcher.rs @@ -828,6 +828,72 @@ impl WasiDispatcher { } } + #[cfg(all(feature = "wasi-filesystem", feature = "std"))] + ("wasi:filesystem/types", "[method]descriptor.stat-at") => { + // Check filesystem capability + if !self.capabilities.filesystem.metadata_access { + return Err(Error::wasi_permission_denied("Metadata access denied")); + } + + // Args: [descriptor_handle, path_flags, path] + let base_handle = match args.first() { + Some(Value::U32(h)) => *h, + _ => return Err(Error::wasi_invalid_argument("Invalid descriptor")), + }; + + let path = match args.get(1) { + Some(Value::String(s)) => s.clone(), + _ => { + // Try arg index 2 if path_flags is present + match args.get(2) { + Some(Value::String(s)) => s.clone(), + _ => return Err(Error::wasi_invalid_argument("Invalid path")), + } + } + }; + + // Look up the base descriptor + let base_entry = self.fd_table.get(&base_handle) + .ok_or_else(|| Error::wasi_invalid_fd("Bad descriptor"))?; + + // Get the base path + let base_path = match &base_entry.fd_type { + FileDescriptorType::PreopenDirectory(p) => p.clone(), + FileDescriptorType::RegularFile(p) => { + // If it's a file, use its parent directory + p.parent().map(|p| p.to_path_buf()) + .unwrap_or_else(|| p.clone()) + } + _ => return Err(Error::wasi_invalid_argument("Not a directory descriptor")), + }; + + // Construct full path + let full_path = base_path.join(&path); + + match std::fs::metadata(&full_path) { + Ok(meta) => { + let file_type = if meta.is_dir() { 3u8 } else if meta.is_file() { 6u8 } else { 0u8 }; + let size = meta.len(); + + let stat_record = Value::Record(vec![ + ("type".to_string(), Value::U8(file_type)), + ("size".to_string(), Value::U64(size)), + ]); + + #[cfg(feature = "tracing")] + trace!(path = %full_path.display(), file_type = file_type, size = size, "stat-at completed"); + + Ok(vec![Value::Result(Ok(Box::new(stat_record)))]) + } + Err(_e) => { + #[cfg(feature = "tracing")] + warn!(path = %full_path.display(), error = %_e, "stat-at failed"); + // Return error code - ENOENT + Ok(vec![Value::Result(Err(Box::new(Value::U32(2))))]) + } + } + } + #[cfg(all(feature = "wasi-filesystem", feature = "std"))] ("wasi:filesystem/types", "[method]descriptor.read-via-stream") => { // Check read capability @@ -1171,8 +1237,8 @@ impl WasiDispatcher { let base_interface = Self::strip_version(interface); - // DEBUG: trace WASI calls - eprintln!("[WASI-TRACE] {}::{} args={:?}", base_interface, function, args); + #[cfg(feature = "tracing")] + trace!(interface = %base_interface, function = %function, args = ?args, "[WASI] dispatch_core"); match (base_interface, function) { // Simple functions that don't need memory @@ -2005,6 +2071,59 @@ impl WasiDispatcher { Ok(vec![CoreValue::I32(data.len() as i32)]) } + // ================================================================ + // wasi:nn/* - WASI-NN 0.2.0-rc-2024-10-28 resource-based interface + // + // The new WASI-NN spec uses resources (graph, tensor, + // graph-execution-context, error) instead of flat function calls. + // These handlers provide explicit "not configured" errors so that + // components that import wasi:nn can link and get a clear error + // at runtime rather than a link-time failure. + // ================================================================ + + // wasi:nn/graph.load(builder, encoding, target) -> result + ("wasi:nn/graph", "load") => { + return Err(kiln_error::Error::wasi_unsupported_operation( + "WASI-NN backend not configured. Enable the wasi-nn feature and configure \ + an NN backend (e.g., tract, onnx-runtime) to use neural network inference.", + )); + } + + // wasi:nn/graph.[method]graph.init-execution-context + ("wasi:nn/graph", "[method]graph.init-execution-context") => { + return Err(kiln_error::Error::wasi_unsupported_operation( + "WASI-NN backend not configured: cannot create execution context.", + )); + } + + // wasi:nn/inference.[method]graph-execution-context.compute + ("wasi:nn/inference", "[method]graph-execution-context.compute") => { + return Err(kiln_error::Error::wasi_unsupported_operation( + "WASI-NN backend not configured: cannot execute inference.", + )); + } + + // wasi:nn/tensor.[constructor]tensor + ("wasi:nn/tensor", "[constructor]tensor") => { + return Err(kiln_error::Error::wasi_unsupported_operation( + "WASI-NN backend not configured: cannot create tensor.", + )); + } + + // wasi:nn/tensor.[method]tensor.data + ("wasi:nn/tensor", "[method]tensor.data") => { + return Err(kiln_error::Error::wasi_unsupported_operation( + "WASI-NN backend not configured: cannot read tensor data.", + )); + } + + // wasi:nn/errors.[method]error.code + ("wasi:nn/errors", "[method]error.code") => { + return Err(kiln_error::Error::wasi_unsupported_operation( + "WASI-NN backend not configured: cannot get error code.", + )); + } + // ================================================================ // wasi:random/* - Random number generation interfaces // ================================================================ @@ -2084,6 +2203,449 @@ impl WasiDispatcher { Ok(vec![CoreValue::I64(val as i64)]) } + // wasi:random/insecure-seed - provides non-cryptographic seed + #[cfg(feature = "wasi-random")] + ("wasi:random/insecure-seed", "insecure-seed") => { + use kiln_platform::random::PlatformRandom; + + // Return a tuple of (u64, u64) as the insecure seed + let mut bytes = [0u8; 16]; + PlatformRandom::get_secure_bytes(&mut bytes) + .map_err(|_| Error::wasi_capability_unavailable("Random not available"))?; + let val1 = u64::from_le_bytes([ + bytes[0], bytes[1], bytes[2], bytes[3], + bytes[4], bytes[5], bytes[6], bytes[7], + ]); + let val2 = u64::from_le_bytes([ + bytes[8], bytes[9], bytes[10], bytes[11], + bytes[12], bytes[13], bytes[14], bytes[15], + ]); + + Ok(vec![CoreValue::I64(val1 as i64), CoreValue::I64(val2 as i64)]) + } + + // ================================================================ + // wasi:filesystem/* - Filesystem interfaces (core ABI) + // ================================================================ + + #[cfg(all(feature = "wasi-filesystem", feature = "std"))] + ("wasi:filesystem/preopens", "get-directories") => { + // Canonical ABI: get-directories() -> list, string>> + // Lowered: (retptr: i32) -> void + // At retptr, write (list_ptr: i32, list_len: i32) + // Each list element: (descriptor: i32, string_ptr: i32, string_len: i32) = 12 bytes + + if !self.capabilities.filesystem.directory_access { + return Err(Error::wasi_permission_denied("Filesystem access denied")); + } + + let retptr = match args.first() { + Some(CoreValue::I32(p)) => *p as u32, + _ => return Ok(vec![]), + }; + + let mem = memory.ok_or_else(|| + Error::wasi_capability_unavailable("Memory required for get-directories"))?; + + if self.preopens.is_empty() { + // Empty list: write (0, 0) + mem.write_bytes(retptr, &0u32.to_le_bytes())?; + mem.write_bytes(retptr + 4, &0u32.to_le_bytes())?; + return Ok(vec![]); + } + + // Calculate total memory needed + let count = self.preopens.len(); + let list_elem_size = 12u32; // (handle: i32, str_ptr: i32, str_len: i32) + let list_data_size = count as u32 * list_elem_size; + + // Collect path bytes + let path_bytes: Vec> = self.preopens.iter() + .map(|(_, p)| p.to_string_lossy().as_bytes().to_vec()) + .collect(); + let total_string_bytes: u32 = path_bytes.iter().map(|b| b.len() as u32).sum(); + + // Use a simple allocation scheme: pack everything after retptr + 8 + // Layout: [list_elements...][string_data...] + let list_base = retptr + 8; + let string_base = list_base + list_data_size; + + // Check memory bounds + let total_needed = string_base + total_string_bytes; + if total_needed > mem.size() as u32 { + return Err(Error::wasi_invalid_argument("Not enough memory for get-directories result")); + } + + // Write list elements and string data + let mut string_offset = string_base; + for (i, (handle, _)) in self.preopens.iter().enumerate() { + let elem_offset = list_base + (i as u32) * list_elem_size; + let str_bytes = &path_bytes[i]; + let str_len = str_bytes.len() as u32; + + // Write handle + mem.write_bytes(elem_offset, &(*handle as i32).to_le_bytes())?; + // Write string ptr + mem.write_bytes(elem_offset + 4, &string_offset.to_le_bytes())?; + // Write string len + mem.write_bytes(elem_offset + 8, &str_len.to_le_bytes())?; + + // Write string data + mem.write_bytes(string_offset, str_bytes)?; + string_offset += str_len; + } + + // Write (list_ptr, list_len) at retptr + mem.write_bytes(retptr, &list_base.to_le_bytes())?; + mem.write_bytes(retptr + 4, &(count as u32).to_le_bytes())?; + + #[cfg(feature = "tracing")] + trace!( + count = count, + retptr = format_args!("0x{:x}", retptr), + list_base = format_args!("0x{:x}", list_base), + "get-directories wrote to memory" + ); + + Ok(vec![]) + } + + #[cfg(all(feature = "wasi-filesystem", feature = "std"))] + ("wasi:filesystem/types", "[method]descriptor.stat-at") => { + // Canonical ABI: stat-at(self, path-flags, path) -> result + // Lowered: (handle: i32, path_flags: i32, path_ptr: i32, path_len: i32, retptr: i32) + // Result layout at retptr: (discriminant: i32, payload...) + // Ok: (0, type: i32, ..padding.., size: i64, ...) + // Err: (1, error_code: i32) + + if !self.capabilities.filesystem.metadata_access { + return Err(Error::wasi_permission_denied("Metadata access denied")); + } + + let mem = memory.ok_or_else(|| + Error::wasi_capability_unavailable("Memory required for stat-at"))?; + + // Parse args: handle, path_flags, path_ptr, path_len, retptr + let base_handle = match args.first() { + Some(CoreValue::I32(h)) => *h as u32, + _ => return Err(Error::wasi_invalid_argument("Invalid descriptor handle")), + }; + + let path_ptr = match args.get(2) { + Some(CoreValue::I32(p)) => *p as u32, + _ => return Err(Error::wasi_invalid_argument("Invalid path pointer")), + }; + + let path_len = match args.get(3) { + Some(CoreValue::I32(l)) => *l as u32, + _ => return Err(Error::wasi_invalid_argument("Invalid path length")), + }; + + let retptr = match args.get(4) { + Some(CoreValue::I32(p)) => *p as u32, + _ => return Err(Error::wasi_invalid_argument("Missing retptr for stat-at")), + }; + + // Read path from memory + let mut path_bytes = vec![0u8; path_len as usize]; + mem.read_bytes(path_ptr, &mut path_bytes)?; + let path_str = String::from_utf8_lossy(&path_bytes); + + // Look up base descriptor + let base_entry = self.fd_table.get(&base_handle) + .ok_or_else(|| Error::wasi_invalid_fd("Bad descriptor"))?; + + let base_path = match &base_entry.fd_type { + FileDescriptorType::PreopenDirectory(p) => p.clone(), + FileDescriptorType::RegularFile(p) => { + p.parent().map(|p| p.to_path_buf()).unwrap_or_else(|| p.clone()) + } + _ => return Err(Error::wasi_invalid_argument("Not a directory descriptor")), + }; + + let full_path = base_path.join(path_str.as_ref()); + + match std::fs::metadata(&full_path) { + Ok(meta) => { + let file_type: u8 = if meta.is_dir() { 3 } else if meta.is_file() { 6 } else { 0 }; + let size = meta.len(); + + // Write Ok result: discriminant=0 + mem.write_bytes(retptr, &0u32.to_le_bytes())?; + // descriptor-stat record: type (u8 as i32), then padding, then size (u64) + // WASI descriptor-stat layout: + // type: descriptor-type (1 byte, aligned to 1) + // link-count: u64 (8 bytes, aligned to 8) + // size: u64 (8 bytes, aligned to 8) + // data-access-timestamp: option + // data-modification-timestamp: option + // status-change-timestamp: option + // Simplified: write type at offset 8, link-count at 16, size at 24 + mem.write_bytes(retptr + 8, &(file_type as u64).to_le_bytes())?; + mem.write_bytes(retptr + 16, &1u64.to_le_bytes())?; // link-count + mem.write_bytes(retptr + 24, &size.to_le_bytes())?; // size + // timestamps: write 0 (none) for all three option + mem.write_bytes(retptr + 32, &0u64.to_le_bytes())?; + mem.write_bytes(retptr + 40, &0u64.to_le_bytes())?; + mem.write_bytes(retptr + 48, &0u64.to_le_bytes())?; + + #[cfg(feature = "tracing")] + trace!(path = %full_path.display(), file_type = file_type, size = size, "stat-at OK"); + } + Err(_e) => { + // Write Err result: discriminant=1, error_code + mem.write_bytes(retptr, &1u32.to_le_bytes())?; + let error_code: u32 = if _e.kind() == std::io::ErrorKind::NotFound { 2 } else { 28 }; // ENOENT or EINVAL + mem.write_bytes(retptr + 4, &error_code.to_le_bytes())?; + + #[cfg(feature = "tracing")] + trace!(path = %full_path.display(), error = %_e, "stat-at Err"); + } + } + + Ok(vec![]) + } + + #[cfg(all(feature = "wasi-filesystem", feature = "std"))] + ("wasi:filesystem/types", "[method]descriptor.stat") => { + // Canonical ABI: stat(self) -> result + // Lowered: (handle: i32, retptr: i32) + + if !self.capabilities.filesystem.metadata_access { + return Err(Error::wasi_permission_denied("Metadata access denied")); + } + + let mem = memory.ok_or_else(|| + Error::wasi_capability_unavailable("Memory required for stat"))?; + + let handle = match args.first() { + Some(CoreValue::I32(h)) => *h as u32, + _ => return Err(Error::wasi_invalid_argument("Invalid descriptor")), + }; + + let retptr = match args.get(1) { + Some(CoreValue::I32(p)) => *p as u32, + _ => return Err(Error::wasi_invalid_argument("Missing retptr for stat")), + }; + + let entry = self.fd_table.get(&handle) + .ok_or_else(|| Error::wasi_invalid_fd("Bad descriptor"))?; + + let path = match &entry.fd_type { + FileDescriptorType::RegularFile(p) | FileDescriptorType::PreopenDirectory(p) => p, + _ => return Err(Error::wasi_invalid_argument("Cannot stat this descriptor type")), + }; + + match std::fs::metadata(path) { + Ok(meta) => { + let file_type: u8 = if meta.is_dir() { 3 } else if meta.is_file() { 6 } else { 0 }; + let size = meta.len(); + mem.write_bytes(retptr, &0u32.to_le_bytes())?; // Ok discriminant + mem.write_bytes(retptr + 8, &(file_type as u64).to_le_bytes())?; + mem.write_bytes(retptr + 16, &1u64.to_le_bytes())?; + mem.write_bytes(retptr + 24, &size.to_le_bytes())?; + mem.write_bytes(retptr + 32, &0u64.to_le_bytes())?; + mem.write_bytes(retptr + 40, &0u64.to_le_bytes())?; + mem.write_bytes(retptr + 48, &0u64.to_le_bytes())?; + } + Err(_e) => { + mem.write_bytes(retptr, &1u32.to_le_bytes())?; + let error_code: u32 = if _e.kind() == std::io::ErrorKind::NotFound { 2 } else { 28 }; + mem.write_bytes(retptr + 4, &error_code.to_le_bytes())?; + } + } + + Ok(vec![]) + } + + #[cfg(all(feature = "wasi-filesystem", feature = "std"))] + ("wasi:filesystem/types", "[method]descriptor.open-at") => { + // Canonical ABI: open-at(self, path-flags, path, open-flags, descriptor-flags) + // -> result, error-code> + // Lowered: (handle, path_flags, path_ptr, path_len, open_flags, desc_flags, retptr) + + if !self.capabilities.filesystem.read_access && !self.capabilities.filesystem.write_access { + return Err(Error::wasi_permission_denied("Filesystem access denied")); + } + + let mem = memory.ok_or_else(|| + Error::wasi_capability_unavailable("Memory required for open-at"))?; + + let base_handle = match args.first() { + Some(CoreValue::I32(h)) => *h as u32, + _ => return Err(Error::wasi_invalid_argument("Invalid descriptor")), + }; + + let path_ptr = match args.get(2) { + Some(CoreValue::I32(p)) => *p as u32, + _ => return Err(Error::wasi_invalid_argument("Invalid path pointer")), + }; + + let path_len = match args.get(3) { + Some(CoreValue::I32(l)) => *l as u32, + _ => return Err(Error::wasi_invalid_argument("Invalid path length")), + }; + + let retptr = match args.last() { + Some(CoreValue::I32(p)) => *p as u32, + _ => return Err(Error::wasi_invalid_argument("Missing retptr for open-at")), + }; + + // Read path from memory + let mut path_bytes = vec![0u8; path_len as usize]; + mem.read_bytes(path_ptr, &mut path_bytes)?; + let path_str = String::from_utf8_lossy(&path_bytes); + + let base_entry = self.fd_table.get(&base_handle) + .ok_or_else(|| Error::wasi_invalid_fd("Bad descriptor"))?; + + let base_path = match &base_entry.fd_type { + FileDescriptorType::PreopenDirectory(p) => p.clone(), + _ => return Err(Error::wasi_invalid_argument("Not a directory descriptor")), + }; + + let full_path = base_path.join(path_str.as_ref()); + + // Safety check - path should not escape sandbox + if let Ok(canonical) = full_path.canonicalize() { + if !canonical.starts_with(&base_path) { + return Err(Error::wasi_permission_denied("Path escapes sandbox")); + } + } + + // Allocate new handle + let path_str_owned = full_path.to_string_lossy(); + let new_handle = self.resource_manager.create_file_descriptor( + &path_str_owned, + self.capabilities.filesystem.read_access, + self.capabilities.filesystem.write_access, + )?; + + self.fd_table.insert(new_handle, FileDescriptorEntry { + fd_type: if full_path.is_dir() { + FileDescriptorType::PreopenDirectory(full_path.clone()) + } else { + FileDescriptorType::RegularFile(full_path.clone()) + }, + read: self.capabilities.filesystem.read_access, + write: self.capabilities.filesystem.write_access, + }); + + // Write result, error-code> to retptr + // Ok: discriminant=0, handle + mem.write_bytes(retptr, &0u32.to_le_bytes())?; + mem.write_bytes(retptr + 4, &(new_handle as i32).to_le_bytes())?; + + #[cfg(feature = "tracing")] + trace!(path = %full_path.display(), handle = new_handle, "open-at OK"); + + Ok(vec![]) + } + + #[cfg(all(feature = "wasi-filesystem", feature = "std"))] + ("wasi:filesystem/types", "[method]descriptor.read-via-stream") => { + // Canonical ABI: read-via-stream(self, offset) -> result, error-code> + // Lowered: (handle: i32, offset: i64, retptr: i32) + + if !self.capabilities.filesystem.read_access { + return Err(Error::wasi_permission_denied("Read access denied")); + } + + let handle = match args.first() { + Some(CoreValue::I32(h)) => *h as u32, + _ => return Err(Error::wasi_invalid_argument("Invalid descriptor")), + }; + + let retptr = match args.last() { + Some(CoreValue::I32(p)) => *p as u32, + _ => return Err(Error::wasi_invalid_argument("Missing retptr")), + }; + + let _entry = self.fd_table.get(&handle) + .ok_or_else(|| Error::wasi_invalid_fd("Bad descriptor"))?; + + // Return the same handle as a stream handle + if let Some(mem) = memory { + mem.write_bytes(retptr, &0u32.to_le_bytes())?; // Ok discriminant + mem.write_bytes(retptr + 4, &(handle as i32).to_le_bytes())?; + } + + Ok(vec![]) + } + + #[cfg(all(feature = "wasi-filesystem", feature = "std"))] + ("wasi:filesystem/types", "[method]descriptor.write-via-stream") => { + // Canonical ABI: write-via-stream(self, offset) -> result, error-code> + // Lowered: (handle: i32, offset: i64, retptr: i32) + + if !self.capabilities.filesystem.write_access { + return Err(Error::wasi_permission_denied("Write access denied")); + } + + let handle = match args.first() { + Some(CoreValue::I32(h)) => *h as u32, + _ => return Err(Error::wasi_invalid_argument("Invalid descriptor")), + }; + + let retptr = match args.last() { + Some(CoreValue::I32(p)) => *p as u32, + _ => return Err(Error::wasi_invalid_argument("Missing retptr")), + }; + + let _entry = self.fd_table.get(&handle) + .ok_or_else(|| Error::wasi_invalid_fd("Bad descriptor"))?; + + if let Some(mem) = memory { + mem.write_bytes(retptr, &0u32.to_le_bytes())?; // Ok discriminant + mem.write_bytes(retptr + 4, &(handle as i32).to_le_bytes())?; + } + + Ok(vec![]) + } + + #[cfg(all(feature = "wasi-filesystem", feature = "std"))] + ("wasi:filesystem/types", "[method]descriptor.get-type") => { + // Canonical ABI: get-type(self) -> result + // Lowered: (handle: i32, retptr: i32) + + let handle = match args.first() { + Some(CoreValue::I32(h)) => *h as u32, + _ => return Err(Error::wasi_invalid_argument("Invalid descriptor")), + }; + + let retptr = match args.get(1) { + Some(CoreValue::I32(p)) => *p as u32, + _ => return Err(Error::wasi_invalid_argument("Missing retptr")), + }; + + let entry = self.fd_table.get(&handle) + .ok_or_else(|| Error::wasi_invalid_fd("Bad descriptor"))?; + + let dtype: u8 = match &entry.fd_type { + FileDescriptorType::PreopenDirectory(_) => 3, // directory + FileDescriptorType::RegularFile(_) => 6, // regular-file + _ => 0, // unknown + }; + + if let Some(mem) = memory { + mem.write_bytes(retptr, &0u32.to_le_bytes())?; // Ok discriminant + mem.write_bytes(retptr + 4, &(dtype as u32).to_le_bytes())?; + } + + Ok(vec![]) + } + + #[cfg(all(feature = "wasi-filesystem", feature = "std"))] + ("wasi:filesystem/types", "[resource-drop]descriptor") => { + // Drop a filesystem descriptor + if let Some(CoreValue::I32(h)) = args.first() { + let handle = *h as u32; + self.fd_table.remove(&handle); + let _ = self.resource_manager.remove_resource(handle); + } + Ok(vec![]) + } + _ => { #[cfg(feature = "tracing")] warn!(interface = %base_interface, function = %function, "unknown WASI function (core)"); diff --git a/kiln-wasi/src/nn/capabilities.rs b/kiln-wasi/src/nn/capabilities.rs index 69d92d0d..7a20902a 100644 --- a/kiln-wasi/src/nn/capabilities.rs +++ b/kiln-wasi/src/nn/capabilities.rs @@ -122,12 +122,26 @@ pub enum ResourceType { } /// Model formats supported by WASI-NN +/// +/// Matches the `graph-encoding` enum from WASI-NN 0.2.0-rc-2024-10-28 spec, +/// plus Kiln-specific extensions. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ModelFormat { + /// OpenVINO IR format + OpenVINO, + /// ONNX format ONNX, + /// TensorFlow SavedModel/frozen graph TensorFlow, + /// PyTorch TorchScript PyTorch, - OpenVINO, + /// TensorFlow Lite format + TensorFlowLite, + /// GGML format (used by llama.cpp and similar) + GGML, + /// Autodetect encoding from model data + Autodetect, + /// Tract native format (Kiln-specific extension) TractNative, } diff --git a/kiln-wasi/src/nn/graph.rs b/kiln-wasi/src/nn/graph.rs index bbf7f92b..54a9c3a9 100644 --- a/kiln-wasi/src/nn/graph.rs +++ b/kiln-wasi/src/nn/graph.rs @@ -26,20 +26,30 @@ use crate::prelude::*; /// Maximum number of graphs that can be loaded const MAX_GRAPHS: usize = 16; -/// Graph encoding formats (matches WIT definition) +/// Graph encoding formats (matches WASI-NN 0.2.0-rc-2024-10-28 WIT definition) +/// +/// These correspond to the `graph-encoding` enum in the WASI-NN specification. +/// The spec defines: openvino, onnx, tensorflow, pytorch, tensorflowlite, ggml, autodetect. +/// We also keep TractNative as a Kiln-specific extension for the Tract backend. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum GraphEncoding { + /// OpenVINO IR format + OpenVINO = 0, /// ONNX format - ONNX, + ONNX = 1, /// TensorFlow SavedModel/frozen graph - TensorFlow, + TensorFlow = 2, /// PyTorch TorchScript - PyTorch, - /// OpenVINO IR format - OpenVINO, - /// Tract native format - TractNative, + PyTorch = 3, + /// TensorFlow Lite format + TensorFlowLite = 4, + /// GGML format (used by llama.cpp and similar) + GGML = 5, + /// Autodetect encoding from model data + Autodetect = 6, + /// Tract native format (Kiln-specific extension, not in WASI-NN spec) + TractNative = 255, } impl GraphEncoding { @@ -51,6 +61,9 @@ impl GraphEncoding { GraphEncoding::PyTorch => ModelFormat::PyTorch, GraphEncoding::OpenVINO => ModelFormat::OpenVINO, GraphEncoding::TractNative => ModelFormat::TractNative, + GraphEncoding::TensorFlowLite => ModelFormat::TensorFlowLite, + GraphEncoding::GGML => ModelFormat::GGML, + GraphEncoding::Autodetect => ModelFormat::Autodetect, } } } diff --git a/kiln-wasi/src/nn/mod.rs b/kiln-wasi/src/nn/mod.rs index 2da0c1bd..96b8a075 100644 --- a/kiln-wasi/src/nn/mod.rs +++ b/kiln-wasi/src/nn/mod.rs @@ -176,7 +176,8 @@ where } /// WASI-NN version information -pub const WASI_NN_VERSION: &str = "0.2.0"; +/// Tracks the WASI-NN specification version: 0.2.0-rc-2024-10-28 +pub const WASI_NN_VERSION: &str = "0.2.0-rc-2024-10-28"; /// Check if WASI-NN is available with the current configuration pub fn is_nn_available() -> bool { @@ -189,7 +190,7 @@ mod tests { #[test] fn test_wasi_nn_version() { - assert_eq!(WASI_NN_VERSION, "0.2.0"); + assert_eq!(WASI_NN_VERSION, "0.2.0-rc-2024-10-28"); } #[test] diff --git a/kiln-wasi/src/nn/sync_bridge.rs b/kiln-wasi/src/nn/sync_bridge.rs index 51748003..30be2542 100644 --- a/kiln-wasi/src/nn/sync_bridge.rs +++ b/kiln-wasi/src/nn/sync_bridge.rs @@ -602,83 +602,40 @@ pub fn wasi_nn_get_output( /// Validate model data format against claimed encoding fn validate_model_format(data: &[u8], encoding: GraphEncoding) -> Result<()> { + // All formats require at least a minimal amount of data + let min_size = match encoding { + GraphEncoding::TensorFlow => 16, + GraphEncoding::ONNX | GraphEncoding::PyTorch => 8, + GraphEncoding::Autodetect => 4, + _ => 4, + }; + + if data.len() < min_size { + return Err(Error::wasi_invalid_argument( + "Model data too short for claimed encoding format", + )); + } + + // Reject all-zeros data for any format + if data.iter().all(|&b| b == 0) { + return Err(Error::wasi_invalid_argument( + "Model appears to be empty/null data", + )); + } + + // Format-specific validation is delegated to the backend. + // This function performs only basic structural validation that applies + // regardless of which backend processes the model. match encoding { - GraphEncoding::ONNX => { - // Basic ONNX format validation - check for ONNX magic bytes - if data.len() < 8 { - return Err(Error::wasi_invalid_argument( - "Model data too short for ONNX format", - )); - } - - // ONNX models typically start with protobuf bytes or have specific structure - // This is a basic validation - in production you'd use a proper ONNX parser - if !data.starts_with(&[0x08]) && !data.starts_with(&[0x08, 0x01]) { - // Many ONNX files start with version info - // For now, accept if it looks like binary data - if data.iter().all(|&b| b == 0) { - return Err(Error::wasi_invalid_argument( - "Model appears to be empty/null data", - )); - } - } - }, - GraphEncoding::TensorFlow => { - // Basic TensorFlow SavedModel validation - if data.len() < 16 { - return Err(Error::wasi_invalid_argument( - "Model data too short for TensorFlow format", - )); - } - // TensorFlow models are typically in SavedModel format or protobuf - // This would require more sophisticated validation in production - }, - GraphEncoding::PyTorch => { - // Basic PyTorch model validation - typically pickle format - if data.len() < 8 { - return Err(Error::wasi_invalid_argument( - "Model data too short for PyTorch format", - )); - } - // PyTorch models often start with pickle protocol bytes - if !data.starts_with(&[0x80]) && !data.starts_with(b"PK") { - // Could be zip format (which PyTorch also uses) - if data.iter().all(|&b| b == 0) { - return Err(Error::wasi_invalid_argument( - "Model appears to be empty/null data", - )); - } - } - }, - GraphEncoding::OpenVINO => { - // OpenVINO IR format validation - if data.len() < 4 { - return Err(Error::wasi_invalid_argument( - "Model data too short for OpenVINO format", - )); - } - // OpenVINO models are typically XML + bin files - // For our purpose, ensure it's not empty/invalid - if data.iter().all(|&b| b == 0) { - return Err(Error::wasi_invalid_argument( - "Model appears to be empty/null data", - )); - } - }, - GraphEncoding::TractNative => { - // Tract native format validation - if data.len() < 4 { - return Err(Error::wasi_invalid_argument( - "Model data too short for Tract format", - )); - } - // Tract has its own serialization format - // Basic validation to ensure it's not obviously invalid - if data.iter().all(|&b| b == 0) { - return Err(Error::wasi_invalid_argument( - "Model appears to be empty/null data", - )); - } + GraphEncoding::ONNX + | GraphEncoding::TensorFlow + | GraphEncoding::PyTorch + | GraphEncoding::OpenVINO + | GraphEncoding::TensorFlowLite + | GraphEncoding::GGML + | GraphEncoding::TractNative + | GraphEncoding::Autodetect => { + // Detailed format validation is performed by the backend during load }, } @@ -707,8 +664,9 @@ mod tests { #[test] fn test_encoding_conversion() { - assert_eq!(GraphEncoding::ONNX.to_wit(), 0); - assert_eq!(GraphEncoding::from_wit(0).unwrap(), GraphEncoding::ONNX); + // ONNX is 1 in the WASI-NN 0.2.0-rc spec (openvino=0, onnx=1, ...) + assert_eq!(GraphEncoding::ONNX.to_wit(), 1); + assert_eq!(GraphEncoding::from_wit(1).unwrap(), GraphEncoding::ONNX); } #[test] diff --git a/kiln-wasi/src/nn/tensor.rs b/kiln-wasi/src/nn/tensor.rs index 0dccd893..000a741d 100644 --- a/kiln-wasi/src/nn/tensor.rs +++ b/kiln-wasi/src/nn/tensor.rs @@ -23,15 +23,21 @@ use crate::prelude::*; pub const MAX_TENSOR_DIMS: usize = 8; /// Tensor data types supported by WASI-NN +/// +/// The first 7 types match the WASI-NN 0.2.0-rc-2024-10-28 spec: +/// FP16, FP32, FP64, BF16, U8, I32, I64 +/// Additional types are Kiln extensions for broader backend support. #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum TensorType { - /// 16-bit floating point + /// 16-bit floating point (IEEE 754) F16, - /// 32-bit floating point + /// 32-bit floating point (IEEE 754) F32, - /// 64-bit floating point + /// 64-bit floating point (IEEE 754) F64, + /// Brain floating point (16-bit, used in ML training/inference) + BF16, /// Unsigned 8-bit integer U8, /// Signed 8-bit integer @@ -56,7 +62,7 @@ impl TensorType { /// Get the size in bytes of this tensor type pub fn size_bytes(&self) -> usize { match self { - TensorType::F16 => 2, + TensorType::F16 | TensorType::BF16 => 2, TensorType::F32 => 4, TensorType::F64 => 8, TensorType::U8 | TensorType::I8 | TensorType::Bool => 1, @@ -68,7 +74,10 @@ impl TensorType { /// Check if this is a floating point type pub fn is_float(&self) -> bool { - matches!(self, TensorType::F16 | TensorType::F32 | TensorType::F64) + matches!( + self, + TensorType::F16 | TensorType::F32 | TensorType::F64 | TensorType::BF16 + ) } /// Check if this is an integer type diff --git a/kiln-wasi/src/nn/tract_backend.rs b/kiln-wasi/src/nn/tract_backend.rs index 7ff3b4f9..b67f5194 100644 --- a/kiln-wasi/src/nn/tract_backend.rs +++ b/kiln-wasi/src/nn/tract_backend.rs @@ -243,6 +243,12 @@ impl ComputeCapable for TractContext { |_| Error::wasi_runtime_error("Failed to create i32 Tract tensor"), )? }, + dt if dt == u8::datum_type() => { + let u8_data: Vec = data.to_vec(); + tract_onnx::prelude::Tensor::from_shape(&shape, &u8_data).map_err( + |_| Error::wasi_runtime_error("Failed to create u8 Tract tensor"), + )? + }, _ => { return Err(Error::wasi_unsupported_operation( "Unsupported tensor type for safe conversion", @@ -336,8 +342,9 @@ fn datum_to_tensor_type(datum_factoid: DatumType) -> Result { } else if datum_factoid == u8::datum_type() { Ok(TensorType::U8) } else { - // Default fallback for unknown types - Ok(TensorType::F32) + Err(Error::wasi_unsupported_operation( + "Unsupported Tract datum type for WASI-NN tensor conversion", + )) } } @@ -388,45 +395,61 @@ impl NeuralNetworkBackend for TractBackend }, }; - // Analyze the model to get input/output info + // Optimize and make runnable + let optimized = model + .into_optimized() + .map_err(|_| Error::wasi_runtime_error("Failed to optimize model"))?; + + let runnable = optimized + .into_runnable() + .map_err(|_| Error::wasi_runtime_error("Failed to make model runnable"))?; + + // Extract input/output metadata from the optimized typed model + // where shapes and types are fully resolved + let typed_model = runnable.model(); let mut input_info = Vec::new(); let mut output_info = Vec::new(); - // Get input facts - for (idx, input) in model.inputs.iter().enumerate() { - let fact = model + // Get input facts from the typed (optimized) model + for input in typed_model.inputs.iter() { + let fact = typed_model .outlet_fact(*input) .map_err(|_| Error::wasi_runtime_error("Failed to get input fact"))?; - // For now, use a simple approach with default shapes - // In a full implementation, we'd properly parse the model metadata - let tensor_dims = TensorDimensions::new(&[1, 224, 224, 3])?; // Common image input - let tensor_type = TensorType::F32; // Default to F32 + let shape: Vec = fact + .shape + .iter() + .map(|d| { + d.to_i64() + .map(|v| v as u32) + .unwrap_or(1) // symbolic dims default to 1 (batch size) + }) + .collect(); + let tensor_dims = TensorDimensions::new(&shape)?; + let tensor_type = datum_to_tensor_type(fact.datum_type)?; input_info.push((tensor_dims, tensor_type)); } - // Get output facts - for output in model.outputs.iter() { - let fact = model + // Get output facts from the typed (optimized) model + for output in typed_model.outputs.iter() { + let fact = typed_model .outlet_fact(*output) .map_err(|_| Error::wasi_runtime_error("Failed to get output fact"))?; - // For now, use a simple approach with default shapes - // In a full implementation, we'd properly parse the model metadata - let tensor_dims = TensorDimensions::new(&[1, 1000])?; // Common classification output - let tensor_type = TensorType::F32; // Default to F32 + let shape: Vec = fact + .shape + .iter() + .map(|d| { + d.to_i64() + .map(|v| v as u32) + .unwrap_or(1) // symbolic dims default to 1 (batch size) + }) + .collect(); + let tensor_dims = TensorDimensions::new(&shape)?; + let tensor_type = datum_to_tensor_type(fact.datum_type)?; output_info.push((tensor_dims, tensor_type)); } - // Optimize and make runnable - let optimized = model - .into_optimized() - .map_err(|_| Error::wasi_runtime_error("Failed to optimize model"))?; - - let runnable = optimized - .into_runnable() - .map_err(|_| Error::wasi_runtime_error("Failed to make model runnable"))?; - Ok(TractModel { id: 1, // Would be assigned by graph store in real usage size: data.len(), @@ -440,14 +463,9 @@ impl NeuralNetworkBackend for TractBackend #[cfg(not(feature = "tract"))] { - // Fallback for when tract feature is not enabled - Ok(TractModel { - id: 1, - size: data.len(), - hash, - input_info: vec![(TensorDimensions::new(&[1, 224, 224, 3])?, TensorType::F32)], - output_info: vec![(TensorDimensions::new(&[1, 1000])?, TensorType::F32)], - }) + Err(Error::wasi_unsupported_operation( + "Tract backend requires the 'tract' feature to be enabled", + )) } } @@ -542,6 +560,12 @@ impl NeuralNetworkBackend for TractBackend Error::wasi_runtime_error("Failed to create i32 Tract tensor") })? }, + dt if dt == u8::datum_type() => { + let u8_data: Vec = tensor.data.clone(); + tract_onnx::prelude::Tensor::from_shape(&shape, &u8_data).map_err(|_| { + Error::wasi_runtime_error("Failed to create u8 Tract tensor") + })? + }, _ => { return Err(Error::wasi_unsupported_operation( "Unsupported tensor type for safe conversion", @@ -629,9 +653,9 @@ impl NeuralNetworkBackend for TractBackend #[cfg(not(feature = "tract"))] { - // For now, return dummy output - let dims = TensorDimensions::new(&[1, 1000])?; - self.create_tensor(dims, TensorType::F32) + Err(Error::wasi_unsupported_operation( + "Tract backend requires the 'tract' feature to be enabled", + )) } } diff --git a/kiln-wasi/src/nn/wit_types.rs b/kiln-wasi/src/nn/wit_types.rs index 2a580a62..2b269127 100644 --- a/kiln-wasi/src/nn/wit_types.rs +++ b/kiln-wasi/src/nn/wit_types.rs @@ -11,24 +11,32 @@ use super::{ }; use crate::prelude::*; -/// Error codes from WIT interface +/// Error codes from WASI-NN 0.2.0-rc-2024-10-28 WIT interface +/// +/// These match the `error-code` enum in the spec: +/// invalid-argument, invalid-encoding, timeout, runtime-error, +/// unsupported-operation, too-large, not-found, security, unknown #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u8)] pub enum ErrorCode { /// Invalid argument provided - InvalidArgument = 1, + InvalidArgument = 0, /// Invalid model encoding - InvalidEncoding = 2, + InvalidEncoding = 1, + /// Operation timed out + Timeout = 2, /// Runtime error during execution RuntimeError = 3, - /// Resource limits exceeded - ResourceExhausted = 4, - /// Operation not supported - UnsupportedOperation = 5, - /// Model verification failed - VerificationFailed = 6, - /// Timeout during execution - Timeout = 7, + /// Operation not supported by this backend + UnsupportedOperation = 4, + /// Input or model too large for available resources + TooLarge = 5, + /// Requested resource not found (graph, context, etc.) + NotFound = 6, + /// Security policy violation + Security = 7, + /// Unknown or unclassified error + Unknown = 8, } impl From for ErrorCode { @@ -39,9 +47,8 @@ impl From for ErrorCode { codes::UNSUPPORTED => ErrorCode::UnsupportedOperation, codes::RESOURCE_LIMIT_EXCEEDED | codes::WASI_RESOURCE_EXHAUSTED - | codes::WASI_RESOURCE_LIMIT => ErrorCode::ResourceExhausted, - // codes::TIMEOUT => ErrorCode::Timeout, // No timeout code exists yet - codes::VERIFICATION_FAILED => ErrorCode::VerificationFailed, + | codes::WASI_RESOURCE_LIMIT => ErrorCode::TooLarge, + codes::VERIFICATION_FAILED => ErrorCode::Security, _ => ErrorCode::RuntimeError, } } @@ -52,13 +59,15 @@ impl From for Error { match code { ErrorCode::InvalidArgument => Error::wasi_invalid_argument("Invalid argument"), ErrorCode::InvalidEncoding => Error::wasi_invalid_argument("Invalid encoding"), + ErrorCode::Timeout => Error::wasi_timeout("Operation timeout"), ErrorCode::RuntimeError => Error::wasi_runtime_error("Runtime error"), - ErrorCode::ResourceExhausted => Error::wasi_resource_exhausted("Resource exhausted"), ErrorCode::UnsupportedOperation => { Error::wasi_unsupported_operation("Unsupported operation") }, - ErrorCode::VerificationFailed => Error::wasi_verification_failed("Verification failed"), - ErrorCode::Timeout => Error::wasi_timeout("Operation timeout"), + ErrorCode::TooLarge => Error::wasi_resource_exhausted("Input or model too large"), + ErrorCode::NotFound => Error::wasi_invalid_argument("Resource not found"), + ErrorCode::Security => Error::wasi_verification_failed("Security policy violation"), + ErrorCode::Unknown => Error::wasi_runtime_error("Unknown error"), } } } @@ -76,6 +85,9 @@ pub trait WitTypeConversion: Sized { } // Implement conversions for tensor types +// Matches WASI-NN 0.2.0-rc-2024-10-28 spec enum order: +// FP16=0, FP32=1, FP64=2, BF16=3, U8=4, I32=5, I64=6 +// Extended with additional types for Kiln backend support impl WitTypeConversion for TensorType { type WitType = u8; @@ -84,15 +96,17 @@ impl WitTypeConversion for TensorType { 0 => Ok(TensorType::F16), 1 => Ok(TensorType::F32), 2 => Ok(TensorType::F64), - 3 => Ok(TensorType::U8), - 4 => Ok(TensorType::I8), - 5 => Ok(TensorType::U16), - 6 => Ok(TensorType::I16), - 7 => Ok(TensorType::U32), - 8 => Ok(TensorType::I32), - 9 => Ok(TensorType::U64), - 10 => Ok(TensorType::I64), - 11 => Ok(TensorType::Bool), + 3 => Ok(TensorType::BF16), + 4 => Ok(TensorType::U8), + 5 => Ok(TensorType::I32), + 6 => Ok(TensorType::I64), + // Extended types (Kiln extensions, not in base spec) + 128 => Ok(TensorType::I8), + 129 => Ok(TensorType::U16), + 130 => Ok(TensorType::I16), + 131 => Ok(TensorType::U32), + 132 => Ok(TensorType::U64), + 133 => Ok(TensorType::Bool), _ => Err(Error::wasi_invalid_argument("Invalid tensor type")), } } @@ -102,41 +116,51 @@ impl WitTypeConversion for TensorType { TensorType::F16 => 0, TensorType::F32 => 1, TensorType::F64 => 2, - TensorType::U8 => 3, - TensorType::I8 => 4, - TensorType::U16 => 5, - TensorType::I16 => 6, - TensorType::U32 => 7, - TensorType::I32 => 8, - TensorType::U64 => 9, - TensorType::I64 => 10, - TensorType::Bool => 11, + TensorType::BF16 => 3, + TensorType::U8 => 4, + TensorType::I32 => 5, + TensorType::I64 => 6, + // Extended types + TensorType::I8 => 128, + TensorType::U16 => 129, + TensorType::I16 => 130, + TensorType::U32 => 131, + TensorType::U64 => 132, + TensorType::Bool => 133, } } } // Implement conversions for graph encoding +// Matches WASI-NN 0.2.0-rc-2024-10-28 spec enum order: +// openvino=0, onnx=1, tensorflow=2, pytorch=3, tensorflowlite=4, ggml=5, autodetect=6 impl WitTypeConversion for GraphEncoding { type WitType = u8; fn from_wit(wit: u8) -> Result { match wit { - 0 => Ok(GraphEncoding::ONNX), - 1 => Ok(GraphEncoding::TensorFlow), - 2 => Ok(GraphEncoding::PyTorch), - 3 => Ok(GraphEncoding::OpenVINO), - 4 => Ok(GraphEncoding::TractNative), + 0 => Ok(GraphEncoding::OpenVINO), + 1 => Ok(GraphEncoding::ONNX), + 2 => Ok(GraphEncoding::TensorFlow), + 3 => Ok(GraphEncoding::PyTorch), + 4 => Ok(GraphEncoding::TensorFlowLite), + 5 => Ok(GraphEncoding::GGML), + 6 => Ok(GraphEncoding::Autodetect), + 255 => Ok(GraphEncoding::TractNative), _ => Err(Error::wasi_invalid_encoding("Invalid graph encoding")), } } fn to_wit(&self) -> u8 { match self { - GraphEncoding::ONNX => 0, - GraphEncoding::TensorFlow => 1, - GraphEncoding::PyTorch => 2, - GraphEncoding::OpenVINO => 3, - GraphEncoding::TractNative => 4, + GraphEncoding::OpenVINO => 0, + GraphEncoding::ONNX => 1, + GraphEncoding::TensorFlow => 2, + GraphEncoding::PyTorch => 3, + GraphEncoding::TensorFlowLite => 4, + GraphEncoding::GGML => 5, + GraphEncoding::Autodetect => 6, + GraphEncoding::TractNative => 255, } } } @@ -291,11 +315,42 @@ mod tests { use super::*; #[test] - fn test_tensor_type_conversion() { - let tensor_type = TensorType::F32; - let wit_type = tensor_type.to_wit(); - let converted = TensorType::from_wit(wit_type).unwrap(); - assert_eq!(tensor_type, converted); + fn test_tensor_type_roundtrip() { + // Test all spec types roundtrip correctly + let spec_types = [ + TensorType::F16, + TensorType::F32, + TensorType::F64, + TensorType::BF16, + TensorType::U8, + TensorType::I32, + TensorType::I64, + ]; + for tt in &spec_types { + let wit = tt.to_wit(); + let converted = TensorType::from_wit(wit).unwrap(); + assert_eq!(*tt, converted, "Roundtrip failed for {:?}", tt); + } + } + + #[test] + fn test_graph_encoding_roundtrip() { + // Test all spec encodings roundtrip correctly + let encodings = [ + GraphEncoding::OpenVINO, + GraphEncoding::ONNX, + GraphEncoding::TensorFlow, + GraphEncoding::PyTorch, + GraphEncoding::TensorFlowLite, + GraphEncoding::GGML, + GraphEncoding::Autodetect, + GraphEncoding::TractNative, + ]; + for enc in &encodings { + let wit = enc.to_wit(); + let converted = GraphEncoding::from_wit(wit).unwrap(); + assert_eq!(*enc, converted, "Roundtrip failed for {:?}", enc); + } } #[test] @@ -308,6 +363,20 @@ mod tests { assert_eq!(error2.category, ErrorCategory::Validation); } + #[test] + fn test_error_code_spec_values() { + // Verify error code discriminant values match the WASI-NN spec + assert_eq!(ErrorCode::InvalidArgument as u8, 0); + assert_eq!(ErrorCode::InvalidEncoding as u8, 1); + assert_eq!(ErrorCode::Timeout as u8, 2); + assert_eq!(ErrorCode::RuntimeError as u8, 3); + assert_eq!(ErrorCode::UnsupportedOperation as u8, 4); + assert_eq!(ErrorCode::TooLarge as u8, 5); + assert_eq!(ErrorCode::NotFound as u8, 6); + assert_eq!(ErrorCode::Security as u8, 7); + assert_eq!(ErrorCode::Unknown as u8, 8); + } + #[test] fn test_tensor_data_conversion() { let data = vec![1.0f32, 2.0, 3.0, 4.0]; diff --git a/kilnd/Cargo.toml b/kilnd/Cargo.toml index 5d526e59..a163cde1 100644 --- a/kilnd/Cargo.toml +++ b/kilnd/Cargo.toml @@ -78,7 +78,7 @@ wasi-cli = ["wasi", "kiln-wasi/wasi-cli"] wasi-clocks = ["wasi", "kiln-wasi/wasi-clocks"] wasi-io = ["wasi", "kiln-wasi/wasi-io"] wasi-random = ["wasi", "kiln-wasi/wasi-random"] -wasi-nn = ["wasi", "kiln-wasi/wasi-nn"] +wasi-nn = ["wasi", "kiln-wasi/wasi-nn", "kiln-wasi/nn-preview2", "kiln-wasi/tract"] # Component model support component-model = ["kiln-execution", "dep:kiln-component", "dep:kiln-decoder", "kiln-component/std", "kiln-component/kiln-execution", "kiln-decoder/std"] diff --git a/kilnd/src/main.rs b/kilnd/src/main.rs index c39cb10a..2142362e 100644 --- a/kilnd/src/main.rs +++ b/kilnd/src/main.rs @@ -132,6 +132,9 @@ pub struct KilndConfig { /// WASI capabilities #[cfg(feature = "wasi")] pub wasi_capabilities: Option, + /// Filesystem paths to preopen for WASI + #[cfg(feature = "wasi")] + pub wasi_fs_paths: Vec, } impl Default for KilndConfig { @@ -153,6 +156,8 @@ impl Default for KilndConfig { wasi_env_vars: Vec::new(), #[cfg(feature = "wasi")] wasi_args: Vec::new(), + #[cfg(feature = "wasi")] + wasi_fs_paths: Vec::new(), #[cfg(feature = "component-model")] enable_component_model: true, #[cfg(feature = "component-model")] @@ -475,20 +480,16 @@ impl KilndEngine { "Component parsed successfully" ); - eprintln!("DEBUG: About to call ComponentInstance::from_parsed"); - // Create and initialize component instance (passes by reference to avoid stack overflow) // This includes executing start functions and transitioning to Running state // Note: WASI functions are already registered in host_registry from init_wasi() use kiln_component::components::component_instantiation::ComponentInstance; - - eprintln!("DEBUG: Calling from_parsed..."); // Wrap host_registry in Arc for passing to component use std::sync::Arc; let registry_arc = Arc::new(self.host_registry.clone()); let mut instance = ComponentInstance::from_parsed(0, &mut *parsed_component, Some(registry_arc)) .map_err(|e| { - eprintln!("[DEBUG] ComponentInstance::from_parsed failed: {:?}", e); + eprintln!("Component instantiation error: {}", e); Error::runtime_error("Failed to create and initialize component instance") })?; // parsed_component is now dropped - we only keep runtime instance @@ -499,7 +500,16 @@ impl KilndEngine { if self.config.enable_wasi { use kiln_wasi::WasiDispatcher; match WasiDispatcher::with_defaults() { - Ok(dispatcher) => { + Ok(mut dispatcher) => { + // Register filesystem preopens + for path in &self.config.wasi_fs_paths { + if let Err(_e) = dispatcher.add_preopen(path) { + let _ = self.logger.handle_minimal_log( + LogLevel::Warn, + "Failed to add preopen path" + ); + } + } instance.set_host_handler(Box::new(dispatcher)); let _ = self.logger.handle_minimal_log( LogLevel::Info, @@ -526,25 +536,12 @@ impl KilndEngine { ); // Check for WASI CLI entry point and invoke it - // Debug: print available exports - #[cfg(feature = "std")] - { - println!("\n=== Available Exports ==="); - println!("Total exports: {}", instance.exports.len()); - for (idx, export) in instance.exports.iter().enumerate() { - println!(" Export[{}]: \"{}\"", idx, export.name); - } - println!(); - } - // Find wasi:cli/run export with any version let run_export = instance.exports.iter() .find(|e| e.name.starts_with("wasi:cli/run@")) .map(|e| e.name.clone()); if let Some(export_name) = run_export { - #[cfg(feature = "std")] - eprintln!("[INFO] Calling {} entry point", export_name); let _ = self.logger.handle_minimal_log( LogLevel::Info, "Calling wasi:cli/run entry point" @@ -653,7 +650,11 @@ impl KilndEngine { #[cfg(feature = "wasi")] if self.config.enable_wasi { match WasiDispatcher::with_defaults() { - Ok(dispatcher) => { + Ok(mut dispatcher) => { + // Register filesystem preopens + for path in &self.config.wasi_fs_paths { + let _ = dispatcher.add_preopen(path); + } engine.set_host_handler(Box::new(dispatcher)); let _ = self.logger.handle_minimal_log(LogLevel::Info, "WASI dispatcher connected"); } @@ -880,7 +881,11 @@ impl KilndEngine { } // Update statistics - self.stats.modules_executed += 1; + if is_component { + self.stats.components_executed += 1; + } else { + self.stats.modules_executed += 1; + } self.stats.fuel_consumed += estimated_fuel; self.stats.peak_memory = self.stats.peak_memory.max(estimated_memory); @@ -935,6 +940,9 @@ pub struct SimpleArgs { /// Component interfaces to register #[cfg(feature = "component-model")] pub component_interfaces: Vec, + /// WASI-NN graph specifications (encoding::path pairs) + #[cfg(feature = "wasi-nn")] + pub nn_graphs: Vec, /// Enable memory profiling pub enable_memory_profiling: bool, /// Enable platform optimizations @@ -966,6 +974,8 @@ impl SimpleArgs { enable_component_model: true, #[cfg(feature = "component-model")] component_interfaces: Vec::new(), + #[cfg(feature = "wasi-nn")] + nn_graphs: Vec::new(), enable_memory_profiling: false, enable_platform_optimizations: true, }; @@ -997,6 +1007,12 @@ impl SimpleArgs { println!(" --component Enable component model support"); println!(" --interface Register component interface"); } + #[cfg(feature = "wasi-nn")] + { + println!(" --nn-graph Pre-load NN model (format: encoding::path)"); + println!(" e.g., --nn-graph onnx::model.onnx"); + println!(" Encodings: onnx, tensorflow, pytorch, openvino"); + } println!(" --help Show this help message"); process::exit(0); }, @@ -1073,6 +1089,13 @@ impl SimpleArgs { result.component_interfaces.push(args[i].clone()); } }, + #[cfg(feature = "wasi-nn")] + "--nn-graph" => { + i += 1; + if i < args.len() { + result.nn_graphs.push(args[i].clone()); + } + }, // Everything after "--" goes to wasi_args "--" => { #[cfg(feature = "wasi")] @@ -1202,6 +1225,7 @@ fn main_with_stack() -> Result<()> { config.wasi_capabilities = Some(capabilities); config.wasi_env_vars = args.wasi_env_vars.clone(); config.wasi_args = args.wasi_args.clone(); + config.wasi_fs_paths = args.wasi_fs_paths.clone(); println!("✓ WASI enabled:"); println!(" - Version: {:?}", config.wasi_version); @@ -1233,6 +1257,76 @@ fn main_with_stack() -> Result<()> { println!("! Platform optimizations disabled"); } + // Initialize WASI-NN if graphs are specified + #[cfg(feature = "wasi-nn")] + { + if !args.nn_graphs.is_empty() { + use kiln_wasi::nn::{ + initialize_nn, initialize_backends, initialize_graph_store, + initialize_context_store, nn_load, + capabilities::DynamicNNCapability, + GraphEncoding, + }; + + // Initialize NN subsystem with default (QM) capability + let capability = Box::new(DynamicNNCapability::with_tracking()); + initialize_nn(capability).map_err(|e| { + eprintln!("Failed to initialize WASI-NN: {}", e); + e + })?; + + // Initialize backends and stores + initialize_backends().ok(); + initialize_graph_store().ok(); + initialize_context_store().ok(); + + println!("✓ WASI-NN enabled:"); + + for spec in &args.nn_graphs { + // Parse "encoding::path" format + let (encoding_str, path) = match spec.split_once("::") { + Some((enc, p)) => (enc, p), + None => { + eprintln!(" ✗ Invalid --nn-graph format: '{}' (expected encoding::path)", spec); + eprintln!(" Example: --nn-graph onnx::model.onnx"); + process::exit(1); + } + }; + + let encoding: u8 = match encoding_str { + "openvino" => 0, + "onnx" => 1, + "tensorflow" | "tf" => 2, + "pytorch" | "pt" => 3, + "tflite" => 4, + "ggml" => 5, + "autodetect" | "auto" => 6, + _ => { + eprintln!(" ✗ Unknown encoding '{}'. Valid: onnx, tensorflow, pytorch, openvino, tflite, ggml, autodetect", encoding_str); + process::exit(1); + } + }; + + // Read model file from disk + let model_data = std::fs::read(path).map_err(|e| { + eprintln!(" ✗ Failed to read model file '{}': {}", path, e); + kiln_error::Error::runtime_error("Failed to read model file") + })?; + + let model_size = model_data.len(); + + // Pre-load into graph store + let graph_id = nn_load(model_data, encoding, 0).map_err(|e| { + eprintln!(" ✗ Failed to load model '{}': {}", path, e); + e + })?; + + println!(" - Loaded {} ({} bytes) as graph_id={} [{}]", + path, model_size, graph_id, encoding_str); + } + } + } + // Check if we have a module to execute if config.module_path.is_none() { println!("Error: No module specified");