diff --git a/kiln-build-core/src/wast_execution.rs b/kiln-build-core/src/wast_execution.rs index fc3fd254..0005b24b 100644 --- a/kiln-build-core/src/wast_execution.rs +++ b/kiln-build-core/src/wast_execution.rs @@ -24,6 +24,26 @@ pub use crate::wast_values::{ results_match_with_either, values_equal, }; +/// Precise type information extracted from the WASM binary. +/// +/// Used to supplement the lossy type info in the runtime Module. +/// The runtime's ValueType loses nullability for abstract heap types +/// (e.g., `(ref func)` and `(ref null func)` both decode to `FuncRef`), +/// and doesn't track rec group membership or finality. +#[derive(Clone, Debug)] +struct PreciseTypeInfo { + /// Rec group boundaries: vec of (start_type_idx, type_count) for each rec group + rec_groups: Vec<(u32, u32)>, + /// Whether each type index is final (cannot be further subtyped) + type_finality: Vec, + /// For each global (imported then defined), precise reference type info: + /// `Some((nullable, heap_type_s33))` for reference-typed globals, + /// `None` for non-reference globals. + /// heap_type_s33 is the s33 heap type value: negative for abstract types + /// (-16 = func, -17 = extern, etc.), non-negative for concrete type indices. + global_precise_ref: Vec>, +} + /// Minimal WAST execution engine for testing /// /// This engine focuses on basic functionality: @@ -42,6 +62,9 @@ pub struct WastEngine { current_module: Option>, /// Current instance ID for module execution current_instance_id: Option, + /// Precise type info per module, keyed by module name. + /// Supplements the lossy type info in the runtime Module. + precise_types: HashMap, } /// No-op host handler for spectest print functions @@ -67,6 +90,7 @@ impl WastEngine { instance_ids: HashMap::new(), current_module: None, current_instance_id: None, + precise_types: HashMap::new(), }) } @@ -78,6 +102,10 @@ impl WastEngine { wasm_binary.len(), e, e.code, e.category) })?; + // Extract precise type info from kiln_module and binary before + // the lossy conversion to runtime Module + let precise_info = extract_precise_type_info(&kiln_module, wasm_binary); + // Validate the module before proceeding (Phase 1 of WAST conformance) crate::wast_validator::WastModuleValidator::validate(&kiln_module) .map_err(|e| anyhow::anyhow!("Module validation failed: {:#}", e))?; @@ -92,7 +120,7 @@ impl WastEngine { // Validate all imports against registered modules and spectest // This catches unknown imports and incompatible types BEFORE instantiation - self.validate_imports(&module)?; + self.validate_imports(&module, &precise_info)?; // Pre-compute the engine instance ID so ModuleInstance.instance_id matches // the engine's key. This ensures FuncRef.instance_id values stamped during @@ -183,16 +211,18 @@ impl WastEngine { // 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)?; + self.validate_imports(&module, &precise_info)?; - // Store the module and instance ID for later reference + // Store the module, instance ID, and precise type info for later reference // Always store as "current" (last loaded module) AND under the given name self.modules.insert("current".to_string(), Arc::clone(&module)); self.instance_ids.insert("current".to_string(), instance_idx); + self.precise_types.insert("current".to_string(), precise_info.clone()); let module_name = name.unwrap_or("current").to_string(); if module_name != "current" { self.modules.insert(module_name.clone(), Arc::clone(&module)); self.instance_ids.insert(module_name.clone(), instance_idx); + self.precise_types.insert(module_name.clone(), precise_info); } // Register instance name for cross-module exception handling @@ -336,6 +366,10 @@ impl WastEngine { // Register the new name for cross-module exception handling self.engine.register_instance_name(instance_id, name); } + // Copy precise type info if available + if let Some(info) = self.precise_types.get(module_name) { + self.precise_types.insert(name.to_string(), info.clone()); + } Ok(()) } else { Err(anyhow::anyhow!( @@ -354,6 +388,7 @@ impl WastEngine { pub fn reset(&mut self) -> Result<()> { self.modules.clear(); self.instance_ids.clear(); + self.precise_types.clear(); self.current_module = None; self.current_instance_id = None; // Create a new engine to reset state, with spectest handler @@ -816,7 +851,7 @@ impl WastEngine { /// - The named export must exist in the source module /// - The export kind must match the import kind (function, global, table, memory, tag) /// - Type signatures must be compatible - fn validate_imports(&self, module: &Module) -> Result<()> { + fn validate_imports(&self, module: &Module, import_precise: &PreciseTypeInfo) -> Result<()> { let import_order = &module.import_order; let import_types = &module.import_types; @@ -836,7 +871,9 @@ impl WastEngine { self.validate_spectest_import(field_name, import_desc, module)?; } else { // Validate against registered modules - self.validate_registered_import(mod_name, field_name, import_desc, module)?; + self.validate_registered_import( + mod_name, field_name, import_desc, module, import_precise, i, + )?; } } @@ -1036,6 +1073,8 @@ impl WastEngine { field_name: &str, import_desc: &RuntimeImportDesc, module: &Module, + import_precise: &PreciseTypeInfo, + import_idx: usize, ) -> Result<()> { // Check if the module is registered let source_module = match self.modules.get(mod_name) { @@ -1048,6 +1087,9 @@ impl WastEngine { } }; + // Get precise type info for the exporting module + let export_precise = self.precise_types.get(mod_name); + // Find the export in the source module let export = match source_module.get_export(field_name) { Some(e) => e, @@ -1071,6 +1113,7 @@ impl WastEngine { // Validate function type signature self.validate_function_type_compatibility( mod_name, field_name, *type_idx, module, source_module, &export, + import_precise, export_precise, )?; } RuntimeImportDesc::Global(import_global_type) => { @@ -1080,9 +1123,11 @@ impl WastEngine { mod_name, field_name, export.kind )); } - // Validate global type compatibility + // Validate global type compatibility using precise type info self.validate_global_type_compatibility( - mod_name, field_name, import_global_type, source_module, &export, + mod_name, field_name, import_global_type, module, + source_module, &export, + import_precise, import_idx, export_precise, )?; } RuntimeImportDesc::Table(import_table_type) => { @@ -1119,6 +1164,7 @@ impl WastEngine { // Validate tag type compatibility self.validate_tag_type_compatibility( mod_name, field_name, import_tag_type, module, source_module, &export, + import_precise, export_precise, )?; } _ => { @@ -1138,6 +1184,8 @@ impl WastEngine { importing_module: &Module, source_module: &Module, export: &kiln_runtime::module::Export, + import_precise: &PreciseTypeInfo, + export_precise: Option<&PreciseTypeInfo>, ) -> Result<()> { // Get the importing module's expected function type let import_func_type = importing_module.types.get(import_type_idx as usize).ok_or_else(|| { @@ -1156,13 +1204,35 @@ impl WastEngine { ) })?; - let export_func_type = source_module.types.get(export_func.type_idx as usize).ok_or_else(|| { + let export_type_idx = export_func.type_idx; + + let export_func_type = source_module.types.get(export_type_idx as usize).ok_or_else(|| { anyhow::anyhow!( "incompatible import type: {}::{} has invalid type index", mod_name, field_name ) })?; + // For function imports, use rec group-aware type matching. + // The export type must be a subtype of the import type, where type + // identity requires matching rec group structure AND finality. + if let Some(ep) = export_precise { + // Use the full rec-group-aware subtype check + let is_match = is_type_idx_match_cross_module( + export_type_idx, source_module, ep, + import_type_idx, importing_module, import_precise, + 0, + ); + + if !is_match { + return Err(anyhow::anyhow!( + "incompatible import type: {}::{} type mismatch (export_idx={}, import_idx={})", + mod_name, field_name, export_type_idx, import_type_idx + )); + } + return Ok(()); + } + // Compare function signatures if import_func_type.params.len() != export_func_type.params.len() || import_func_type.results.len() != export_func_type.results.len() @@ -1216,8 +1286,12 @@ impl WastEngine { mod_name: &str, field_name: &str, import_global_type: &GlobalType, + module: &Module, source_module: &Module, export: &kiln_runtime::module::Export, + import_precise: &PreciseTypeInfo, + import_idx: usize, + export_precise: Option<&PreciseTypeInfo>, ) -> Result<()> { let export_global_idx = export.index as usize; @@ -1233,19 +1307,41 @@ impl WastEngine { )); } + // Get precise ref type info for import and export globals. + // The import_idx is the position in the module's full import list. + // The global_precise_ref vec is indexed by global index (imported + defined). + // We need to find which global-import-index corresponds to import_idx. + let import_global_idx = count_global_imports_up_to( + &module.import_types, import_idx, + ); + let import_ref = import_precise.global_precise_ref + .get(import_global_idx) + .copied() + .flatten(); + let export_ref = export_precise + .and_then(|ep| ep.global_precise_ref.get(export_global_idx)) + .copied() + .flatten(); + // For mutable globals, the value type must match exactly // For immutable globals, the import type must be a supertype of the export type if import_global_type.mutable { - // Mutable: exact type match required - if import_global_type.value_type != source_type.value_type { + // Mutable: exact type match required, including nullability + if !precise_ref_types_equal( + &import_global_type.value_type, import_ref, + &source_type.value_type, export_ref, + ) { return Err(anyhow::anyhow!( "incompatible import type: {}::{} value type mismatch ({:?} vs {:?})", mod_name, field_name, import_global_type.value_type, source_type.value_type )); } } else { - // Immutable: import type must be supertype of export type - if !is_value_type_subtype(&source_type.value_type, &import_global_type.value_type) { + // Immutable: export type must be subtype of import type + if !precise_ref_type_is_subtype( + &source_type.value_type, export_ref, + &import_global_type.value_type, import_ref, + ) { return Err(anyhow::anyhow!( "incompatible import type: {}::{} value type mismatch ({:?} is not subtype of {:?})", mod_name, field_name, source_type.value_type, import_global_type.value_type @@ -1422,6 +1518,8 @@ impl WastEngine { importing_module: &Module, source_module: &Module, export: &kiln_runtime::module::Export, + import_precise: &PreciseTypeInfo, + export_precise: Option<&PreciseTypeInfo>, ) -> Result<()> { // Get the import's function type from the type index let import_func_type = importing_module.types.get(import_tag_type.type_idx as usize); @@ -1449,6 +1547,22 @@ impl WastEngine { }; if let (Some(import_ft), Some(export_tt)) = (import_func_type, export_tag_type) { + // Tags require EXACT type matching, including rec group membership. + // Two structurally identical (func) types in different rec groups + // are considered different types per the GC spec. + if let Some(ep) = export_precise { + if !are_types_rec_group_compatible( + import_tag_type.type_idx, import_precise, + export_tt.type_idx, ep, + importing_module, source_module, + ) { + return Err(anyhow::anyhow!( + "incompatible import type: {}::{} tag type in different rec group structure", + mod_name, field_name + )); + } + } + let export_func_type = source_module.types.get(export_tt.type_idx as usize); if let Some(export_ft) = export_func_type { // Tag types must match exactly (params only, tags have no results) @@ -1474,6 +1588,659 @@ impl WastEngine { } } +/// Extract precise type information from a decoded KilnModule and raw binary. +/// +/// The runtime Module loses precision for: +/// - Global reference type nullability (both `(ref func)` and `(ref null func)` map to FuncRef) +/// - Type finality (`sub final` vs `sub`) +/// - Rec group membership (which types share a rec group) +/// +/// This function extracts that info from the richer format-level module. +fn extract_precise_type_info( + kiln_module: &kiln_format::module::Module, + wasm_binary: &[u8], +) -> PreciseTypeInfo { + // Extract rec group boundaries and finality from the type section + let mut rec_groups = Vec::new(); + let mut type_finality = Vec::new(); + + for rg in &kiln_module.rec_groups { + let start = rg.start_type_index; + let count = rg.types.len() as u32; + rec_groups.push((start, count)); + for sub_type in &rg.types { + type_finality.push(sub_type.is_final); + } + } + + // Extract precise global reference types from the binary. + // We need to walk the import section and global section to find + // global types with their exact nullable/non-nullable encoding. + let global_precise_ref = extract_precise_global_ref_types(wasm_binary); + + PreciseTypeInfo { + rec_groups, + type_finality, + global_precise_ref, + } +} + +/// Parse the raw WASM binary to extract precise reference type info for globals. +/// +/// Returns a vec of `Option<(nullable, heap_type_s33)>` for each global +/// (imported globals first, then defined globals), where: +/// - `None` = not a reference type +/// - `Some((true, -16))` = (ref null func) +/// - `Some((false, -16))` = (ref func) +/// - `Some((true, -17))` = (ref null extern) / externref +/// - `Some((true, idx))` = (ref null $idx) +/// - `Some((false, idx))` = (ref $idx) +fn extract_precise_global_ref_types(wasm_binary: &[u8]) -> Vec> { + let mut result = Vec::new(); + + // Parse WASM binary sections to find import section (id=2) and global section (id=6) + if wasm_binary.len() < 8 { + return result; + } + + let mut offset = 8; // Skip magic + version + + while offset < wasm_binary.len() { + let section_id = wasm_binary[offset]; + offset += 1; + + let (section_len, bytes_read) = match read_leb128_u32_from_slice(wasm_binary, offset) { + Some(v) => v, + None => break, + }; + offset += bytes_read; + + let section_end = offset + section_len as usize; + if section_end > wasm_binary.len() { + break; + } + + match section_id { + 2 => { + // Import section - extract global import ref types + parse_import_section_global_refs(wasm_binary, offset, section_end, &mut result); + } + 6 => { + // Global section - extract defined global ref types + parse_global_section_refs(wasm_binary, offset, section_end, &mut result); + } + _ => {} + } + + offset = section_end; + } + + result +} + +/// Parse the import section to extract precise ref types for global imports. +fn parse_import_section_global_refs( + data: &[u8], mut offset: usize, end: usize, + result: &mut Vec>, +) { + let (count, br) = match read_leb128_u32_from_slice(data, offset) { + Some(v) => v, + None => return, + }; + offset += br; + + for _ in 0..count { + if offset >= end { return; } + // Skip module name (length-prefixed string) + let (name_len, br) = match read_leb128_u32_from_slice(data, offset) { + Some(v) => v, + None => return, + }; + offset += br + name_len as usize; + + // Skip field name + let (name_len, br) = match read_leb128_u32_from_slice(data, offset) { + Some(v) => v, + None => return, + }; + offset += br + name_len as usize; + + if offset >= end { return; } + let kind = data[offset]; + offset += 1; + + match kind { + 0x00 => { + // Function import: skip type index + let (_, br) = match read_leb128_u32_from_slice(data, offset) { + Some(v) => v, + None => return, + }; + offset += br; + } + 0x01 => { + // Table import: skip ref_type + limits + if offset >= end { return; } + let rt_byte = data[offset]; + offset += 1; + if rt_byte == 0x63 || rt_byte == 0x64 { + // Consume heap type LEB128 + let (_, br) = match read_leb128_i64_from_slice(data, offset) { + Some(v) => v, + None => return, + }; + offset += br; + } + // Skip limits + if offset >= end { return; } + let flags = data[offset]; + offset += 1; + let is_table64 = flags & 0x04 != 0; + offset = skip_leb128_limit(data, offset, flags & 0x01 != 0, is_table64); + } + 0x02 => { + // Memory import: skip limits + if offset >= end { return; } + let flags = data[offset]; + offset += 1; + let is_mem64 = flags & 0x04 != 0; + let has_max = flags & 0x01 != 0; + let has_page_size = flags & 0x08 != 0; + offset = skip_leb128_limit(data, offset, has_max, is_mem64); + if has_page_size { + let (_, br) = match read_leb128_u32_from_slice(data, offset) { + Some(v) => v, + None => return, + }; + offset += br; + } + } + 0x03 => { + // Global import: THIS IS WHAT WE WANT + let ref_info = parse_precise_ref_type(data, &mut offset); + result.push(ref_info); + // Skip mutability byte + if offset < end { + offset += 1; + } + } + 0x04 => { + // Tag import: skip attribute + type_idx + offset += 1; // attribute + let (_, br) = match read_leb128_u32_from_slice(data, offset) { + Some(v) => v, + None => return, + }; + offset += br; + } + _ => return, + } + } +} + +/// Parse the global section to extract precise ref types for defined globals. +fn parse_global_section_refs( + data: &[u8], mut offset: usize, end: usize, + result: &mut Vec>, +) { + let (count, br) = match read_leb128_u32_from_slice(data, offset) { + Some(v) => v, + None => return, + }; + offset += br; + + for _ in 0..count { + if offset >= end { return; } + // Parse value type + let ref_info = parse_precise_ref_type(data, &mut offset); + result.push(ref_info); + // Skip mutability byte + if offset < end { + offset += 1; + } + // Skip init expression (scan for END = 0x0B) + while offset < end && data[offset] != 0x0B { + offset += 1; + } + if offset < end { + offset += 1; // skip END + } + } +} + +/// Parse a value type at the given offset and return precise ref type info. +/// Returns `Some((nullable, heap_type_s33))` for reference types, `None` otherwise. +/// Advances `offset` past the value type bytes. +fn parse_precise_ref_type(data: &[u8], offset: &mut usize) -> Option<(bool, i64)> { + if *offset >= data.len() { return None; } + let byte = data[*offset]; + match byte { + 0x70 => { *offset += 1; Some((true, -16)) } // funcref = (ref null func) + 0x6F => { *offset += 1; Some((true, -17)) } // externref = (ref null extern) + 0x6E => { *offset += 1; Some((true, -18)) } // anyref + 0x6D => { *offset += 1; Some((true, -19)) } // eqref + 0x6C => { *offset += 1; Some((true, -20)) } // i31ref + 0x6B => { *offset += 1; Some((true, -21)) } // structref + 0x6A => { *offset += 1; Some((true, -22)) } // arrayref + 0x69 => { *offset += 1; Some((true, -23)) } // exnref + 0x73 => { *offset += 1; Some((true, -13)) } // nofunc + 0x72 => { *offset += 1; Some((true, -14)) } // noextern + 0x71 => { *offset += 1; Some((true, -15)) } // none + 0x74 => { *offset += 1; Some((true, -12)) } // noexn + 0x63 | 0x64 => { + // ref null heaptype (0x63) or ref heaptype (0x64) + let nullable = byte == 0x63; + *offset += 1; + let (ht, br) = read_leb128_i64_from_slice(data, *offset)?; + *offset += br; + Some((nullable, ht)) + } + // Non-reference types + _ => { *offset += 1; None } + } +} + +/// Read a u32 LEB128 from a byte slice at the given offset. +/// Returns (value, bytes_consumed). +fn read_leb128_u32_from_slice(data: &[u8], offset: usize) -> Option<(u32, usize)> { + let mut result: u32 = 0; + let mut shift = 0u32; + let mut i = 0usize; + loop { + if offset + i >= data.len() { return None; } + let byte = data[offset + i]; + i += 1; + result |= ((byte & 0x7F) as u32) << shift; + if byte & 0x80 == 0 { + return Some((result, i)); + } + shift += 7; + if shift >= 35 { return None; } // overflow protection + } +} + +/// Read an i64 (s33-compatible) LEB128 from a byte slice at the given offset. +/// Returns (value, bytes_consumed). +fn read_leb128_i64_from_slice(data: &[u8], offset: usize) -> Option<(i64, usize)> { + let mut result: i64 = 0; + let mut shift = 0u32; + let mut i = 0usize; + let mut last_byte = 0u8; + loop { + if offset + i >= data.len() { return None; } + last_byte = data[offset + i]; + i += 1; + result |= ((last_byte & 0x7F) as i64) << shift; + shift += 7; + if last_byte & 0x80 == 0 { + break; + } + if shift >= 70 { return None; } // overflow protection + } + // Sign extend if the sign bit of the last byte is set + if shift < 64 && (last_byte & 0x40) != 0 { + result |= !0i64 << shift; + } + Some((result, i)) +} + +/// Skip LEB128-encoded limits (min, optional max) at the given offset. +fn skip_leb128_limit(data: &[u8], mut offset: usize, has_max: bool, is_64: bool) -> usize { + if is_64 { + if let Some((_, br)) = read_leb128_i64_from_slice(data, offset) { offset += br; } + if has_max { + if let Some((_, br)) = read_leb128_i64_from_slice(data, offset) { offset += br; } + } + } else { + if let Some((_, br)) = read_leb128_u32_from_slice(data, offset) { offset += br; } + if has_max { + if let Some((_, br)) = read_leb128_u32_from_slice(data, offset) { offset += br; } + } + } + offset +} + +/// Check if two types belong to structurally compatible rec groups. +/// +/// Per the GC spec, two types are only compatible if they occupy the same +/// position within rec groups of the same structure. A type in a singleton +/// rec group is different from a type in a multi-type rec group, even if +/// structurally identical. +fn are_types_rec_group_compatible( + type_a_idx: u32, precise_a: &PreciseTypeInfo, + type_b_idx: u32, precise_b: &PreciseTypeInfo, + module_a: &Module, module_b: &Module, +) -> bool { + // Find which rec group each type belongs to + let rg_a = find_rec_group(type_a_idx, &precise_a.rec_groups); + let rg_b = find_rec_group(type_b_idx, &precise_b.rec_groups); + + + let (rg_a_start, rg_a_count) = match rg_a { + Some(rg) => rg, + None => return true, // No rec group info, assume compatible + }; + let (rg_b_start, rg_b_count) = match rg_b { + Some(rg) => rg, + None => return true, + }; + + // Rec groups must have the same number of types + if rg_a_count != rg_b_count { + return false; + } + + // The type must be at the same offset within its rec group + let offset_a = type_a_idx - rg_a_start; + let offset_b = type_b_idx - rg_b_start; + if offset_a != offset_b { + return false; + } + + // For each pair of types in the rec group, check structural compatibility + // This includes checking that supertypes match and composite types match + for i in 0..rg_a_count { + let idx_a = rg_a_start + i; + let idx_b = rg_b_start + i; + + // Supertypes must match (relative to their rec group starts) + let super_a = module_a.type_supertypes.get(idx_a as usize).copied().flatten(); + let super_b = module_b.type_supertypes.get(idx_b as usize).copied().flatten(); + match (super_a, super_b) { + (None, None) => {} + (Some(sa), Some(sb)) => { + // Supertype indices must refer to the same relative position. + // Both must be either inside or outside their rec groups. + let sa_inside = sa >= rg_a_start && sa < rg_a_start + rg_a_count; + let sb_inside = sb >= rg_b_start && sb < rg_b_start + rg_b_count; + + if sa_inside != sb_inside { + + return false; // One inside, one outside -> mismatch + } + if sa_inside { + // Both inside their respective rec groups: compare offsets + let sa_offset = sa - rg_a_start; + let sb_offset = sb - rg_b_start; + if sa_offset != sb_offset { + return false; + } + } else { + // Both outside their rec groups: must refer to + // rec-group-compatible types (recursively) + if !are_types_rec_group_compatible( + sa, precise_a, sb, precise_b, module_a, module_b, + ) { + + return false; + } + } + } + _ => return false, // One has supertype, other doesn't + } + + // Check that finality matches + let final_a = precise_a.type_finality.get(idx_a as usize).copied().unwrap_or(true); + let final_b = precise_b.type_finality.get(idx_b as usize).copied().unwrap_or(true); + if final_a != final_b { + return false; + } + + // Check structural compatibility of the composite types. + // First check GC type kind (func vs struct vs array) matches + let gc_a = module_a.gc_types.get(idx_a as usize); + let gc_b = module_b.gc_types.get(idx_b as usize); + match (gc_a, gc_b) { + (Some(kiln_runtime::module::GcTypeInfo::Struct(fields_a)), + Some(kiln_runtime::module::GcTypeInfo::Struct(fields_b))) => { + // Struct types: compare field counts and storage types. + // Field reference types that point to type indices must be + // resolved relative to rec groups (handled by the overall check). + if fields_a.len() != fields_b.len() { + return false; + } + for (fa, fb) in fields_a.iter().zip(fields_b.iter()) { + if fa.mutable != fb.mutable { + return false; + } + // Compare field storage types, relativizing type index references + // to their rec group positions for cross-module comparison + use kiln_runtime::module::GcFieldStorage; + match (&fa.storage, &fb.storage) { + (GcFieldStorage::Ref(idx_a), GcFieldStorage::Ref(idx_b)) + | (GcFieldStorage::RefNull(idx_a), GcFieldStorage::RefNull(idx_b)) => { + // Check if the referenced types are inside their rec groups + let a_inside = *idx_a >= rg_a_start && *idx_a < rg_a_start + rg_a_count; + let b_inside = *idx_b >= rg_b_start && *idx_b < rg_b_start + rg_b_count; + if a_inside && b_inside { + // Both inside: compare relative offsets + if idx_a - rg_a_start != idx_b - rg_b_start { + return false; + } + } else if !a_inside && !b_inside { + // Both outside: check recursively + if !are_types_rec_group_compatible( + *idx_a, precise_a, *idx_b, precise_b, module_a, module_b, + ) { + return false; + } + } else { + return false; // One inside, one outside + } + } + (a_stor, b_stor) if a_stor == b_stor => {} // Non-ref types: direct equality + _ => return false, // Different storage kinds + } + } + } + (Some(kiln_runtime::module::GcTypeInfo::Array(elem_a)), + Some(kiln_runtime::module::GcTypeInfo::Array(elem_b))) => { + if elem_a.storage != elem_b.storage || elem_a.mutable != elem_b.mutable { + return false; + } + } + (Some(kiln_runtime::module::GcTypeInfo::Func(..)), + Some(kiln_runtime::module::GcTypeInfo::Func(..))) => { + // Func types: use structural comparison + if !are_func_types_structurally_compatible(idx_a, module_a, idx_b, module_b, 0) { + return false; + } + } + (None, None) => { + // No GC info: fall back to func type comparison + if !are_func_types_structurally_compatible(idx_a, module_a, idx_b, module_b, 0) { + return false; + } + } + _ => { + // Different GC type kinds (e.g., Func vs Struct) + return false; + } + } + } + + true +} + +/// Find which rec group a type index belongs to. +/// Returns (start_type_idx, type_count) for the rec group, or None. +fn find_rec_group(type_idx: u32, rec_groups: &[(u32, u32)]) -> Option<(u32, u32)> { + for &(start, count) in rec_groups { + if type_idx >= start && type_idx < start + count { + return Some((start, count)); + } + } + None +} + +/// Count how many global imports appear at or before `import_idx` in the import list. +/// Returns the global-specific index for the import at position `import_idx`. +fn count_global_imports_up_to(import_types: &[RuntimeImportDesc], import_idx: usize) -> usize { + let mut global_count = 0; + for (i, desc) in import_types.iter().enumerate() { + if matches!(desc, RuntimeImportDesc::Global(_)) { + if i == import_idx { + return global_count; + } + global_count += 1; + } + } + global_count +} + +/// Check if two precise reference types are exactly equal. +/// +/// Uses the precise ref info (from binary parsing) when available to distinguish +/// nullable from non-nullable abstract heap types that the runtime ValueType +/// conflates (e.g., both `(ref null func)` and `(ref func)` map to FuncRef). +fn precise_ref_types_equal( + a_vt: &ValueType, a_precise: Option<(bool, i64)>, + b_vt: &ValueType, b_precise: Option<(bool, i64)>, +) -> bool { + // If we have precise info for both, use it for definitive comparison + match (a_precise, b_precise) { + (Some((a_null, a_ht)), Some((b_null, b_ht))) => { + a_null == b_null && a_ht == b_ht + } + (Some(_), None) | (None, Some(_)) => { + // One has precise info, one doesn't. For abstract ref types that + // lose info (FuncRef, ExternRef), the precise info is the authority. + // For non-ref or concrete ref types, ValueType is sufficient. + // If the ValueTypes match AND neither is an abstract ref type that + // could be lossy, they're equal. + if a_vt == b_vt { + // Check if this is a potentially lossy type + match a_vt { + ValueType::FuncRef | ValueType::ExternRef + | ValueType::AnyRef | ValueType::EqRef + | ValueType::I31Ref | ValueType::ExnRef => { + // These could be lossy - can't confirm equality without + // precise info on both sides. Be conservative: they match + // only if we have no evidence of mismatch. + // The side with precise info tells us the nullability. + // The side without precise info is assumed nullable + // (since the shorthand forms like 0x70 are nullable). + let (precise_null, _) = a_precise.or(b_precise).unwrap(); + // If the precise side says non-nullable but the other side + // decoded to a nullable shorthand, they're different. + precise_null // If nullable, they match; if non-nullable, mismatch + } + _ => true, // Non-ref or concrete ref types are not lossy + } + } else { + false + } + } + (None, None) => { + // No precise info - fall back to ValueType comparison + a_vt == b_vt + } + } +} + +/// Check if `sub_vt` (with precise info) is a subtype of `sup_vt` (with precise info). +/// +/// This extends `is_value_type_subtype` with precise nullability info from the binary. +fn precise_ref_type_is_subtype( + sub_vt: &ValueType, sub_precise: Option<(bool, i64)>, + sup_vt: &ValueType, sup_precise: Option<(bool, i64)>, +) -> bool { + // First check: if we have precise info, use it for definitive nullability + if let (Some((sub_null, sub_ht)), Some((sup_null, sup_ht))) = (sub_precise, sup_precise) { + // Non-nullable is subtype of nullable but not vice versa + if sub_null && !sup_null { + return false; + } + // Heap type subtyping + return is_heap_type_subtype(sub_ht, sup_ht); + } + + // If only one side has precise info, use it to refine the check + if let Some((sup_null, sup_ht)) = sup_precise { + if !sup_null { + // Supertype is non-nullable. Check if sub is nullable. + if let Some((sub_null, _)) = sub_precise { + if sub_null { + return false; // nullable is NOT subtype of non-nullable + } + } else { + // Sub has no precise info. If it's FuncRef (which is nullable), + // it's NOT a subtype of a non-nullable sup. + match sub_vt { + ValueType::FuncRef | ValueType::ExternRef + | ValueType::AnyRef | ValueType::EqRef => { + return false; + } + _ => {} + } + } + } + // Check if sub's heap type is a subtype of sup's heap type + if let Some((_, sub_ht)) = sub_precise { + return is_heap_type_subtype(sub_ht, sup_ht); + } + } + + if let Some((sub_null, _sub_ht)) = sub_precise { + if sub_null { + // Sub is nullable. Sup must also be nullable for subtyping. + if let Some((sup_null, _)) = sup_precise { + if !sup_null { + return false; + } + } + // Without precise sup info, check if sup ValueType represents nullable + // FuncRef = (ref null func), ExternRef = (ref null extern) - all nullable + } + } + + // Fall back to standard ValueType subtyping + is_value_type_subtype(sub_vt, sup_vt) +} + +/// Check if heap type `sub_ht` (as s33) is a subtype of `sup_ht` (as s33). +/// +/// Heap type hierarchy: +/// - func is supertype of all function types (concrete indices that are func types) +/// - extern is supertype of all extern types +/// - any > eq > i31, struct, array +/// - nofunc <: all func types, noextern <: all extern types, none <: all any types +fn is_heap_type_subtype(sub_ht: i64, sup_ht: i64) -> bool { + if sub_ht == sup_ht { + return true; + } + // Abstract heap type codes (negative s33): + // -16 = func, -17 = extern, -18 = any, -19 = eq + // -20 = i31, -21 = struct, -22 = array, -23 = exn + // -13 = nofunc, -14 = noextern, -15 = none, -12 = noexn + match (sub_ht, sup_ht) { + // nofunc is bottom of func hierarchy + (-13, -16) => true, // nofunc <: func + (-13, _) if sup_ht >= 0 => true, // nofunc <: any concrete func type + // noextern is bottom of extern hierarchy + (-14, -17) => true, // noextern <: extern + // none is bottom of any hierarchy + (-15, -18) => true, // none <: any + (-15, -19) => true, // none <: eq + (-15, -20) => true, // none <: i31 + (-15, -21) => true, // none <: struct + (-15, -22) => true, // none <: array + // any hierarchy + (-19, -18) => true, // eq <: any + (-20, -18) => true, // i31 <: any + (-20, -19) => true, // i31 <: eq + (-21, -18) => true, // struct <: any + (-21, -19) => true, // struct <: eq + (-22, -18) => true, // array <: any + (-22, -19) => true, // array <: eq + // noexn is bottom of exn hierarchy + (-12, -23) => true, // noexn <: exn + // Concrete type index <: func (all concrete func types are subtypes of func) + (idx, -16) if idx >= 0 => true, + _ => false, + } +} + /// Check if value type `sub` is a subtype of value type `sup` /// /// Per the WebAssembly spec, subtyping rules for reference types: @@ -1558,6 +2325,73 @@ fn is_value_type_subtype_cross_module( } } +/// Check if type `sub_idx` in `sub_module` matches `sup_idx` in `sup_module`, +/// using rec group structure and finality for type identity. +/// +/// This is used for function import matching. The sub_idx type must be either +/// identical to or a subtype of the sup_idx type. Type identity requires: +/// - Matching rec group structure (same size, same supertypes, same finality) +/// - Same position within the rec group +/// - Structurally compatible composite types +fn is_type_idx_match_cross_module( + sub_idx: u32, sub_module: &Module, sub_precise: &PreciseTypeInfo, + sup_idx: u32, sup_module: &Module, sup_precise: &PreciseTypeInfo, + depth: u32, +) -> bool { + if depth > MAX_SUBTYPE_RECURSION_DEPTH { + return false; + } + + // Check type identity using rec group and finality awareness + if are_types_identical_cross_module( + sub_idx, sub_module, sub_precise, + sup_idx, sup_module, sup_precise, + ) { + return true; + } + + // Walk the supertype chain of sub_idx + if let Some(Some(parent_idx)) = sub_module.type_supertypes.get(sub_idx as usize) { + return is_type_idx_match_cross_module( + *parent_idx, sub_module, sub_precise, + sup_idx, sup_module, sup_precise, + depth + 1, + ); + } + + false +} + +/// Check if two types are identical across modules, accounting for rec group +/// structure and finality. +/// +/// Two types are identical if: +/// 1. They have the same finality +/// 2. They belong to rec groups with the same structure +/// 3. They occupy the same position within their rec groups +/// 4. Their composite types (params, results) are structurally compatible +fn are_types_identical_cross_module( + idx_a: u32, module_a: &Module, precise_a: &PreciseTypeInfo, + idx_b: u32, module_b: &Module, precise_b: &PreciseTypeInfo, +) -> bool { + // Check finality matches + let final_a = precise_a.type_finality.get(idx_a as usize).copied().unwrap_or(true); + let final_b = precise_b.type_finality.get(idx_b as usize).copied().unwrap_or(true); + if final_a != final_b { + return false; + } + + // Check rec group compatibility + if !are_types_rec_group_compatible( + idx_a, precise_a, idx_b, precise_b, module_a, module_b, + ) { + return false; + } + + // Check structural compatibility of the types themselves + are_func_types_structurally_compatible(idx_a, module_a, idx_b, module_b, 0) +} + /// Check if type index `sub_idx` in `sub_module` is a subtype of `sup_idx` in `sup_module`. /// /// This walks the supertype chain of `sub_idx` checking for structural equivalence diff --git a/kiln-build-core/src/wast_validator.rs b/kiln-build-core/src/wast_validator.rs index 990eaf91..a387e175 100644 --- a/kiln-build-core/src/wast_validator.rs +++ b/kiln-build-core/src/wast_validator.rs @@ -291,6 +291,12 @@ const WASM_MAX_MEMORY_PAGES: u32 = 65536; impl WastModuleValidator { /// Validate a module pub fn validate(module: &Module) -> Result<()> { + { + use std::io::Write; + if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open("/tmp/kiln_debug.log") { + writeln!(f, "[VALIDATE] called with {} functions, {} types", module.functions.len(), module.types.len()).ok(); + } + } // Validate memory, table, and tag limits Self::validate_memory_limits(module)?; Self::validate_table_limits(module)?; @@ -925,6 +931,7 @@ impl WastModuleValidator { module: &Module, declared_functions: &HashSet, ) -> Result<()> { + // (debug removed) // Build local variable types: parameters first, then locals let mut local_types = Vec::new(); @@ -1444,6 +1451,14 @@ impl WastModuleValidator { return Err(anyhow!("type mismatch")); } for &expected in frame.output_types.iter().rev() { + { + use std::io::Write; + if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open("/tmp/kiln_block_end_debug.log") { + let actual_top = stack.last(); + writeln!(f, "[BLOCK_END] reachable: expected={:?}, actual_top={:?}, stack_len={}, frame_height={}", + expected, actual_top, stack.len(), frame_height).ok(); + } + } if !Self::pop_type_with_module(&mut stack, expected, frame_height, false, Some(module)) { return Err(anyhow!("type mismatch")); } @@ -1500,11 +1515,12 @@ impl WastModuleValidator { Some(module), )?; - // After br_if, the stack types must be updated to the label's result types - // This is because from the perspective of subsequent code, the values - // could have been branched with (and cast to the label's type). - // GC spec: br_if with typed values narrows to label's result type. - if (label_idx as usize) < frames.len() && !Self::is_unreachable(&frames) { + // After br_if, the stack types must be updated to the label's result types. + // Per the spec, br_if : [t* i32] -> [t*] where labels[l] = [t*]. + // The fall-through path always has the label's types on the stack. + // This applies even in unreachable code: br_if consumes t* (polymorphic + // underflow is OK) and produces t* (concrete label types). + if (label_idx as usize) < frames.len() { let target_frame = &frames[frames.len() - 1 - label_idx as usize]; let label_types = if target_frame.frame_type == FrameType::Loop { target_frame.input_types.clone() @@ -1512,15 +1528,23 @@ impl WastModuleValidator { target_frame.output_types.clone() }; - // Pop the branch values and push back the label's types - // This changes the stack types to match the label's expected types let num_values = label_types.len(); - if num_values > 0 && stack.len() >= frame_height + num_values { - // Pop the original values - for _ in 0..num_values { - stack.pop(); + if num_values > 0 { + // Pop the branch values (polymorphic underflow OK in unreachable) + let unreachable = Self::is_unreachable(&frames); + for i in 0..num_values { + let expected = label_types[label_types.len() - 1 - i]; + if !Self::pop_type_with_module( + &mut stack, + expected, + frame_height, + unreachable, + Some(module), + ) { + return Err(anyhow!("type mismatch")); + } } - // Push the label's result types (more general types) + // Push the label's result types for the fall-through path for ty in &label_types { stack.push(*ty); } @@ -2484,7 +2508,7 @@ impl WastModuleValidator { local_init[local_idx as usize] = true; }, 0x22 => { - // local.tee + // local.tee: [t] -> [t] where t is the local's type let (local_idx, new_offset) = Self::parse_varuint32(code, offset)?; offset = new_offset; @@ -2492,15 +2516,17 @@ impl WastModuleValidator { return Err(anyhow!("local.tee: invalid local index {}", local_idx)); } - // In unreachable code, the stack is polymorphic - if !Self::is_unreachable(&frames) { - let expected = StackType::from_value_type(local_types[local_idx as usize]); - if stack.last() != Some(&expected) - && stack.last() != Some(&StackType::Unknown) - { - return Err(anyhow!("local.tee: type mismatch")); - } + let expected = StackType::from_value_type(local_types[local_idx as usize]); + let frame_height = Self::current_frame_height(&frames); + let unreachable = Self::is_unreachable(&frames); + + // Pop and push to properly model the [t] -> [t] stack effect. + // In unreachable code with polymorphic underflow, pop succeeds + // and push adds the local's concrete type to the stack. + if !Self::pop_type(&mut stack, expected, frame_height, unreachable) { + return Err(anyhow!("type mismatch")); } + stack.push(expected); // Mark the local as initialized local_init[local_idx as usize] = true; @@ -2693,28 +2719,27 @@ impl WastModuleValidator { }; // Check that operands are numeric types (not reference types) - // Untyped select cannot be used with funcref, externref, etc. - if !unreachable { - // In reachable code: reject reference types and mismatched types - if !type1.is_numeric() || !type2.is_numeric() { - return Err(anyhow!("type mismatch")); - } - if type1 != type2 { - return Err(anyhow!("type mismatch")); - } - stack.push(type1); - } else { - // In unreachable code: the stack is polymorphic. - // Any concrete types from unreachable instructions are valid. - // Per spec, select in unreachable code always succeeds. - // Push the most specific type, or Unknown if both are polymorphic. - let result_type = if type1 != StackType::Unknown { - type1 - } else { - type2 - }; - stack.push(result_type); + // and that both operands have the same type. + // Per the spec, these checks apply even in unreachable code + // for concrete (non-polymorphic) values on the stack. + // Polymorphic (Unknown) values from underflow are compatible with anything. + if type1 != StackType::Unknown && !type1.is_numeric() { + return Err(anyhow!("type mismatch")); + } + if type2 != StackType::Unknown && !type2.is_numeric() { + return Err(anyhow!("type mismatch")); + } + if type1 != StackType::Unknown && type2 != StackType::Unknown && type1 != type2 { + return Err(anyhow!("type mismatch")); } + // Push the result type: use the most specific concrete type, + // or Unknown if both are polymorphic. + let result_type = if type1 != StackType::Unknown { + type1 + } else { + type2 + }; + stack.push(result_type); }, 0x1C => { // select t* (typed select) @@ -3610,11 +3635,21 @@ impl WastModuleValidator { if src_table as usize >= Self::total_tables(module) { return Err(anyhow!("unknown table {}", src_table)); } + // Validate element type compatibility: src element type must be subtype of dst + if let (Some(dst_elem), Some(src_elem)) = ( + Self::get_table_element_type(module, dst_table), + Self::get_table_element_type(module, src_table), + ) { + if !Self::is_ref_type_subtype(&src_elem, &dst_elem, module) { + return Err(anyhow!("type mismatch")); + } + } let dst64 = Self::is_table64(module, dst_table); let src64 = Self::is_table64(module, src_table); let it_d = if dst64 { StackType::I64 } else { StackType::I32 }; let it_s = if src64 { StackType::I64 } else { StackType::I32 }; - let it_n = if dst64 || src64 { StackType::I64 } else { StackType::I32 }; + // Per spec: length operand uses i64 only when BOTH tables are table64 + let it_n = if dst64 && src64 { StackType::I64 } else { StackType::I32 }; // Pop n (length), s (source), d (dest) in reverse if !Self::pop_type(&mut stack, it_n, frame_height, unreachable) { return Err(anyhow!("type mismatch")); @@ -4359,60 +4394,85 @@ impl WastModuleValidator { stack.push(StackType::Unknown); }, // br_on_cast: flags(1 byte) + label(u32) + ht1(s33) + ht2(s33) - // Spec: rt2 <: rt1 required + // Spec: rt2 <: rt1 required, branch carries rt2, fall-through carries rt1\rt2 0x18 => { if offset >= code.len() { return Err(anyhow!("unexpected end in br_on_cast")); } let flags = code[offset]; offset += 1; - let (_label, new_off) = Self::parse_varuint32(code, offset)?; + let (label, new_off) = Self::parse_varuint32(code, offset)?; offset = new_off; let (ht1_raw, new_off2) = Self::read_leb128_signed(code, offset)?; offset = new_off2; let (ht2_raw, new_off3) = Self::read_leb128_signed(code, offset)?; offset = new_off3; - // Validate: rt2 must be a subtype of rt1 let ht1_nullable = (flags & 1) != 0; let ht2_nullable = (flags & 2) != 0; let ht1_st = Self::heap_type_to_stack_type(ht1_raw, ht1_nullable); let ht2_st = Self::heap_type_to_stack_type(ht2_raw, ht2_nullable); + // Validate: rt2 <: rt1 (both heap type and nullability) + // Nullability: nullable is NOT a subtype of non-nullable + if ht2_nullable && !ht1_nullable { + return Err(anyhow!("type mismatch")); + } + // Check heap type subtyping if ht1_st != StackType::Unknown && ht2_st != StackType::Unknown - && !Self::is_subtype_of_in_module(&ht2_st, &ht1_st, module) + && !Self::is_heap_subtype_for_cast(&ht2_st, ht2_raw, &ht1_st, ht1_raw, module) { return Err(anyhow!("type mismatch")); } - // Keep permissive stack handling for fall-through + // Validate label type decomposition for br_on_cast: + // The label type must end with unpack(rt2) + Self::validate_br_on_cast_label( + label, &ht2_st, &frames, unreachable, module, + )?; + // Pop rt1 (the source ref type) Self::pop_type(&mut stack, StackType::Unknown, frame_height, unreachable); - stack.push(StackType::Unknown); + // Push fall-through type: rt1 \ rt2 (diff type) + let diff_nullable = ht1_nullable && !ht2_nullable; + let diff_st = Self::heap_type_to_stack_type(ht1_raw, diff_nullable); + stack.push(diff_st); }, // br_on_cast_fail: flags(1 byte) + label(u32) + ht1(s33) + ht2(s33) - // Spec: rt2 <: rt1 required + // Spec: rt2 <: rt1 required, branch carries rt1\rt2, fall-through carries rt2 0x19 => { if offset >= code.len() { return Err(anyhow!("unexpected end in br_on_cast_fail")); } let flags = code[offset]; offset += 1; - let (_label, new_off) = Self::parse_varuint32(code, offset)?; + let (label, new_off) = Self::parse_varuint32(code, offset)?; offset = new_off; let (ht1_raw, new_off2) = Self::read_leb128_signed(code, offset)?; offset = new_off2; let (ht2_raw, new_off3) = Self::read_leb128_signed(code, offset)?; offset = new_off3; - // Validate: rt2 must be a subtype of rt1 let ht1_nullable = (flags & 1) != 0; let ht2_nullable = (flags & 2) != 0; let ht1_st = Self::heap_type_to_stack_type(ht1_raw, ht1_nullable); let ht2_st = Self::heap_type_to_stack_type(ht2_raw, ht2_nullable); + // Validate: rt2 <: rt1 (both heap type and nullability) + if ht2_nullable && !ht1_nullable { + return Err(anyhow!("type mismatch")); + } + // Check heap type subtyping if ht1_st != StackType::Unknown && ht2_st != StackType::Unknown - && !Self::is_subtype_of_in_module(&ht2_st, &ht1_st, module) + && !Self::is_heap_subtype_for_cast(&ht2_st, ht2_raw, &ht1_st, ht1_raw, module) { return Err(anyhow!("type mismatch")); } - // Keep permissive stack handling for fall-through + // Validate label type decomposition for br_on_cast_fail: + // The label type must end with unpack(rt1\rt2) + let diff_nullable = ht1_nullable && !ht2_nullable; + let diff_st = Self::heap_type_to_stack_type(ht1_raw, diff_nullable); + Self::validate_br_on_cast_label( + label, &diff_st, &frames, unreachable, module, + )?; + // Pop rt1 (the source ref type) Self::pop_type(&mut stack, StackType::Unknown, frame_height, unreachable); - stack.push(StackType::Unknown); + // Push fall-through type: rt2 (the cast succeeded) + stack.push(ht2_st); }, // any.convert_extern: [(ref extern)] -> [(ref any)] 0x1A => { @@ -5807,11 +5867,6 @@ impl WastModuleValidator { return Err(anyhow!("br: label index {} out of range", label_idx)); } - // In unreachable code, the stack is polymorphic - any values are acceptable - if unreachable { - return Ok(()); - } - // Get the current frame (innermost) to check our available stack values let current_frame = frames.last().ok_or_else(|| anyhow!("no control frame"))?; let current_stack_height = current_frame.stack_height; @@ -5832,6 +5887,33 @@ impl WastModuleValidator { // Values below current_stack_height belong to parent frames and cannot be used let available_values = stack.len().saturating_sub(current_stack_height); + if unreachable { + // In unreachable code, the stack is polymorphic for underflow. + // If there are fewer values than expected, that's fine (polymorphic). + // But any concrete values that ARE on the stack must still type-check + // against the corresponding expected types. + let check_count = available_values.min(expected_types.len()); + for i in 0..check_count { + // Check from the top of the stack against the end of expected_types + let stack_idx = stack.len() - 1 - i; + let expected_idx = expected_types.len() - 1 - i; + let actual = &stack[stack_idx]; + let expected = &expected_types[expected_idx]; + // Unknown (polymorphic) values match anything + if *actual == StackType::Unknown { + continue; + } + if let Some(m) = module { + if !Self::is_subtype_of_in_module(actual, expected, m) { + return Err(anyhow!("type mismatch")); + } + } else if !actual.is_subtype_of(expected) { + return Err(anyhow!("type mismatch")); + } + } + return Ok(()); + } + // Check that the current frame has enough values for the branch if available_values < expected_types.len() { // Not enough values in the current frame's scope @@ -6609,6 +6691,72 @@ impl WastModuleValidator { } } + /// Validate that a br_on_cast/br_on_cast_fail label type is compatible with + /// the branch ref type. + /// + /// Per the spec's algorithmic typing rules, the label type `[t*]` must + /// decompose as `[t1* t']` where `t'` equals `unpack(rt_branch)`. + /// This is an EQUALITY check (not subtyping) per the spec. + /// + /// If the label type is empty, the instruction is invalid because there + /// must be at least one type slot for the branch ref type. + fn validate_br_on_cast_label( + label_idx: u32, + branch_ref_st: &StackType, + frames: &[ControlFrame], + unreachable: bool, + _module: &Module, + ) -> Result<()> { + if unreachable { + return Ok(()); + } + if label_idx as usize >= frames.len() { + return Err(anyhow!("br_on_cast: label index {} out of range", label_idx)); + } + let target_frame = &frames[frames.len() - 1 - label_idx as usize]; + let expected_types = if target_frame.frame_type == FrameType::Loop { + &target_frame.input_types + } else { + &target_frame.output_types + }; + // The label type must have at least one type (the branch ref type) + if expected_types.is_empty() { + return Err(anyhow!("type mismatch")); + } + // The last element of the label type must be a supertype of the branch + // ref type. This is checked using subtype matching: the branch ref type + // must be a subtype of the label's last declared type. + let label_last = &expected_types[expected_types.len() - 1]; + if !Self::is_subtype_of_in_module(branch_ref_st, label_last, _module) { + return Err(anyhow!("type mismatch")); + } + Ok(()) + } + + /// Check if heap type `sub` is a subtype of heap type `sup` for br_on_cast validation. + /// + /// This is similar to `is_subtype_of_in_module` but works on the raw heap type + /// values to handle the case where abstract heap types lose nullability info in + /// StackType conversion. The nullability check must be done separately before + /// calling this function. + /// + /// For concrete types (ht_raw >= 0), the StackType already encodes the type index. + /// For abstract types (ht_raw < 0), both types are mapped to their abstract StackType + /// and standard heap type subtyping applies. Additionally, we must check that + /// concrete and abstract types are in the same hierarchy (e.g., a func type is not + /// a subtype of anyref). + fn is_heap_subtype_for_cast( + sub_st: &StackType, + _sub_raw: i32, + sup_st: &StackType, + _sup_raw: i32, + module: &Module, + ) -> bool { + // Delegate to the existing module-aware subtype check. + // This handles concrete<->concrete, concrete<->abstract, and abstract<->abstract. + Self::is_subtype_of_in_module(sub_st, sup_st, module) + } + /// Check if two ValueTypes are equal with module context for concrete indices. fn are_value_types_equal_in_module(v1: ValueType, v2: ValueType, module: &Module) -> bool { match (v1, v2) { diff --git a/kiln-component/src/components/component_instantiation.rs b/kiln-component/src/components/component_instantiation.rs index 9c4cf468..1fe82d38 100644 --- a/kiln-component/src/components/component_instantiation.rs +++ b/kiln-component/src/components/component_instantiation.rs @@ -837,7 +837,7 @@ impl ComponentInstance { parsed: &mut kiln_format::component::Component, host_registry: Option>, ) -> Result { - Self::from_parsed_with_handler(id, parsed, host_registry, None) + Self::from_parsed_internal(id, parsed, host_registry, None, false) } /// Create a ComponentInstance with an optional host handler for WASI dispatch during start functions. @@ -846,6 +846,28 @@ impl ComponentInstance { parsed: &mut kiln_format::component::Component, host_registry: Option>, host_handler: Option>, + ) -> Result { + Self::from_parsed_internal(id, parsed, host_registry, host_handler, false) + } + + /// Create a ComponentInstance in library mode (no _start required). + /// Used for nested/library components in WAC-composed P3 components. + pub fn from_parsed_library( + id: InstanceId, + parsed: &mut kiln_format::component::Component, + host_registry: Option>, + host_handler: Option>, + ) -> Result { + Self::from_parsed_internal(id, parsed, host_registry, host_handler, true) + } + + /// Internal constructor supporting both command and library modes. + fn from_parsed_internal( + id: InstanceId, + parsed: &mut kiln_format::component::Component, + host_registry: Option>, + host_handler: Option>, + library_mode: bool, ) -> Result { #[cfg(feature = "tracing")] trace!("from_parsed: ENTERED, Component passed by reference"); @@ -914,7 +936,11 @@ impl ComponentInstance { let mut linker = ComponentLinker::new(); #[cfg(feature = "tracing")] trace!("from_parsed: About to link_imports"); - let resolved_imports = linker.link_imports(&parsed.imports)?; + let resolved_imports = if library_mode { + linker.link_imports_lenient(&parsed.imports)? + } else { + linker.link_imports(&parsed.imports)? + }; #[cfg(feature = "tracing")] trace!("from_parsed: link_imports completed"); @@ -987,6 +1013,11 @@ impl ComponentInstance { #[cfg(feature = "std")] let mut inline_exports_map: BTreeMap)>> = BTreeMap::new(); + // Deferred InlineExports: (core_instance_idx, source_idx, export_mappings) + // For WAC-composed components where source instance is instantiated after InlineExports + #[cfg(feature = "std")] + let mut deferred_inline_exports: Vec<(usize, usize, Vec<(String, String, CoreSort, Option)>)> = + Vec::new(); // Track which core instance index exports _start (the main executable module) // This is the GENERIC way to find the main module - it's the one that exports _start let mut start_export_instance_idx: Option = None; @@ -1788,9 +1819,10 @@ impl ComponentInstance { // Store the actual export names for later use when linking instance imports inline_exports_map.insert(core_instance_idx, export_mappings); } else { - return Err(Error::runtime_error( - "InlineExports source instance not instantiated", - )); + // Source instance not yet instantiated — defer resolution. + // This handles WAC-composed components where InlineExports + // references an instance instantiated later in the section order. + deferred_inline_exports.push((core_instance_idx, src_idx as usize, export_mappings)); } } else if !canon_lowered_exports.is_empty() { // All exports are canon-lowered functions - this is a canonical function provider @@ -1842,10 +1874,23 @@ impl ComponentInstance { } } + // Resolve deferred InlineExports now that all core instances are created + #[cfg(feature = "std")] + for (core_inst_idx, src_idx, export_mappings) in deferred_inline_exports { + if let Some(&source_handle) = core_instances_map.get(&src_idx) { + core_instances_map.insert(core_inst_idx, source_handle); + inline_exports_map.insert(core_inst_idx, export_mappings); + } else { + return Err(Error::runtime_error( + "InlineExports source instance not instantiated (deferred resolution failed)", + )); + } + } + // Phase 7b: Process nested component instances // Nested components are instantiated and their exports can be aliased #[cfg(feature = "std")] - let nested_instances = { + let mut nested_instances = { use crate::types::{NestedComponentInstance, NestedExportKind, NestedExportRef}; use kiln_format::component::{InstanceExpr, Sort}; @@ -1959,9 +2004,9 @@ impl ComponentInstance { let mut nested_component_clone = nested_component.clone(); // Recursively instantiate the nested component - // Note: We pass None for host_registry - imports should come from args - // TODO: Implement proper import resolution from arg_refs - match Self::from_parsed(nested_id, &mut nested_component_clone, None) { + // Pass the parent's host_registry so nested components can resolve WASI imports + + match Self::from_parsed_library(nested_id, &mut nested_component_clone, host_registry.clone(), None) { Ok(child_instance) => { // Build exports map for this nested instance @@ -2010,20 +2055,19 @@ impl ComponentInstance { // Provide detailed error context for debugging via println // (the static error message is complemented by this output) - // Use static error message - dynamic details are already printed - // Check if this looks like a circular dependency - let is_circular = e.to_string().contains("nesting depth"); - let static_msg = if is_circular { - "Failed to instantiate nested component: possible circular dependency" - } else { - "Failed to instantiate nested component" - }; - - return Err(Error::new( - kiln_error::ErrorCategory::ComponentRuntime, - kiln_error::codes::COMPONENT_INSTANTIATION_RUNTIME_ERROR, - static_msg, - )); + // For non-critical nested components, continue without them. + // The parent can still function if only some nested components fail. + // This handles WAC-composed components where some nested components + // have inter-component imports we can't resolve yet. + #[cfg(feature = "tracing")] + tracing::warn!( + comp_idx = comp_idx, + inst_idx = inst_idx, + error = %e, + "Nested component instantiation failed, continuing" + ); + // Store empty exports for this instance so alias resolution doesn't crash + component_instance_exports.insert(inst_idx, std::collections::HashMap::new()); }, } } @@ -2092,6 +2136,37 @@ impl ComponentInstance { instance.imports = resolved_imports; } + // Check if any nested component has _start (for WAC-composed P3 components) + // Must happen BEFORE storing nested_instances since we may take the engine. + #[cfg(feature = "std")] + let mut nested_engine_for_start: Option<(Box, InstanceHandle)> = None; + #[cfg(feature = "std")] + { + let core_entry_points_for_nested = ["_start", "wasi:cli/run@0.2.3#run", "wasi:cli/run@0.2.6#run"]; + for nested in nested_instances.iter_mut() { + if let Some(ref nested_engine) = nested.instance.runtime_engine { + // Search ALL instance handles (0..16) to find _start + 'outer: for idx in 0..16u32 { + let h = InstanceHandle::from_index(idx as usize); + for ep in &core_entry_points_for_nested { + let has = nested_engine.has_function(h, ep).unwrap_or(false); + if has { + } + if has { + if let Some(ne) = nested.instance.runtime_engine.take() { + nested_engine_for_start = Some((ne, h)); + } + break 'outer; + } + } + } + } + if nested_engine_for_start.is_some() { + break; + } + } + } + // Store nested component instances { instance.nested_component_instances = nested_instances; @@ -2145,17 +2220,27 @@ impl ComponentInstance { } } - // Store the engine in the instance so it can be used for executing functions + // Find main_handle and possibly swap engine with nested component's engine #[cfg(feature = "kiln-execution")] { - instance.runtime_engine = Some(Box::new(engine)); - // Store the main module's instance handle // The main module is the one that exports _start - we found this earlier // by scanning the aliases. This is GENERIC and works for any component. // NO FALLBACKS - per spec, command components MUST export _start - let main_handle = match start_export_instance_idx { + let main_handle = if library_mode { + // Library mode: no _start required. Use first available instance. + // The parent component will call specific exports, not _start. + if let Some(&handle) = core_instances_map.values().next() { + instance.main_instance_handle = Some(handle); + handle + } else { + use kiln_runtime::engine::InstanceHandle; + let handle = InstanceHandle::from_index(0); + instance.main_instance_handle = Some(handle); + handle + } + } else { match start_export_instance_idx { Some(start_idx) => match core_instances_map.get(&(start_idx as usize)) { Some(&handle) => { #[cfg(feature = "tracing")] @@ -2230,8 +2315,16 @@ impl ComponentInstance { } } - // NO FALLBACK: Per CLAUDE.md rules, we must not guess. - // If _start is not found, the component is malformed or our loading is broken. + // If not found in parent's core instances, check nested components. + // WAC-composed P3 components have _start inside nested components. + #[cfg(feature = "std")] + if found_handle.is_none() { + if let Some((nested_eng, nested_handle)) = nested_engine_for_start.take() { + engine = *nested_eng; + found_handle = Some(nested_handle); + } + } + if let Some(handle) = found_handle { #[cfg(feature = "tracing")] tracing::info!( @@ -2255,7 +2348,7 @@ impl ComponentInstance { )); } }, - }; + } }; // close match + close else #[cfg(feature = "tracing")] if is_interface_style { @@ -2266,6 +2359,10 @@ impl ComponentInstance { } else { tracing::info!(?main_handle, "Main instance selected (exports _start)"); } + + // Store the engine AFTER main_handle search (which may have swapped + // the engine with a nested component's engine for P3 components) + instance.runtime_engine = Some(Box::new(engine)); } Ok(instance) @@ -3888,6 +3985,25 @@ impl ComponentInstance { }) .collect(); + // For P3 components: the main_instance_handle might not export _start + // Search all handles in the engine for the entry point + use kiln_runtime::engine::InstanceHandle; + let entry_names = ["_start", "wasi:cli/run@0.2.3#run", "wasi:cli/run@0.2.6#run"]; + let mut effective_handle = instance_handle; + for idx in 0..16u32 { + let h = InstanceHandle::from_index(idx as usize); + for ep in &entry_names { + if engine.has_function(h, ep).unwrap_or(false) { + effective_handle = h; + break; + } + } + if effective_handle != instance_handle { + break; + } + } + let instance_handle = effective_handle; + // Try _initialize first (important for TinyGo components) match engine.execute(instance_handle, "_initialize", &[]) { Ok(_) => { @@ -3989,7 +4105,11 @@ impl ComponentInstance { num_import_functions: 0, gc_types: Vec::new(), type_supertypes: Vec::new(), + type_is_final: Vec::new(), table_init_exprs: Vec::new(), + type_canonical_ids: Vec::new(), + type_rec_group_id: Vec::new(), + rec_group_ranges: Vec::new(), }; m.load_from_binary(&binary_clone) } diff --git a/kiln-component/src/components/component_linker.rs b/kiln-component/src/components/component_linker.rs index b9ddd874..ba2bbb63 100644 --- a/kiln-component/src/components/component_linker.rs +++ b/kiln-component/src/components/component_linker.rs @@ -262,6 +262,26 @@ impl ComponentLinker { pub fn link_imports( &mut self, imports: &[kiln_format::component::Import], + ) -> Result> { + self.link_imports_inner(imports, false) + } + + /// Link imports in lenient mode — unresolvable non-WASI imports are accepted + /// as empty stubs. Used for nested library components in WAC-composed P3 components + /// where inter-component imports are resolved by the parent at a higher level. + #[cfg(feature = "std")] + pub fn link_imports_lenient( + &mut self, + imports: &[kiln_format::component::Import], + ) -> Result> { + self.link_imports_inner(imports, true) + } + + #[cfg(feature = "std")] + fn link_imports_inner( + &mut self, + imports: &[kiln_format::component::Import], + lenient: bool, ) -> Result> { let mut resolved = Vec::with_capacity(imports.len()); @@ -290,7 +310,25 @@ impl ComponentLinker { } } - // STEP 3: FAIL LOUD - no provider found + // STEP 3: No provider found + if lenient { + // In lenient mode, create a stub WASI-like instance for the unresolved import. + // This allows nested library components to proceed — inter-component + // imports will be wired up by the parent component. + if let Some(ref mut wasi_provider) = self.wasi_provider { + if let Ok(stub) = wasi_provider.create_instance(&name) { + resolved.push(crate::instantiation::ResolvedImport::Instance(stub)); + continue; + } + } + // If even stub creation fails, push a placeholder with empty exports + resolved.push(crate::instantiation::ResolvedImport::Instance( + crate::instantiation::InstanceImport { + exports: std::collections::BTreeMap::new(), + }, + )); + continue; + } #[cfg(feature = "tracing")] tracing_warn!(import_name = %name, "Unresolved import - not found in internal components or host providers"); return Err(Error::new( diff --git a/kiln-decoder/src/streaming_decoder.rs b/kiln-decoder/src/streaming_decoder.rs index 531e003d..e6c65d38 100644 --- a/kiln-decoder/src/streaming_decoder.rs +++ b/kiln-decoder/src/streaming_decoder.rs @@ -839,8 +839,9 @@ impl<'a> StreamingDecoder<'a> { // A rec group counts as one entry in the type section count i += 1; }, - COMPOSITE_TYPE_SUB | COMPOSITE_TYPE_SUB_FINAL => { + COMPOSITE_TYPE_SUB | COMPOSITE_TYPE_SUB_FINAL | 0x4D | 0x4C => { // subtype: 0x50/0x4F supertype* comptype + // descriptor/describes: 0x4D/0x4C type_idx comptype (custom-descriptors) // 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; @@ -932,6 +933,41 @@ impl<'a> StreamingDecoder<'a> { type_index, } }, + // Custom descriptors proposal: descriptor (0x4D) and describes (0x4C) + // Format: (0x4D idx)? (0x4C idx)? composite_type + // Each clause can appear at most once; duplicates are rejected by the test suite. + 0x4D | 0x4C => { + let mut has_descriptor = false; + let mut has_describes = false; + // Parse up to one descriptor and one describes clause + while offset < data.len() && (data[offset] == 0x4D || data[offset] == 0x4C) { + let clause = data[offset]; + if clause == 0x4D { + if has_descriptor { + return Err(Error::parse_error("malformed definition type")); + } + has_descriptor = true; + } else { + if has_describes { + return Err(Error::parse_error("malformed definition type")); + } + has_describes = true; + } + offset += 1; // skip marker + let (_ref_type_idx, bytes_read) = read_leb128_u32(data, offset)?; + offset += bytes_read; + } + // Parse the composite type + 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, + } + }, COMPOSITE_TYPE_FUNC | COMPOSITE_TYPE_STRUCT | COMPOSITE_TYPE_ARRAY => { // Direct composite type (implicitly final with no supertypes) let (new_offset, composite_kind) = self.parse_composite_type(data, offset)?; @@ -1099,11 +1135,15 @@ impl<'a> StreamingDecoder<'a> { let (storage_type, new_offset) = self.parse_storage_type(data, offset)?; offset = new_offset; - // Parse mutability flag + // Parse mutability flag (must be 0 or 1) if offset >= data.len() { return Err(Error::parse_error("Unexpected end of array type")); } - let mutable = data[offset] != 0; + let mut_byte = data[offset]; + if mut_byte > 1 { + return Err(Error::parse_error("malformed mutability")); + } + let mutable = mut_byte != 0; offset += 1; let gc_element = GcFieldType { storage_type, mutable }; @@ -3456,12 +3496,15 @@ impl<'a> StreamingDecoder<'a> { /// Process custom section /// Returns the number of bytes consumed (entire section for custom sections). fn process_custom_section(&mut self, data: &[u8]) -> Result { + // Custom sections must contain at least a name (name-length LEB128 + name bytes). + // An empty custom section is malformed per the spec. + if data.is_empty() { + return Err(Error::parse_error("unexpected end")); + } // Validate custom section name is valid UTF-8 per WebAssembly spec - if !data.is_empty() { - let (name_bytes, _name_end) = read_name(data, 0)?; - if core::str::from_utf8(name_bytes).is_err() { - return Err(Error::parse_error("malformed UTF-8 encoding")); - } + let (name_bytes, _name_end) = read_name(data, 0)?; + if core::str::from_utf8(name_bytes).is_err() { + return Err(Error::parse_error("malformed UTF-8 encoding")); } // Custom sections are otherwise skipped - consume all bytes Ok(data.len()) diff --git a/kiln-runtime/src/module.rs b/kiln-runtime/src/module.rs index f8ab57cc..4bb3b023 100644 --- a/kiln-runtime/src/module.rs +++ b/kiln-runtime/src/module.rs @@ -649,6 +649,10 @@ pub enum GcFieldStorage { I8, /// Packed i16 I16, + /// Non-nullable reference to a concrete type index: (ref $t) + Ref(u32), + /// Nullable reference to a concrete type index: (ref null $t) + RefNull(u32), } /// A single field in a GC struct type @@ -666,8 +670,10 @@ pub struct GcField { /// 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, + /// This type index is a function type. + /// Carries param and result types so that canonical type ID computation + /// can distinguish structurally different function signatures. + Func(Vec, Vec), /// Struct type with field definitions Struct(Vec), /// Array type with element definition @@ -680,6 +686,7 @@ impl GcField { match self.storage { GcFieldStorage::I8 => 1, GcFieldStorage::I16 => 2, + GcFieldStorage::Ref(_) | GcFieldStorage::RefNull(_) => 4, // Reference types are 4 bytes GcFieldStorage::Value(byte) => match byte { 0x7F => 4, // i32 0x7E => 8, // i64 @@ -712,11 +719,137 @@ impl GcField { GcFieldStorage::Value(0x6D) => Value::StructRef(None), // eqref GcFieldStorage::Value(0x63) | GcFieldStorage::Value(0x64) => Value::FuncRef(None), // ref null/non-null + GcFieldStorage::Ref(_) => Value::FuncRef(None), // non-null concrete ref + GcFieldStorage::RefNull(_) => Value::FuncRef(None), // nullable concrete ref _ => Value::I32(0), } } } +/// A normalized type reference used during canonical type ID computation. +/// Internal references point to a position within the same rec group, +/// while external references use already-computed canonical IDs. +#[derive(Debug, Clone, PartialEq, Eq)] +enum NormalizedRef { + /// Reference to a type at position `pos` within the same rec group + Internal(usize), + /// Reference to a type outside the rec group, identified by canonical ID + External(u32), +} + +/// A value type with type references normalized for canonical comparison. +#[derive(Debug, Clone, PartialEq, Eq)] +enum NormalizedValueType { + /// A plain value type with no type index reference (I32, I64, FuncRef, etc.) + Plain(ValueType), + /// A typed reference with normalized type index: StructRef, ArrayRef, TypedFuncRef + TypedRef(NormalizedRef, u8), // ref, variant discriminant (for StructRef vs ArrayRef vs TypedFuncRef) +} + +/// Normalized GC field storage for canonical comparison. +#[derive(Debug, Clone, PartialEq, Eq)] +enum NormalizedFieldStorage { + /// Non-reference storage (same as original) + Plain(GcFieldStorage), + /// Non-nullable reference with normalized type index + Ref(NormalizedRef), + /// Nullable reference with normalized type index + RefNull(NormalizedRef), +} + +/// Normalized GC field for canonical comparison. +#[derive(Debug, Clone, PartialEq, Eq)] +struct NormalizedGcField { + storage: NormalizedFieldStorage, + mutable: bool, +} + +/// Normalized GC type info for canonical comparison. +#[derive(Debug, Clone, PartialEq, Eq)] +enum NormalizedGcInfo { + Func(Vec, Vec), + Struct(Vec), + Array(NormalizedGcField), +} + +/// A single normalized type entry within a rec group, used for rec-group comparison. +#[derive(Debug, Clone, PartialEq, Eq)] +struct NormalizedType { + is_final: bool, + supertype: Option, + info: NormalizedGcInfo, +} + +/// Normalize a ValueType by replacing type index references with NormalizedRef. +fn normalize_value_type( + vt: ValueType, + rg_start: usize, + rg_count: usize, + canonical_ids: &[u32], +) -> NormalizedValueType { + match vt { + ValueType::StructRef(idx) => { + let idx_usize = idx as usize; + let norm_ref = if idx_usize >= rg_start && idx_usize < rg_start + rg_count { + NormalizedRef::Internal(idx_usize - rg_start) + } else { + NormalizedRef::External(canonical_ids.get(idx_usize).copied().unwrap_or(idx)) + }; + NormalizedValueType::TypedRef(norm_ref, 0) + } + ValueType::ArrayRef(idx) => { + let idx_usize = idx as usize; + let norm_ref = if idx_usize >= rg_start && idx_usize < rg_start + rg_count { + NormalizedRef::Internal(idx_usize - rg_start) + } else { + NormalizedRef::External(canonical_ids.get(idx_usize).copied().unwrap_or(idx)) + }; + NormalizedValueType::TypedRef(norm_ref, 1) + } + ValueType::TypedFuncRef(idx, nullable) => { + let idx_usize = idx as usize; + let norm_ref = if idx_usize >= rg_start && idx_usize < rg_start + rg_count { + NormalizedRef::Internal(idx_usize - rg_start) + } else { + NormalizedRef::External(canonical_ids.get(idx_usize).copied().unwrap_or(idx)) + }; + // Encode nullability in the discriminant + NormalizedValueType::TypedRef(norm_ref, if nullable { 2 } else { 3 }) + } + other => NormalizedValueType::Plain(other), + } +} + +/// Normalize a GcFieldStorage, relativizing type index references within rec groups. +fn normalize_field_storage( + storage: &GcFieldStorage, + rg_start: usize, + rg_count: usize, + canonical_ids: &[u32], +) -> NormalizedFieldStorage { + match storage { + GcFieldStorage::Ref(idx) => { + let idx_usize = *idx as usize; + let norm_ref = if idx_usize >= rg_start && idx_usize < rg_start + rg_count { + NormalizedRef::Internal(idx_usize - rg_start) + } else { + NormalizedRef::External(canonical_ids.get(idx_usize).copied().unwrap_or(*idx)) + }; + NormalizedFieldStorage::Ref(norm_ref) + } + GcFieldStorage::RefNull(idx) => { + let idx_usize = *idx as usize; + let norm_ref = if idx_usize >= rg_start && idx_usize < rg_start + rg_count { + NormalizedRef::Internal(idx_usize - rg_start) + } else { + NormalizedRef::External(canonical_ids.get(idx_usize).copied().unwrap_or(*idx)) + }; + NormalizedFieldStorage::RefNull(norm_ref) + } + other => NormalizedFieldStorage::Plain(other.clone()), + } +} + /// Represents a WebAssembly module in the runtime #[derive(Debug, Clone, PartialEq, Eq)] pub struct Module { @@ -778,6 +911,19 @@ pub struct Module { /// Supertype index for each type (None if no supertype declared) /// Used by ref_test_value to walk the type hierarchy for subtype checking pub type_supertypes: Vec>, + /// Whether each type is final (cannot be further subtyped). + /// Indexed by type index, parallel to gc_types and type_supertypes. + pub type_is_final: Vec, + /// Canonical type ID for each type index. + /// Types that are structurally equivalent (same definition, same canonical + /// supertype, and same finality) share the same canonical ID. Used for + /// ref.test / br_on_cast with concrete types and call_indirect type checking. + pub type_canonical_ids: Vec, + /// Rec-group ID for each type index. + /// Types in the same rec group share the same rec-group ID. + pub type_rec_group_id: Vec, + /// Rec-group ranges: (start_type_idx, count) for each rec group. + pub rec_group_ranges: Vec<(u32, u32)>, /// Init expression bytes for tables with explicit init expressions. /// Indexed by table definition index (not including imported tables). /// None means the table uses the default null value for its element type. @@ -785,6 +931,127 @@ pub struct Module { } impl Module { + /// Compute canonical type IDs using iso-recursive equivalence at the rec-group level. + /// + /// Per the WebAssembly GC spec, two types from different rec groups are + /// canonically equivalent only if their entire rec groups are structurally + /// equivalent (with internal type references relativized to positions within + /// the group). + pub fn compute_canonical_type_ids(&mut self) { + let n = self.gc_types.len(); + if n == 0 { + return; + } + let num_rgs = self.rec_group_ranges.len(); + if num_rgs == 0 { + // No rec group info available; fall back to identity mapping + self.type_canonical_ids = (0..n as u32).collect(); + return; + } + + let mut canonical_ids: Vec = (0..n as u32).collect(); + + // Process rec groups in order. Earlier groups get canonical IDs first, + // so later groups can reference them via canonical IDs. + let mut seen_groups: Vec<(Vec, usize)> = Vec::new(); + + for rg_idx in 0..num_rgs { + let (rg_start, rg_count) = self.rec_group_ranges[rg_idx]; + let rg_start = rg_start as usize; + let rg_count = rg_count as usize; + + // Build normalized representation for this rec group + let mut normalized: Vec = Vec::with_capacity(rg_count); + for pos in 0..rg_count { + let type_idx = rg_start + pos; + let is_final = self.type_is_final.get(type_idx).copied().unwrap_or(true); + + // Normalize supertype reference + let norm_super = self.type_supertypes.get(type_idx).copied().flatten() + .map(|s| { + let s_usize = s as usize; + if s_usize >= rg_start && s_usize < rg_start + rg_count { + NormalizedRef::Internal(s_usize - rg_start) + } else { + NormalizedRef::External(canonical_ids[s_usize]) + } + }); + + // Normalize GC type info + let norm_info = self.normalize_gc_type_info( + type_idx, rg_start, rg_count, &canonical_ids, + ); + + normalized.push(NormalizedType { + is_final, + supertype: norm_super, + info: norm_info, + }); + } + + // Check if we've seen an equivalent rec group before + let mut matched_rg: Option = None; + for (seen_norm, seen_rg_idx) in &seen_groups { + if *seen_norm == normalized { + matched_rg = Some(*seen_rg_idx); + break; + } + } + + if let Some(prev_rg_idx) = matched_rg { + // This rec group matches a previous one + let (prev_start, _) = self.rec_group_ranges[prev_rg_idx]; + for pos in 0..rg_count { + canonical_ids[rg_start + pos] = canonical_ids[prev_start as usize + pos]; + } + } else { + // New unique rec group + for pos in 0..rg_count { + canonical_ids[rg_start + pos] = (rg_start + pos) as u32; + } + seen_groups.push((normalized, rg_idx)); + } + } + + self.type_canonical_ids = canonical_ids; + } + + /// Normalize a GcTypeInfo for canonical comparison. + fn normalize_gc_type_info( + &self, + type_idx: usize, + rg_start: usize, + rg_count: usize, + canonical_ids: &[u32], + ) -> NormalizedGcInfo { + match &self.gc_types[type_idx] { + GcTypeInfo::Func(params, results) => { + let norm_params: Vec = params.iter() + .map(|vt| normalize_value_type(*vt, rg_start, rg_count, canonical_ids)) + .collect(); + let norm_results: Vec = results.iter() + .map(|vt| normalize_value_type(*vt, rg_start, rg_count, canonical_ids)) + .collect(); + NormalizedGcInfo::Func(norm_params, norm_results) + } + GcTypeInfo::Struct(fields) => { + let norm_fields: Vec = fields.iter() + .map(|f| NormalizedGcField { + storage: normalize_field_storage(&f.storage, rg_start, rg_count, canonical_ids), + mutable: f.mutable, + }) + .collect(); + NormalizedGcInfo::Struct(norm_fields) + } + GcTypeInfo::Array(field) => { + NormalizedGcInfo::Array(NormalizedGcField { + storage: normalize_field_storage(&field.storage, rg_start, rg_count, canonical_ids), + mutable: field.mutable, + }) + } + } + } + /// Push memory pub fn push_memory(&mut self, memory: MemoryWrapper) -> Result<()> { self.memories.push(memory); @@ -1157,12 +1424,27 @@ impl Module { // any.convert_extern: [(ref extern)] -> [(ref any)] 0x1A => { let val = stack.pop().ok_or_else(|| Error::parse_error("Stack underflow in any.convert_extern"))?; - stack.push(val); // Type conversion (representation unchanged) + let result = match val { + Value::ExternRef(None) => Value::I31Ref(None), + Value::ExternRef(Some(er)) => Value::Ref(er.index), + other => other, + }; + stack.push(result); } // extern.convert_any: [(ref any)] -> [(ref extern)] 0x1B => { let val = stack.pop().ok_or_else(|| Error::parse_error("Stack underflow in extern.convert_any"))?; - stack.push(val); // Type conversion (representation unchanged) + let result = match &val { + Value::StructRef(None) | Value::ArrayRef(None) + | Value::I31Ref(None) | Value::ExternRef(None) + | Value::ExnRef(None) | Value::FuncRef(None) => Value::ExternRef(None), + Value::StructRef(Some(_)) | Value::ArrayRef(Some(_)) + | Value::I31Ref(Some(_)) | Value::Ref(_) => { + Value::ExternRef(Some(kiln_foundation::values::ExternRef { index: 0 })) + } + _ => val, + }; + stack.push(result); } _ => { return Err(Error::parse_error("Unsupported GC opcode in constant expression")); @@ -1528,12 +1810,27 @@ impl Module { // any.convert_extern 0x1A => { let val = stack.pop().ok_or_else(|| Error::parse_error("Stack underflow in any.convert_extern"))?; - stack.push(val); + let result = match val { + Value::ExternRef(None) => Value::I31Ref(None), + Value::ExternRef(Some(er)) => Value::Ref(er.index), + other => other, + }; + stack.push(result); } // extern.convert_any 0x1B => { let val = stack.pop().ok_or_else(|| Error::parse_error("Stack underflow in extern.convert_any"))?; - stack.push(val); + let result = match &val { + Value::StructRef(None) | Value::ArrayRef(None) + | Value::I31Ref(None) | Value::ExternRef(None) + | Value::ExnRef(None) | Value::FuncRef(None) => Value::ExternRef(None), + Value::StructRef(Some(_)) | Value::ArrayRef(Some(_)) + | Value::I31Ref(Some(_)) | Value::Ref(_) => { + Value::ExternRef(Some(kiln_foundation::values::ExternRef { index: 0 })) + } + _ => val, + }; + stack.push(result); } _ => { return Err(Error::parse_error("Unsupported GC opcode in deferred const expr")); @@ -1582,6 +1879,10 @@ impl Module { num_import_functions: 0, gc_types: Vec::new(), type_supertypes: Vec::new(), + type_is_final: Vec::new(), + type_canonical_ids: Vec::new(), + type_rec_group_id: Vec::new(), + rec_group_ranges: Vec::new(), table_init_exprs: Vec::new(), }) } @@ -1592,9 +1893,9 @@ impl Module { kiln_format::module::GcStorageType::I8 => GcFieldStorage::I8, kiln_format::module::GcStorageType::I16 => GcFieldStorage::I16, kiln_format::module::GcStorageType::Value(b) => GcFieldStorage::Value(*b), - // Reference types use 0x64/0x63 (ref/ref null) encoding byte for field storage - kiln_format::module::GcStorageType::RefType(_) => GcFieldStorage::Value(0x64), - kiln_format::module::GcStorageType::RefTypeNull(_) => GcFieldStorage::Value(0x63), + // Reference types carry the concrete type index for canonical comparison + kiln_format::module::GcStorageType::RefType(idx) => GcFieldStorage::Ref(*idx), + kiln_format::module::GcStorageType::RefTypeNull(idx) => GcFieldStorage::RefNull(*idx), } } @@ -1635,6 +1936,10 @@ impl Module { num_import_functions: 0, // Will be set after processing imports gc_types: Vec::new(), // Will be populated from rec_groups type_supertypes: Vec::new(), // Will be populated from rec_groups + type_is_final: Vec::new(), // Will be populated from rec_groups + type_canonical_ids: Vec::new(), // Will be computed after gc_types and type_supertypes + type_rec_group_id: Vec::new(), // Will be populated from rec_groups + rec_group_ranges: Vec::new(), // Will be populated from rec_groups table_init_exprs: Vec::new(), // Will be populated from table section }; @@ -1649,12 +1954,28 @@ impl Module { // Collect types ordered by type_index let mut gc_type_entries: Vec<(u32, GcTypeInfo)> = Vec::new(); - for rec_group in &kiln_module.rec_groups { + // Also collect rec-group membership info + let mut rec_group_id_entries: Vec<(u32, usize)> = Vec::new(); + for (rg_idx, rec_group) in kiln_module.rec_groups.iter().enumerate() { + runtime_module.rec_group_ranges.push((rec_group.start_type_index, rec_group.types.len() as u32)); for sub_type in &rec_group.types { + rec_group_id_entries.push((sub_type.type_index, rg_idx)); + // Look up function signature from kiln_module.types for Func variants + let func_sig = kiln_module.types.get(sub_type.type_index as usize) + .map(|ft| (ft.params.clone(), ft.results.clone())); 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::Func => { + let (params, results) = func_sig.unwrap_or_default(); + GcTypeInfo::Func(params, results) + } + CompositeTypeKind::Struct => { + let (params, results) = func_sig.unwrap_or_default(); + GcTypeInfo::Func(params, results) + } + CompositeTypeKind::Array => { + let (params, results) = func_sig.unwrap_or_default(); + GcTypeInfo::Func(params, results) + } CompositeTypeKind::StructWithFields(fields) => { let gc_fields: Vec = fields.iter().map(|f| { let storage = Self::convert_gc_storage_type(&f.storage_type); @@ -1670,24 +1991,36 @@ impl Module { gc_type_entries.push((sub_type.type_index, info)); } } - // Also collect supertype info + // Also collect supertype and finality info let mut supertype_entries: Vec<(u32, Option)> = Vec::new(); + let mut finality_entries: Vec<(u32, bool)> = Vec::new(); for rec_group in &kiln_module.rec_groups { for sub_type in &rec_group.types { let supertype = sub_type.supertype_indices.first().copied(); supertype_entries.push((sub_type.type_index, supertype)); + finality_entries.push((sub_type.type_index, sub_type.is_final)); } } - // Sort by type index and fill both vectors + // Sort by type index and fill all vectors gc_type_entries.sort_by_key(|(idx, _)| *idx); supertype_entries.sort_by_key(|(idx, _)| *idx); + finality_entries.sort_by_key(|(idx, _)| *idx); + rec_group_id_entries.sort_by_key(|(idx, _)| *idx); for (_, info) in gc_type_entries { runtime_module.gc_types.push(info); } for (_, supertype) in supertype_entries { runtime_module.type_supertypes.push(supertype); } + for (_, is_final) in finality_entries { + runtime_module.type_is_final.push(is_final); + } + for (_, rg_id) in rec_group_id_entries { + runtime_module.type_rec_group_id.push(rg_id); + } + // Compute canonical type IDs for structural equivalence + runtime_module.compute_canonical_type_ids(); } // Convert types @@ -2349,6 +2682,99 @@ impl Module { // active segments re-resolve via item_exprs eval_stack.push(KilnValue::I32(0)); } + KilnInstr::StructNew(type_idx) => { + use kiln_foundation::values::GcStructRef; + let field_count = match runtime_module.gc_types.get(*type_idx as usize) { + Some(GcTypeInfo::Struct(fields)) => fields.len(), + _ => 0, + }; + let mut field_values = Vec::new(); + for _ in 0..field_count { + if let Some(val) = eval_stack.pop() { + field_values.push(val); + } + } + field_values.reverse(); + if let Ok(mut s) = kiln_foundation::values::StructRef::new( + *type_idx, + kiln_foundation::traits::DefaultMemoryProvider::default(), + ) { + for val in field_values { + let _ = s.add_field(val); + } + eval_stack.push(KilnValue::StructRef(Some(GcStructRef::new(s)))); + } + } + KilnInstr::StructNewDefault(type_idx) => { + use kiln_foundation::values::GcStructRef; + if let Ok(mut s) = kiln_foundation::values::StructRef::new( + *type_idx, + kiln_foundation::traits::DefaultMemoryProvider::default(), + ) { + if let Some(GcTypeInfo::Struct(fields)) = runtime_module.gc_types.get(*type_idx as usize) { + for field in fields { + let _ = s.add_field(field.default_value()); + } + } + eval_stack.push(KilnValue::StructRef(Some(GcStructRef::new(s)))); + } + } + KilnInstr::ArrayNew(type_idx) => { + use kiln_foundation::values::GcArrayRef; + let length = match eval_stack.pop() { + Some(KilnValue::I32(n)) => n as u32, + _ => 0, + }; + let init_val = eval_stack.pop().unwrap_or(KilnValue::I32(0)); + if let Ok(mut a) = kiln_foundation::values::ArrayRef::new( + *type_idx, + kiln_foundation::traits::DefaultMemoryProvider::default(), + ) { + for _ in 0..length { + let _ = a.push(init_val.clone()); + } + eval_stack.push(KilnValue::ArrayRef(Some(GcArrayRef::new(a)))); + } + } + KilnInstr::ArrayNewDefault(type_idx) => { + use kiln_foundation::values::GcArrayRef; + let length = match eval_stack.pop() { + Some(KilnValue::I32(n)) => n as u32, + _ => 0, + }; + let default_val = match runtime_module.gc_types.get(*type_idx as usize) { + Some(GcTypeInfo::Array(field)) => field.default_value(), + _ => KilnValue::I32(0), + }; + if let Ok(mut a) = kiln_foundation::values::ArrayRef::new( + *type_idx, + kiln_foundation::traits::DefaultMemoryProvider::default(), + ) { + for _ in 0..length { + let _ = a.push(default_val.clone()); + } + eval_stack.push(KilnValue::ArrayRef(Some(GcArrayRef::new(a)))); + } + } + KilnInstr::ArrayNewFixed(type_idx, count) => { + use kiln_foundation::values::GcArrayRef; + let mut values = Vec::new(); + for _ in 0..*count { + if let Some(val) = eval_stack.pop() { + values.push(val); + } + } + values.reverse(); + if let Ok(mut a) = kiln_foundation::values::ArrayRef::new( + *type_idx, + kiln_foundation::traits::DefaultMemoryProvider::default(), + ) { + for val in values { + let _ = a.push(val); + } + eval_stack.push(KilnValue::ArrayRef(Some(GcArrayRef::new(a)))); + } + } KilnInstr::End => break, _ => {} } @@ -3082,6 +3508,10 @@ impl Module { num_import_functions: 0, gc_types: Vec::new(), type_supertypes: Vec::new(), + type_is_final: Vec::new(), + type_canonical_ids: Vec::new(), + type_rec_group_id: Vec::new(), + rec_group_ranges: Vec::new(), table_init_exprs: Vec::new(), }; @@ -3530,6 +3960,10 @@ impl kiln_foundation::traits::FromBytes for Module { num_import_functions: 0, gc_types: Vec::new(), type_supertypes: Vec::new(), + type_is_final: Vec::new(), + type_canonical_ids: Vec::new(), + type_rec_group_id: Vec::new(), + rec_group_ranges: Vec::new(), table_init_exprs: Vec::new(), }; diff --git a/kiln-runtime/src/module_instance.rs b/kiln-runtime/src/module_instance.rs index 71859692..a6caedeb 100644 --- a/kiln-runtime/src/module_instance.rs +++ b/kiln-runtime/src/module_instance.rs @@ -1064,6 +1064,99 @@ impl ModuleInstance { eval_stack.push(KilnValue::I31Ref(Some(n & 0x7FFFFFFF))); } } + KilnInstr::StructNew(type_idx) => { + use kiln_foundation::values::GcStructRef; + let field_count = match self.module.gc_types.get(*type_idx as usize) { + Some(crate::module::GcTypeInfo::Struct(fields)) => fields.len(), + _ => 0, + }; + let mut field_values = Vec::new(); + for _ in 0..field_count { + if let Some(val) = eval_stack.pop() { + field_values.push(val); + } + } + field_values.reverse(); + if let Ok(mut s) = kiln_foundation::values::StructRef::new( + *type_idx, + kiln_foundation::traits::DefaultMemoryProvider::default(), + ) { + for val in field_values { + let _ = s.add_field(val); + } + eval_stack.push(KilnValue::StructRef(Some(GcStructRef::new(s)))); + } + } + KilnInstr::StructNewDefault(type_idx) => { + use kiln_foundation::values::GcStructRef; + if let Ok(mut s) = kiln_foundation::values::StructRef::new( + *type_idx, + kiln_foundation::traits::DefaultMemoryProvider::default(), + ) { + if let Some(crate::module::GcTypeInfo::Struct(fields)) = self.module.gc_types.get(*type_idx as usize) { + for field in fields { + let _ = s.add_field(field.default_value()); + } + } + eval_stack.push(KilnValue::StructRef(Some(GcStructRef::new(s)))); + } + } + KilnInstr::ArrayNew(type_idx) => { + use kiln_foundation::values::GcArrayRef; + let length = match eval_stack.pop() { + Some(KilnValue::I32(n)) => n as u32, + _ => 0, + }; + let init_val = eval_stack.pop().unwrap_or(KilnValue::I32(0)); + if let Ok(mut a) = kiln_foundation::values::ArrayRef::new( + *type_idx, + kiln_foundation::traits::DefaultMemoryProvider::default(), + ) { + for _ in 0..length { + let _ = a.push(init_val.clone()); + } + eval_stack.push(KilnValue::ArrayRef(Some(GcArrayRef::new(a)))); + } + } + KilnInstr::ArrayNewDefault(type_idx) => { + use kiln_foundation::values::GcArrayRef; + let length = match eval_stack.pop() { + Some(KilnValue::I32(n)) => n as u32, + _ => 0, + }; + let default_val = match self.module.gc_types.get(*type_idx as usize) { + Some(crate::module::GcTypeInfo::Array(field)) => field.default_value(), + _ => KilnValue::I32(0), + }; + if let Ok(mut a) = kiln_foundation::values::ArrayRef::new( + *type_idx, + kiln_foundation::traits::DefaultMemoryProvider::default(), + ) { + for _ in 0..length { + let _ = a.push(default_val.clone()); + } + eval_stack.push(KilnValue::ArrayRef(Some(GcArrayRef::new(a)))); + } + } + KilnInstr::ArrayNewFixed(type_idx, count) => { + use kiln_foundation::values::GcArrayRef; + let mut values = Vec::new(); + for _ in 0..*count { + if let Some(val) = eval_stack.pop() { + values.push(val); + } + } + values.reverse(); + if let Ok(mut a) = kiln_foundation::values::ArrayRef::new( + *type_idx, + kiln_foundation::traits::DefaultMemoryProvider::default(), + ) { + for val in values { + let _ = a.push(val); + } + eval_stack.push(KilnValue::ArrayRef(Some(GcArrayRef::new(a)))); + } + } KilnInstr::End => { // End of expression - stop evaluating break; @@ -1475,6 +1568,10 @@ impl FromBytes for ModuleInstance { num_import_functions: 0, gc_types: Vec::new(), type_supertypes: Vec::new(), + type_is_final: Vec::new(), + type_canonical_ids: Vec::new(), + type_rec_group_id: Vec::new(), + rec_group_ranges: Vec::new(), table_init_exprs: Vec::new(), }; diff --git a/kiln-runtime/src/stackless/engine.rs b/kiln-runtime/src/stackless/engine.rs index 6cc7c8f1..1c41d80f 100644 --- a/kiln-runtime/src/stackless/engine.rs +++ b/kiln-runtime/src/stackless/engine.rs @@ -30,6 +30,14 @@ use kiln_debug::runtime_traits::{RuntimeDebugger, RuntimeState, DebugAction}; // GC heap reference wrapper types for WebAssembly GC proposal use kiln_foundation::values::{GcStructRef, GcArrayRef}; +// Side table for extern.convert_any / any.convert_extern round-trip. +// Stores the original anyref value so it can be recovered when externalized +// and then re-internalized. Uses thread-local storage for safety. +use std::cell::RefCell; +thread_local! { + static EXTERN_TABLE: RefCell> = RefCell::new(Vec::new()); +} + use kiln_error::Result; use kiln_foundation::{ traits::BoundedCapacity, @@ -85,6 +93,32 @@ fn func_types_match(expected: &kiln_foundation::types::FuncType, actual: &kiln_f true } +/// Check if a function type matches for call_indirect, using canonical type IDs +/// when available (for proper GC subtype support), or falling back to structural comparison. +/// +/// Falls back to structural comparison when canonical type IDs are not available +/// (e.g., modules without GC types). +fn call_indirect_type_matches( + expected_type_idx: u32, + actual_type_idx: u32, + module: &crate::module::Module, +) -> bool { + // If canonical type IDs are available, use them for proper GC type matching + if !module.type_canonical_ids.is_empty() { + return is_runtime_subtype_canonical( + actual_type_idx, + expected_type_idx, + &module.type_supertypes, + &module.type_canonical_ids, + ); + } + // Fallback for modules without canonical IDs: structural comparison + match (module.types.get(expected_type_idx as usize), module.types.get(actual_type_idx as usize)) { + (Some(expected), Some(actual)) => func_types_match(expected, actual), + _ => false, + } +} + /// Maximum number of concurrent module instances /// Set to 512 to handle large WAST test files that may have hundreds of module directives /// (e.g., align.wast has 117 module directives including assert_invalid) @@ -244,6 +278,18 @@ pub struct StacklessEngine { /// Not used directly as a field - the trampoline in execute() uses a local Vec instead. /// Kept for potential future use (e.g., stack inspection). call_stack: Vec, + // ── P3 Async Task Protocol State ────────────────────────────────── + // These fields implement the host-provided builtins that Meld-fused + // P3 components import from the $root namespace. + /// Per-(instance, context_index) context slot for [context-get-N]/[context-set-N]. + /// Used by wit-bindgen's async support to thread FutureState pointers. + p3_context_slots: HashMap<(usize, u32), i32>, + /// Last [task-return] values per instance. The async-lift wrapper's + /// implementation calls [task-return]N to signal completion with a result. + /// We capture the values here for the caller to retrieve. + p3_task_return_values: HashMap>, + /// Monotonic handle counter for waitable sets. + p3_next_waitable_set: u32, } /// Simple RuntimeState implementation for debugger callbacks @@ -407,6 +453,10 @@ impl StacklessEngine { lowered_functions: HashMap::new(), instance_registry: HashMap::new(), call_stack: Vec::with_capacity(256), + // P3 async protocol state + p3_context_slots: HashMap::new(), + p3_task_return_values: HashMap::new(), + p3_next_waitable_set: 1, // 0 is reserved } } @@ -511,6 +561,115 @@ impl StacklessEngine { None } + // ── P3 Async Task Built-in Intrinsics ───────────────────────────── + // + // Meld fuses P3 async components into a single core module that imports + // these builtins from the "$root" namespace. For synchronous interpreter + // execution the semantics collapse to simple state-machine operations. + // + // Returns Some(results) if the import was a P3 builtin, None otherwise. + fn try_dispatch_p3_builtin( + &mut self, + instance_id: usize, + module_name: &str, + field_name: &str, + args: &[Value], + ) -> Option>> { + // Only handle $root and [export]$root namespaces + if module_name != "$root" && module_name != "[export]$root" { + return None; + } + + // Strip optional instance suffix ($2, $3, …) added by Meld for + // second/third component instance of the same type. + let base_name = if let Some(pos) = field_name.rfind('$') { + // Only strip if the suffix is numeric + if field_name[pos+1..].chars().all(|c| c.is_ascii_digit()) { + &field_name[..pos] + } else { + field_name + } + } else { + field_name + }; + + match base_name { + // ── [context-get-N] ────────────────────────────────────── + // () -> i32 — return the value stored in context slot N + name if name.starts_with("[context-get-") => { + let slot_str = &name["[context-get-".len()..name.len()-1]; + let slot: u32 = slot_str.parse().unwrap_or(0); + let val = self.p3_context_slots + .get(&(instance_id, slot)) + .copied() + .unwrap_or(0); + Some(Ok(vec![Value::I32(val)])) + } + + // ── [context-set-N] ────────────────────────────────────── + // (i32) -> () — store value in context slot N + name if name.starts_with("[context-set-") => { + let slot_str = &name["[context-set-".len()..name.len()-1]; + let slot: u32 = slot_str.parse().unwrap_or(0); + let val = match args.first() { + Some(Value::I32(v)) => *v, + _ => 0, + }; + self.p3_context_slots.insert((instance_id, slot), val); + Some(Ok(vec![])) + } + + // ── [task-return]N ─────────────────────────────────────── + // Variadic signature — captures ALL args as the task result. + // The async-lift wrapper calls this to signal completion. + name if name.starts_with("[task-return]") => { + self.p3_task_return_values + .insert(instance_id, args.to_vec()); + Some(Ok(vec![])) + } + + // ── [waitable-set-new] ─────────────────────────────────── + // () -> i32 — allocate a new waitable set handle + "[waitable-set-new]" => { + let handle = self.p3_next_waitable_set; + self.p3_next_waitable_set += 1; + Some(Ok(vec![Value::I32(handle as i32)])) + } + + // ── [waitable-set-poll] ────────────────────────────────── + // (i32, i32) -> i32 — poll set; synchronous mode: return 0 + // (no pending async work in single-threaded interpreter) + "[waitable-set-poll]" => { + Some(Ok(vec![Value::I32(0)])) + } + + // ── [waitable-set-drop] ────────────────────────────────── + // (i32) -> () — destroy waitable set (no-op) + "[waitable-set-drop]" => { + Some(Ok(vec![])) + } + + // ── [waitable-join] ────────────────────────────────────── + // (i32, i32) -> () — join waitable to set (no-op for sync) + "[waitable-join]" => { + Some(Ok(vec![])) + } + + // ── [task-cancel] ──────────────────────────────────────── + // () -> () or (i32) -> i32 — cancel task (no-op for sync) + "[task-cancel]" => { + Some(Ok(vec![])) + } + + _ => None, // Not a P3 builtin + } + } + + /// Retrieve and clear the task-return values captured for an instance. + pub fn take_p3_task_return_values(&mut self, instance_id: usize) -> Option> { + self.p3_task_return_values.remove(&instance_id) + } + /// Check if a function is a lowered function (from canon.lower) fn is_lowered_function(&self, instance_id: usize, func_idx: usize) -> bool { self.lowered_functions.contains_key(&(instance_id, func_idx)) @@ -1415,6 +1574,29 @@ impl StacklessEngine { }, } } + // Try P3 async builtin dispatch before falling through to defaults. + // Fused P3 modules import builtins from "$root" and "[export]$root". + if let Some(result) = self.try_dispatch_p3_builtin( + instance_id, &module_name, &field_name, &args, + ) { + return result.map(ExecutionOutcome::Complete); + } + + // Try host handler for unlinked WASI imports + #[cfg(feature = "wasi")] + if let Some(ref mut handler) = self.host_handler { + let instance = self.instances.get(&instance_id) + .cloned(); + if let Some(inst) = instance { + let mem_wrapper = inst.memory(0).ok(); + let memory: Option<&dyn kiln_foundation::MemoryAccessor> = mem_wrapper.as_ref() + .map(|m| m.0.as_ref() as &dyn kiln_foundation::MemoryAccessor); + if let Ok(results) = handler.call_import(&module_name, &field_name, &args, memory) { + return Ok(ExecutionOutcome::Complete(results)); + } + } + } + // 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 @@ -1925,6 +2107,29 @@ impl StacklessEngine { // Not linked or link unresolvable - fall through to WASI dispatch } + // Try P3 async builtins first (fused modules import from $root) + { + let p3_args = Self::collect_function_args(&module, func_idx as usize, &mut operand_stack); + if let Some(result) = self.try_dispatch_p3_builtin( + instance_id, &module_name, &field_name, &p3_args, + ) { + match result { + Ok(results) => { + for r in results { + operand_stack.push(r); + } + pc += 1; + continue; + } + Err(e) => return Err(e), + } + } + // Not a P3 builtin — push args back for WASI dispatch + for arg in p3_args.into_iter().rev() { + operand_stack.push(arg); + } + } + // Dispatch to WASI implementation #[cfg(feature = "tracing")] trace!( @@ -2365,19 +2570,19 @@ impl StacklessEngine { let func_type = module.types.get(func.type_idx as usize) .ok_or_else(|| kiln_error::Error::runtime_error("Invalid function type"))?; - // Validate type matches expected type (structural equivalence) - let expected_type = module.types.get(type_idx as usize) - .ok_or_else(|| kiln_error::Error::runtime_error("Invalid expected function type"))?; - - if !func_types_match(expected_type, func_type) { + // Validate type matches expected type using canonical IDs when available + if !call_indirect_type_matches(type_idx, func.type_idx, &module) { #[cfg(feature = "tracing")] - warn!( - expected_params = expected_type.params.len(), - expected_results = expected_type.results.len(), - got_params = func_type.params.len(), - got_results = func_type.results.len(), - "[CALL_INDIRECT] Type mismatch" - ); + { + let expected_type = module.types.get(type_idx as usize); + warn!( + expected_params = expected_type.map(|t| t.params.len()).unwrap_or(0), + expected_results = expected_type.map(|t| t.results.len()).unwrap_or(0), + got_params = func_type.params.len(), + got_results = func_type.results.len(), + "[CALL_INDIRECT] Type mismatch" + ); + } return Err(kiln_error::Error::runtime_trap("indirect call type mismatch")); } @@ -2446,15 +2651,43 @@ impl StacklessEngine { return_state: Some(saved_state), }); } else { - // Not linked - dispatch to WASI if applicable - #[cfg(feature = "tracing")] - trace!( - module_name = %module_name, - field_name = %field_name, - "[CALL_INDIRECT] Import not linked, trying WASI dispatch" - ); + // Not linked — try P3 builtins first, then WASI + if let Some(result) = self.try_dispatch_p3_builtin( + instance_id, &module_name, &field_name, &call_args, + ) { + match result { + Ok(results) => { + for r in results { + operand_stack.push(r); + } + pc += 1; + continue; + } + Err(e) => return Err(e), + } + } + + // Not P3 — try host handler for unlinked imports + #[cfg(feature = "wasi")] + { + let inst = self.instances.get(&instance_id).cloned(); + if let Some(ref mut handler) = self.host_handler { + if let Some(inst_arc) = inst { + let mem_wrapper = inst_arc.memory(0).ok(); + let memory: Option<&dyn kiln_foundation::MemoryAccessor> = mem_wrapper.as_ref() + .map(|m| m.0.as_ref() as &dyn kiln_foundation::MemoryAccessor); + if let Ok(results) = handler.call_import(&module_name, &field_name, &call_args, memory) { + for r in results { + operand_stack.push(r); + } + pc += 1; + continue; + } + } + } + } - // Call WASI function (need to push args back for call_wasi_function) + // Fall through to legacy WASI dispatch for arg in call_args.iter().rev() { operand_stack.push(arg.clone()); } @@ -2716,15 +2949,12 @@ impl StacklessEngine { return Err(kiln_error::Error::runtime_trap("return_call_indirect: function index out of bounds")); } - // Get function type and validate + // Get function type and validate using canonical IDs when available 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("Invalid function type"))?; - let expected_type = module.types.get(type_idx as usize) - .ok_or_else(|| kiln_error::Error::runtime_error("Invalid expected function type"))?; - - if !func_types_match(expected_type, func_type) { + if !call_indirect_type_matches(type_idx, func.type_idx, &module) { return Err(kiln_error::Error::runtime_trap("indirect call type mismatch")); } @@ -6290,6 +6520,7 @@ impl StacklessEngine { Value::StructRef(Some(_)) => 0i32, Value::ArrayRef(None) => 1i32, Value::ArrayRef(Some(_)) => 0i32, + Value::Ref(_) => 0i32, // internalized extern is never null _ => { #[cfg(feature = "tracing")] error!("RefIsNull: expected reference type, got {:?}", ref_val); @@ -6316,7 +6547,8 @@ impl StacklessEngine { } Value::FuncRef(Some(_)) | Value::ExternRef(Some(_)) | Value::ExnRef(Some(_)) | Value::I31Ref(Some(_)) - | Value::StructRef(Some(_)) | Value::ArrayRef(Some(_)) => { + | Value::StructRef(Some(_)) | Value::ArrayRef(Some(_)) + | Value::Ref(_) => { operand_stack.push(ref_val); } _ => { @@ -6495,8 +6727,8 @@ impl StacklessEngine { trace!("BrOnNonNull: label={}, is_null={}", br_label_idx, is_null); if !is_null { // Not null - push reference and branch - // (matches Br handler structure: set pc so that pc += 1 - // at end of iteration lands on the correct instruction) + // br_on_non_null branches with the label's arity values + // The ref is part of the branch values (pushed back on stack) operand_stack.push(ref_val); if br_label_idx as usize >= block_stack.len() { #[cfg(feature = "tracing")] @@ -6504,14 +6736,36 @@ impl StacklessEngine { break; } let stack_idx = 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(stack_idx).copied() { - // Keep branch value, restore stack below - let branch_val = operand_stack.pop(); + if let Some((block_type, start_pc, block_type_idx, entry_stack_height)) = block_stack.get(stack_idx).copied() { + // Determine how many values to preserve (same logic as Br) + let values_to_preserve = if block_type == "loop" { + match block_type_idx { + 0x40 => 0, + 0x7F | 0x7E | 0x7D | 0x7C | 0x7B | 0x70 | 0x6F => 0, + _ => module.types.get(block_type_idx as usize) + .map_or(0, |ft| ft.params.len()), + } + } else { + match block_type_idx { + 0x40 => 0, + 0x7F | 0x7E | 0x7D | 0x7C | 0x7B | 0x70 | 0x6F => 1, + _ => module.types.get(block_type_idx as usize) + .map_or(1, |ft| ft.results.len()), + } + }; + // Save the values to preserve from top of stack + let mut preserved = Vec::new(); + for _ in 0..values_to_preserve { + if let Some(v) = operand_stack.pop() { + preserved.push(v); + } + } while operand_stack.len() > entry_stack_height { operand_stack.pop(); } - if let Some(bv) = branch_val { - operand_stack.push(bv); + // Restore preserved values in correct order + for v in preserved.into_iter().rev() { + operand_stack.push(v); } if block_type == "loop" { @@ -10587,6 +10841,9 @@ impl StacklessEngine { Instruction::ArrayNewElem(type_idx, elem_idx) => { // array.new_elem: [offset i32, size i32] -> [arrayref] + // Creates an array from elements of a passive element segment. + // Uses resolved_elem_items which contain pre-evaluated values + // (arrays, structs, i31refs, etc. — not just function indices). #[cfg(feature = "tracing")] trace!("ArrayNewElem: type_idx={}, elem_idx={}", type_idx, elem_idx); let size = match operand_stack.pop() { @@ -10597,9 +10854,22 @@ impl StacklessEngine { 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() { + // Use resolved_elem_items for pre-evaluated values (GC types), + // fall back to raw items for plain funcref segments. + // Dropped segments have effective length 0. + let resolved = module.resolved_elem_items.get(elem_idx as usize); + let elem_len = if instance.is_element_segment_dropped(elem_idx) { + 0 + } else if let Some(r) = resolved { + if !r.is_empty() { r.len() } else { + module.elements.get(elem_idx as usize) + .map_or(0, |e| e.items.len()) + } + } else { + module.elements.get(elem_idx as usize) + .map_or(0, |e| e.items.len()) + }; + if offset_val as u64 + size as u64 > elem_len as u64 { return Err(kiln_error::Error::runtime_trap("out of bounds table access")); } let mut array_ref = kiln_foundation::values::ArrayRef::new( @@ -10607,9 +10877,23 @@ impl StacklessEngine { 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))); + let idx = offset_val as usize + i; + let item_val = if let Some(resolved_items) = resolved { + if !resolved_items.is_empty() && idx < resolved_items.len() { + resolved_items[idx].clone() + } else { + // Fallback to raw function index + let elem_segment = &module.elements[elem_idx as usize]; + let func_idx = elem_segment.items.get(idx) + .map_err(|_| kiln_error::Error::runtime_trap("array.new_elem: element access failed"))?; + Value::FuncRef(Some(kiln_foundation::values::FuncRef::from_index(func_idx))) + } + } else { + let elem_segment = &module.elements[elem_idx as usize]; + let func_idx = elem_segment.items.get(idx) + .map_err(|_| kiln_error::Error::runtime_trap("array.new_elem: element access failed"))?; + Value::FuncRef(Some(kiln_foundation::values::FuncRef::from_index(func_idx))) + }; array_ref.push(item_val).map_err(|_| kiln_error::Error::runtime_error("Failed to push to array"))?; } @@ -10762,16 +11046,35 @@ impl StacklessEngine { 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() { + // Check if element segment has been dropped (effective length 0) + let effective_len = if instance.is_element_segment_dropped(elem_idx) { + 0 + } else { + elem_segment.items.len() + }; + if src_offset as u64 + len as u64 > effective_len as u64 { return Err(kiln_error::Error::runtime_trap("out of bounds table access")); } - if dst_offset + len > a.len() as u32 { + if dst_offset as u64 + len as u64 > a.len() as u64 { return Err(kiln_error::Error::runtime_trap("out of bounds array access")); } + // Use resolved_elem_items for pre-evaluated values + let resolved = module.resolved_elem_items.get(elem_idx as usize); 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))); + let idx = src_offset as usize + i; + let item_val = if let Some(resolved_items) = resolved { + if !resolved_items.is_empty() && idx < resolved_items.len() { + resolved_items[idx].clone() + } else { + let item_idx = elem_segment.items.get(idx) + .map_err(|_| kiln_error::Error::runtime_trap("array.init_elem: element access failed"))?; + Value::FuncRef(Some(kiln_foundation::values::FuncRef::from_index(item_idx))) + } + } else { + let item_idx = elem_segment.items.get(idx) + .map_err(|_| kiln_error::Error::runtime_trap("array.init_elem: element access failed"))?; + 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"))?; } @@ -10998,24 +11301,64 @@ impl StacklessEngine { Instruction::AnyConvertExtern => { // any.convert_extern: [externref] -> [anyref] // Convert an externref to an anyref (internalize) + // Per the spec, null stays null, non-null becomes an opaque anyref + // that is NOT eq/i31/struct/array. We represent this as Value::Ref. #[cfg(feature = "tracing")] 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); + let result = match val { + Value::ExternRef(None) => Value::I31Ref(None), // null extern -> null any + Value::ExternRef(Some(er)) => { + // Check if this externref was created by extern.convert_any + // (marked with high bit set in the index) + if er.index & 0x80000000 != 0 { + let table_idx = (er.index & 0x7FFFFFFF) as usize; + EXTERN_TABLE.with(|table| { + let t = table.borrow(); + if let Some(original) = t.get(table_idx) { + original.clone() + } else { + Value::Ref(er.index) + } + }) + } else { + Value::Ref(er.index) // opaque any for host-provided externrefs + } + } + other => other, // pass through (shouldn't happen per spec) + }; + operand_stack.push(result); } Instruction::ExternConvertAny => { // extern.convert_any: [anyref] -> [externref] // Convert an anyref to an externref (externalize) + // Per the spec, null stays null, non-null becomes an externref. #[cfg(feature = "tracing")] 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); + let result = match &val { + Value::StructRef(None) | Value::ArrayRef(None) + | Value::I31Ref(None) | Value::ExternRef(None) + | Value::ExnRef(None) | Value::FuncRef(None) => Value::ExternRef(None), + Value::StructRef(Some(_)) | Value::ArrayRef(Some(_)) + | Value::I31Ref(Some(_)) | Value::Ref(_) => { + // Store the original value in the extern table so + // any.convert_extern can recover it for round-trip identity. + // Use high bit (0x80000000) to distinguish from host externrefs. + let idx = EXTERN_TABLE.with(|table| { + let mut t = table.borrow_mut(); + let idx = t.len() as u32; + t.push(val.clone()); + idx | 0x80000000 + }); + Value::ExternRef(Some(kiln_foundation::values::ExternRef { index: idx })) + } + _ => val, // pass through for non-null ExternRef etc. + }; + operand_stack.push(result); } // ======================================== @@ -11886,6 +12229,7 @@ fn ref_test_value_with_module( HeapType::Extern => matches!(val, Value::ExternRef(Some(_))), HeapType::Any => matches!(val, Value::StructRef(Some(_)) | Value::ArrayRef(Some(_)) | Value::I31Ref(Some(_)) + | Value::Ref(_) // internalized extern (via any.convert_extern) ), HeapType::Eq => matches!(val, Value::StructRef(Some(_)) | Value::ArrayRef(Some(_)) | Value::I31Ref(Some(_)) @@ -11914,13 +12258,16 @@ fn ref_test_value_with_module( }; match val_type_idx { - Some(val_idx) if val_idx == *target_idx => true, Some(val_idx) => { - // Walk the supertype chain to check subtyping if let Some(module) = module { - is_runtime_subtype(val_idx, *target_idx, &module.type_supertypes) + is_runtime_subtype_canonical( + val_idx, + *target_idx, + &module.type_supertypes, + &module.type_canonical_ids, + ) } else { - false + val_idx == *target_idx } }, None => false, @@ -11929,15 +12276,30 @@ fn ref_test_value_with_module( } } -/// Check if child_idx is a subtype of parent_idx by walking the declared supertype chain. -fn is_runtime_subtype(child_idx: u32, parent_idx: u32, supertypes: &[Option]) -> bool { - if child_idx == parent_idx { +/// Check if child_idx is a subtype of parent_idx using canonical type IDs. +/// +/// Two types are considered equal if they have the same canonical ID. Subtyping +/// is checked by walking the declared supertype chain and comparing canonical IDs +/// at each step. This handles structurally equivalent types (e.g., `$t1` and `$t1'` +/// both defined as `(sub $t0 (struct (field i32)))`) correctly. +fn is_runtime_subtype_canonical( + child_idx: u32, + parent_idx: u32, + supertypes: &[Option], + canonical_ids: &[u32], +) -> bool { + // Resolve to canonical IDs + let canon_child = canonical_ids.get(child_idx as usize).copied().unwrap_or(child_idx); + let canon_parent = canonical_ids.get(parent_idx as usize).copied().unwrap_or(parent_idx); + + if canon_child == canon_parent { return true; } let mut current = child_idx; let mut visited = 0u32; // Simple cycle guard while let Some(Some(super_idx)) = supertypes.get(current as usize) { - if *super_idx == parent_idx { + let canon_super = canonical_ids.get(*super_idx as usize).copied().unwrap_or(*super_idx); + if canon_super == canon_parent { return true; } current = *super_idx; diff --git a/kilnd/src/main.rs b/kilnd/src/main.rs index 29f249dd..985d5164 100644 --- a/kilnd/src/main.rs +++ b/kilnd/src/main.rs @@ -93,6 +93,70 @@ use kiln_wasi::{ set_global_wasi_args, }; +/// Chained host import handler for WAC-composed P3 components. +/// Routes WASI calls to WasiDispatcher and inter-component calls +/// to nested component engines. +#[cfg(all(feature = "wasi", feature = "kiln-execution"))] +struct InterComponentHandler { + wasi: WasiDispatcher, + engines: std::collections::HashMap< + usize, + std::sync::Arc>, + >, + /// Maps interface name → nested component index + routes: std::collections::HashMap, +} + +#[cfg(all(feature = "wasi", feature = "kiln-execution"))] +impl kiln_foundation::traits::HostImportHandler for InterComponentHandler { + fn call_import( + &mut self, + module: &str, + function: &str, + args: &[kiln_foundation::Value], + memory: Option<&dyn kiln_foundation::traits::MemoryAccessor>, + ) -> kiln_error::Result> { + // Try WASI first + if module.starts_with("wasi:") { + return self.wasi.call_import(module, function, args, memory); + } + + // Try inter-component routing + if let Some(&comp_idx) = self.routes.get(module) { + if let Some(engine_arc) = self.engines.get(&comp_idx) { + if let Ok(mut engine) = engine_arc.lock() { + // Find the function in the nested engine's instances + use kiln_runtime::engine::{CapabilityEngine, InstanceHandle}; + // Try multiple name formats for the function: + // 1. Plain name: "fibonacci" + // 2. Interface-qualified: "compute:concurrent/tasks@1.0.0#fibonacci" + // 3. Async-lift: "[async-lift]compute:concurrent/tasks@1.0.0#fibonacci" + let candidates = [ + function.to_string(), + format!("{}#{}", module, function), + format!("[async-lift]{}#{}", module, function), + ]; + for idx in 0..16u32 { + let handle = InstanceHandle::from_index(idx as usize); + for candidate in &candidates { + if engine.has_function(handle, candidate).unwrap_or(false) { + return engine.execute(handle, candidate, args); + } + } + } + } + } + } + + // Fallback: try WASI dispatcher for any unrecognized calls + self.wasi.call_import(module, function, args, memory) + } + + fn set_args_allocation(&mut self, list_ptr: u32, string_ptrs: Vec<(u32, u32)>) { + self.wasi.set_args_allocation(list_ptr, string_ptrs); + } +} + /// Configuration for the runtime daemon #[derive(Debug, Clone)] pub struct KilndConfig { @@ -458,6 +522,9 @@ impl KilndEngine { } /// Execute a component using the component model + /// + /// For WAC-composed P3 components with nested library components, + /// inter-component calls are routed through the nested component engines. #[cfg(feature = "component-model")] fn execute_component(&mut self, data: &[u8]) -> Result<()> { eprintln!("[VXDBG] execute_component: {} bytes", data.len()); @@ -526,6 +593,43 @@ impl KilndEngine { "Component initialized and running successfully" ); + // For P3-style components where the engine was swapped to a nested component's + // engine, set up a chained handler that routes: + // 1. WASI calls → WasiDispatcher + // 2. Inter-component calls → nested component engines + #[cfg(all(feature = "kiln-execution", feature = "wasi"))] + if self.config.enable_wasi && !instance.nested_component_instances.is_empty() { + if let Ok(wasi_dispatcher) = kiln_wasi::WasiDispatcher::with_defaults() { + // Build inter-component route map and extract nested engines + let mut routes: std::collections::HashMap = + std::collections::HashMap::new(); + let mut engines: std::collections::HashMap< + usize, + std::sync::Arc>, + > = std::collections::HashMap::new(); + + for nested in instance.nested_component_instances.iter_mut() { + for (export_name, _) in &nested.exports { + routes.insert(export_name.clone(), nested.instance_index as usize); + } + // Take the engine from the nested instance and wrap in Arc + if let Some(engine) = nested.instance.runtime_engine.take() { + engines.insert( + nested.instance_index as usize, + std::sync::Arc::new(std::sync::Mutex::new(*engine)), + ); + } + } + + let handler = InterComponentHandler { + wasi: wasi_dispatcher, + engines, + routes, + }; + instance.set_host_handler(Box::new(handler)); + } + } + // Pre-allocate WASI args memory via cabi_realloc before calling entry point. // This is needed for components that call get-arguments (e.g., calculator). #[cfg(all(feature = "kiln-execution", feature = "wasi"))] @@ -710,28 +814,54 @@ impl KilndEngine { let function_name = self.config.function_name.as_deref().unwrap_or("_start"); let _ = self.logger.handle_minimal_log(LogLevel::Info, "Executing function"); - // Check if function exists before execution - if !engine.has_function(instance, function_name).map_err(|_e| { - Error::runtime_function_not_found("Failed to check function existence") - })? { - let _ = self - .logger - .handle_minimal_log(LogLevel::Error, "Function not found in module exports"); - return Err(Error::runtime_function_not_found("Function not found")); - } + // Check if function exists — if not, try Meld-fused P3 module entry points + let has_main = engine.has_function(instance, function_name).unwrap_or(false); - let results = engine - .execute(instance, function_name, &[]) - .map_err(|_| Error::runtime_execution_error("Function execution failed"))?; + if has_main { + let results = engine + .execute(instance, function_name, &[]) + .map_err(|e| { + Error::runtime_execution_error("Function execution failed") + })?; - // Display execution results - if !results.is_empty() { - println!("\n✓ Function '{}' returned {} value(s):", function_name, results.len()); - for (i, value) in results.iter().enumerate() { - println!(" [{}] {:?}", i, value); + if !results.is_empty() { + println!("\n✓ Function '{}' returned {} value(s):", function_name, results.len()); + for (i, value) in results.iter().enumerate() { + println!(" [{}] {:?}", i, value); + } + } else { + println!("\n✓ Function '{}' completed (no return values)", function_name); } } else { - println!("\n✓ Function '{}' completed (no return values)", function_name); + // No _start — check for Meld-fused P3 module. + // These export [async-lift] functions and import P3 builtins from $root. + // The numbered exports (0, 1, 2, ...) are the wasi:cli/run entry points. + // Try calling export "0" which is typically the first component's entry. + let mut executed = false; + + // Look for wasi:cli/run entry via numbered exports + for entry in &["0", "1", "_start", "main"] { + if engine.has_function(instance, entry).unwrap_or(false) { + let _ = self.logger.handle_minimal_log(LogLevel::Info, + "Executing Meld-fused P3 module entry point"); + match engine.execute(instance, entry, &[]) { + Ok(results) => { + if !results.is_empty() { + println!("\n✓ Entry '{}' returned: {:?}", entry, results); + } + executed = true; + break; + } + Err(_) => continue, + } + } + } + + if !executed { + let _ = self.logger.handle_minimal_log( + LogLevel::Error, "No entry point found (_start or P3 fused entries)"); + return Err(Error::runtime_function_not_found("Function not found")); + } } self.stats.modules_executed += 1;