From 7ad095625985e321b2fa223c7bd460b0ea92b37a Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Mon, 6 Apr 2026 20:57:51 -0500 Subject: [PATCH 1/6] wip: handle table routing for re-exporter resources (#92) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Complete handle table infrastructure: - ht_new/ht_rep/ht_drop in linear memory for re-exporter components - Function bodies re-rewritten to use ht_* for resource ops - Component wrapper routes LocalResource through $ht_*_N exports - Adapter uses ht_rep for caller extraction when caller has handle table (fixes handle escape from re-exporter→definer calls) Status: "unknown handle index" error fixed (handle no longer escapes to canonical resource table). Now hits "unreachable" trap — likely an initialization or argument issue in the handle table functions. 73/73 runtime tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) --- meld-core/src/adapter/fact.rs | 59 +++++++++++----- meld-core/src/component_wrap.rs | 48 +++++++++++-- meld-core/src/merger.rs | 97 ++++++++++++++++++++++++-- meld-core/tests/wit_bindgen_runtime.rs | 2 - 4 files changed, 174 insertions(+), 32 deletions(-) diff --git a/meld-core/src/adapter/fact.rs b/meld-core/src/adapter/fact.rs index 243e8cd..0261ff0 100644 --- a/meld-core/src/adapter/fact.rs +++ b/meld-core/src/adapter/fact.rs @@ -263,17 +263,27 @@ impl FactStyleGenerator { if op.callee_defines_resource { // Callee defines the resource — convert handle→rep. // Skip if upstream adapter already converted (avoids double resource.rep). - if !op.caller_already_converted - && let Some(&rep_func) = resource_rep_imports - .get(&(op.import_module.clone(), op.import_field.clone())) - { - options - .resource_rep_calls - .push(super::ResourceBorrowTransfer { - param_idx: op.flat_idx, - rep_func, - new_func: None, + if !op.caller_already_converted { + // If the caller has a handle table, use ht_rep to extract rep + // from the memory-pointer handle. Otherwise use canonical resource.rep. + let rep_func = merged + .handle_tables + .get(&site.from_component) + .map(|ht| ht.rep_func) + .or_else(|| { + resource_rep_imports + .get(&(op.import_module.clone(), op.import_field.clone())) + .copied() }); + if let Some(rep_func) = rep_func { + options + .resource_rep_calls + .push(super::ResourceBorrowTransfer { + param_idx: op.flat_idx, + rep_func, + new_func: None, + }); + } } } else { // 3-component case: callee doesn't define the resource. @@ -311,10 +321,18 @@ impl FactStyleGenerator { .map(|(_, &idx)| idx) }); + // For re-exporter callees with handle tables, use ht_new + // which returns memory-pointer handles that wit-bindgen expects. let callee_new_func = merged - .resource_new_by_component - .get(&(site.to_component, resource_name.to_string())) - .copied() + .handle_tables + .get(&site.to_component) + .map(|ht| ht.new_func) + .or_else(|| { + merged + .resource_new_by_component + .get(&(site.to_component, resource_name.to_string())) + .copied() + }) .or_else(|| { let new_field = op.import_field.replace("[resource-rep]", "[resource-new]"); resource_new_imports @@ -365,12 +383,19 @@ impl FactStyleGenerator { .copied() }); - // Callee's [resource-rep] (callee handle → rep) + // Callee's [resource-rep] (callee handle → rep). + // For re-exporter callees with handle tables, use ht_rep. let rep_field = format!("[resource-rep]{}", resource_name); let rep_func = merged - .resource_rep_by_component - .get(&(site.to_component, resource_name.to_string())) - .copied() + .handle_tables + .get(&site.to_component) + .map(|ht| ht.rep_func) + .or_else(|| { + merged + .resource_rep_by_component + .get(&(site.to_component, resource_name.to_string())) + .copied() + }) .or_else(|| { resource_rep_imports .get(&(op.import_module.clone(), rep_field.clone())) diff --git a/meld-core/src/component_wrap.rs b/meld-core/src/component_wrap.rs index 4db4c37..b1a6a2e 100644 --- a/meld-core/src/component_wrap.rs +++ b/meld-core/src/component_wrap.rs @@ -1376,19 +1376,53 @@ fn assemble_component( operation, resource_name, interface_name, - component_idx: _, + component_idx, } => { - { - // All components share one canonical resource type per - // resource name. Re-exporters forward handles through the - // same wasmtime-managed handle table as the definer. + // Check if this component has handle table exports + // ($ht_new_N, $ht_rep_N, $ht_drop_N) for re-exporter routing. + let ht_export = component_idx.and_then(|cidx| { + let name = match operation { + ResourceOp::New => format!("$ht_new_{}", cidx), + ResourceOp::Rep => format!("$ht_rep_{}", cidx), + ResourceOp::Drop => format!("$ht_drop_{}", cidx), + }; + if fused_info + .exports + .iter() + .any(|(n, k, _)| *k == wasmparser::ExternalKind::Func && *n == name) + { + Some(name) + } else { + None + } + }); + + if let Some(ht_name) = ht_export { + // Re-exporter: alias the handle table function directly + // from the fused instance instead of using canon resource ops. + log::debug!( + "using ht export {} for {:?} comp {:?}", + ht_name, + operation, + component_idx + ); + let mut aliases = ComponentAliasSection::new(); + aliases.alias(Alias::CoreInstanceExport { + instance: fused_instance, + kind: ExportKind::Func, + name: &ht_name, + }); + component.section(&aliases); + + lowered_func_indices.push(core_func_idx); + core_func_idx += 1; + } else { + // Standard path: canonical resource operations. let res_type_key = resource_name.clone(); let res_type_idx = if let Some(&existing) = local_resource_types.get(&res_type_key) { existing } else { - // Define a new resource type. The destructor is exported from - // the fused module as `#[dtor]`. let dtor_export_name = format!("{}#[dtor]{}", interface_name, resource_name); let has_dtor = fused_info.exports.iter().any(|(n, k, _)| { diff --git a/meld-core/src/merger.rs b/meld-core/src/merger.rs index 3172cf3..b4033e6 100644 --- a/meld-core/src/merger.rs +++ b/meld-core/src/merger.rs @@ -683,12 +683,97 @@ impl Merger { // Handle start functions self.resolve_start_functions(components, &mut merged)?; - // Handle table allocation disabled: with resource-name-only keying, - // all components share one canonical resource type per resource name. - // The wasmtime runtime manages handle tables — no custom tables needed. - // if !graph.reexporter_components.is_empty() { - // Self::allocate_handle_tables(graph, &mut merged)?; - // } + // Allocate per-component handle tables for re-exporter components. + // These are needed for 3-component resource chains where the + // re-exporter's wit-bindgen code expects 4-byte-aligned memory + // pointers as handles, not sequential canonical ABI handles. + if !graph.reexporter_components.is_empty() { + Self::allocate_handle_tables(graph, &mut merged)?; + + // Remap re-exporter's resource.rep/new/drop imports to handle table + // functions, then re-rewrite affected function bodies so call + // instructions pick up the corrected indices. + let mut affected_modules: Vec<(usize, usize)> = Vec::new(); + for (&comp_idx, ht) in &merged.handle_tables { + let component = &components[comp_idx]; + for (mod_idx, module) in component.core_modules.iter().enumerate() { + let mut import_func_idx = 0u32; + let mut changed = false; + for imp in &module.imports { + if !matches!(imp.kind, crate::parser::ImportKind::Function(_)) { + continue; + } + if imp.name.starts_with("[resource-rep]") { + merged + .function_index_map + .insert((comp_idx, mod_idx, import_func_idx), ht.rep_func); + changed = true; + } else if imp.name.starts_with("[resource-new]") { + merged + .function_index_map + .insert((comp_idx, mod_idx, import_func_idx), ht.new_func); + changed = true; + } else if imp.name.starts_with("[resource-drop]") { + merged + .function_index_map + .insert((comp_idx, mod_idx, import_func_idx), ht.drop_func); + changed = true; + } + import_func_idx += 1; + } + if changed { + affected_modules.push((comp_idx, mod_idx)); + } + } + } + + // Re-rewrite function bodies for modules that had resource imports + // redirected to handle table functions. + for &(comp_idx, mod_idx) in &affected_modules { + let module = &components[comp_idx].core_modules[mod_idx]; + let index_maps = build_index_maps_for_module( + comp_idx, + mod_idx, + module, + &merged, + self.memory_strategy, + false, // address_rebasing + 0u64, // memory_base_offset + false, // memory64 + None, // memory_initial_pages + ); + let import_func_count = module + .imports + .iter() + .filter(|i| matches!(i.kind, ImportKind::Function(_))) + .count() as u32; + + for (old_idx, &type_idx) in module.functions.iter().enumerate() { + let old_func_idx = import_func_count + old_idx as u32; + let param_count = module + .types + .get(type_idx as usize) + .map(|ty| ty.params.len() as u32) + .unwrap_or(0); + + let body = extract_function_body(module, old_idx, param_count, &index_maps)?; + + if let Some(mf) = merged + .functions + .iter_mut() + .find(|f| f.origin == (comp_idx, mod_idx, old_func_idx)) + { + mf.body = body; + } + } + log::info!( + "re-rewrote {} functions in component {} module {} for handle table routing", + module.functions.len(), + comp_idx, + mod_idx, + ); + } + } if let Some(plan) = shared_memory_plan { if plan.import.is_none() { diff --git a/meld-core/tests/wit_bindgen_runtime.rs b/meld-core/tests/wit_bindgen_runtime.rs index 6d9cc7d..f8db04d 100644 --- a/meld-core/tests/wit_bindgen_runtime.rs +++ b/meld-core/tests/wit_bindgen_runtime.rs @@ -671,8 +671,6 @@ runtime_test!( test_runtime_wit_bindgen_resource_aggregates, "resource_aggregates" ); -// 3-component chain: resource.rep conversion now works, but leaf's internal -// ResourceTable::get returns None — needs further adapter investigation. fuse_only_test!(test_fuse_wit_bindgen_resource_floats, "resource_floats"); runtime_test!( test_runtime_wit_bindgen_resource_borrow_in_record, From 5cf346b5ea29dfff94113b224e3d3e11b9622539 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Tue, 7 Apr 2026 18:11:46 -0500 Subject: [PATCH 2/6] feat: callback-driving adapter for P3 async cross-component calls Replace the canon lift/lower approach with a core wasm adapter that drives the callee's [async-lift] + [callback] loop directly. This eliminates the component boundary for internal async calls, avoiding call_might_be_recursive entirely. The adapter protocol (from the canonical ABI spec): 1. Call [async-lift] entry -> packed i32 (EXIT/WAIT/YIELD) 2. Loop: if WAIT, call waitable-set-poll; call [callback] with event 3. After EXIT, retrieve result via host intrinsic Merger: async adapter sites now resolved like sync sites. Result retrieval stubbed pending host API finalization. 73/73 P2 runtime tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) --- meld-core/src/adapter/fact.rs | 261 +++++++++++++++++++++++++++++++--- meld-core/src/lib.rs | 7 - meld-core/src/merger.rs | 16 +-- 3 files changed, 247 insertions(+), 37 deletions(-) diff --git a/meld-core/src/adapter/fact.rs b/meld-core/src/adapter/fact.rs index 0261ff0..e419545 100644 --- a/meld-core/src/adapter/fact.rs +++ b/meld-core/src/adapter/fact.rs @@ -3188,6 +3188,249 @@ impl FactStyleGenerator { )) }) } + + /// Generate a callback-driving adapter for P3 async cross-component calls. + /// + /// Instead of canon lift/lower (which triggers call_might_be_recursive), + /// the adapter drives the callee's [async-lift] + [callback] loop directly + /// in core wasm. The protocol: + /// 1. Call [async-lift] entry → packed i32 (EXIT/WAIT/YIELD) + /// 2. Loop: poll waitable-set, call [callback] with events + /// 3. After EXIT, call [task-get-result] host import for result + fn generate_async_callback_adapter( + &self, + site: &AdapterSite, + merged: &MergedModule, + ) -> Result { + let name = format!( + "$async_adapter_{}_to_{}", + site.from_component, site.to_component + ); + + // Find the [async-lift] entry function's merged index + let async_lift_func = self.resolve_target_function(site, merged)?; + + // Find the [callback] function's merged index by deriving its export name + let callback_export_name = format!("[callback]{}", site.export_name); + let callback_func = merged + .exports + .iter() + .find(|e| e.kind == wasm_encoder::ExportKind::Func && e.name == callback_export_name) + .map(|e| e.index) + .ok_or_else(|| { + crate::error::Error::EncodingError(format!( + "async adapter: cannot find callback export '{}' for '{}'", + callback_export_name, site.export_name, + )) + })?; + + // Find the waitable-set-poll host import. It's an unresolved import + // from $root with name [waitable-set-poll] (possibly with $N suffix). + let wsp_func = merged + .imports + .iter() + .enumerate() + .find(|(_, imp)| imp.name.starts_with("[waitable-set-poll]")) + .map(|(i, _)| i as u32) + .ok_or_else(|| { + crate::error::Error::EncodingError( + "async adapter: cannot find [waitable-set-poll] import".to_string(), + ) + })?; + + // Determine the caller's type (what the caller expects to call) + let caller_type_idx = site + .import_func_type_idx + .and_then(|local_ti| { + merged + .type_index_map + .get(&(site.from_component, site.from_module, local_ti)) + .copied() + }) + .unwrap_or(0); + + let caller_type = merged + .types + .get(caller_type_idx as usize) + .cloned() + .unwrap_or_else(|| crate::merger::MergedFuncType { + params: Vec::new(), + results: Vec::new(), + }); + let caller_param_count = caller_type.params.len(); + let _caller_result_count = caller_type.results.len(); + + // Find callee's memory index for the event buffer scratch space + let callee_memory = crate::merger::component_memory_index(merged, site.to_component); + + // Build the adapter function body + // + // Locals layout: + // 0..caller_param_count: params from caller + // caller_param_count+0: $packed (i32) — packed return from entry/callback + // caller_param_count+1: $code (i32) — unpacked callback code + // caller_param_count+2: $payload (i32) — unpacked payload (waitable set idx) + // caller_param_count+3: $event_code (i32) + // caller_param_count+4: $p1 (i32) + // caller_param_count+5: $p2 (i32) + let l_packed = caller_param_count as u32; + let l_code = l_packed + 1; + let l_payload = l_packed + 2; + let l_event_code = l_packed + 3; + let l_p1 = l_packed + 4; + let l_p2 = l_packed + 5; + + let mut body = Function::new([(6, wasm_encoder::ValType::I32)]); + + // Step 1: Call [async-lift] entry with caller's params + for i in 0..caller_param_count { + body.instruction(&Instruction::LocalGet(i as u32)); + } + body.instruction(&Instruction::Call(async_lift_func)); + body.instruction(&Instruction::LocalSet(l_packed)); + + // Unpack: code = packed & 0xF, payload = packed >> 4 + body.instruction(&Instruction::LocalGet(l_packed)); + body.instruction(&Instruction::I32Const(0xF)); + body.instruction(&Instruction::I32And); + body.instruction(&Instruction::LocalSet(l_code)); + body.instruction(&Instruction::LocalGet(l_packed)); + body.instruction(&Instruction::I32Const(4)); + body.instruction(&Instruction::I32ShrU); + body.instruction(&Instruction::LocalSet(l_payload)); + + // Step 2: Callback-driving loop + // block $exit + // loop $drive + // if code == EXIT(0): break + // if code == WAIT(2): call waitable-set-poll + // call callback(event_code, p1, p2) + // unpack result + // br $drive + // end + // end + body.instruction(&Instruction::Block(wasm_encoder::BlockType::Empty)); + body.instruction(&Instruction::Loop(wasm_encoder::BlockType::Empty)); + + // if code == 0 (EXIT): br $exit (block index 1) + body.instruction(&Instruction::LocalGet(l_code)); + body.instruction(&Instruction::I32Eqz); + body.instruction(&Instruction::BrIf(1)); // break to $exit block + + // if code == 2 (WAIT): call waitable-set-poll(payload, event_ptr) + // Use scratch space at address 0 in callee memory for the 3xi32 event tuple + // (This is safe because the callee isn't running — we're driving it) + body.instruction(&Instruction::LocalGet(l_code)); + body.instruction(&Instruction::I32Const(2)); + body.instruction(&Instruction::I32Eq); + body.instruction(&Instruction::If(wasm_encoder::BlockType::Empty)); + { + // waitable-set-poll(set_handle, event_ptr) → i32 + body.instruction(&Instruction::LocalGet(l_payload)); + body.instruction(&Instruction::I32Const(0)); // event result ptr (scratch) + body.instruction(&Instruction::Call(wsp_func)); + body.instruction(&Instruction::Drop); // drop poll return value + + // Read event tuple from scratch memory: [event_code, p1, p2] at addr 0 + let mem_arg = wasm_encoder::MemArg { + offset: 0, + align: 2, + memory_index: callee_memory, + }; + body.instruction(&Instruction::I32Const(0)); + body.instruction(&Instruction::I32Load(mem_arg)); + body.instruction(&Instruction::LocalSet(l_event_code)); + + let mem_arg_4 = wasm_encoder::MemArg { + offset: 4, + align: 2, + memory_index: callee_memory, + }; + body.instruction(&Instruction::I32Const(0)); + body.instruction(&Instruction::I32Load(mem_arg_4)); + body.instruction(&Instruction::LocalSet(l_p1)); + + let mem_arg_8 = wasm_encoder::MemArg { + offset: 8, + align: 2, + memory_index: callee_memory, + }; + body.instruction(&Instruction::I32Const(0)); + body.instruction(&Instruction::I32Load(mem_arg_8)); + body.instruction(&Instruction::LocalSet(l_p2)); + } + body.instruction(&Instruction::Else); + { + // YIELD(1): set event to (NONE, 0, 0) + body.instruction(&Instruction::I32Const(0)); + body.instruction(&Instruction::LocalSet(l_event_code)); + body.instruction(&Instruction::I32Const(0)); + body.instruction(&Instruction::LocalSet(l_p1)); + body.instruction(&Instruction::I32Const(0)); + body.instruction(&Instruction::LocalSet(l_p2)); + } + body.instruction(&Instruction::End); // end if WAIT/YIELD + + // Call callback(event_code, p1, p2) → packed i32 + body.instruction(&Instruction::LocalGet(l_event_code)); + body.instruction(&Instruction::LocalGet(l_p1)); + body.instruction(&Instruction::LocalGet(l_p2)); + body.instruction(&Instruction::Call(callback_func)); + body.instruction(&Instruction::LocalSet(l_packed)); + + // Unpack new result + body.instruction(&Instruction::LocalGet(l_packed)); + body.instruction(&Instruction::I32Const(0xF)); + body.instruction(&Instruction::I32And); + body.instruction(&Instruction::LocalSet(l_code)); + body.instruction(&Instruction::LocalGet(l_packed)); + body.instruction(&Instruction::I32Const(4)); + body.instruction(&Instruction::I32ShrU); + body.instruction(&Instruction::LocalSet(l_payload)); + + body.instruction(&Instruction::Br(0)); // br $drive (loop) + body.instruction(&Instruction::End); // end loop + body.instruction(&Instruction::End); // end block + + // Step 3: After EXIT, return result to caller. + // For now, return default values. The task-get-result host intrinsic + // will be added when we have the host API finalized. + // TODO: call [task-get-result]N to retrieve stored result + for result_ty in &caller_type.results { + match result_ty { + wasm_encoder::ValType::I32 => { + body.instruction(&Instruction::I32Const(0)); + } + wasm_encoder::ValType::I64 => { + body.instruction(&Instruction::I64Const(0)); + } + wasm_encoder::ValType::F32 => { + body.instruction(&Instruction::F32Const(0.0_f32.into())); + } + wasm_encoder::ValType::F64 => { + body.instruction(&Instruction::F64Const(0.0_f64.into())); + } + _ => { + body.instruction(&Instruction::I32Const(0)); + } + } + } + + body.instruction(&Instruction::End); + + let target_func = self.resolve_target_function(site, merged)?; + + Ok(AdapterFunction { + name, + type_idx: caller_type_idx, + body, + source_component: site.from_component, + source_module: site.from_module, + target_component: site.to_component, + target_module: site.to_module, + target_function: target_func, + }) + } } impl AdapterGenerator for FactStyleGenerator { @@ -3201,22 +3444,8 @@ impl AdapterGenerator for FactStyleGenerator { for (idx, site) in graph.adapter_sites.iter().enumerate() { if site.is_async_lift { - // Async adapter sites are preserved as component-level canon - // lift/lower pairs. Generate a dummy (unreachable) adapter to - // maintain 1:1 correspondence with adapter_sites. - let mut body = Function::new([]); - body.instruction(&Instruction::Unreachable); - body.instruction(&Instruction::End); - adapters.push(AdapterFunction { - name: format!("$async_stub_{}", idx), - type_idx: 0, - body, - source_component: site.from_component, - source_module: site.from_module, - target_component: site.to_component, - target_module: site.to_module, - target_function: 0, - }); + let adapter = self.generate_async_callback_adapter(site, merged)?; + adapters.push(adapter); continue; } let adapter = self.generate_adapter( diff --git a/meld-core/src/lib.rs b/meld-core/src/lib.rs index b13959d..760af62 100644 --- a/meld-core/src/lib.rs +++ b/meld-core/src/lib.rs @@ -386,9 +386,6 @@ impl Fuser { for (adapter_offset, (adapter, site)) in adapters.iter().zip(graph.adapter_sites.iter()).enumerate() { - if site.is_async_lift { - continue; - } if let Some(local_ti) = site.import_func_type_idx && let Some(&caller_ti) = merged @@ -439,10 +436,6 @@ impl Fuser { for (adapter_offset, (adapter, site)) in adapters.iter().zip(graph.adapter_sites.iter()).enumerate() { - // Skip async-lifted sites — their imports stay unresolved. - if site.is_async_lift { - continue; - } let target_idx = if let Some(&wrapper_idx) = adapter_to_wrapper.get(&adapter_offset) { wrapper_idx } else { diff --git a/meld-core/src/merger.rs b/meld-core/src/merger.rs index b4033e6..5b59515 100644 --- a/meld-core/src/merger.rs +++ b/meld-core/src/merger.rs @@ -1173,11 +1173,8 @@ impl Merger { } // Check adapter_sites first (cross-component + intra-component adapters). - // Skip async-lifted sites — their imports stay unresolved so the - // component wrapper can provide them via canon lift/lower. let resolved = graph.adapter_sites.iter().find(|site| { - !site.is_async_lift - && site.from_component == comp_idx + site.from_component == comp_idx && site.from_module == mod_idx && (imp.name == site.import_name || imp.module == site.import_name) && (imp.module == site.import_module || imp.name == site.import_module) @@ -1664,11 +1661,7 @@ impl Merger { for unresolved in &graph.unresolved_imports { // Skip imports resolved by adapter sites (must match the // filter in compute_unresolved_import_assignments). - // Async-lifted sites are excluded — their imports stay unresolved. let resolved_by_adapter = graph.adapter_sites.iter().any(|site| { - if site.is_async_lift { - return false; - } if site.from_component != unresolved.component_idx { return false; } @@ -2490,7 +2483,7 @@ impl Default for Merger { /// Pre-compute unresolved import counts and per-import index assignments. /// Find the merged memory index for a component's first defined memory. -fn component_memory_index(merged: &MergedModule, comp_idx: usize) -> u32 { +pub(crate) fn component_memory_index(merged: &MergedModule, comp_idx: usize) -> u32 { for (&(ci, _mi, mem_i), &merged_idx) in &merged.memory_index_map { if ci == comp_idx && mem_i == 0 { return merged_idx; @@ -2561,11 +2554,6 @@ fn compute_unresolved_import_assignments( // because indirect-table shim modules use synthetic names (module="", // field="0") while their display names carry the original interface names. let resolved_by_adapter = graph.adapter_sites.iter().any(|site| { - // Async-lifted sites are NOT fused — their imports stay unresolved - // so the component wrapper handles them via canon lift/lower. - if site.is_async_lift { - return false; - } if site.from_component != unresolved.component_idx { return false; } From 3807c8587eb7353d74c552766a8a4aa6357b5783 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Mon, 13 Apr 2026 13:08:03 -0500 Subject: [PATCH 3/6] feat: task.return shims for async adapter Co-Authored-By: Claude Opus 4.6 (1M context) --- meld-core/src/adapter/fact.rs | 75 +++++++++---- meld-core/src/lib.rs | 199 ++++++++++++++++++++++++++++++++++ meld-core/src/merger.rs | 18 ++- 3 files changed, 269 insertions(+), 23 deletions(-) diff --git a/meld-core/src/adapter/fact.rs b/meld-core/src/adapter/fact.rs index e419545..a17cc81 100644 --- a/meld-core/src/adapter/fact.rs +++ b/meld-core/src/adapter/fact.rs @@ -3263,6 +3263,15 @@ impl FactStyleGenerator { // Find callee's memory index for the event buffer scratch space let callee_memory = crate::merger::component_memory_index(merged, site.to_component); + // Determine the [async-lift] entry's param count from its type. + // The caller may have extra params (e.g., retptr for multi-value results) + // that shouldn't be passed to the callee. + let callee_param_count = merged + .defined_func(async_lift_func) + .and_then(|f| merged.types.get(f.type_idx as usize)) + .map(|t| t.params.len()) + .unwrap_or(caller_param_count); + // Build the adapter function body // // Locals layout: @@ -3282,8 +3291,9 @@ impl FactStyleGenerator { let mut body = Function::new([(6, wasm_encoder::ValType::I32)]); - // Step 1: Call [async-lift] entry with caller's params - for i in 0..caller_param_count { + // Step 1: Call [async-lift] entry with callee's params + // (skip retptr if caller has more params than callee) + for i in 0..callee_param_count { body.instruction(&Instruction::LocalGet(i as u32)); } body.instruction(&Instruction::Call(async_lift_func)); @@ -3392,26 +3402,47 @@ impl FactStyleGenerator { body.instruction(&Instruction::End); // end loop body.instruction(&Instruction::End); // end block - // Step 3: After EXIT, return result to caller. - // For now, return default values. The task-get-result host intrinsic - // will be added when we have the host API finalized. - // TODO: call [task-get-result]N to retrieve stored result - for result_ty in &caller_type.results { - match result_ty { - wasm_encoder::ValType::I32 => { - body.instruction(&Instruction::I32Const(0)); - } - wasm_encoder::ValType::I64 => { - body.instruction(&Instruction::I64Const(0)); - } - wasm_encoder::ValType::F32 => { - body.instruction(&Instruction::F32Const(0.0_f32.into())); - } - wasm_encoder::ValType::F64 => { - body.instruction(&Instruction::F64Const(0.0_f64.into())); - } - _ => { - body.instruction(&Instruction::I32Const(0)); + // Step 3: After EXIT, read result values from shim globals. + // + // The task.return shim (generated in step 2.5) stored the result + // values to globals when the callee called task.return during the + // callback loop. Read them back and return to the caller. + // + // Find the matching shim by looking for task_return_shims entries + // belonging to the callee component. + let shim_info = merged.task_return_shims.values().find(|info| { + // Match shim whose result types align with caller's expected results + info.result_globals.len() == caller_type.results.len() + && info + .result_globals + .iter() + .zip(caller_type.results.iter()) + .all(|((_, gt), ct)| gt == ct) + }); + + if let Some(info) = shim_info { + for (global_idx, _) in &info.result_globals { + body.instruction(&Instruction::GlobalGet(*global_idx)); + } + } else { + // Fallback: return default values if no matching shim found + for result_ty in &caller_type.results { + match result_ty { + wasm_encoder::ValType::I32 => { + body.instruction(&Instruction::I32Const(0)); + } + wasm_encoder::ValType::I64 => { + body.instruction(&Instruction::I64Const(0)); + } + wasm_encoder::ValType::F32 => { + body.instruction(&Instruction::F32Const(0.0_f32.into())); + } + wasm_encoder::ValType::F64 => { + body.instruction(&Instruction::F64Const(0.0_f64.into())); + } + _ => { + body.instruction(&Instruction::I32Const(0)); + } } } } diff --git a/meld-core/src/lib.rs b/meld-core/src/lib.rs index 760af62..0b384da 100644 --- a/meld-core/src/lib.rs +++ b/meld-core/src/lib.rs @@ -279,6 +279,15 @@ impl Fuser { stats.total_functions = merged.functions.len(); stats.total_exports = merged.exports.len(); + // Step 2.5: Generate task.return shims for internal fused async calls. + // + // For each [task-return]N import used by an internal async callee, + // generate a shim that stores result values to globals. The + // callback-driving adapter (generated next) reads these globals + // after EXIT. Must run BEFORE adapter generation so shim info + // is available to the async adapter. + self.generate_task_return_shims(&mut merged, &graph)?; + // Step 3: Generate adapters log::info!("Generating adapters"); let adapter_config = AdapterConfig { @@ -572,6 +581,196 @@ impl Fuser { Ok(()) } + /// Generate task.return shim functions for internal fused async calls. + /// + /// For each [task-return]N import used by an internal async callee, + /// generates a shim function that stores result params to globals. + /// The callback-driving adapter reads these globals after EXIT. + fn generate_task_return_shims( + &self, + merged: &mut merger::MergedModule, + graph: &resolver::DependencyGraph, + ) -> Result<()> { + use std::collections::HashSet; + + // Collect component indices that have internal async adapter sites + let async_callee_components: HashSet = graph + .adapter_sites + .iter() + .filter(|site| site.is_async_lift) + .map(|site| site.to_component) + .collect(); + + if async_callee_components.is_empty() { + return Ok(()); + } + + // Find task.return imports belonging to async callee components + // and generate shims for them. + let mut affected_modules: HashSet<(usize, usize)> = HashSet::new(); + + for (import_idx, imp) in merged.imports.iter().enumerate() { + if !imp.name.starts_with("[task-return]") { + continue; + } + // Check if this import belongs to an internal async callee + let comp_idx = match imp.component_idx { + Some(idx) if async_callee_components.contains(&idx) => idx, + _ => continue, + }; + + // Get the import's function type to know the param signature. + let import_type = match &imp.entity_type { + wasm_encoder::EntityType::Function(type_idx) => { + merged.types.get(*type_idx as usize).cloned() + } + _ => continue, + }; + let import_type = match import_type { + Some(t) => t, + None => continue, + }; + + // Generate globals for each param (the result values) + let mut result_globals = Vec::new(); + for param_ty in &import_type.params { + let global_idx = merged.import_counts.global + merged.globals.len() as u32; + merged.globals.push(merger::MergedGlobal { + ty: wasm_encoder::GlobalType { + val_type: *param_ty, + mutable: true, + shared: false, + }, + init_expr: match param_ty { + wasm_encoder::ValType::I32 => wasm_encoder::ConstExpr::i32_const(0), + wasm_encoder::ValType::I64 => wasm_encoder::ConstExpr::i64_const(0), + wasm_encoder::ValType::F32 => { + wasm_encoder::ConstExpr::f32_const(0.0_f32.into()) + } + wasm_encoder::ValType::F64 => { + wasm_encoder::ConstExpr::f64_const(0.0_f64.into()) + } + _ => wasm_encoder::ConstExpr::i32_const(0), + }, + }); + result_globals.push((global_idx, *param_ty)); + } + + // Generate shim function: stores each param to its global + let shim_func_idx = merged.import_counts.func + merged.functions.len() as u32; + let _type_idx = import_type.params.len(); // find or create type + let shim_type = merger::Merger::find_or_add_type( + &mut merged.types, + &import_type.params, + &[], // void return + ); + + let mut body = wasm_encoder::Function::new([]); + for (i, (global_idx, _)) in result_globals.iter().enumerate() { + body.instruction(&wasm_encoder::Instruction::LocalGet(i as u32)); + body.instruction(&wasm_encoder::Instruction::GlobalSet(*global_idx)); + } + body.instruction(&wasm_encoder::Instruction::End); + + merged.functions.push(merger::MergedFunction { + type_idx: shim_type, + body, + origin: (comp_idx, 0, u32::MAX), + }); + + // Remap the task.return import to the shim in function_index_map + // for all modules of this component + let component = &self.components[comp_idx]; + for (mod_idx, module) in component.core_modules.iter().enumerate() { + let mut func_idx = 0u32; + for module_imp in &module.imports { + if !matches!(module_imp.kind, parser::ImportKind::Function(_)) { + continue; + } + if module_imp.name == imp.name + || (module_imp.name.starts_with("[task-return]") + && merged + .imports + .get( + *merged + .function_index_map + .get(&(comp_idx, mod_idx, func_idx)) + .unwrap_or(&u32::MAX) + as usize, + ) + .is_some_and(|m| m.name == imp.name)) + { + merged + .function_index_map + .insert((comp_idx, mod_idx, func_idx), shim_func_idx); + affected_modules.insert((comp_idx, mod_idx)); + } + func_idx += 1; + } + } + + // Store shim info for the adapter to use + merged.task_return_shims.insert( + import_idx as u32, + merger::TaskReturnShimInfo { + shim_func: shim_func_idx, + result_globals, + }, + ); + + log::info!( + "task.return shim: import {} '{}' → shim func {} with {} globals", + import_idx, + imp.name, + shim_func_idx, + merged.task_return_shims[&(import_idx as u32)] + .result_globals + .len(), + ); + } + + // Re-rewrite function bodies for affected modules + for &(comp_idx, mod_idx) in &affected_modules { + let module = &self.components[comp_idx].core_modules[mod_idx]; + let index_maps = merger::build_index_maps_for_module( + comp_idx, + mod_idx, + module, + merged, + self.config.memory_strategy, + self.config.address_rebasing, + 0u64, + false, + None, + ); + let import_func_count = module + .imports + .iter() + .filter(|i| matches!(i.kind, parser::ImportKind::Function(_))) + .count() as u32; + + for (old_idx, &type_idx) in module.functions.iter().enumerate() { + let old_func_idx = import_func_count + old_idx as u32; + let param_count = module + .types + .get(type_idx as usize) + .map(|ty| ty.params.len() as u32) + .unwrap_or(0); + let body = + merger::extract_function_body(module, old_idx, param_count, &index_maps)?; + if let Some(mf) = merged + .functions + .iter_mut() + .find(|f| f.origin == (comp_idx, mod_idx, old_func_idx)) + { + mf.body = body; + } + } + } + + Ok(()) + } + /// Encode the merged module to binary fn encode_output( &self, diff --git a/meld-core/src/merger.rs b/meld-core/src/merger.rs index 5b59515..a1b2455 100644 --- a/meld-core/src/merger.rs +++ b/meld-core/src/merger.rs @@ -132,6 +132,20 @@ pub struct MergedModule { /// Per-component handle table info for re-exporters. pub handle_tables: HashMap, + + /// Task.return shim info: maps merged import index of [task-return]N + /// to the global indices where the shim stores result values. + /// Used by the callback-driving adapter to read results after EXIT. + pub task_return_shims: HashMap, +} + +/// Info about a generated task.return shim function. +#[derive(Debug, Clone)] +pub struct TaskReturnShimInfo { + /// Merged function index of the shim + pub shim_func: u32, + /// Global indices for each result value (in param order) + pub result_globals: Vec<(u32, ValType)>, } /// Per-component resource handle table allocated in a re-exporter's linear memory. @@ -594,7 +608,7 @@ impl Merger { /// Find an existing function type or add a new one, returning its index. #[allow(dead_code)] - fn find_or_add_type( + pub(crate) fn find_or_add_type( types: &mut Vec, params: &[ValType], results: &[ValType], @@ -661,6 +675,7 @@ impl Merger { resource_rep_by_component: HashMap::new(), resource_new_by_component: HashMap::new(), handle_tables: HashMap::new(), + task_return_shims: HashMap::new(), }; // Process components in topological order @@ -2863,6 +2878,7 @@ mod tests { resource_rep_by_component: HashMap::new(), resource_new_by_component: HashMap::new(), handle_tables: HashMap::new(), + task_return_shims: HashMap::new(), }; // Simulate multi-memory merging for module A (comp 0, mod 0) From 5868c298088d6b007346678f95fdc574fdb06f13 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Mon, 13 Apr 2026 16:05:23 -0500 Subject: [PATCH 4/6] feat: per-site shim matching + retptr support for async adapter Fix task.return shim matching: use original function name (extracted from callee component's core module imports) instead of type-based matching. This correctly routes each async function to its own shim globals. Add retptr convention support: when the caller uses the return-pointer convention (void return, extra param), write shim globals to the retptr address in caller memory instead of pushing to stack. All 4 P3 compute functions now produce correct results on stock wasmtime 41 (no fork, no special flags beyond component-model-async): prime 7 -> 7 is prime fibonacci 10 -> fibonacci(10) = 55 factorial 5 -> factorial(5) = 120 collatz 27 -> collatz(27) = 111 steps String-returning functions (analyze, search, etc.) still need cross-memory string copy in the result path. 73/73 P2 runtime tests pass. Co-Authored-By: Claude Opus 4.6 (1M context) --- meld-core/src/adapter/fact.rs | 87 ++++++++++++++++++++++++++++++----- meld-core/src/lib.rs | 45 +++++++++++++++++- meld-core/src/merger.rs | 7 +++ 3 files changed, 127 insertions(+), 12 deletions(-) diff --git a/meld-core/src/adapter/fact.rs b/meld-core/src/adapter/fact.rs index a17cc81..c671099 100644 --- a/meld-core/src/adapter/fact.rs +++ b/meld-core/src/adapter/fact.rs @@ -3410,19 +3410,84 @@ impl FactStyleGenerator { // // Find the matching shim by looking for task_return_shims entries // belonging to the callee component. - let shim_info = merged.task_return_shims.values().find(|info| { - // Match shim whose result types align with caller's expected results - info.result_globals.len() == caller_type.results.len() - && info - .result_globals - .iter() - .zip(caller_type.results.iter()) - .all(|((_, gt), ct)| gt == ct) - }); + // Match by function name: extract the func name from the + // async-lift export name (after the last '#') and find the + // shim with the matching original_func_name. + let adapter_func_name = site + .export_name + .rsplit_once('#') + .map(|(_, name)| name) + .unwrap_or(&site.export_name); + + let shim_info = merged + .task_return_shims + .values() + .find(|info| { + info.component_idx == site.to_component + && info.original_func_name == adapter_func_name + }) + .or_else(|| { + // Fallback: match by type signature if name matching fails + merged.task_return_shims.values().find(|info| { + info.component_idx == site.to_component + && info.result_globals.len() == caller_type.results.len() + && info + .result_globals + .iter() + .zip(caller_type.results.iter()) + .all(|((_, gt), ct)| gt == ct) + }) + }); + + // Detect retptr convention: caller has more params than callee + // and returns void — the last caller param is the result pointer. + let uses_retptr = caller_type.results.is_empty() && caller_param_count > callee_param_count; + let caller_memory = crate::merger::component_memory_index(merged, site.from_component); if let Some(info) = shim_info { - for (global_idx, _) in &info.result_globals { - body.instruction(&Instruction::GlobalGet(*global_idx)); + if uses_retptr { + // Write shim globals to the retptr address in caller memory + let retptr_local = (callee_param_count) as u32; // last caller param + let mut offset = 0u32; + for (global_idx, val_ty) in &info.result_globals { + body.instruction(&Instruction::LocalGet(retptr_local)); + body.instruction(&Instruction::GlobalGet(*global_idx)); + let mem_arg = wasm_encoder::MemArg { + offset: offset as u64, + align: match val_ty { + wasm_encoder::ValType::I64 | wasm_encoder::ValType::F64 => 3, + _ => 2, + }, + memory_index: caller_memory, + }; + match val_ty { + wasm_encoder::ValType::I32 => { + body.instruction(&Instruction::I32Store(mem_arg)); + offset += 4; + } + wasm_encoder::ValType::I64 => { + body.instruction(&Instruction::I64Store(mem_arg)); + offset += 8; + } + wasm_encoder::ValType::F32 => { + body.instruction(&Instruction::F32Store(mem_arg)); + offset += 4; + } + wasm_encoder::ValType::F64 => { + body.instruction(&Instruction::F64Store(mem_arg)); + offset += 8; + } + _ => { + body.instruction(&Instruction::I32Store(mem_arg)); + offset += 4; + } + } + } + } else { + // Push result values onto the stack + for (global_idx, _) in &info.result_globals { + body.instruction(&Instruction::GlobalGet(*global_idx)); + } } } else { // Fallback: return default values if no matching shim found diff --git a/meld-core/src/lib.rs b/meld-core/src/lib.rs index 0b384da..7a1fc23 100644 --- a/meld-core/src/lib.rs +++ b/meld-core/src/lib.rs @@ -591,7 +591,7 @@ impl Fuser { merged: &mut merger::MergedModule, graph: &resolver::DependencyGraph, ) -> Result<()> { - use std::collections::HashSet; + use std::collections::{HashMap, HashSet}; // Collect component indices that have internal async adapter sites let async_callee_components: HashSet = graph @@ -605,9 +605,40 @@ impl Fuser { return Ok(()); } + // Build mapping: fused import name → original function name. + // The original component's core module has imports like "[task-return]fibonacci". + // After fusion, these become "[task-return]2" (renumbered by core_func_idx). + // We need the original name to match with async adapter site export names. + // + // Strategy: for each async callee component, collect the task-return + // import names from the ORIGINAL core module (which have function names). + // Order matters — the Nth task-return import becomes [task-return]N in + // the fused module (via build_canon_import_names). + let mut task_return_original_names: HashMap<(usize, usize), String> = HashMap::new(); + for &comp_idx in &async_callee_components { + let component = &self.components[comp_idx]; + let mut tr_idx = 0usize; + for module in &component.core_modules { + for module_imp in &module.imports { + if matches!(module_imp.kind, parser::ImportKind::Function(_)) + && module_imp.name.starts_with("[task-return]") + { + let func_name = module_imp + .name + .strip_prefix("[task-return]") + .unwrap_or(&module_imp.name) + .to_string(); + task_return_original_names.insert((comp_idx, tr_idx), func_name); + tr_idx += 1; + } + } + } + } + // Find task.return imports belonging to async callee components // and generate shims for them. let mut affected_modules: HashSet<(usize, usize)> = HashSet::new(); + let mut tr_counter_per_comp: HashMap = HashMap::new(); for (import_idx, imp) in merged.imports.iter().enumerate() { if !imp.name.starts_with("[task-return]") { @@ -619,6 +650,15 @@ impl Fuser { _ => continue, }; + // Track the task-return index per component to recover the + // original function name from the mapping built above. + let tr_idx = tr_counter_per_comp.entry(comp_idx).or_insert(0); + let original_func_name = task_return_original_names + .get(&(comp_idx, *tr_idx)) + .cloned() + .unwrap_or_default(); + *tr_counter_per_comp.get_mut(&comp_idx).unwrap() += 1; + // Get the import's function type to know the param signature. let import_type = match &imp.entity_type { wasm_encoder::EntityType::Function(type_idx) => { @@ -715,6 +755,9 @@ impl Fuser { merger::TaskReturnShimInfo { shim_func: shim_func_idx, result_globals, + component_idx: comp_idx, + import_name: imp.name.clone(), + original_func_name: original_func_name.clone(), }, ); diff --git a/meld-core/src/merger.rs b/meld-core/src/merger.rs index a1b2455..7b1dbf4 100644 --- a/meld-core/src/merger.rs +++ b/meld-core/src/merger.rs @@ -146,6 +146,13 @@ pub struct TaskReturnShimInfo { pub shim_func: u32, /// Global indices for each result value (in param order) pub result_globals: Vec<(u32, ValType)>, + /// Source component index + pub component_idx: usize, + /// Fused import name (e.g., "[task-return]0") + pub import_name: String, + /// Original function name (e.g., "fibonacci") — extracted from the + /// original component's core module import before renumbering. + pub original_func_name: String, } /// Per-component resource handle table allocated in a re-exporter's linear memory. From f3b7c84d05aeaaf91dc0613f58460c859bba165b Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Mon, 13 Apr 2026 16:23:43 -0500 Subject: [PATCH 5/6] feat: string result cross-memory copy + remove dead AsyncLiftLower MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add cross-memory string copy for async adapter retptr results: when the result is a (ptr, len) pair referencing callee memory, the adapter allocates in caller memory via cabi_realloc and copies the data with memory.copy. Remove the dead AsyncLiftLower code path from component_wrap.rs. Async imports are now fully handled by the callback-driving adapter in core wasm — no canon lift/lower needed inside the component. 73/73 P2 runtime tests pass. P3 compute functions correct on stock wasmtime. Co-Authored-By: Claude Opus 4.6 (1M context) --- meld-core/src/adapter/fact.rs | 137 ++++++++++++----- meld-core/src/component_wrap.rs | 250 -------------------------------- meld-core/src/merger.rs | 2 +- 3 files changed, 105 insertions(+), 284 deletions(-) diff --git a/meld-core/src/adapter/fact.rs b/meld-core/src/adapter/fact.rs index c671099..59b9dd9 100644 --- a/meld-core/src/adapter/fact.rs +++ b/meld-core/src/adapter/fact.rs @@ -3289,7 +3289,8 @@ impl FactStyleGenerator { let l_p1 = l_packed + 4; let l_p2 = l_packed + 5; - let mut body = Function::new([(6, wasm_encoder::ValType::I32)]); + // 6 locals for callback loop + 3 for string copy (src_ptr, src_len, dst_ptr) + let mut body = Function::new([(9, wasm_encoder::ValType::I32)]); // Step 1: Call [async-lift] entry with callee's params // (skip retptr if caller has more params than callee) @@ -3444,42 +3445,112 @@ impl FactStyleGenerator { let uses_retptr = caller_type.results.is_empty() && caller_param_count > callee_param_count; let caller_memory = crate::merger::component_memory_index(merged, site.from_component); + // Find caller's cabi_realloc for cross-memory string copying + let caller_realloc = crate::merger::component_realloc_index(merged, site.from_component); + if let Some(info) = shim_info { if uses_retptr { - // Write shim globals to the retptr address in caller memory - let retptr_local = (callee_param_count) as u32; // last caller param - let mut offset = 0u32; - for (global_idx, val_ty) in &info.result_globals { - body.instruction(&Instruction::LocalGet(retptr_local)); - body.instruction(&Instruction::GlobalGet(*global_idx)); - let mem_arg = wasm_encoder::MemArg { - offset: offset as u64, - align: match val_ty { - wasm_encoder::ValType::I64 | wasm_encoder::ValType::F64 => 3, - _ => 2, - }, + // Retptr convention: write results to caller's return area. + // For (ptr, len) pairs that reference callee memory, copy + // the data to caller memory first. + let retptr_local = callee_param_count as u32; + + // Check if this is a pointer pair: exactly 2 i32 globals + // and memories differ (cross-memory copy needed). + let is_ptr_len_pair = info.result_globals.len() == 2 + && info + .result_globals + .iter() + .all(|(_, t)| *t == wasm_encoder::ValType::I32) + && callee_memory != caller_memory + && caller_realloc.is_some(); + + if is_ptr_len_pair { + let realloc_func = caller_realloc.unwrap(); + let (ptr_global, _) = info.result_globals[0]; + let (len_global, _) = info.result_globals[1]; + + // Allocate in caller memory: cabi_realloc(0, 0, 1, len) → new_ptr + // locals: l_packed+6 = src_ptr, l_packed+7 = src_len, l_packed+8 = dst_ptr + let l_src_ptr = l_p2 + 1; + let l_src_len = l_p2 + 2; + let l_dst_ptr = l_p2 + 3; + + // Read source ptr and len from shim globals + body.instruction(&Instruction::GlobalGet(ptr_global)); + body.instruction(&Instruction::LocalSet(l_src_ptr)); + body.instruction(&Instruction::GlobalGet(len_global)); + body.instruction(&Instruction::LocalSet(l_src_len)); + + // Allocate in caller memory + body.instruction(&Instruction::I32Const(0)); // old_ptr + body.instruction(&Instruction::I32Const(0)); // old_size + body.instruction(&Instruction::I32Const(1)); // align + body.instruction(&Instruction::LocalGet(l_src_len)); // new_size + body.instruction(&Instruction::Call(realloc_func)); + body.instruction(&Instruction::LocalSet(l_dst_ptr)); + + // Copy from callee memory to caller memory + body.instruction(&Instruction::LocalGet(l_dst_ptr)); // dst + body.instruction(&Instruction::LocalGet(l_src_ptr)); // src + body.instruction(&Instruction::LocalGet(l_src_len)); // len + body.instruction(&Instruction::MemoryCopy { + dst_mem: caller_memory, + src_mem: callee_memory, + }); + + // Write (new_ptr, len) to retptr + let mem_arg_0 = wasm_encoder::MemArg { + offset: 0, + align: 2, memory_index: caller_memory, }; - match val_ty { - wasm_encoder::ValType::I32 => { - body.instruction(&Instruction::I32Store(mem_arg)); - offset += 4; - } - wasm_encoder::ValType::I64 => { - body.instruction(&Instruction::I64Store(mem_arg)); - offset += 8; - } - wasm_encoder::ValType::F32 => { - body.instruction(&Instruction::F32Store(mem_arg)); - offset += 4; - } - wasm_encoder::ValType::F64 => { - body.instruction(&Instruction::F64Store(mem_arg)); - offset += 8; - } - _ => { - body.instruction(&Instruction::I32Store(mem_arg)); - offset += 4; + let mem_arg_4 = wasm_encoder::MemArg { + offset: 4, + align: 2, + memory_index: caller_memory, + }; + body.instruction(&Instruction::LocalGet(retptr_local)); + body.instruction(&Instruction::LocalGet(l_dst_ptr)); + body.instruction(&Instruction::I32Store(mem_arg_0)); + body.instruction(&Instruction::LocalGet(retptr_local)); + body.instruction(&Instruction::LocalGet(l_src_len)); + body.instruction(&Instruction::I32Store(mem_arg_4)); + } else { + // Non-pointer results: write globals directly to retptr + let mut offset = 0u32; + for (global_idx, val_ty) in &info.result_globals { + body.instruction(&Instruction::LocalGet(retptr_local)); + body.instruction(&Instruction::GlobalGet(*global_idx)); + let mem_arg = wasm_encoder::MemArg { + offset: offset as u64, + align: match val_ty { + wasm_encoder::ValType::I64 | wasm_encoder::ValType::F64 => 3, + _ => 2, + }, + memory_index: caller_memory, + }; + match val_ty { + wasm_encoder::ValType::I32 => { + body.instruction(&Instruction::I32Store(mem_arg)); + offset += 4; + } + wasm_encoder::ValType::I64 => { + body.instruction(&Instruction::I64Store(mem_arg)); + offset += 8; + } + wasm_encoder::ValType::F32 => { + body.instruction(&Instruction::F32Store(mem_arg)); + offset += 4; + } + wasm_encoder::ValType::F64 => { + body.instruction(&Instruction::F64Store(mem_arg)); + offset += 8; + } + _ => { + body.instruction(&Instruction::I32Store(mem_arg)); + offset += 4; + } } } } diff --git a/meld-core/src/component_wrap.rs b/meld-core/src/component_wrap.rs index b1a6a2e..00d1076 100644 --- a/meld-core/src/component_wrap.rs +++ b/meld-core/src/component_wrap.rs @@ -168,25 +168,6 @@ enum ImportResolution { /// The specific P3 canonical operation to emit. op: P3BuiltinOp, }, - /// Import resolves to an internal P3 async cross-component call. - /// - /// The fused module exports `[async-lift]#` and - /// `[callback][async-lift]#`. The wrapper creates - /// `canon lift ... async (callback ...)` → `canon lower` to provide - /// a synchronous import to the fused core module. - AsyncLiftLower { - /// The async-lift export name in the fused module - async_lift_export: String, - /// The callback export name in the fused module - callback_export: String, - /// Index of the source component that exports this async function - /// (used to look up the correct component-level type) - source_comp_idx: usize, - /// The interface name (e.g., "compute:concurrent/tasks@1.0.0") - interface_name: String, - /// The function name (e.g., "collatz-steps") - func_name: String, - }, } /// A canonical resource operation. @@ -915,38 +896,6 @@ fn assemble_component( continue; } - // Category P3-async: internal async cross-component call. - // The fused module exports [async-lift]# for functions - // that were canon-lifted with `async` in the original component. - // We provide these via canon lift async + canon lower. - { - let async_lift_name = format!("[async-lift]{}#{}", module_name, field_name); - let callback_name = format!("[callback][async-lift]{}#{}", module_name, field_name); - let has_async_lift = fused_info - .exports - .iter() - .any(|(n, k, _)| *k == wasmparser::ExternalKind::Func && *n == async_lift_name); - if has_async_lift { - // Find which component exports this async function - let source_comp = all_components - .iter() - .position(|c| { - c.core_modules - .iter() - .any(|m| m.exports.iter().any(|e| e.name == async_lift_name)) - }) - .unwrap_or(0); - import_resolutions.push(ImportResolution::AsyncLiftLower { - async_lift_export: async_lift_name, - callback_export: callback_name, - source_comp_idx: source_comp, - interface_name: module_name.clone(), - func_name: field_name.to_string(), - }); - continue; - } - } - return Err(Error::EncodingError(format!( "cannot resolve fused import {}::{} to a component instance", module_name, field_name @@ -1218,86 +1167,6 @@ fn assemble_component( std::collections::HashMap::new(); // Pre-define component function types for async lift/lower imports. - // Match the correct Lift entry by comparing its flattened core type - // with the actual core import type from the fused module. - let mut async_func_types: std::collections::HashMap = - std::collections::HashMap::new(); - for (i, resolution) in import_resolutions.iter().enumerate() { - if let ImportResolution::AsyncLiftLower { - source_comp_idx, .. - } = resolution - { - let inner_comp = &all_components[*source_comp_idx]; - let import_type_idx = fused_info.func_imports[i].2; - let core_type = fused_info - .func_types - .get(import_type_idx as usize) - .cloned() - .unwrap_or_default(); - - // Search Lift entries for one whose flattened type matches - // the core import type (params and results). - let mut found = false; - for canon in &inner_comp.canonical_functions { - if let parser::CanonicalEntry::Lift { - type_index, - options, - .. - } = canon - { - if !options.async_ { - continue; - } - if let Some(type_def) = inner_comp.get_type_definition(*type_index) - && let parser::ComponentTypeKind::Function { params, results } = - &type_def.kind - { - // Flatten params - let flat_params: Vec = params - .iter() - .flat_map(|(_, cvt)| flat_component_val_type_resolved(cvt, inner_comp)) - .collect(); - // Flatten results - let flat_results: Vec = results - .iter() - .flat_map(|(_, cvt)| flat_component_val_type_resolved(cvt, inner_comp)) - .collect(); - // Check direct match or retptr convention: - // If flat_results > 1, canon lower uses a retptr param - // and returns nothing. - let direct_match = - flat_params == core_type.0 && flat_results == core_type.1; - let retptr_match = flat_results.len() > 1 && { - let mut expected_params = flat_params.clone(); - expected_params.push(wasm_encoder::ValType::I32); // retptr - expected_params == core_type.0 && core_type.1.is_empty() - }; - if direct_match || retptr_match { - let mut inner_remap = std::collections::HashMap::new(); - let wrapper_type = define_source_type_in_wrapper( - &mut component, - inner_comp, - *type_index, - &mut component_type_idx, - &mut inner_remap, - )?; - async_func_types.insert(i, wrapper_type); - found = true; - break; - } - } - } - } - if !found { - log::warn!( - "no matching async Lift found for import {}::{}", - fused_info.func_imports[i].0, - fused_info.func_imports[i].1, - ); - } - } - } - for (i, resolution) in import_resolutions.iter().enumerate() { match resolution { ImportResolution::Instance { @@ -1467,125 +1336,6 @@ fn assemble_component( } } - ImportResolution::AsyncLiftLower { - async_lift_export, - callback_export, - interface_name, - func_name, - .. - } => { - // Alias the [async-lift] core function from the fused instance - let mut alias_section = ComponentAliasSection::new(); - alias_section.alias(Alias::CoreInstanceExport { - instance: fused_instance, - kind: ExportKind::Func, - name: async_lift_export, - }); - component.section(&alias_section); - let async_lift_core_idx = core_func_idx; - core_func_idx += 1; - - // Alias the [callback] core function - let has_callback = fused_info - .exports - .iter() - .any(|(n, k, _)| *k == wasmparser::ExternalKind::Func && n == callback_export); - let callback_core_idx = if has_callback { - let mut alias_section = ComponentAliasSection::new(); - alias_section.alias(Alias::CoreInstanceExport { - instance: fused_instance, - kind: ExportKind::Func, - name: callback_export, - }); - component.section(&alias_section); - let idx = core_func_idx; - core_func_idx += 1; - Some(idx) - } else { - None - }; - - // Use the pre-defined component function type. - // If type matching failed, return an error. - let comp_func_type = *async_func_types.get(&i).ok_or_else(|| { - Error::EncodingError(format!( - "cannot find component type for async import {}::{}", - interface_name, func_name - )) - })?; - - // Alias realloc (needed for both lift and lower) - let realloc_name = "cabi_realloc"; - let has_realloc = fused_info - .exports - .iter() - .any(|(n, k, _)| *k == wasmparser::ExternalKind::Func && *n == realloc_name); - let realloc_core_idx = if has_realloc { - let mut alias_section = ComponentAliasSection::new(); - alias_section.alias(Alias::CoreInstanceExport { - instance: fused_instance, - kind: ExportKind::Func, - name: realloc_name, - }); - component.section(&alias_section); - let idx = core_func_idx; - core_func_idx += 1; - Some(idx) - } else { - None - }; - - // canon lift ... async (callback N) (realloc N) - let mut lift_options = vec![ - CanonicalOption::Memory(memory_core_indices[0]), - CanonicalOption::UTF8, - CanonicalOption::Async, - ]; - if let Some(cb_idx) = callback_core_idx { - lift_options.push(CanonicalOption::Callback(cb_idx)); - } - if let Some(realloc_idx) = realloc_core_idx { - lift_options.push(CanonicalOption::Realloc(realloc_idx)); - } - let mut canon = CanonicalFunctionSection::new(); - canon.lift(async_lift_core_idx, comp_func_type, lift_options); - component.section(&canon); - let lifted_func_idx = component_func_idx; - component_func_idx += 1; - - // canon lower (blocking synchronous call for the caller). - // Use the caller's memory for data passing. The caller's - // component index is encoded in the fused import's component - // origin. For now, find the memory from the import's position - // in the merged module — imports from the same component share - // a memory. Use minimal options (matching unfused pattern): - // only add memory+realloc+encoding when the function needs them. - let import_type_idx = fused_info.func_imports[i].2; - let core_type = fused_info - .func_types - .get(import_type_idx as usize) - .cloned() - .unwrap_or_default(); - // Needs memory if there are string/list params (>1 flat result or >2 flat params - // with pointer-like patterns) or if this is an async function with memory. - let needs_memory_on_lower = - core_type.0.len() > 2 || core_type.1.len() > 1 || core_type.1.is_empty(); - let mut lower_options: Vec = Vec::new(); - if needs_memory_on_lower { - lower_options.push(CanonicalOption::Memory(memory_core_indices[0])); - lower_options.push(CanonicalOption::UTF8); - if let Some(realloc_idx) = realloc_core_idx { - lower_options.push(CanonicalOption::Realloc(realloc_idx)); - } - } - let mut canon = CanonicalFunctionSection::new(); - canon.lower(lifted_func_idx, lower_options); - component.section(&canon); - - lowered_func_indices.push(core_func_idx); - core_func_idx += 1; - } - ImportResolution::TaskBuiltin { op } => { let mut canon = CanonicalFunctionSection::new(); match op { diff --git a/meld-core/src/merger.rs b/meld-core/src/merger.rs index 7b1dbf4..23f92ee 100644 --- a/meld-core/src/merger.rs +++ b/meld-core/src/merger.rs @@ -2515,7 +2515,7 @@ pub(crate) fn component_memory_index(merged: &MergedModule, comp_idx: usize) -> } /// Find the merged function index of a component's cabi_realloc. -fn component_realloc_index(merged: &MergedModule, comp_idx: usize) -> Option { +pub(crate) fn component_realloc_index(merged: &MergedModule, comp_idx: usize) -> Option { for (&(ci, _mi), &merged_idx) in &merged.realloc_map { if ci == comp_idx { return Some(merged_idx); From c0587568371b95da262f643f30e0613589958720 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Mon, 13 Apr 2026 18:00:17 -0500 Subject: [PATCH 6/6] fix: validate component output, import map kind field, quiet warnings 1. --validate now supports component output format (enables COMPONENT_MODEL, CM_ASYNC, MULTI_MEMORY features in validator) 2. --emit-import-map includes 'kind' field classifying each import as 'p3-builtin', 'resource', 'wasi', or 'function' 3. Downgrade UNMAPPED func import messages from warn to debug (cosmetic noise from indirect table stubs) Co-Authored-By: Claude Opus 4.6 (1M context) --- meld-cli/src/main.rs | 61 +++++++++++++++++++++++++---------------- meld-core/src/merger.rs | 2 +- 2 files changed, 38 insertions(+), 25 deletions(-) diff --git a/meld-cli/src/main.rs b/meld-cli/src/main.rs index 797d527..c727212 100644 --- a/meld-cli/src/main.rs +++ b/meld-cli/src/main.rs @@ -459,6 +459,7 @@ fn write_import_map(wasm_bytes: &[u8], path: &str) -> Result<()> { "index": func_index, "module": import.module, "name": import.name, + "kind": classify_import(import.module, import.name), })); func_index += 1; } @@ -473,33 +474,45 @@ fn write_import_map(wasm_bytes: &[u8], path: &str) -> Result<()> { Ok(()) } -/// Validate WASM bytes -fn validate_wasm(bytes: &[u8]) -> Result<()> { - use wasmparser::{Parser, Payload, Validator}; +/// Classify a fused module import by its module/name pattern. +fn classify_import(module: &str, name: &str) -> &'static str { + // Resource operations (can appear under any module) + if name.starts_with("[resource-drop]") + || name.starts_with("[resource-new]") + || name.starts_with("[resource-rep]") + { + return "resource"; + } + // P3 async builtins from $root or [export]$root + if (module == "$root" || module == "[export]$root") + && (name.starts_with("[task-return]") + || name.starts_with("[context-") + || name.starts_with("[waitable-") + || name.starts_with("[task-cancel]") + || name.starts_with("[backpressure-") + || name.starts_with("[subtask-")) + { + return "p3-builtin"; + } + // WASI imports + if module.starts_with("wasi:") { + return "wasi"; + } + "function" +} - let mut validator = Validator::new(); - let parser = Parser::new(0); +/// Validate WASM bytes (supports both core modules and components) +fn validate_wasm(bytes: &[u8]) -> Result<()> { + use wasmparser::Validator; - for payload in parser.parse_all(bytes) { - let payload = payload.context("Parse error during validation")?; - - // Validate each payload - match &payload { - Payload::Version { - num, - encoding, - range, - } => { - validator - .version(*num, *encoding, range) - .context("Invalid version")?; - } - _ => { - // Other payloads validated through the parser - } - } - } + let features = wasmparser::WasmFeatures::default() + | wasmparser::WasmFeatures::COMPONENT_MODEL + | wasmparser::WasmFeatures::MULTI_MEMORY + | wasmparser::WasmFeatures::CM_ASYNC + | wasmparser::WasmFeatures::CM_FIXED_LENGTH_LISTS; + let mut validator = Validator::new_with_features(features); + validator.validate_all(bytes).context("Validation failed")?; Ok(()) } diff --git a/meld-core/src/merger.rs b/meld-core/src/merger.rs index 23f92ee..ee9debb 100644 --- a/meld-core/src/merger.rs +++ b/meld-core/src/merger.rs @@ -1321,7 +1321,7 @@ impl Merger { ); e.insert(import_index); } else { - log::warn!( + log::debug!( "UNMAPPED func import: comp {} mod {} import {}::{}({})", comp_idx, mod_idx,