diff --git a/meld-core/src/adapter/fact.rs b/meld-core/src/adapter/fact.rs index 7266dd3..7340294 100644 --- a/meld-core/src/adapter/fact.rs +++ b/meld-core/src/adapter/fact.rs @@ -40,6 +40,27 @@ fn alignment_for_encoding(encoding: StringEncoding) -> i32 { } } +/// Build a lookup from `(module, field)` → merged function index for resource imports. +/// +/// Scans the merged module's imports to find `[resource-rep]` function imports +/// and records their merged function indices. +fn build_resource_import_map( + merged: &MergedModule, +) -> std::collections::HashMap<(String, String), u32> { + use wasm_encoder::EntityType; + let mut map = std::collections::HashMap::new(); + let mut func_idx = 0u32; + for imp in &merged.imports { + if matches!(imp.entity_type, EntityType::Function(_)) { + if imp.name.starts_with("[resource-rep]") { + map.insert((imp.module.clone(), imp.name.clone()), func_idx); + } + func_idx += 1; + } + } + map +} + /// FACT-style adapter generator pub struct FactStyleGenerator { #[allow(dead_code)] @@ -58,6 +79,7 @@ impl FactStyleGenerator { site: &AdapterSite, merged: &MergedModule, _adapter_idx: usize, + resource_imports: &std::collections::HashMap<(String, String), u32>, ) -> Result { let name = format!( "$adapter_{}_{}_to_{}_{}", @@ -65,7 +87,7 @@ impl FactStyleGenerator { ); // Determine adapter options based on call site - let options = self.analyze_call_site(site, merged); + let options = self.analyze_call_site(site, merged, resource_imports); // Generate the adapter function body let (type_idx, body) = if site.crosses_memory && options.needs_transcoding() { @@ -73,7 +95,7 @@ impl FactStyleGenerator { } else if site.crosses_memory { self.generate_memory_copy_adapter(site, merged, &options)? } else { - self.generate_direct_adapter(site, merged)? + self.generate_direct_adapter(site, merged, &options)? }; Ok(AdapterFunction { @@ -89,7 +111,12 @@ impl FactStyleGenerator { } /// Analyze a call site to determine adapter options - fn analyze_call_site(&self, site: &AdapterSite, merged: &MergedModule) -> AdapterOptions { + fn analyze_call_site( + &self, + site: &AdapterSite, + merged: &MergedModule, + resource_imports: &std::collections::HashMap<(String, String), u32>, + ) -> AdapterOptions { let mut options = AdapterOptions::default(); // Determine memory indices @@ -162,6 +189,35 @@ impl FactStyleGenerator { options.callee_post_return = Some(merged_pr_idx); } + // Resolve resource BORROW params → [resource-rep] merged function indices. + // + // Per the canonical ABI spec, `borrow` params where T is defined by + // the callee receive the representation (raw pointer), not the handle. + // The adapter must call resource.rep(handle) → rep for these. + // + // `own` params receive the handle directly — the callee's core + // function calls from_handle/resource.rep internally, so the adapter + // must NOT convert them (that would cause double conversion). + // + // Results are never converted — own results have resource.new called + // by the callee's core function, and borrows cannot appear in results. + for op in &site.requirements.resource_params { + if op.is_owned { + continue; // own: callee handles conversion internally + } + if let Some(&func_idx) = + resource_imports.get(&(op.import_module.clone(), op.import_field.clone())) + { + options.resource_rep_calls.push((op.flat_idx, func_idx)); + } else { + log::debug!( + "Resource rep import not found: ({}, {})", + op.import_module, + op.import_field + ); + } + } + options } @@ -170,9 +226,9 @@ impl FactStyleGenerator { &self, site: &AdapterSite, merged: &MergedModule, + options: &AdapterOptions, ) -> Result<(u32, Function)> { let target_func = self.resolve_target_function(site, merged)?; - let options = self.analyze_call_site(site, merged); // Find the target function's type (convert wasm index to array position) let type_idx = merged @@ -186,36 +242,45 @@ impl FactStyleGenerator { let result_types: Vec = func_type.map(|t| t.results.clone()).unwrap_or_default(); - // If post-return is specified, we need scratch locals to save results let has_post_return = options.callee_post_return.is_some(); + let has_resource_ops = !options.resource_rep_calls.is_empty(); - if has_post_return && result_count > 0 { - // Need locals to save results across the post-return call - let locals: Vec<(u32, wasm_encoder::ValType)> = - result_types.iter().map(|t| (1u32, *t)).collect(); - let mut func = Function::new(locals); + if has_resource_ops || (has_post_return && result_count > 0) { + let mut locals: Vec<(u32, wasm_encoder::ValType)> = Vec::new(); let result_base = param_count as u32; - - // Load all parameters and call target - for i in 0..param_count { - func.instruction(&Instruction::LocalGet(i as u32)); + if has_post_return && result_count > 0 { + locals.extend(result_types.iter().map(|t| (1u32, *t))); } - func.instruction(&Instruction::Call(target_func)); + let mut func = Function::new(locals); - // Save results to locals (pop in reverse order) - for i in (0..result_count).rev() { - func.instruction(&Instruction::LocalSet(result_base + i as u32)); + // Phase 0: Convert borrow resource handles → representations + for &(param_idx, rep_func) in &options.resource_rep_calls { + func.instruction(&Instruction::LocalGet(param_idx)); + func.instruction(&Instruction::Call(rep_func)); + func.instruction(&Instruction::LocalSet(param_idx)); } - // Call post-return with the saved results - for i in 0..result_count { - func.instruction(&Instruction::LocalGet(result_base + i as u32)); + for i in 0..param_count { + func.instruction(&Instruction::LocalGet(i as u32)); } - func.instruction(&Instruction::Call(options.callee_post_return.unwrap())); + func.instruction(&Instruction::Call(target_func)); - // Push saved results back onto stack - for i in 0..result_count { - func.instruction(&Instruction::LocalGet(result_base + i as u32)); + if has_post_return && result_count > 0 { + // Save results to locals (pop in reverse order) + for i in (0..result_count).rev() { + func.instruction(&Instruction::LocalSet(result_base + i as u32)); + } + // Call post-return with saved results + for i in 0..result_count { + func.instruction(&Instruction::LocalGet(result_base + i as u32)); + } + func.instruction(&Instruction::Call(options.callee_post_return.unwrap())); + // Push saved results back onto stack + for i in 0..result_count { + func.instruction(&Instruction::LocalGet(result_base + i as u32)); + } + } else if has_post_return { + func.instruction(&Instruction::Call(options.callee_post_return.unwrap())); } func.instruction(&Instruction::End); @@ -230,7 +295,6 @@ impl FactStyleGenerator { func.instruction(&Instruction::Call(target_func)); if has_post_return { - // No results to save, just call post-return func.instruction(&Instruction::Call(options.callee_post_return.unwrap())); } @@ -324,33 +388,41 @@ impl FactStyleGenerator { // If memories are the same, just do direct call (with post-return if needed) if options.caller_memory == options.callee_memory { let has_post_return = options.callee_post_return.is_some(); + let has_resource_ops = !options.resource_rep_calls.is_empty(); - if has_post_return && result_count > 0 { - // Need scratch locals to save results across post-return call - let locals: Vec<(u32, wasm_encoder::ValType)> = - result_types.iter().map(|t| (1u32, *t)).collect(); - let mut func = Function::new(locals); + if has_resource_ops || (has_post_return && result_count > 0) { + let mut locals: Vec<(u32, wasm_encoder::ValType)> = Vec::new(); let result_base = param_count as u32; - - for i in 0..param_count { - func.instruction(&Instruction::LocalGet(i as u32)); + if has_post_return && result_count > 0 { + locals.extend(result_types.iter().map(|t| (1u32, *t))); } - func.instruction(&Instruction::Call(target_func)); + let mut func = Function::new(locals); - // Save results to locals (pop in reverse order) - for i in (0..result_count).rev() { - func.instruction(&Instruction::LocalSet(result_base + i as u32)); + // Phase 0: Convert borrow resource handles → representations + for &(param_idx, rep_func) in &options.resource_rep_calls { + func.instruction(&Instruction::LocalGet(param_idx)); + func.instruction(&Instruction::Call(rep_func)); + func.instruction(&Instruction::LocalSet(param_idx)); } - // Call post-return with saved results - for i in 0..result_count { - func.instruction(&Instruction::LocalGet(result_base + i as u32)); + for i in 0..param_count { + func.instruction(&Instruction::LocalGet(i as u32)); } - func.instruction(&Instruction::Call(options.callee_post_return.unwrap())); + func.instruction(&Instruction::Call(target_func)); - // Push saved results back onto stack - for i in 0..result_count { - func.instruction(&Instruction::LocalGet(result_base + i as u32)); + if has_post_return && result_count > 0 { + for i in (0..result_count).rev() { + func.instruction(&Instruction::LocalSet(result_base + i as u32)); + } + for i in 0..result_count { + func.instruction(&Instruction::LocalGet(result_base + i as u32)); + } + func.instruction(&Instruction::Call(options.callee_post_return.unwrap())); + for i in 0..result_count { + func.instruction(&Instruction::LocalGet(result_base + i as u32)); + } + } else if has_post_return { + func.instruction(&Instruction::Call(options.callee_post_return.unwrap())); } func.instruction(&Instruction::End); @@ -363,7 +435,6 @@ impl FactStyleGenerator { func.instruction(&Instruction::Call(target_func)); if has_post_return { - // No results to save, just call post-return func.instruction(&Instruction::Call(options.callee_post_return.unwrap())); } @@ -390,16 +461,19 @@ impl FactStyleGenerator { let needs_post_return_save = !needs_result_copy && options.callee_post_return.is_some() && result_count > 0; - // We need result-save locals for post-return AND/OR conditional result copy + // We need result-save locals for post-return or conditional result copy. let needs_result_save = (needs_post_return_save || needs_conditional_result_copy) && result_count > 0; - // If no copying and no post-return save needed, direct call + let has_resource_ops = !options.resource_rep_calls.is_empty(); + + // If no copying, no post-return save, and no resource ops needed, direct call if !needs_outbound_copy && !needs_conditional_copy && !needs_result_copy && !needs_conditional_result_copy && !needs_post_return_save + && !has_resource_ops { let mut func = Function::new([]); for i in 0..param_count { @@ -443,6 +517,13 @@ impl FactStyleGenerator { let mut func = Function::new(local_decls); + // Phase 0: Convert borrow resource handles → representations + for &(param_idx, rep_func) in &options.resource_rep_calls { + func.instruction(&Instruction::LocalGet(param_idx)); + func.instruction(&Instruction::Call(rep_func)); + func.instruction(&Instruction::LocalSet(param_idx)); + } + // Assign scratch local indices (after params) let base = param_count as u32; // For outbound copy: @@ -655,7 +736,7 @@ impl FactStyleGenerator { func.instruction(&Instruction::LocalGet(callee_ret_len_local)); } - // Post-return and/or conditional result copy for non-result-copy case + // Post-return and/or conditional result copy if !needs_result_copy && (needs_conditional_result_copy || options.callee_post_return.is_some()) { @@ -814,6 +895,13 @@ impl FactStyleGenerator { let local_decls = vec![(scratch_count, wasm_encoder::ValType::I32)]; let mut func = Function::new(local_decls); + // Phase 0: Convert borrow resource handles → representations + for &(param_idx, rep_func) in &options.resource_rep_calls { + func.instruction(&Instruction::LocalGet(param_idx)); + func.instruction(&Instruction::Call(rep_func)); + func.instruction(&Instruction::LocalSet(param_idx)); + } + // --- Phase 1: Outbound copy of ALL pointer pairs (caller → callee) --- if let Some(callee_realloc) = options .callee_realloc @@ -953,6 +1041,7 @@ impl FactStyleGenerator { // load/store instructions based on the canonical ABI memory layout. let result_layouts = &site.requirements.result_copy_layouts; let return_area_slots = &site.requirements.return_area_slots; + if let Some(caller_realloc) = options .caller_realloc .filter(|_| !result_ptr_offsets.is_empty()) @@ -1462,6 +1551,13 @@ impl FactStyleGenerator { } let mut func = Function::new(local_decls); + // Phase 0: Convert borrow resource handles → representations + for &(param_idx, rep_func) in &options.resource_rep_calls { + func.instruction(&Instruction::LocalGet(param_idx)); + func.instruction(&Instruction::Call(rep_func)); + func.instruction(&Instruction::LocalSet(param_idx)); + } + // Generate transcoding logic based on encoding pair match ( @@ -2350,10 +2446,11 @@ impl AdapterGenerator for FactStyleGenerator { merged: &MergedModule, graph: &DependencyGraph, ) -> Result> { + let resource_imports = build_resource_import_map(merged); let mut adapters = Vec::new(); for (idx, site) in graph.adapter_sites.iter().enumerate() { - let adapter = self.generate_adapter(site, merged, idx)?; + let adapter = self.generate_adapter(site, merged, idx, &resource_imports)?; adapters.push(adapter); } diff --git a/meld-core/src/adapter/mod.rs b/meld-core/src/adapter/mod.rs index 5254206..0bf8b11 100644 --- a/meld-core/src/adapter/mod.rs +++ b/meld-core/src/adapter/mod.rs @@ -123,6 +123,25 @@ pub struct AdapterOptions { /// Post-return function index in merged module (if any). /// Called after results have been copied back, to allow callee cleanup. pub callee_post_return: Option, + + /// Resource borrow params needing handle→representation conversion. + /// Each entry: `(flat_param_idx, merged_func_idx of [resource-rep])`. + /// + /// Per the canonical ABI spec, `borrow` params where T is defined by + /// the callee receive the **representation** (raw pointer), not the handle. + /// The `lower_borrow` function has `if cx.inst is t.rt.impl: return rep`. + /// So the adapter must call `[resource-rep]R(handle)` to convert before + /// forwarding to the callee's core function. + /// + /// `own` params always receive a **handle** (table index) because + /// `lower_own` unconditionally creates a handle entry. The callee's core + /// function calls `from_handle` / `[resource-rep]` internally, so the + /// adapter must NOT convert own params. + /// + /// Results are never converted by the adapter — the callee's core function + /// already calls `[resource-new]R` internally for own results, and borrows + /// cannot appear in results. + pub resource_rep_calls: Vec<(u32, u32)>, } impl Default for AdapterOptions { @@ -136,6 +155,7 @@ impl Default for AdapterOptions { callee_realloc: None, returns_pointer_pair: false, callee_post_return: None, + resource_rep_calls: Vec::new(), } } } diff --git a/meld-core/src/parser.rs b/meld-core/src/parser.rs index 2f98197..35fb1d6 100644 --- a/meld-core/src/parser.rs +++ b/meld-core/src/parser.rs @@ -407,6 +407,22 @@ pub enum ComponentValType { Type(u32), } +/// Position of a resource-typed parameter or result in the flat ABI layout. +/// +/// Used by the adapter generator to emit `[resource-rep]` (handle → representation) +/// or `[resource-new]` (representation → handle) calls at the correct positions. +#[derive(Debug, Clone)] +pub struct ResourcePosition { + /// Index in the flat (stack) ABI layout (e.g., param 0, 1, 2…) + pub flat_idx: u32, + /// Byte offset in the canonical ABI memory layout (for return-area results) + pub byte_offset: u32, + /// `true` for `own`, `false` for `borrow` + pub is_owned: bool, + /// Component-level type index of the resource + pub resource_type_id: u32, +} + /// Primitive value types in the Component Model #[derive(Debug, Clone, Copy)] pub enum PrimitiveValType { @@ -1569,6 +1585,139 @@ impl ParsedComponent { } } + /// Resolve a `ComponentValType` to its resource type, if any. + /// + /// Returns `Some((resource_type_id, is_owned))` for `Own(T)`, `Borrow(T)`, + /// and `Type(idx)` that resolves to a `Defined(Own(T))` or `Defined(Borrow(T))`. + fn resolve_to_resource(&self, ty: &ComponentValType) -> Option<(u32, bool)> { + match ty { + ComponentValType::Own(id) => Some((*id, true)), + ComponentValType::Borrow(id) => Some((*id, false)), + ComponentValType::Type(idx) => { + // Follow type definition chain + if let Some(ct) = self.get_type_definition(*idx) + && let ComponentTypeKind::Defined(inner) = &ct.kind + { + match inner { + ComponentValType::Own(id) => Some((*id, true)), + ComponentValType::Borrow(id) => Some((*id, false)), + _ => None, + } + } else { + None + } + } + _ => None, + } + } + + /// Identify resource-typed parameters and their flat-ABI positions. + /// + /// For each `own` or `borrow` parameter (including through `Type(idx)` + /// indirection), returns a `ResourcePosition` with the flat param index and + /// resource type ID. Used by the adapter to emit `[resource-rep]` calls that + /// convert handles to representations. + pub fn resource_param_positions( + &self, + params: &[(String, ComponentValType)], + ) -> Vec { + let mut positions = Vec::new(); + let mut flat_idx = 0u32; + for (_, ty) in params { + if let Some((resource_type_id, is_owned)) = self.resolve_to_resource(ty) { + positions.push(ResourcePosition { + flat_idx, + byte_offset: 0, // not used for params + is_owned, + resource_type_id, + }); + } + flat_idx += self.flat_count(ty); + } + positions + } + + /// Identify resource-typed results and their flat-ABI / byte-offset positions. + /// + /// Returns both the flat index (for non-retptr results) and the canonical ABI + /// byte offset (for retptr return-area results). Used by the adapter to emit + /// `[resource-new]` calls that convert representations to handles. + pub fn resource_result_positions( + &self, + results: &[(Option, ComponentValType)], + ) -> Vec { + let mut positions = Vec::new(); + let mut flat_idx = 0u32; + let mut byte_offset = 0u32; + for (_, ty) in results { + let align = self.canonical_abi_align(ty); + byte_offset = align_up(byte_offset, align); + if let Some((resource_type_id, is_owned)) = self.resolve_to_resource(ty) { + positions.push(ResourcePosition { + flat_idx, + byte_offset, + is_owned, + resource_type_id, + }); + } + flat_idx += self.flat_count(ty); + byte_offset += self.canonical_abi_size_unpadded(ty); + } + positions + } + + /// Resolve a component type index to `(module_name, type_name)` for resource ops. + /// + /// Traces the type index through `component_type_defs` and `component_aliases` + /// to find the WASI interface module name and the resource's exported name. + /// Returns `None` if the type cannot be traced to an import. + pub fn resolve_resource_type(&self, type_id: u32) -> Option<(String, String)> { + // Build instance → import name map + let mut comp_instance_to_import: std::collections::HashMap = + std::collections::HashMap::new(); + for (inst_idx, def) in self.component_instance_defs.iter().enumerate() { + if let ComponentInstanceDef::Import(import_idx) = def + && let Some(imp) = self.imports.get(*import_idx) + { + comp_instance_to_import.insert(inst_idx as u32, imp.name.clone()); + } + } + + if let Some(def) = self.component_type_defs.get(type_id as usize) { + match def { + ComponentTypeDef::InstanceExportAlias(alias_idx) => { + if let Some(ComponentAliasEntry::InstanceExport { + instance_index, + name, + .. + }) = self.component_aliases.get(*alias_idx) + && let Some(module_name) = comp_instance_to_import.get(instance_index) + { + return Some((module_name.clone(), name.clone())); + } + } + ComponentTypeDef::Import(import_idx) => { + if let Some(imp) = self.imports.get(*import_idx) { + // Extract resource name from WASI path: + // "wasi:io/error@0.2.6" → "error" + let without_version = imp + .name + .rfind('@') + .map(|pos| &imp.name[..pos]) + .unwrap_or(&imp.name); + let resource_name = without_version + .rfind('/') + .map(|pos| &without_version[pos + 1..]) + .unwrap_or(without_version); + return Some((imp.name.clone(), resource_name.to_string())); + } + } + _ => {} + } + } + None + } + /// Compute flat core param indices where (ptr, len) pairs start. /// /// In the canonical ABI, `string` and `list` params flatten to diff --git a/meld-core/src/resolver.rs b/meld-core/src/resolver.rs index a162a2e..d48c838 100644 --- a/meld-core/src/resolver.rs +++ b/meld-core/src/resolver.rs @@ -137,6 +137,24 @@ pub struct ConditionalPointerPair { pub discriminant_byte_size: u32, } +/// A resolved resource operation for adapter generation. +/// +/// Maps a resource-typed parameter or result to the `(module, field)` import +/// pair for the corresponding `[resource-rep]` or `[resource-new]` function. +#[derive(Debug, Clone)] +pub struct ResolvedResourceOp { + /// Index in the flat (stack) ABI layout + pub flat_idx: u32, + /// Byte offset in the canonical ABI memory layout (for return-area results) + pub byte_offset: u32, + /// `true` for `own`, `false` for `borrow` + pub is_owned: bool, + /// Import module name (e.g., `"[export]test:resources/resources"`) + pub import_module: String, + /// Import field name (e.g., `"[resource-rep]y"`) + pub import_field: String, +} + /// Requirements for an adapter function #[derive(Debug, Clone, Default)] pub struct AdapterRequirements { @@ -186,6 +204,12 @@ pub struct AdapterRequirements { /// Used by the adapter to emit correctly-sized load/store instructions (e.g., /// `i64.load`/`i64.store` for 8-byte values like f64/i64). pub return_area_slots: Vec, + /// Resource-typed parameters needing handle→representation conversion. + /// The adapter calls `[resource-rep]` for each before forwarding to callee. + pub resource_params: Vec, + /// Resource-typed results needing representation→handle conversion. + /// The adapter calls `[resource-new]` for each before returning to caller. + pub resource_results: Vec, } /// Resolution of module-level imports within a component @@ -596,13 +620,43 @@ fn build_canon_import_names(component: &ParsedComponent) -> HashMap { + if let Some((inst_idx, type_name)) = + comp_type_to_instance_export.get(resource) + { + if let Some(module_name) = + comp_instance_to_import_name.get(inst_idx) + { + let field = format!("[resource-new]{}", type_name); + result.insert(core_func_idx, (module_name.clone(), field)); + } + } else if let Some(import_name) = comp_type_import_names.get(resource) { + let resource_name = extract_wasi_resource_name(import_name); + let field = format!("[resource-new]{}", resource_name); + result.insert(core_func_idx, (import_name.clone(), field)); + } + } + CanonicalEntry::ResourceRep { resource } => { + if let Some((inst_idx, type_name)) = + comp_type_to_instance_export.get(resource) + { + if let Some(module_name) = + comp_instance_to_import_name.get(inst_idx) + { + let field = format!("[resource-rep]{}", type_name); + result.insert(core_func_idx, (module_name.clone(), field)); + } + } else if let Some(import_name) = comp_type_import_names.get(resource) { + let resource_name = extract_wasi_resource_name(import_name); + let field = format!("[resource-rep]{}", resource_name); + result.insert(core_func_idx, (import_name.clone(), field)); + } + } _ => {} } } @@ -645,6 +699,162 @@ fn extract_wasi_resource_name(import_name: &str) -> &str { } } +/// Build a map from resource type ID → `(module, field)` for resource canonical +/// functions (`[resource-rep]`, `[resource-new]`) in a component. +/// +/// This works by: +/// 1. Scanning canonical functions to find ResourceRep/ResourceNew entries and +/// their core func indices. +/// 2. Scanning `FromExports` core instances to find which field name each core +/// func index is exported as (e.g., `"[resource-new]x"`). +/// 3. Scanning `Instantiate` core instances to find which module name each +/// `FromExports` instance provides (e.g., `"[export]exports"`). +fn build_resource_type_to_import( + component: &ParsedComponent, +) -> HashMap<(u32, &'static str), (String, String)> { + use crate::parser::{CanonicalEntry, CoreEntityDef, InstanceKind}; + + // Step 1: Build resource_type → (core_func_idx, kind) from canonical functions + let mut resource_core_funcs: Vec<(u32, u32, &'static str)> = Vec::new(); // (resource_type, core_func_idx, kind) + let mut core_func_idx = 0u32; + for def in &component.core_entity_order { + match def { + CoreEntityDef::CanonicalFunction(canon_idx) => { + if let Some(entry) = component.canonical_functions.get(*canon_idx) { + match entry { + CanonicalEntry::ResourceRep { resource } => { + resource_core_funcs.push((*resource, core_func_idx, "[resource-rep]")); + } + CanonicalEntry::ResourceNew { resource } => { + resource_core_funcs.push((*resource, core_func_idx, "[resource-new]")); + } + _ => {} + } + } + core_func_idx += 1; + } + CoreEntityDef::CoreAlias(alias_idx) => { + if let Some(crate::parser::ComponentAliasEntry::CoreInstanceExport { + kind: wasmparser::ExternalKind::Func, + .. + }) = component.component_aliases.get(*alias_idx) + { + core_func_idx += 1; + } + } + } + } + + if resource_core_funcs.is_empty() { + return HashMap::new(); + } + + // Step 2: Build core_func_idx → (instance_idx, field_name) from FromExports instances. + // A FromExports instance maps field names to core entity indices. + let mut core_func_to_field: HashMap = HashMap::new(); // core_func_idx → (instance_idx, field_name) + for inst in &component.instances { + if let InstanceKind::FromExports(exports) = &inst.kind { + for (name, kind, index) in exports { + if *kind == wasmparser::ExternalKind::Func { + core_func_to_field.insert(*index, (inst.index, name.clone())); + } + } + } + } + + // Step 3: Build instance_idx → module_name from Instantiate args. + // When a core module is instantiated, args map (module_name → Instance(idx)). + let mut instance_to_module: HashMap = HashMap::new(); + for inst in &component.instances { + if let InstanceKind::Instantiate { args, .. } = &inst.kind { + for (module_name, arg) in args { + if let crate::parser::InstanceArg::Instance(inst_idx) = arg { + instance_to_module.insert(*inst_idx, module_name.clone()); + } + } + } + } + + // Step 4: Combine: resource_type → (module_name, field_name) + let mut map = HashMap::new(); + for (resource_type, cf_idx, kind) in &resource_core_funcs { + if let Some((from_exports_idx, field_name)) = core_func_to_field.get(cf_idx) + && let Some(module_name) = instance_to_module.get(from_exports_idx) + { + map.insert( + (*resource_type, *kind), + (module_name.clone(), field_name.clone()), + ); + } + } + + // Step 5: Infer missing operations from existing ones. + // + // The component model's `canon lift` handles resource conversions + // internally, so a component may have ResourceNew for a type but not + // ResourceRep (or vice versa). If the adapter needs the missing one, + // we can infer it: same module name, same resource name, different + // prefix ("[resource-new]" vs "[resource-rep]"). + let known_types: Vec = map.keys().map(|&(rt, _)| rt).collect(); + for rt in known_types { + let has_rep = map.contains_key(&(rt, "[resource-rep]")); + let has_new = map.contains_key(&(rt, "[resource-new]")); + + if has_new && !has_rep { + if let Some((module, field)) = map.get(&(rt, "[resource-new]")).cloned() + && let Some(name) = field.strip_prefix("[resource-new]") + { + let rep_field = format!("[resource-rep]{}", name); + map.insert((rt, "[resource-rep]"), (module, rep_field)); + } + } else if has_rep + && !has_new + && let Some((module, field)) = map.get(&(rt, "[resource-rep]")).cloned() + && let Some(name) = field.strip_prefix("[resource-rep]") + { + let new_field = format!("[resource-new]{}", name); + map.insert((rt, "[resource-new]"), (module, new_field)); + } + } + + map +} + +/// Resolve resource positions to `(module, field)` import pairs. +/// +/// Uses `build_resource_type_to_import` to map resource type IDs found in +/// function signatures to their `[resource-rep]` or `[resource-new]` core +/// import names. The `field_prefix` selects which canonical function kind +/// to look up: `"[resource-rep]"` for params, `"[resource-new]"` for results. +fn resolve_resource_positions( + resource_map: &HashMap<(u32, &'static str), (String, String)>, + positions: &[crate::parser::ResourcePosition], + field_prefix: &'static str, +) -> Vec { + let mut resolved = Vec::new(); + for pos in positions { + if let Some((module_name, field_name)) = + resource_map.get(&(pos.resource_type_id, field_prefix)) + { + resolved.push(ResolvedResourceOp { + flat_idx: pos.flat_idx, + byte_offset: pos.byte_offset, + is_owned: pos.is_owned, + import_module: module_name.clone(), + import_field: field_name.clone(), + }); + } else { + log::debug!( + "Could not resolve resource type {} for {} at flat_idx {}", + pos.resource_type_id, + field_prefix, + pos.flat_idx, + ); + } + } + resolved +} + /// Dependency resolver pub struct Resolver { /// Whether to allow unresolved imports @@ -728,9 +938,108 @@ impl Resolver { // module_resolutions entries to adapter_sites. self.identify_intra_component_adapter_sites(components, &mut graph)?; + // Synthesize missing resource imports. + // + // The component model's `canon lift` handles `resource.rep` and + // `resource.new` internally, so a component binary may lack an + // explicit `ResourceRep` or `ResourceNew` canonical function even + // though the adapter needs one. Detect missing resource imports and + // add them as synthetic unresolved imports so the merger includes them. + Self::synthesize_missing_resource_imports(components, &mut graph); + Ok(graph) } + /// Add synthetic unresolved imports for `[resource-rep]` / `[resource-new]` + /// functions that adapters need but no source core module imports directly. + /// + /// For each adapter site, check whether the resource operations it needs + /// already exist as unresolved imports (meaning some core module imports + /// them). If not, add a synthetic unresolved import so the merger includes + /// them in the fused module's import list. + fn synthesize_missing_resource_imports( + components: &[ParsedComponent], + graph: &mut DependencyGraph, + ) { + use std::collections::HashSet; + + // Collect all (module, field) pairs from adapter requirements + let mut needed: Vec<(String, String, usize)> = Vec::new(); // (module, field, callee_comp_idx) + for site in &graph.adapter_sites { + for op in &site.requirements.resource_params { + needed.push(( + op.import_module.clone(), + op.import_field.clone(), + site.to_component, + )); + } + for op in &site.requirements.resource_results { + needed.push(( + op.import_module.clone(), + op.import_field.clone(), + site.to_component, + )); + } + } + + if needed.is_empty() { + return; + } + + // Check which (module, field) pairs already exist as unresolved imports + let existing: HashSet<(String, String)> = graph + .unresolved_imports + .iter() + .filter(|u| matches!(u.kind, ImportKind::Function(_))) + .map(|u| { + let module = u.display_module.as_deref().unwrap_or(&u.module_name); + let field = u.display_field.as_deref().unwrap_or(&u.field_name); + (module.to_string(), field.to_string()) + }) + .collect(); + + let mut added: HashSet<(String, String)> = HashSet::new(); + for (module, field, callee_comp_idx) in &needed { + let key = (module.clone(), field.clone()); + if existing.contains(&key) || added.contains(&key) { + continue; + } + + // Find a function type index for (i32) -> (i32) in the callee's + // first core module. This is the canonical type for resource ops. + let comp = &components[*callee_comp_idx]; + let i32_to_i32 = comp + .core_modules + .first() + .and_then(|m| { + m.types.iter().position(|t| { + t.params == [wasm_encoder::ValType::I32] + && t.results == [wasm_encoder::ValType::I32] + }) + }) + .unwrap_or(0) as u32; + + log::debug!( + "Synthesizing resource import ({}, {}) for component {}", + module, + field, + callee_comp_idx + ); + + graph.unresolved_imports.push(UnresolvedImport { + component_idx: *callee_comp_idx, + module_idx: 0, + module_name: module.clone(), + field_name: field.clone(), + kind: ImportKind::Function(i32_to_i32), + display_module: Some(module.clone()), + display_field: Some(field.clone()), + }); + + added.insert(key); + } + } + /// Build an index of all exports across components fn build_export_index<'a>( &self, @@ -1454,6 +1763,7 @@ impl Resolver { ); let mut per_func_matched = false; let callee_lift_info = to_component.lift_info_by_core_func(); + let callee_resource_map = build_resource_type_to_import(to_component); // Provenance-based maps for correct core func index lookup. // These account for interleaved canon lower / alias entries. let callee_export_to_core = build_module_export_to_core_func(to_component); @@ -1545,6 +1855,21 @@ impl Resolver { ); requirements.return_area_slots = to_component.return_area_slots(results); + // Collect resource-typed params and results + requirements.resource_params = + resolve_resource_positions( + &callee_resource_map, + &to_component + .resource_param_positions(comp_params), + "[resource-rep]", + ); + requirements.resource_results = + resolve_resource_positions( + &callee_resource_map, + &to_component + .resource_result_positions(results), + "[resource-new]", + ); } } diff --git a/meld-core/tests/wit_bindgen_runtime.rs b/meld-core/tests/wit_bindgen_runtime.rs index dbe5f9a..88ba6e2 100644 --- a/meld-core/tests/wit_bindgen_runtime.rs +++ b/meld-core/tests/wit_bindgen_runtime.rs @@ -580,29 +580,11 @@ fn test_runtime_wit_bindgen_resources() { if !fixture_exists("resources") { return; } - // SR-25: P2 wrapping now handles resource types. Component fusion and - // validation work. Runtime execution may still fail due to adapter-level - // issues with resource pointer alignment (separate from wrapping). + // SR-25: Resource handle translation in cross-component adapters. + // The adapter calls [resource-rep] to convert handles → representations + // for params. Results are passed through as-is (the callee core function + // already calls [resource-new] internally). let fused = fuse_fixture("resources", OutputFormat::Component) .expect("resources: component fusion should succeed"); - match run_wasi_component(&fused) { - Ok(()) => {} - Err(e) => { - let msg = format!("{e:?}"); - // Known issue: the fused adapter code has alignment issues with - // resource pointer data. This is a core fusion / adapter bug, - // not a P2 wrapping issue (SR-25 covers wrapping only). - if msg.contains("misaligned") - || msg.contains("unreachable") - || msg.contains("wasm trap") - { - eprintln!( - "resources: runtime execution failed (adapter alignment issue, \ - not a wrapping bug): {e}" - ); - } else { - panic!("resources: unexpected runtime error: {msg}"); - } - } - } + run_wasi_component(&fused).expect("resources: runtime execution should succeed"); }