diff --git a/crates/js-component-bindgen/src/core.rs b/crates/js-component-bindgen/src/core.rs index 6847134b..b6dfc93d 100644 --- a/crates/js-component-bindgen/src/core.rs +++ b/crates/js-component-bindgen/src/core.rs @@ -54,16 +54,28 @@ use wasmtime_environ::{EntityIndex, MemoryIndex, ModuleTranslation, PrimaryMap}; fn unimplemented_try_table() -> wasm_encoder::Instruction<'static> { unimplemented!() } + pub enum Translation<'a> { Normal(ModuleTranslation<'a>), Augmented { original: ModuleTranslation<'a>, wasm: Vec, + name: Option, imports_removed: HashSet<(String, String)>, imports_added: Vec<(String, String, MemoryIndex, AugmentedOp)>, }, } +impl<'a> Translation<'a> { + pub(crate) fn name(&self) -> Option<&str> { + match self { + Translation::Normal(mt) => mt.module.name.as_deref(), + Translation::Augmented { name, .. } => name.as_deref(), + } + } +} + +#[derive(Debug)] pub enum AugmentedImport<'a> { CoreDef(&'a CoreDef), Memory { mem: &'a CoreDef, op: AugmentedOp }, @@ -132,6 +144,7 @@ impl<'a> Translation<'a> { let wasm = augmenter.run()?; Ok(Translation::Augmented { wasm, + name: translation.module.name.clone(), imports_removed: augmenter.imports_removed, imports_added: augmenter.imports_added, original: translation, @@ -534,6 +547,7 @@ macro_rules! define_visit { ($( @$proposal:ident $op:ident $({ $($arg:ident: $argty:ty),* })? => $visit:ident ($($ann:tt)*))*) => { $( #[allow(unreachable_code)] + #[allow(unused)] fn $visit(&mut self $( $( ,$arg: $argty)* )?) { define_visit!(augment self $op $($($arg)*)?); } @@ -675,6 +689,7 @@ macro_rules! define_translate { $( #[allow(unreachable_code)] #[allow(dropping_copy_types)] + #[allow(unused)] fn $visit(&mut self $(, $($arg: $argty),*)?) { #[allow(unused_imports)] use wasm_encoder::{ @@ -747,6 +762,7 @@ macro_rules! define_translate { // wasm-encoder and then create the new instruction. (translate $self:ident $op:ident $($arg:ident)*) => {{ $( + #[allow(unused)] let $arg = define_translate!(map $self $arg $arg); )* let insn = define_translate!(mk $op $($arg)*); diff --git a/crates/js-component-bindgen/src/esm_bindgen.rs b/crates/js-component-bindgen/src/esm_bindgen.rs index 278b20d0..915216f7 100644 --- a/crates/js-component-bindgen/src/esm_bindgen.rs +++ b/crates/js-component-bindgen/src/esm_bindgen.rs @@ -396,6 +396,9 @@ impl EsmBindgen { maybe_quote_member(specifier) ); for (external_name, local_name) in bound_external_names { + // For imports that are functions, ensure that they are noted as host provided + uwriteln!(output, "{local_name}._isHostProvided = true;"); + uwriteln!( output, r#" @@ -436,6 +439,8 @@ impl EsmBindgen { } else { uwriteln!(output, "{local_name} from '{specifier}';"); } + uwriteln!(output, "{local_name}._isHostProvided = true;"); + for other_local_name in &binding_local_names[1..] { uwriteln!(output, "const {other_local_name} = {local_name};"); } @@ -473,9 +478,13 @@ impl EsmBindgen { } uwriteln!(output, "}} = {iface_local_name};"); - // Ensure that the imports we destructured were defined - // (if they were not, the user is likely missing an import @ instantiation time) + // Process all external host-provided imports for (member_name, local_name) in generated_member_names { + // For imports that are functions, ensure that they are noted as host provided + uwriteln!(output, "{local_name}._isHostProvided = true;"); + + // Ensure that the imports we destructured were defined + // (if they were not, the user is likely missing an import @ instantiation time) uwriteln!( output, r#" diff --git a/crates/js-component-bindgen/src/function_bindgen.rs b/crates/js-component-bindgen/src/function_bindgen.rs index 898cc3e7..53d777ab 100644 --- a/crates/js-component-bindgen/src/function_bindgen.rs +++ b/crates/js-component-bindgen/src/function_bindgen.rs @@ -322,15 +322,19 @@ impl FunctionBindgen<'_> { } /// Start the current task + /// + /// The code generated by this function *may* also start a subtask + /// where appropriate. fn start_current_task(&mut self, instr: &Instruction) { let is_async = self.is_async; let fn_name = self.callee; let err_handling = self.err.to_js_string(); - let callback_fn_name = self + let callback_fn_js = self .canon_opts .callback .as_ref() - .map(|v| format!("callback_{}", v.as_u32())); + .map(|v| format!("callback_{}", v.as_u32())) + .unwrap_or_else(|| "null".into()); let prefix = match instr { Instruction::CallWasm { .. } => "_wasm_call_", Instruction::CallInterface { .. } => "_interface_call_", @@ -344,18 +348,16 @@ impl FunctionBindgen<'_> { uwriteln!( self.src, - " - const [_, {prefix}currentTaskID] = {start_current_task_fn}({{ - componentIdx: {component_instance_idx}, - isAsync: {is_async}, - entryFnName: '{fn_name}', - getCallbackFn: () => {callback_fn_name}, - callbackFnName: '{callback_fn_name}', - errHandling: '{err_handling}', - }}); - ", - // NOTE: callback functions are missing on async imports that are host defined - callback_fn_name = callback_fn_name.unwrap_or_else(|| "null".into()), + r#" + const [task, {prefix}currentTaskID] = {start_current_task_fn}({{ + componentIdx: {component_instance_idx}, + isAsync: {is_async}, + entryFnName: '{fn_name}', + getCallbackFn: () => {callback_fn_js}, + callbackFnName: '{callback_fn_js}', + errHandling: '{err_handling}', + }}); + "#, ); } @@ -1256,7 +1258,12 @@ impl Bindgen for FunctionBindgen<'_> { has_post_return = self.post_return.is_some(), ); + // Write out whether the caller was host provided + // (if we're calling into wasm then we know it was not) + uwriteln!(self.src, "const hostProvided = false;"); + // Inject machinery for starting a 'current' task + // (this will define the 'task' variable) self.start_current_task(inst); // TODO: trap if this component is already on the call stack (re-entrancy) @@ -1307,31 +1314,98 @@ impl Bindgen for FunctionBindgen<'_> { // Call to an interface, usually but not always an externally imported interface Instruction::CallInterface { func, async_ } => { - // let get_or_create_async_state_fn = self.intrinsic(Intrinsic::Component( - // ComponentIntrinsic::GetOrCreateAsyncState, - // )); - // let current_task_get_fn = - // self.intrinsic(Intrinsic::AsyncTask(AsyncTaskIntrinsic::GetCurrentTask)); - // let component_instance_idx = self.canon_opts.instance.as_u32(); - let debug_log_fn = self.intrinsic(Intrinsic::DebugLog); + let start_current_task_fn = + self.intrinsic(Intrinsic::AsyncTask(AsyncTaskIntrinsic::StartCurrentTask)); + let current_task_get_fn = + self.intrinsic(Intrinsic::AsyncTask(AsyncTaskIntrinsic::GetCurrentTask)); + uwriteln!( self.src, - "{debug_log_fn}('{prefix} [Instruction::CallInterface] (async? {async_}, @ enter)');", + "{debug_log_fn}('{prefix} [Instruction::CallInterface] ({async_}, @ enter)');", prefix = self.tracing_prefix, async_ = async_.then_some("async").unwrap_or("sync"), ); - // // Inject machinery for starting a 'current' task - // self.start_current_task( - // inst, - // *async_, - // &func.name, - // self.canon_opts - // .callback - // .as_ref() - // .map(|v| format!("callback_{}", v.as_u32())), - // ); + // Determine the callee function and arguments + let (fn_js, args_js) = if self.callee_resource_dynamic { + ( + format!("{}.{}", operands[0], self.callee), + operands[1..].join(", "), + ) + } else { + (self.callee.into(), operands.join(", ")) + }; + + // Write out whether the caller was host provided + uwriteln!(self.src, "const hostProvided = {fn_js}._isHostProvided;"); + + let component_instance_idx = self.canon_opts.instance.as_u32(); + + // Start the necessary subtasks and/or host task + // + // We must create a subtask in the case of an async host import. + // + // If there's no parent task, we're not executing in a subtask situation, + // so we can just create the new task and immediately continue execution. + // + // If there *is* a parent task, then we are likely about to create new task that + // matches/belongs to an existing subtask in the parent task. + // + // If we're dealing with a function that has been marked as a host import, then + // we expect that `Trampoline::LowerImport` and relevant intrinsics were called before + // this, and a subtask has been set up. + // + // TODO: we use getLatestSubtask(), but could ordering change? Not under threads (assuming same thread)? + uwriteln!( + self.src, + r#" + let parentTask; + let task; + let subtask; + + const createTask = () => {{ + const results = {start_current_task_fn}({{ + componentIdx: {component_instance_idx}, + isAsync: {is_async}, + entryFnName: '{fn_name}', + getCallbackFn: () => {callback_fn_js}, + callbackFnName: '{callback_fn_js}', + errHandling: '{err_handling}', + }}); + task = results[0]; + }}; + + taskCreation: {{ + parentTask = {current_task_get_fn}({component_instance_idx})?.task; + if (!parentTask) {{ + createTask(); + break taskCreation; + }} + + createTask(); + + const isHostAsyncImport = hostProvided && {is_async}; + if (isHostAsyncImport) {{ + subtask = parentTask.getLatestSubtask(); + if (!subtask) {{ + throw new Error("Missing subtask for host import call, has the import been lowered?"); + }} + subtask.setChildTask(task); + task.setParentSubtask(subtask); + }} + }} + "#, + is_async = self.is_async, + fn_name = self.callee, + err_handling = self.err.to_js_string(), + callback_fn_js = self + .canon_opts + .callback + .as_ref() + .map(|v| format!("callback_{}", v.as_u32())) + .unwrap_or_else(|| "null".into()), + ); let results_length = if func.result.is_none() { 0 } else { 1 }; let maybe_await = if self.requires_async_porcelain | async_ { @@ -1340,17 +1414,8 @@ impl Bindgen for FunctionBindgen<'_> { "" }; - // Build the call - let call = if self.callee_resource_dynamic { - format!( - "{maybe_await} {}.{}({})", - operands[0], - self.callee, - operands[1..].join(", ") - ) - } else { - format!("{maybe_await} {}({})", self.callee, operands.join(", ")) - }; + // Build the JS expression that calls the callee + let call = format!("{maybe_await} {fn_js}({args_js})",); match self.err { // If configured to do *no* error handling at all or throw @@ -1379,13 +1444,13 @@ impl Bindgen for FunctionBindgen<'_> { uwriteln!( self.src, r#" - let ret; - try {{ - ret = {{ tag: 'ok', val: {call} }}; - }} catch (e) {{ - ret = {{ tag: 'err', val: {err_payload}(e) }}; - }} - "#, + let ret; + try {{ + ret = {{ tag: 'ok', val: {call} }}; + }} catch (e) {{ + ret = {{ tag: 'err', val: {err_payload}(e) }}; + }} + "#, ); results.push("ret".to_string()); } @@ -1452,10 +1517,10 @@ impl Bindgen for FunctionBindgen<'_> { self.clear_resource_borrows = false; } - // // For non-async calls, the current task can end immediately - // if !async_ { - // self.end_current_task(); - // } + // For non-async calls, the current task can end immediately + if !async_ { + self.end_current_task(); + } } Instruction::Return { @@ -2162,16 +2227,21 @@ impl Bindgen for FunctionBindgen<'_> { // - '[task-return]some-func' // // At this point in code generation, the following things have already been set: + // - `parentTask`: A parent task, if one was executing before + // - `subtask`: A subtask, if the current task is a subtask of a parent task + // - `task`: the currently executing task // - `ret`: the original function return value, via (i.e. via `CallWasm`/`CallInterface`) + // - `hostProvided`: whether the original function was a host-provided (i.e. host provided import) // Instruction::AsyncTaskReturn { name, params } => { let debug_log_fn = self.intrinsic(Intrinsic::DebugLog); uwriteln!( self.src, - "{debug_log_fn}('{prefix} [Instruction::AsyncTaskReturn]', {{ + "{debug_log_fn}('{prefix} [Instruction::AsyncTaskReturn]', {{ funcName: '{name}', paramCount: {param_count}, - postReturn: {post_return_present} + postReturn: {post_return_present}, + hostProvided, }});", param_count = params.len(), post_return_present = self.post_return.is_some(), @@ -2201,8 +2271,7 @@ impl Bindgen for FunctionBindgen<'_> { let get_or_create_async_state_fn = self.intrinsic(Intrinsic::Component( ComponentIntrinsic::GetOrCreateAsyncState, )); - let current_task_get_fn = - self.intrinsic(Intrinsic::AsyncTask(AsyncTaskIntrinsic::GetCurrentTask)); + let component_instance_idx = self.canon_opts.instance.as_u32(); let is_async_js = self.requires_async_porcelain | self.is_async; @@ -2212,17 +2281,31 @@ impl Bindgen for FunctionBindgen<'_> { // // e.g. right now a correctly formatted record would trigger the code below, // and it should not. + // + // NOTE: if the import was host provided we *already* have the result via + // JSPI and simply calling the host provided JS function -- there is no need + // to drive the async loop as with an async import that came from a component. + // + // + // If a subtask is defined, then we're in the case of a lowered async import, + // which means that the first async call (to the callee fn) has occurred, + // and a subtask has been created, but has not been triggered as started. + // + // TODO: can we know that the latest subtask is the right one? Is it possible + // to have two subtasks prepped for this task on the same thread? + // uwriteln!( self.src, r#" - const componentState = {get_or_create_async_state_fn}({component_instance_idx}); - if (!componentState) {{ throw new Error('failed to lookup current component state'); }} + if (hostProvided) {{ return ret; }} - const taskMeta = {current_task_get_fn}({component_instance_idx}); - if (!taskMeta) {{ throw new Error('failed to find current task metadata'); }} + const currentSubtask = task.getLatestSubtask(); + if (currentSubtask) {{ + currentSubtask.onStart(); + }} - const task = taskMeta.task; - if (!task) {{ throw new Error('missing/invalid task in current task metadata'); }} + const componentState = {get_or_create_async_state_fn}({component_instance_idx}); + if (!componentState) {{ throw new Error('failed to lookup current component state'); }} new Promise(async (resolve, reject) => {{ try {{ diff --git a/crates/js-component-bindgen/src/intrinsics/component.rs b/crates/js-component-bindgen/src/intrinsics/component.rs index 524c4849..bf45d06d 100644 --- a/crates/js-component-bindgen/src/intrinsics/component.rs +++ b/crates/js-component-bindgen/src/intrinsics/component.rs @@ -52,18 +52,6 @@ pub enum ComponentIntrinsic { /// A class that encapsulates component-level async state ComponentAsyncStateClass, - /// Intrinsic used when components lower imports to be used - /// from other components or the host. - /// - /// # Component Intrinsic implementation function - /// - /// The function that implements this intrinsic has the following definition: - /// - /// ```ts - /// ``` - /// - LowerImport, - /// Intrinsic used to set all component async states to error. /// /// Practically, this stops all individual component event loops (`AsyncComponentState#tick()`) @@ -93,7 +81,6 @@ impl ComponentIntrinsic { Self::BackpressureInc => "backpressureInc", Self::BackpressureDec => "backpressureDec", Self::ComponentAsyncStateClass => "ComponentAsyncState", - Self::LowerImport => "_intrinsic_component_lowerImport", Self::ComponentStateSetAllError => "_ComponentStateSetAllError", } } @@ -167,14 +154,16 @@ impl ComponentIntrinsic { #errored = null; mayLeave = true; - waitableSets = new {rep_table_class}(); - waitables = new {rep_table_class}(); - subtasks = new {rep_table_class}(); + + waitableSets; + waitables; + subtasks; constructor(args) {{ this.#componentIdx = args.componentIdx; - const self = this; - + this.waitableSets = new {rep_table_class}({{ target: `component [${{this.#componentIdx}}] waitable sets` }}); + this.waitables = new {rep_table_class}({{ target: `component [${{this.#componentIdx}}] waitables` }}); + this.subtasks = new {rep_table_class}({{ target: `component [${{this.#componentIdx}}] subtasks` }}); }}; componentIdx() {{ return this.#componentIdx; }} @@ -315,6 +304,7 @@ impl ComponentIntrinsic { }} tick() {{ + {debug_log_fn}('[{class_name}#tick()]', {{ suspendedTaskIDs: this.#suspendedTaskIDs }}); let resumedTask = false; for (const taskID of this.#suspendedTaskIDs.filter(t => t !== null)) {{ const meta = this.#suspendedTasksByTaskID.get(taskID); @@ -354,22 +344,6 @@ impl ComponentIntrinsic { )); } - // NOTE: LowerImport is called but is *not used* as a function, - // instead having a chance to do some modification *before* the final - // creation of instantiated modules' exports - Self::LowerImport => { - let debug_log_fn = Intrinsic::DebugLog.name(); - let lower_import_fn = Self::LowerImport.name(); - output.push_str(&format!( - " - function {lower_import_fn}(args) {{ - {debug_log_fn}('[{lower_import_fn}()] args', args); - throw new Error('runtime LowerImport not implmented'); - }} - " - )); - } - Self::ComponentStateSetAllError => { let debug_log_fn = Intrinsic::DebugLog.name(); let async_state_map = Self::GlobalAsyncStateMap.name(); diff --git a/crates/js-component-bindgen/src/intrinsics/lower.rs b/crates/js-component-bindgen/src/intrinsics/lower.rs index 0d1f86a8..ef1f848a 100644 --- a/crates/js-component-bindgen/src/intrinsics/lower.rs +++ b/crates/js-component-bindgen/src/intrinsics/lower.rs @@ -450,15 +450,27 @@ impl LowerIntrinsic { Self::LowerFlatRecord => { let debug_log_fn = Intrinsic::DebugLog.name(); output.push_str(&format!(" - function _lowerFlatRecord(size, memory, vals, storagePtr, storageLen) {{ - {debug_log_fn}('[_lowerFlatVariant()] args', {{ size, memory, vals, storagePtr, storageLen }}); - const [start] = vals; - if (size > storageLen) {{ - throw new Error('not enough storage remaining for record flat lower'); + function _lowerFlatRecord(fieldMetas) {{ + return (size, memory, vals, storagePtr, storageLen) => {{ + const params = [...arguments].slice(5); + {debug_log_fn}('[_lowerFlatRecord()] args', {{ + size, + memory, + vals, + storagePtr, + storageLen, + params, + fieldMetas + }}); + + const [start] = vals; + if (size > storageLen) {{ + throw new Error('not enough storage remaining for record flat lower'); + }} + const data = new Uint8Array(memory.buffer, start, size); + new Uint8Array(memory.buffer, storagePtr, size).set(data); + return data.byteLength; }} - const data = new Uint8Array(memory.buffer, start, size); - new Uint8Array(memory.buffer, storagePtr, size).set(data); - return data.byteLength; }} ")); } @@ -466,15 +478,27 @@ impl LowerIntrinsic { Self::LowerFlatVariant => { let debug_log_fn = Intrinsic::DebugLog.name(); output.push_str(&format!(" - function _lowerFlatVariant(size, memory, vals, storagePtr, storageLen) {{ - {debug_log_fn}('[_lowerFlatVariant()] args', {{ size, memory, vals, storagePtr, storageLen }}); - let [start, totalSize] = vals; - if (size > storageLen) {{ - throw new Error('not enough storage remaining for variant flat lower'); + function _lowerFlatVariant(variantMetas, extra) {{ + return (size, memory, vals, storagePtr, storageLen) => {{ + const params = [...arguments].slice(5); + {debug_log_fn}('[_lowerFlatVariant()] args', {{ + size, + memory, + vals, + storagePtr, + storageLen, + params, + variantMetas, + extra, + }}); + let [start, totalSize] = vals; + if (size > storageLen) {{ + throw new Error('not enough storage remaining for variant flat lower'); + }} + const data = new Uint8Array(memory.buffer, start, totalSize); + new Uint8Array(memory.buffer, storagePtr, totalSize).set(data); + return data.byteLength; }} - const data = new Uint8Array(memory.buffer, start, totalSize); - new Uint8Array(memory.buffer, storagePtr, totalSize).set(data); - return data.byteLength; }} ")); } @@ -556,20 +580,23 @@ impl LowerIntrinsic { ")); } + // Results are just a special case of lowering variants Self::LowerFlatResult => { let debug_log_fn = Intrinsic::DebugLog.name(); - output.push_str(&format!(r#" - function _lowerFlatResult(size, memory, vals, storagePtr, storageLen) {{ - {debug_log_fn}('[_lowerFlatResult()] args', {{ size, memory, vals, storagePtr, storageLen }}); - let [start, totalSize] = vals; - if (totalSize !== storageLen) {{ - throw new Error("storage length [" + storageLen + "] does not match variant size [" + totalSize + "]"); - }} - const data = new Uint8Array(memory.buffer, start, totalSize); - new Uint8Array(memory.buffer, storagePtr, totalSize).set(data); - return data.byteLength; + let lower_variant_fn = Self::LowerFlatVariant.name(); + output.push_str(&format!( + r#" + function _lowerFlatResult(variantMetas) {{ + const invalidTag = variantMetas.find(t => t.tag !== 'ok' && t.tag !== 'error') + if (invalidTag) {{ throw new Error(`invalid variant tag [${{invalidTag}}] found for result`); }} + + return function _lowerFlatResultInner(ctx) {{ + {debug_log_fn}('[_lowerFlatResult()] args', {{ variantMetas }}); + {lower_variant_fn}(variantMetas, {{ forResult: true }}); + }}; }} - "#)); + "# + )); } Self::LowerFlatOwn => { diff --git a/crates/js-component-bindgen/src/intrinsics/mod.rs b/crates/js-component-bindgen/src/intrinsics/mod.rs index 4174f77b..82020b5b 100644 --- a/crates/js-component-bindgen/src/intrinsics/mod.rs +++ b/crates/js-component-bindgen/src/intrinsics/mod.rs @@ -137,6 +137,9 @@ pub enum Intrinsic { /// Generally the only kind of type that can be borrowed is a resource /// handle, so this helper checks for that. IsBorrowedType, + + /// Async lower functions that are saved by component instance + GlobalComponentAsyncLowersClass, } /// Profile for determinism to be used by async implementation @@ -184,6 +187,8 @@ pub fn render_intrinsics(args: RenderIntrinsicsArgs) -> Source { // Intrinsics that should just always be present args.intrinsics.insert(Intrinsic::DebugLog); args.intrinsics.insert(Intrinsic::GlobalAsyncDeterminism); + args.intrinsics + .insert(Intrinsic::GlobalComponentAsyncLowersClass); args.intrinsics.insert(Intrinsic::CoinFlip); args.intrinsics.insert(Intrinsic::ConstantI32Min); args.intrinsics.insert(Intrinsic::ConstantI32Max); @@ -832,9 +837,14 @@ pub fn render_intrinsics(args: RenderIntrinsicsArgs) -> Source { output.push_str(&format!(" class {rep_table_class} {{ #data = [0, null]; + #target; + + constructor(args) {{ + if (args.target) {{ this.target = args.target }} + }} insert(val) {{ - {debug_log_fn}('[{rep_table_class}#insert()] args', {{ val }}); + {debug_log_fn}('[{rep_table_class}#insert()] args', {{ val, target: this.target }}); const freeIdx = this.#data[0]; if (freeIdx === 0) {{ this.#data.push(val); @@ -849,20 +859,20 @@ pub fn render_intrinsics(args: RenderIntrinsicsArgs) -> Source { }} get(rep) {{ - {debug_log_fn}('[{rep_table_class}#get()] args', {{ rep }}); + {debug_log_fn}('[{rep_table_class}#get()] args', {{ rep, target: this.target }}); const baseIdx = rep << 1; const val = this.#data[baseIdx]; return val; }} contains(rep) {{ - {debug_log_fn}('[{rep_table_class}#contains()] args', {{ rep }}); + {debug_log_fn}('[{rep_table_class}#contains()] args', {{ rep, target: this.target }}); const baseIdx = rep << 1; return !!this.#data[baseIdx]; }} remove(rep) {{ - {debug_log_fn}('[{rep_table_class}#remove()] args', {{ rep }}); + {debug_log_fn}('[{rep_table_class}#remove()] args', {{ rep, target: this.target }}); if (this.#data.length === 2) {{ throw new Error('invalid'); }} const baseIdx = rep << 1; @@ -876,12 +886,49 @@ pub fn render_intrinsics(args: RenderIntrinsicsArgs) -> Source { }} clear() {{ - {debug_log_fn}('[{rep_table_class}#clear()] args', {{ rep }}); + {debug_log_fn}('[{rep_table_class}#clear()] args', {{ rep, target: this.target }}); this.#data = [0, null]; }} }} ")); } + + Intrinsic::GlobalComponentAsyncLowersClass => { + let global_component_lowers_class = Intrinsic::GlobalComponentAsyncLowersClass.name(); + output.push_str(&format!( + r#" + class {global_component_lowers_class} {{ + static map = new Map(); + + constructor() {{ throw new Error('{global_component_lowers_class} should not be constructed'); }} + + static define(args) {{ + const {{ componentIdx, importName, fn }} = args; + let inner = {global_component_lowers_class}.map.get(componentIdx); + if (!{global_component_lowers_class}.map.has(componentIdx)) {{ + inner = new Map(); + {global_component_lowers_class}.map.set(componentIdx, inner); + }} + inner.set(importName, fn); + }} + + static lookup(componentIdx, importName) {{ + let inner = {global_component_lowers_class}.map.get(componentIdx); + if (!inner) {{ + return () => {{ throw new Error(`no such component [${{componentIdx}}]`); }}; + }} + + const found = inner.get(importName); + if (found) {{ return found; }} + + return () => {{ + throw new Error(`component [${{componentIdx}}] has no lower for [${{importName}}]`); + }}; + }} + }} + "# + )); + } } } @@ -992,6 +1039,7 @@ impl Intrinsic { Intrinsic::GlobalAsyncDeterminism => "ASYNC_DETERMINISM", Intrinsic::AwaitableClass => "Awaitable", Intrinsic::CoinFlip => "_coinFlip", + Intrinsic::GlobalComponentAsyncLowersClass => "GlobalComponentAsyncLowers", // Data structures Intrinsic::RepTableClass => "RepTable", diff --git a/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs b/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs index 8dda86f7..c9d3c9ad 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs @@ -229,6 +229,18 @@ pub enum AsyncTaskIntrinsic { /// function asyncDriverLoop(args: DriverLoopArgs): Promise; /// ``` DriverLoop, + + /// Intrinsic used when components lower imports to be used + /// from other components or the host. + /// + /// # Component Intrinsic implementation function + /// + /// The function that implements this intrinsic has the following definition: + /// + /// ```ts + /// ``` + /// + LowerImport, } impl AsyncTaskIntrinsic { @@ -258,6 +270,8 @@ impl AsyncTaskIntrinsic { "taskCancel", "taskReturn", "unpackCallbackResult", + "_driverLoop", + "_lowerImport", ] } @@ -282,6 +296,7 @@ impl AsyncTaskIntrinsic { Self::Yield => "asyncYield", Self::UnpackCallbackResult => "unpackCallbackResult", Self::DriverLoop => "_driverLoop", + Self::LowerImport => "_lowerImport", } } @@ -961,6 +976,7 @@ impl AsyncTaskIntrinsic { const cstate = {get_or_create_async_state_fn}(this.#componentIdx); + setTimeout(() => cstate.tick(), 0); const taskWait = await cstate.suspendTask({{ task: this, readyFn }}); const keepGoing = await taskWait; return keepGoing; @@ -1050,6 +1066,8 @@ impl AsyncTaskIntrinsic { return newSubtask; }} + getLatestSubtask() {{ return this.#subtasks.at(-1); }} + currentSubtask() {{ {debug_log_fn}('[{task_class}#currentSubtask()]'); if (this.#subtasks.length === 0) {{ return undefined; }} @@ -1113,7 +1131,9 @@ impl AsyncTaskIntrinsic { #componentRep = null; constructor(args) {{ - if (!args.componentIdx) {{ throw new Error('missing componentIdx for subtask creation'); }} + if (typeof args.componentIdx !== 'number') {{ + throw new Error('ivnalid componentIdx for subtask creation'); + }} this.#componentIdx = args.componentIdx; if (!args.parentTask) {{ throw new Error('missing parent task during subtask creation'); }} @@ -1149,6 +1169,13 @@ impl AsyncTaskIntrinsic { componentIdx() {{ return this.#componentIdx; }} + setChildTask(t) {{ + if (!t) {{ throw new Error('cannot set missing/invalid child task on subtask'); }} + if (this.#childTask) {{ throw new Error('child task is already set on subtask'); }} + this.#childTask = t; + }} + getChildTask(t) {{ return this.#childTask; }} + setCallbackFn(f, name) {{ if (!f) {{ return; }} if (this.#callbackFn) {{ throw new Error('callback fn can only be set once'); }} @@ -1172,8 +1199,13 @@ impl AsyncTaskIntrinsic { this.#onProgressFn = f; }} - onStart(f) {{ + onStart() {{ if (!this.#onProgressFn) {{ throw new Error('missing on progress function'); }} + {debug_log_fn}('[{subtask_class}#onStart()] args', {{ + componentIdx: this.#componentIdx, + taskID: this.#id, + parentTaskID: this.parentTaskID(), + }}); this.#onProgressFn(); this.#state = {subtask_class}.State.STARTED; }} @@ -1360,16 +1392,19 @@ impl AsyncTaskIntrinsic { let callbackCode; let waitableSetRep; let unpacked; + if (!({i32_typecheck}(callbackResult))) {{ throw new Error('invalid callback result [' + callbackResult + '], not a number'); }} - if (callbackResult < 0 || callbackResult > 3) {{ - throw new Error('invalid async return value, outside callback code range'); - }} + unpacked = {unpack_callback_result_fn}(callbackResult); callbackCode = unpacked[0]; waitableSetRep = unpacked[1]; + if (callbackCode < 0 || callbackCode > 3) {{ + throw new Error('invalid async return value, outside callback code range'); + }} + let eventCode; let index; let result; @@ -1410,9 +1445,6 @@ impl AsyncTaskIntrinsic { taskID: task.id(), waitableSetRep, }}); - if (eventCode === 1 && waitableSetRep === task.currentSubtask().getWaitableRep()) {{ - task.currentSubtask().doTheThing(); - }} asyncRes = await task.waitUntil({{ readyFn: () => true, waitableSetRep, @@ -1473,6 +1505,81 @@ impl AsyncTaskIntrinsic { "#, )); } + + Self::LowerImport => { + let debug_log_fn = Intrinsic::DebugLog.name(); + let lower_import_fn = Self::LowerImport.name(); + let current_task_get_fn = Self::GetCurrentTask.name(); + let get_or_create_async_state_fn = + Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState).name(); + let async_event_code_enum = Intrinsic::AsyncEventCodeEnum.name(); + + // EXAMPLE OUTPUT FOR LOWER IMPORT + // + // LOWER IMPORT { + // args: { + // functionIdx: 1, + // componentIdx: 0, + // isAsync: false, + // paramLiftFns: [ [Function: _liftFlatResultInner] ], + // resultLowerFns: [], + // getCallbackFn: [Function: getCallbackFn], + // getPostReturnFn: [Function: getPostReturnFn], + // isCancellable: false + // }, + // params: [ 1058192 ] + // } + + // TODO: param lift functions should NOT be used until after subtask start! + // + // TODO: lower the imports @ task start via the subtask + // + // TODO: params needs to be packaged into a ctx, and be run via the param lifting functions + // similar to the code in `Self::TaskReturn` impl + output.push_str(&format!( + r#" + function {lower_import_fn}(args) {{ + const params = [...arguments].slice(1); + {debug_log_fn}('[{lower_import_fn}()] args', {{ args, params }}); + const {{ functionIdx, componentIdx, isAsync, paramLiftFns, resultLowerFns, metadata }} = args; + + const parentTaskMeta = {current_task_get_fn}(componentIdx); + const parentTask = parentTaskMeta?.task; + if (!parentTask) {{ throw new Error('missing parent task during lower of import'); }} + + const cstate = {get_or_create_async_state_fn}(componentIdx); + + const subtask = parentTask.createSubtask({{ + componentIdx, + parentTask, + }}); + + const rep = cstate.subtasks.insert(subtask); + subtask.setRep(rep); + + subtask.setOnProgressFn(() => {{ + subtask.setPendingEventFn(() => {{ + if (subtask.resolved()) {{ subtask.deliverResolve(); }} + return {{ + code: {async_event_code_enum}.SUBTASK, + index: rep, + result: subtask.getStateNumber(), + }} + }}); + }}); + + // TODO: run driver loop?? + + const subtaskState = subtask.getStateNumber(); + if (subtaskState < 0 || subtaskState > 2**5) {{ + throw new Error('invalid subtask state, out of valid range'); + }} + + return Number(subtask.waitableRep()) << 4 | subtaskState; + }} + "# + )); + } } } } diff --git a/crates/js-component-bindgen/src/intrinsics/p3/host.rs b/crates/js-component-bindgen/src/intrinsics/p3/host.rs index 5cd4d791..eb0915fe 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/host.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/host.rs @@ -283,7 +283,7 @@ impl HostIntrinsic { const subtaskState = subtask.getStateNumber(); if (subtaskState < 0 || subtaskState > 2**5) {{ - throw new Error('invalid substack state, out of valid range'); + throw new Error('invalid subtask state, out of valid range'); }} const callerComponentState = {get_or_create_async_state_fn}(subtask.componentIdx()); diff --git a/crates/js-component-bindgen/src/intrinsics/p3/waitable.rs b/crates/js-component-bindgen/src/intrinsics/p3/waitable.rs index 79785b2b..2758f4ea 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/waitable.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/waitable.rs @@ -160,7 +160,7 @@ impl WaitableIntrinsic { }; // TODO: remove the public mutable members that are eagerly exposed for early impl - output.push_str(&format!(" + output.push_str(&format!(r#" class {waitable_set_class} {{ #componentInstanceID; #waitables = []; @@ -197,13 +197,18 @@ impl WaitableIntrinsic { }} hasPendingEvent() {{ - {debug_log_fn}('[{waitable_set_class}#hasPendingEvent()] args', {{ }}); + {debug_log_fn}('[{waitable_set_class}#hasPendingEvent()] args', {{ + componentIdx: this.#componentInstanceID + }}); + console.log("WAITABLES?", this.#waitables); const waitable = this.#waitables.find(w => w.hasPendingEvent()); return waitable !== undefined; }} getPendingEvent() {{ - {debug_log_fn}('[{waitable_set_class}#getPendingEvent()] args', {{ }}); + {debug_log_fn}('[{waitable_set_class}#getPendingEvent()] args', {{ + componentIdx: this.#componentInstanceID + }}); for (const waitable of this.#waitables) {{ if (!waitable.hasPendingEvent()) {{ continue; }} return waitable.getPendingEvent(); @@ -212,7 +217,9 @@ impl WaitableIntrinsic { }} async poll() {{ - {debug_log_fn}('[{waitable_set_class}#poll()] args', {{ }}); + {debug_log_fn}('[{waitable_set_class}#poll()] args', {{ + componentIdx: this.#componentInstanceID + }}); const state = {get_or_create_async_state_fn}(this.#componentInstanceID); @@ -229,7 +236,7 @@ impl WaitableIntrinsic { throw new Error('{waitable_set_class}#poll() not implemented'); }} }} - ")); + "#)); } Self::WaitableClass => { @@ -237,7 +244,7 @@ impl WaitableIntrinsic { let waitable_class = Self::WaitableClass.name(); let get_or_create_async_state_fn = Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState).name(); - output.push_str(&format!(" + output.push_str(&format!(r#" class {waitable_class} {{ #componentInstanceID; #pendingEventFn = null; @@ -250,10 +257,12 @@ impl WaitableIntrinsic { }} hasPendingEvent() {{ + console.log("pendinge vent?", this.#pendingEventFn); return this.#pendingEventFn !== null; }} setPendingEventFn(fn) {{ + console.log("SETTING PENDING EVENT", fn.toString()); this.#pendingEventFn = fn; }} @@ -294,7 +303,8 @@ impl WaitableIntrinsic { this.#waitableSet = waitableSet; }} }} - ")); + "# + )); } Self::WaitableSetNew => { diff --git a/crates/js-component-bindgen/src/intrinsics/string.rs b/crates/js-component-bindgen/src/intrinsics/string.rs index 01d94db6..d9f379a4 100644 --- a/crates/js-component-bindgen/src/intrinsics/string.rs +++ b/crates/js-component-bindgen/src/intrinsics/string.rs @@ -90,7 +90,7 @@ impl StringIntrinsic { let utf8EncodedLen = 0; function utf8Encode(s, realloc, memory) { if (typeof s !== 'string') \ - throw new TypeError('expected a string'); + throw new TypeError('expected a string, received [' + typeof s + ']'); if (s.length === 0) { utf8EncodedLen = 0; return 1; diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index fa33f4b1..70d291f7 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -2,6 +2,7 @@ use std::cell::RefCell; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::fmt::Write; use std::mem; +use std::ops::Index; use base64::Engine as _; use base64::engine::general_purpose; @@ -33,6 +34,7 @@ use crate::function_bindgen::{ }; use crate::intrinsics::component::ComponentIntrinsic; use crate::intrinsics::lift::LiftIntrinsic; +use crate::intrinsics::lower::LowerIntrinsic; use crate::intrinsics::p3::async_future::AsyncFutureIntrinsic; use crate::intrinsics::p3::async_stream::AsyncStreamIntrinsic; use crate::intrinsics::p3::async_task::AsyncTaskIntrinsic; @@ -697,6 +699,7 @@ impl<'a> Instantiator<'a, '_> { } fn instantiate(&mut self) { + // Handle all built in trampolines for (i, trampoline) in self.translation.trampolines.iter() { let Trampoline::LowerImport { index, @@ -732,26 +735,82 @@ impl<'a> Instantiator<'a, '_> { } } - // We push lower import initializers down to right before instantiate, so that the - // memory, realloc and postReturn functions are available to the import lowerings - // for optimized bindgen + // Process global initializers + // + // The order of initialization is unfortunately quite fragile. + // + // We take care in processing module instantiations because we must ensure that + // $wit-component.fixups must be instantiated directly after $wit-component.shim + // let mut lower_import_initializers = Vec::new(); + let mut found_shim = false; + let mut processed_fixup_init = false; + + // Attempt to find the the wit-component fixups + let fixup_init = self.component.initializers.iter().find(|init| { + if let GlobalInitializer::InstantiateModule(InstantiateModule::Static(idx, _)) = init + && let Some(translation) = self.modules.get(*idx) + && let Some("wit-component:fixups") = translation.name() + { + return true; + } else { + return false; + } + }); + + // Process first n lower import initializers until the first instantiate module initializer for init in self.component.initializers.iter() { match init { - GlobalInitializer::InstantiateModule(_) => { - for init in lower_import_initializers.drain(..) { - self.instantiation_global_initializer(init); + GlobalInitializer::InstantiateModule(m) => { + // Ensure lower import initializers are processed before the first module instantiation + for lower_import_init in lower_import_initializers.drain(..) { + self.instantiation_global_initializer(lower_import_init); + } + + // If we're dealing with the shim, then we *should make sure that + // the next module instantiation we process is the one for fixups + if let InstantiateModule::Static(idx, _) = m + && let Some(translation) = self.modules.get(*idx) + && let Some("wit-component:shim") = translation.name() + { + found_shim = true; } } + + // We push lower import initializers down to right before instantiate, so that the + // memory, realloc and postReturn functions are available to the import lowerings + // for optimized bindgen GlobalInitializer::LowerImport { .. } => { lower_import_initializers.push(init); continue; } _ => {} } - self.instantiation_global_initializer(init); + + match (found_shim, processed_fixup_init) { + // If we haven't found the shim, or we've found and processed it and the fixup, + // process initializations like normal + (false, false) | (true, true) => { + self.instantiation_global_initializer(init); + } + + // If we just found the shim, ensure to process the fixup immediately + (true, false) => { + self.instantiation_global_initializer(init); + + if found_shim && let Some(fixup_init) = fixup_init { + self.instantiation_global_initializer(fixup_init); + } + processed_fixup_init = true; + } + + // It's impossible to enter the case where we didn't find the shim but processed the fixup + // the fixup would be processed via normal means otherwise + (false, true) => unreachable!(), + } } + // Process lower import initializers that were discovered after the last module instantiation for init in lower_import_initializers.drain(..) { self.instantiation_global_initializer(init); } @@ -901,7 +960,7 @@ impl<'a> Instantiator<'a, '_> { .unwrap(); if let Some(dtor) = &resource_def.dtor { - (false, format!("\n{}(rep);", self.core_def(dtor))) + (false, format!("\n{}(rep);", self.core_def(dtor, false))) } else { (false, "".into()) } @@ -979,6 +1038,7 @@ impl<'a> Instantiator<'a, '_> { | Trampoline::ErrorContextDrop { .. } | Trampoline::ErrorContextNew { .. } | Trampoline::ErrorContextTransfer + | Trampoline::LowerImport { .. } | Trampoline::PrepareCall { .. } | Trampoline::ResourceDrop(_) | Trampoline::ResourceNew(_) @@ -1666,32 +1726,6 @@ impl<'a> Instantiator<'a, '_> { .map(|v| (v.as_u32().to_string(), format!("postReturn{}", v.as_u32()))) .unwrap_or_else(|| ("null".into(), "null".into())); - // TODO: need to find the callee[adapter0] for this - // it's known when we instantiateCore for a given component - // - // FOR EXAMPLE: - // - // ```js - // ({ exports: exports7 } = yield instantiateCore(yield module8, { - // async: { - // '[start-call]adapter0': trampoline45, - // }, - // callback: { - // f0: exports1['[callback][async-lift]local:local/sleep-post-return#[async]run'], - // }, - // callee: { - // adapter0: exports1['[async-lift]local:local/sleep-post-return#[async]run'], - // }, - // flags: { - // instance1: instanceFlags1, - // instance4: instanceFlags4, - // }, - // sync: { - // '[prepare-call]adapter0': trampoline46, - // }, - // })); - // ``` - uwriteln!( self.src.js, "const trampoline{i} = {async_start_call_fn}.bind( @@ -1707,16 +1741,21 @@ impl<'a> Instantiator<'a, '_> { ); } - // NOTE: lower import trampoline is called, and can generate a function, - // but that is *not currently used* by the generated code. - // - // The approach that probably works here is to WRAP the actual function (which is called `trampoline`) - // and do the relevant functionality that is inherent to canon_lower Trampoline::LowerImport { index, lower_ty, options, } => { + let lower_import_fn = self + .bindgen + .intrinsic(Intrinsic::AsyncTask(AsyncTaskIntrinsic::LowerImport)); + let global_component_lowers_class = self + .bindgen + .intrinsic(Intrinsic::GlobalComponentAsyncLowersClass); + + // TODO: Work to figure out the combination of the component(?) and the + // function index INTERNAL? + let canon_opts = self .component .options @@ -1725,51 +1764,67 @@ impl<'a> Instantiator<'a, '_> { let fn_idx = index.as_u32(); - let lower_import_fn = self - .bindgen - .intrinsic(Intrinsic::Component(ComponentIntrinsic::LowerImport)); + let component_idx = canon_opts.instance.as_u32(); + let is_async = canon_opts.async_; + let cancellable = canon_opts.cancellable; - let _ = (lower_ty, canon_opts); + let func_ty = self.types.index(*lower_ty); - // TODO: this trampoline (trampoline{i}) is is *already present*? - // current lower input globalizer code already handles it?? - // - // Maybe we need a special function name for this? OR does the other trampoline - // get called all the time as well? It can't be, because it *creates* the function - // that gets called? - // - // Maybe that is actually the lower that gets called all the time and should create the - // subtask! + // Build list of lift functions for the params of the lowered import + let param_types = &self.types.index(func_ty.params).types; + let param_lift_fns_js = gen_flat_lift_fn_list_js_expr( + self, + self.types, + param_types.iter().as_slice(), + canon_opts, + ); - // TODO: the original trampoline (trampoline{i}) MAY point to a function that is - // lowered for use inside another component. - // - // In the post-return test we know that #17 is the async sleep millis and it IS - // fed into an instantiated component. - // - // ``` - // const trampolineXX = WebAssembly.suspending(...) - // ``` + // Build list of lower functions for the results of the lowered import + let result_types = &self.types.index(func_ty.results).types; + let result_lower_fns_js = gen_flat_lower_fn_list_js_expr( + self, + self.types, + result_types.iter().as_slice(), + canon_opts, + ); - // TODO: prepare call & start call are called BEFORE the wasm call that IS a subtask starts. - // this is our only way to distinguish between a regular host call and a host call from inside - // a component. - // - // This means one of them has to create the subtask that the rust side is going to be looking for. + let get_callback_fn_js = canon_opts + .callback + .map(|idx| format!("() => callback_{}", idx.as_u32())) + .unwrap_or_else(|| "() => null".into()); + let get_post_return_fn_js = canon_opts + .post_return + .map(|idx| format!("() => postReturn{}", idx.as_u32())) + .unwrap_or_else(|| "() => null".into()); - // NOTE: this means that start_call is a guest->guest *only* thing previously prepared - // In our case the only valid thign is going to + // NOTE: we make this lowering trampoline identifiable by two things: + // - component idx + // - type index of exported function (in the relevant component) // - // The functionidx is useless it seems, trampoline idx *does* match though - + // This is required to be able to wire up the [async-lower] import + // that will be put on the glue/shim module (via which the host wires up trampolines). uwriteln!( self.src.js, - "const trampoline_lower_{i} = {lower_import_fn}.bind( - null, - {{ - functionIdx: {fn_idx}, - }}, - );", + r#" + {global_component_lowers_class}.define({{ + componentIdx: {component_idx}, + importName: lowered_import_{fn_idx}_metadata.importName, + fn: {lower_import_fn}.bind( + null, + {{ + trampolineIdx: {i}, + componentIdx: {component_idx}, + isAsync: {is_async}, + paramLiftFns: {param_lift_fns_js}, + metadata: lowered_import_{fn_idx}_metadata, + resultLowerFns: {result_lower_fns_js}, + getCallbackFn: {get_callback_fn_js}, + getPostReturnFn: {get_post_return_fn_js}, + isCancellable: {cancellable}, + }}, + ), + }}); + "#, ); } @@ -1901,7 +1956,7 @@ impl<'a> Instantiator<'a, '_> { format!( " {}(handleEntry.rep);", - self.core_def(dtor) + self.core_def(dtor, false) ) } else { "".into() @@ -2176,40 +2231,82 @@ impl<'a> Instantiator<'a, '_> { // into the component after a related suspension. GlobalInitializer::ExtractCallback(ExtractCallback { index, def }) => { let callback_idx = index.as_u32(); - let core_def = self.core_def(def); + let core_def = self.core_def(def, false); uwriteln!(self.src.js, "let callback_{callback_idx};",); uwriteln!(self.src.js_init, "callback_{callback_idx} = {core_def};"); } GlobalInitializer::InstantiateModule(m) => match m { - InstantiateModule::Static(idx, args) => self.instantiate_static_module(*idx, args), + InstantiateModule::Static(idx, args) => { + // TODO: we must be careful about the order in which we instantiate modules, + // because there are likely to be two modules that must be executed in quick succession: + // + // - $wit-component.shim + // - $wit-component.fixups + // + // The instantiation of $wit-component.fixups actually *resolves* indirect + // calls, and thus must be instantiated immediately after, given that other + // module instantiations may *depend* on $wit-component.shim exports. + // + // This is required because $wit-component.shim exports could point to missing + // functions in the table, but those exported functions act as *no-ops* -- + // any modules that try to use those imports will get useless functions, UNTIL + // $wit-component.fixups is instantiated. + + // match &self.modules[*idx] { + // core::Translation::Augmented { name, .. } => { + // eprintln!("DEALING WITH AUGMENTED MODULE [{name:?}]"); + // } + // core::Translation::Normal(mt) => { + // eprintln!("DEALING WITH RAW MODULE [{:?}]", mt.module.name); + // } + // } + self.instantiate_static_module(*idx, args); + } // This is only needed when instantiating an imported core wasm // module which while easy to implement here is not possible to // test at this time so it's left unimplemented. InstantiateModule::Import(..) => unimplemented!(), }, + GlobalInitializer::LowerImport { index, import } => { + let fn_idx = index.as_u32(); + let (import_index, _path) = &self.component.imports[*import]; + let (import_name, _type_def) = &self.component.import_types[*import_index]; + uwriteln!( + self.src.js, + r#" + let lowered_import_{fn_idx}_metadata = {{ + importName: '{import_name}', + }}; + "#, + ); self.lower_import(*index, *import); } + GlobalInitializer::ExtractMemory(m) => { - let def = self.core_export_var_name(&m.export); + let def = self.core_export_var_name(&m.export, true); let idx = m.index.as_u32(); uwriteln!(self.src.js, "let memory{idx};"); uwriteln!(self.src.js_init, "memory{idx} = {def};"); } + GlobalInitializer::ExtractRealloc(r) => { - let def = self.core_def(&r.def); + let def = self.core_def(&r.def, false); let idx = r.index.as_u32(); uwriteln!(self.src.js, "let realloc{idx};"); uwriteln!(self.src.js_init, "realloc{idx} = {def};",); } + GlobalInitializer::ExtractPostReturn(p) => { - let def = self.core_def(&p.def); + let def = self.core_def(&p.def, false); let idx = p.index.as_u32(); uwriteln!(self.src.js, "let postReturn{idx};"); uwriteln!(self.src.js_init, "postReturn{idx} = {def};"); } + GlobalInitializer::Resource(_) => {} + GlobalInitializer::ExtractTable(extract_table) => { let _ = extract_table; } @@ -2223,7 +2320,7 @@ impl<'a> Instantiator<'a, '_> { // differences between Wasmtime's and JS's embedding API. let mut import_obj = BTreeMap::new(); for (module, name, arg) in self.modules[idx].imports(args) { - let def = self.augmented_import_def(arg); + let def = self.augmented_import_def(&arg); let dst = import_obj.entry(module).or_insert(BTreeMap::new()); let prev = dst.insert(name, def); assert!( @@ -2232,6 +2329,8 @@ impl<'a> Instantiator<'a, '_> { ); assert!(prev.is_none()); } + + // Build list of imports let mut imports = String::new(); if !import_obj.is_empty() { imports.push_str(", {\n"); @@ -2266,7 +2365,7 @@ impl<'a> Instantiator<'a, '_> { self.src.js_init, "({{ exports: exports{iu32} }} = {instantiate}(module{}{imports}));", idx.as_u32(), - ) + ); } } } @@ -2347,7 +2446,6 @@ impl<'a> Instantiator<'a, '_> { } WorldItem::Type(_) => unreachable!("unexpected imported world item type"), }; - // eprintln!("\nGENERATED FUNCTION NAME FOR IMPORT: {func_name} (import name? {import_name})"); let is_async = is_async_fn(func, options); @@ -2358,7 +2456,7 @@ impl<'a> Instantiator<'a, '_> { ); } - // Host lifted async (i.e. JSPI) + // Host lifted async import (i.e. JSPI) let requires_async_porcelain = requires_async_porcelain( FunctionIdentifier::Fn(func), import_name, @@ -2859,7 +2957,7 @@ impl<'a> Instantiator<'a, '_> { .unwrap(); if let Some(dtor) = &resource_def.dtor { - dtor_str = Some(self.core_def(dtor)); + dtor_str = Some(self.core_def(dtor, false)); } } @@ -3347,11 +3445,11 @@ impl<'a> Instantiator<'a, '_> { self.src.js("}"); } - fn augmented_import_def(&self, def: core::AugmentedImport<'_>) -> String { + fn augmented_import_def(&self, def: &core::AugmentedImport<'_>) -> String { match def { - core::AugmentedImport::CoreDef(def) => self.core_def(def), + core::AugmentedImport::CoreDef(def) => self.core_def(def, true), core::AugmentedImport::Memory { mem, op } => { - let mem = self.core_def(mem); + let mem = self.core_def(mem, false); match op { core::AugmentedOp::I32Load => { format!( @@ -3441,9 +3539,9 @@ impl<'a> Instantiator<'a, '_> { } } - fn core_def(&self, def: &CoreDef) -> String { + fn core_def(&self, def: &CoreDef, delay_eval: bool) -> String { match def { - CoreDef::Export(e) => self.core_export_var_name(e), + CoreDef::Export(e) => self.core_export_var_name(e, delay_eval), CoreDef::Trampoline(i) => format!("trampoline{}", i.as_u32()), CoreDef::InstanceFlags(i) => { // SAFETY: short-lived borrow-mut. @@ -3453,13 +3551,20 @@ impl<'a> Instantiator<'a, '_> { } } - fn core_export_var_name(&self, export: &CoreExport) -> String + fn core_export_var_name(&self, export: &CoreExport, delay_eval: bool) -> String where T: Into + Copy, { let name = match &export.item { ExportItem::Index(idx) => { - let module = &self.modules[self.instances[export.instance]]; + let module_idx = self + .instances + .get(export.instance) + .expect("unexpectedly missing export instance"); + let module = &self + .modules + .get(*module_idx) + .expect("unexpectedly missing module by idx"); let idx = (*idx).into(); module .exports() @@ -3470,7 +3575,49 @@ impl<'a> Instantiator<'a, '_> { ExportItem::Name(s) => s, }; let i = export.instance.as_u32() as usize; - format!("exports{i}{}", maybe_quote_member(name)) + let quoted = maybe_quote_member(name); + + // If we're dealing with explicit export, or delaying eval was not set, + // we should return the variable as-is. + // + // An example of a export that would require this functionality is a memory + // (as opposed to a regular function export) + // + let is_explicit_export = quoted.starts_with('.'); + if is_explicit_export || !delay_eval { + return format!("exports{i}{quoted}"); + } + + // NOTE: Sometimes, non-explicit imports (e.g. `someImport: exports0['1']`) of a given module + // may depend on the exports via a table of another. In these cases, the generated + // code ends up doing th following: + // + // ``` + // const { exportsA } = WebAssembly.instantiate(moduleA); + // // ... other code ... + // const { exportsA } = WebAssembly.instantiate(moduleB, { + // $table: exportsA.$someTable, + // ... + // }); + // ``` + // + // The instantiation of `moduleB` may *fill out* `exportsA.$someTable` that is provided by moduleA, + // via an (elem) section, which means that instantiating moduleB fills out the callable functions + // in moduleA. + // + // In between instantiations of moduleA and moduleB (the 'other code' section), relying on + // any functions provided by exportsA can be dangerous. + // + // For example, if `exportsA['someFn']` is a function that does an indirect call via + // the table in moduleA (which may not be filled out!), and some code depends on it, + // they will receive a function that does *nothing* when executed, rather than the *eventual* + // function that will be filled out by the instantiation of moduleB later down in the code. + // + // TO avoid this, we only provide functions that curry evaluation to the export in question, + // with the assumption that calls will be necessarily performed after all instantiations. + // + // + format!("(...args) => {{ exports{i}{quoted}(...args); }}") } fn exports(&mut self, exports: &NameMap) { @@ -3675,7 +3822,7 @@ impl<'a> Instantiator<'a, '_> { }; // Start building early variable declarations - let core_export_fn = self.core_def(def); + let core_export_fn = self.core_def(def, false); let callee = match self .bindgen .local_names @@ -3930,6 +4077,25 @@ fn string_encoding_js_literal(val: &wasmtime_environ::component::StringEncoding) } } +/// Generate the javascript that corresponds to a list of lifting functions for a given list of types +pub fn gen_flat_lift_fn_list_js_expr( + intrinsic_mgr: &mut impl ManagesIntrinsics, + component_types: &ComponentTypes, + types: &[InterfaceType], + canon_opts: &CanonicalOptions, +) -> String { + let mut lift_fns: Vec = Vec::with_capacity(types.len()); + for ty in types.iter() { + lift_fns.push(gen_flat_lift_fn_js_expr( + intrinsic_mgr, + component_types, + ty, + canon_opts, + )); + } + format!("[{}]", lift_fns.join(",")) +} + /// Generate the javascript lifting function for a given type /// /// This function will a function object that can be executed with the right @@ -4162,3 +4328,273 @@ pub fn gen_flat_lift_fn_js_expr( } } } + +/// Generate the javascript that corresponds to a list of lowering functions for a given list of types +pub fn gen_flat_lower_fn_list_js_expr( + intrinsic_mgr: &mut impl ManagesIntrinsics, + component_types: &ComponentTypes, + types: &[InterfaceType], + canon_opts: &CanonicalOptions, +) -> String { + let mut lower_fns: Vec = Vec::with_capacity(types.len()); + for ty in types.iter() { + lower_fns.push(gen_flat_lower_fn_js_expr( + intrinsic_mgr, + component_types, + ty, + canon_opts, + )); + } + format!("[{}]", lower_fns.join(",")) +} + +/// Generate the javascript lowering function for a given type +/// +/// This function will a function object that can be executed with the right +/// context in order to perform the lower. For example, running this for bool +/// will produce the following: +/// +/// ``` +/// _lowerFlatBool +/// ``` +/// +/// This is becasue all it takes to lower a flat boolean is to run the _lowerFlatBool function intrinsic. +/// +/// The intrinsic it guaranteed to be in scope once execution time because it wlil be used in the relevant branch. +/// +pub fn gen_flat_lower_fn_js_expr( + intrinsic_mgr: &mut impl ManagesIntrinsics, + component_types: &ComponentTypes, + ty: &InterfaceType, + canon_opts: &CanonicalOptions, +) -> String { + //let ty_abi = component_types.canonical_abi(ty); + let string_encoding = canon_opts.string_encoding; + match ty { + InterfaceType::Bool => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatBool)); + Intrinsic::Lower(LowerIntrinsic::LowerFlatBool) + .name() + .into() + } + InterfaceType::S8 => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatS8)); + Intrinsic::Lower(LowerIntrinsic::LowerFlatS8).name().into() + } + InterfaceType::U8 => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatU8)); + Intrinsic::Lower(LowerIntrinsic::LowerFlatU8).name().into() + } + InterfaceType::S16 => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatS16)); + Intrinsic::Lower(LowerIntrinsic::LowerFlatS16).name().into() + } + InterfaceType::U16 => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatU16)); + Intrinsic::Lower(LowerIntrinsic::LowerFlatU16).name().into() + } + InterfaceType::S32 => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatS32)); + Intrinsic::Lower(LowerIntrinsic::LowerFlatS32).name().into() + } + InterfaceType::U32 => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatU32)); + Intrinsic::Lower(LowerIntrinsic::LowerFlatU32).name().into() + } + InterfaceType::S64 => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatS64)); + Intrinsic::Lower(LowerIntrinsic::LowerFlatS64).name().into() + } + InterfaceType::U64 => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatU64)); + Intrinsic::Lower(LowerIntrinsic::LowerFlatU64).name().into() + } + InterfaceType::Float32 => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatFloat32)); + Intrinsic::Lower(LowerIntrinsic::LowerFlatFloat32) + .name() + .into() + } + InterfaceType::Float64 => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatFloat64)); + Intrinsic::Lower(LowerIntrinsic::LowerFlatFloat64) + .name() + .into() + } + InterfaceType::Char => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatChar)); + Intrinsic::Lower(LowerIntrinsic::LowerFlatChar) + .name() + .into() + } + InterfaceType::String => match string_encoding { + wasmtime_environ::component::StringEncoding::Utf8 => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatStringUtf8)); + Intrinsic::Lower(LowerIntrinsic::LowerFlatStringUtf8) + .name() + .into() + } + wasmtime_environ::component::StringEncoding::Utf16 => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatStringUtf16)); + Intrinsic::Lower(LowerIntrinsic::LowerFlatStringUtf16) + .name() + .into() + } + wasmtime_environ::component::StringEncoding::CompactUtf16 => { + todo!("latin1+utf8 not supported") + } + }, + InterfaceType::Record(ty_idx) => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatRecord)); + let lower_fn = Intrinsic::Lower(LowerIntrinsic::LowerFlatRecord).name(); + let record_ty = &component_types[*ty_idx]; + let mut keys_and_lowers_expr = String::from("["); + for f in &record_ty.fields { + // For each field we build a list of [name, lowerFn, 32bit alignment] + // so that the record lowering function (which is a higher level function) + // can properly generate a function that lowers the fields. + keys_and_lowers_expr.push_str(&format!( + "{{ field: '{}', lowerFn: {}, align32: {} }},", + f.name, + gen_flat_lower_fn_js_expr(intrinsic_mgr, component_types, &f.ty, canon_opts), + component_types.canonical_abi(ty).align32, + )); + } + keys_and_lowers_expr.push(']'); + format!("{lower_fn}({keys_and_lowers_expr})") + } + InterfaceType::Variant(ty_idx) => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatVariant)); + let lower_fn = Intrinsic::Lower(LowerIntrinsic::LowerFlatVariant).name(); + let variant_ty = &component_types[*ty_idx]; + let mut cases_and_lowers_expr = String::from("["); + for (name, maybe_ty) in &variant_ty.cases { + cases_and_lowers_expr.push_str(&format!( + "{{ tag: '{}', lowerFn: {}, align32: {} }},", + name, + maybe_ty + .as_ref() + .map(|ty| gen_flat_lower_fn_js_expr( + intrinsic_mgr, + component_types, + ty, + canon_opts + )) + .unwrap_or(String::from("null")), + maybe_ty + .as_ref() + .map(|ty| component_types.canonical_abi(ty).align32) + .map(|n| n.to_string()) + .unwrap_or(String::from("null")), + )); + } + cases_and_lowers_expr.push(']'); + format!("{lower_fn}({cases_and_lowers_expr})") + } + InterfaceType::List(_ty_idx) => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatList)); + Intrinsic::Lower(LowerIntrinsic::LowerFlatList) + .name() + .into() + } + InterfaceType::Tuple(_ty_idx) => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatTuple)); + Intrinsic::Lower(LowerIntrinsic::LowerFlatTuple) + .name() + .into() + } + InterfaceType::Flags(_ty_idx) => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatFlags)); + Intrinsic::Lower(LowerIntrinsic::LowerFlatFlags) + .name() + .into() + } + InterfaceType::Enum(_ty_idx) => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatEnum)); + Intrinsic::Lower(LowerIntrinsic::LowerFlatEnum) + .name() + .into() + } + InterfaceType::Option(_ty_idx) => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatOption)); + Intrinsic::Lower(LowerIntrinsic::LowerFlatOption) + .name() + .into() + } + InterfaceType::Result(ty_idx) => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatResult)); + let lower_fn = Intrinsic::Lower(LowerIntrinsic::LowerFlatResult).name(); + let result_ty = &component_types[*ty_idx]; + let mut cases_and_lowers_expr = String::from("["); + cases_and_lowers_expr.push_str(&format!( + "{{ tag: '{}', lowerFn: {}, align32: {} }},", + "ok", + result_ty + .ok + .as_ref() + .map(|ty| gen_flat_lower_fn_js_expr( + intrinsic_mgr, + component_types, + ty, + canon_opts + )) + .unwrap_or(String::from("null")), + result_ty + .ok + .as_ref() + .map(|ty| component_types.canonical_abi(ty).align32) + .map(|n| n.to_string()) + .unwrap_or(String::from("null")), + )); + cases_and_lowers_expr.push_str(&format!( + "{{ tag: '{}', lowerFn: {}, align32: {} }},", + "error", + result_ty + .err + .as_ref() + .map(|ty| gen_flat_lower_fn_js_expr( + intrinsic_mgr, + component_types, + ty, + canon_opts + )) + .unwrap_or(String::from("null")), + result_ty + .err + .as_ref() + .map(|ty| component_types.canonical_abi(ty).align32) + .map(|n| n.to_string()) + .unwrap_or(String::from("null")), + )); + + cases_and_lowers_expr.push(']'); + format!("{lower_fn}({cases_and_lowers_expr})") + } + InterfaceType::Own(_ty_idx) => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatOwn)); + Intrinsic::Lower(LowerIntrinsic::LowerFlatOwn).name().into() + } + InterfaceType::Borrow(_ty_idx) => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatOwn)); + Intrinsic::Lower(LowerIntrinsic::LowerFlatOwn).name().into() + } + InterfaceType::Future(_ty_idx) => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatFuture)); + Intrinsic::Lower(LowerIntrinsic::LowerFlatFuture) + .name() + .into() + } + InterfaceType::Stream(_ty_idx) => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatStream)); + Intrinsic::Lower(LowerIntrinsic::LowerFlatStream) + .name() + .into() + } + InterfaceType::ErrorContext(_ty_idx) => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lower(LowerIntrinsic::LowerFlatErrorContext)); + Intrinsic::Lower(LowerIntrinsic::LowerFlatErrorContext) + .name() + .into() + } + } +} diff --git a/packages/jco/src/cmd/transpile.js b/packages/jco/src/cmd/transpile.js index f88eb971..ff1b7b0d 100644 --- a/packages/jco/src/cmd/transpile.js +++ b/packages/jco/src/cmd/transpile.js @@ -179,6 +179,11 @@ export async function transpileComponent(component, opts = {}) { 'async exports cannot be specified in sync mode (consider adding --async-mode=jspi)' ); } + if (asyncMode === 'sync' && asyncImports.size > 0) { + throw new Error( + 'async imports cannot be specified in sync mode (consider adding --async-mode=jspi)' + ); + } let asyncModeObj; if (asyncMode === 'sync') { asyncModeObj = null; diff --git a/packages/jco/test/fixtures/components/p3/async-simple-import.wasm b/packages/jco/test/fixtures/components/p3/async-simple-import.wasm new file mode 100644 index 00000000..90cdaf28 Binary files /dev/null and b/packages/jco/test/fixtures/components/p3/async-simple-import.wasm differ diff --git a/packages/jco/test/fixtures/components/p3/async-simple-return.wasm b/packages/jco/test/fixtures/components/p3/async-simple-return.wasm new file mode 100644 index 00000000..e1adcf11 Binary files /dev/null and b/packages/jco/test/fixtures/components/p3/async-simple-return.wasm differ diff --git a/packages/jco/test/fixtures/components/p3/async-simple-string-return.wasm b/packages/jco/test/fixtures/components/p3/async-simple-string-return.wasm deleted file mode 100644 index d313bf4b..00000000 Binary files a/packages/jco/test/fixtures/components/p3/async-simple-string-return.wasm and /dev/null differ diff --git a/packages/jco/test/helpers.js b/packages/jco/test/helpers.js index 51b9f2d1..6f9b18a9 100644 --- a/packages/jco/test/helpers.js +++ b/packages/jco/test/helpers.js @@ -115,6 +115,30 @@ export async function getTmpDir() { /** * Set up an async test to be run * + * Example: + * ``` + * const { instance, cleanup } = await setupAsyncTest({ + * component: { + * path: join( + * P3_COMPONENT_FIXTURES_DIR, + * 'async-simple-import.wasm', + * ), + * imports: { + * ...new WASIShim().getImportObject(), + * loadString: async () => "loaded", + * loadU32: async () => 43, + * }, + * }, + * jco: { + * transpile: { + * extraArgs: { + * minify: false, // for ease of debugging + * } + * } + * } + * }); + * ``` + * * @param {object} args - Arguments for running the async test * @param {function} args.testFn - Arguments for running the async test * @param {object} args.jco - JCO-related confguration for running the async test @@ -124,7 +148,7 @@ export async function getTmpDir() { * @param {object} args.component - configuration for an existing component that should be transpiled * @param {string} args.component.name - name of the component * @param {string} args.component.path - path to the WebAssembly binary for the existing component - * @param {object[]} args.component.import - imports that should be provided to the module at instantiation time + * @param {object} args.component.imports - imports that should be provided to the module at instantiation time * @param {object} args.component.build - configuration for building an ephemeral component to be tested * @param {object} args.component.js.source - Javascript source code for a component * @param {object} args.component.wit.source - WIT definitions (inlined) for a component @@ -215,7 +239,7 @@ export async function setupAsyncTest(args) { // Build a directory for the transpiled component output to be put in // (possibly inside the passed in outputDir) - const moduleOutputDir = join(outputDir, component.name); + const moduleOutputDir = join(outputDir, componentName); try { await stat(moduleOutputDir); } catch (err) { diff --git a/packages/jco/test/p3/async.js b/packages/jco/test/p3/async.js index 2b3b6588..1bf440a2 100644 --- a/packages/jco/test/p3/async.js +++ b/packages/jco/test/p3/async.js @@ -9,7 +9,7 @@ import { P3_COMPONENT_FIXTURES_DIR } from '../common.js'; suite('Async (WASI P3)', () => { // see: https://github.com/bytecodealliance/jco/issues/1076 - test('incorrect task return params offloading', async () => { + test.skip('incorrect task return params offloading', async () => { const name = 'async-flat-param-adder'; const { instance, cleanup } = await setupAsyncTest({ component: { @@ -32,23 +32,63 @@ suite('Async (WASI P3)', () => { }); // https://bytecodealliance.zulipchat.com/#narrow/channel/206238-general/topic/Should.20StringLift.20be.20emitted.20for.20async.20return.20values.3F/with/561133720 - test('simple string return', async () => { - const name = 'async-simple-string-return'; + test.skip('simple async returns', async () => { const { instance, cleanup } = await setupAsyncTest({ component: { - name, path: join( P3_COMPONENT_FIXTURES_DIR, - `${name}.wasm`, + 'async-simple-return.wasm', ), imports: new WASIShim().getImportObject(), }, }); - assert.typeOf(instance.asyncGetLiteral, 'function'); + assert.typeOf(instance.asyncGetString, 'function'); + assert.strictEqual("literal", await instance.asyncGetString()); + + assert.typeOf(instance.asyncGetU32, 'function'); + assert.strictEqual(42, await instance.asyncGetU32()); + + await cleanup(); + }); + + // https://github.com/bytecodealliance/jco/issues/1150 + test('simple async imports', async () => { + const { instance, cleanup } = await setupAsyncTest({ + component: { + path: join( + P3_COMPONENT_FIXTURES_DIR, + 'async-simple-import.wasm', + ), + imports: { + ...new WASIShim().getImportObject(), + '[async]load-string': { default: async () => "loaded" }, + '[async]load-u32': { default: async () => 43 }, + }, + }, + jco: { + transpile: { + extraArgs: { + minify: false, + asyncMode: 'jspi', + asyncImports: [ + 'load-string', + 'load-u32', + ], + asyncExports: [ + 'get-string', + 'get-u32', + ], + } + } + } + }); + + assert.typeOf(instance.asyncGetString, 'function'); + assert.strictEqual("loaded", await instance.asyncGetString()); - const result = await instance.asyncGetLiteral(); - assert.strictEqual(result, "literal"); + assert.typeOf(instance.asyncGetU32, 'function'); + assert.strictEqual(43, await instance.asyncGetU32()); await cleanup(); }); diff --git a/packages/jco/test/p3/ported/wasmtime/component-async/common.js b/packages/jco/test/p3/ported/wasmtime/component-async/common.js index 0e433472..e7310748 100644 --- a/packages/jco/test/p3/ported/wasmtime/component-async/common.js +++ b/packages/jco/test/p3/ported/wasmtime/component-async/common.js @@ -106,8 +106,6 @@ export async function composeCallerCallee(args) { calleePath, "--output", outputComponentPath, - // TODO: validation in wasm-tools compose should arguably have async turned on - // https://github.com/bytecodealliance/wasm-tools/pull/2354 "--skip-validation", ].join(" "); await exec(cmd);