diff --git a/Cargo.lock b/Cargo.lock index 7b0c2ca2..d6737e34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1034,8 +1034,6 @@ dependencies = [ [[package]] name = "wit-bindgen" version = "0.47.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a374235c3c0dff10537040b437073d09f1e38f13216b5f3cbc809c6226814e5c" dependencies = [ "wit-bindgen-rust-macro", ] @@ -1043,8 +1041,6 @@ dependencies = [ [[package]] name = "wit-bindgen-core" version = "0.47.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdf62e62178415a705bda25dc01c54ed65c0f956e4efd00ca89447a9a84f4881" dependencies = [ "anyhow", "heck 0.5.0", @@ -1054,8 +1050,6 @@ dependencies = [ [[package]] name = "wit-bindgen-rust" version = "0.47.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6d585319871ca18805056f69ddec7541770fc855820f9944029cb2b75ea108f" dependencies = [ "anyhow", "heck 0.5.0", @@ -1070,8 +1064,6 @@ dependencies = [ [[package]] name = "wit-bindgen-rust-macro" version = "0.47.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde589435d322e88b8f708f70e313f60dfb7975ac4e7c623fef6f1e5685d90e8" dependencies = [ "anyhow", "prettyplease", diff --git a/Cargo.toml b/Cargo.toml index a727dfec..fd3e140f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,8 +40,13 @@ wasmparser = { version = "0.240.0", default-features = false } wasmprinter = { version = "0.240.0", default-features = false } wasmtime-environ = { version = "37.0.1", features= [ "component-model", "compile" ] } wat = { version = "1.240.0", default-features = false } -wit-bindgen = { version = "0.47.0", default-features = false } -wit-bindgen-core = { version = "0.47.0", default-features = false } + +#wit-bindgen = { git = "https://github.com/bytecodealliance/wit-bindgen", rev = "799fdb6838b319f98b8d6472cb52f9301218791b", default-features = false } +#wit-bindgen-core = { git = "https://github.com/bytecodealliance/wit-bindgen", rev = "799fdb6838b319f98b8d6472cb52f9301218791b", default-features = false } + +wit-bindgen = { path = "../wit-bindgen/crates/guest-rust", default-features = false } +wit-bindgen-core = { path = "../wit-bindgen/crates/core", default-features = false } + wit-component = { version = "0.240.0", features = ["dummy-module"] } wit-parser = { version = "0.240.0", default-features = false } diff --git a/crates/js-component-bindgen/src/core.rs b/crates/js-component-bindgen/src/core.rs index 09667c47..f1849ab9 100644 --- a/crates/js-component-bindgen/src/core.rs +++ b/crates/js-component-bindgen/src/core.rs @@ -43,6 +43,7 @@ use wasm_encoder::{ CodeSection, EntityType, ExportKind, ExportSection, Function, FunctionSection, ImportSection, Module, TypeSection, }; +use wasmparser::types::TypeIdentifier as _; use wasmparser::{ Export, ExternalKind, FunctionBody, Import, Parser, Payload, TypeRef, Validator, VisitOperator, VisitSimdOperator, WasmFeatures, @@ -475,7 +476,55 @@ fn valtype(ty: wasmparser::ValType) -> wasm_encoder::ValType { wasmparser::ValType::F32 => wasm_encoder::ValType::F32, wasmparser::ValType::F64 => wasm_encoder::ValType::F64, wasmparser::ValType::V128 => wasm_encoder::ValType::V128, - wasmparser::ValType::Ref(_) => unimplemented!(), + wasmparser::ValType::Ref(t) => wasm_encoder::ValType::Ref(wasm_encoder::RefType { + nullable: t.is_nullable(), + heap_type: match t.heap_type() { + wasmparser::HeapType::Abstract { shared, ty } => wasm_encoder::HeapType::Abstract { + shared, + ty: match ty { + wasmparser::AbstractHeapType::Func => wasm_encoder::AbstractHeapType::Func, + wasmparser::AbstractHeapType::Extern => { + wasm_encoder::AbstractHeapType::Extern + } + wasmparser::AbstractHeapType::Any => wasm_encoder::AbstractHeapType::Any, + wasmparser::AbstractHeapType::None => wasm_encoder::AbstractHeapType::None, + wasmparser::AbstractHeapType::NoExtern => { + wasm_encoder::AbstractHeapType::NoExtern + } + wasmparser::AbstractHeapType::NoFunc => { + wasm_encoder::AbstractHeapType::NoFunc + } + wasmparser::AbstractHeapType::Eq => wasm_encoder::AbstractHeapType::Eq, + wasmparser::AbstractHeapType::Struct => { + wasm_encoder::AbstractHeapType::Struct + } + wasmparser::AbstractHeapType::Array => { + wasm_encoder::AbstractHeapType::Array + } + wasmparser::AbstractHeapType::I31 => wasm_encoder::AbstractHeapType::I31, + wasmparser::AbstractHeapType::Exn => wasm_encoder::AbstractHeapType::Exn, + wasmparser::AbstractHeapType::NoExn => { + wasm_encoder::AbstractHeapType::NoExn + } + wasmparser::AbstractHeapType::Cont => wasm_encoder::AbstractHeapType::Cont, + wasmparser::AbstractHeapType::NoCont => { + wasm_encoder::AbstractHeapType::NoCont + } + }, + }, + wasmparser::HeapType::Concrete(unpacked_idx) => match unpacked_idx { + wasmparser::UnpackedIndex::Module(idx) + | wasmparser::UnpackedIndex::RecGroup(idx) => { + wasm_encoder::HeapType::Concrete(idx) + } + wasmparser::UnpackedIndex::Id(core_type_id) => { + wasm_encoder::HeapType::Concrete( + u32::try_from(core_type_id.index()).unwrap(), + ) + } + }, + }, + }), } } @@ -873,3 +922,5 @@ impl Translator<'_, '_> { self.func.instruction(&Call(func)); } } + + diff --git a/crates/js-component-bindgen/src/esm_bindgen.rs b/crates/js-component-bindgen/src/esm_bindgen.rs index d3d6101d..927e0897 100644 --- a/crates/js-component-bindgen/src/esm_bindgen.rs +++ b/crates/js-component-bindgen/src/esm_bindgen.rs @@ -256,6 +256,21 @@ impl EsmBindgen { js_string.contains("\"") || js_string.contains("'") || js_string.contains("`") } + /// Render a block of imports + /// + /// This is normally right before the instantiation code in the generated output, e.g.: + /// + /// ``` + /// const { someFn } = imports['ns:pkg/iface']; + /// const { asyncOtherFn } = imports['ns:pkg/iface2']; + /// const { someFn, asyncSomeFn } = imports['ns:pkg/iface3']; + /// const { getDirectories } = imports['wasi:filesystem/preopens']; + /// const { Descriptor, filesystemErrorCode } = imports['wasi:filesystem/types']; + /// const { Error: Error$1 } = imports['wasi:io/error']; + /// const { InputStream, OutputStream } = imports['wasi:io/streams']; + /// let gen = (function* _initGenerator () { + /// ``` + /// pub fn render_imports( &mut self, output: &mut Source, @@ -263,7 +278,9 @@ impl EsmBindgen { local_names: &mut LocalNames, ) { let mut iface_imports = Vec::new(); + for (specifier, binding) in &self.imports { + // Build IDL binding if the specifier uses specifal WebIDL support let idl_binding = if specifier.starts_with("webidl:") { let iface_idx = specifier.find('/').unwrap() + 1; let iface_name = if let Some(version_idx) = specifier.find('@') { @@ -275,13 +292,19 @@ impl EsmBindgen { } else { None }; + if imports_object.is_some() || idl_binding.is_some() { uwrite!(output, "const "); } else { uwrite!(output, "import "); } + match binding { + // For interfaces we import the entire object as one ImportBinding::Interface(bindings) => { + // If we there is no import object and it's not an IDL binding and there's only + // *one* binding, then we can directly import rather than attempting to extract + // individual imports if imports_object.is_none() && idl_binding.is_none() && bindings.len() == 1 { let (import_name, import) = bindings.iter().next().unwrap(); if import_name == "default" { @@ -305,8 +328,13 @@ impl EsmBindgen { continue; } } + uwrite!(output, "{{"); + let mut first = true; + let mut bound_external_names = Vec::new(); + // Generate individual imports for all the bindings that were provided, + // to generate the lhs of the destructured assignment for (external_name, import) in bindings { match import { ImportBinding::Interface(iface) => { @@ -328,7 +356,12 @@ impl EsmBindgen { } else { uwrite!(output, "{external_name} as {iface_local_name}"); } + bound_external_names.push(( + external_name.to_string(), + iface_local_name.to_string(), + )); } + ImportBinding::Local(local_names) => { for local_name in local_names { if first { @@ -344,19 +377,34 @@ impl EsmBindgen { } else { uwrite!(output, "{external_name} as {local_name}"); } + bound_external_names + .push((external_name.to_string(), local_name.to_string())); } } }; } + if !first { output.push_str(" "); } + + // End the destructured assignment if let Some(imports_object) = imports_object { uwriteln!( output, "}} = {imports_object}{};", maybe_quote_member(specifier) ); + for (external_name, local_name) in bound_external_names { + uwriteln!( + output, + r#" + if ({local_name} === undefined) {{ + throw new Error("unexpectedly undefined instance import '{local_name}', was '{external_name}' available at instantiation?"); + }} + "#, + ); + } } else if let Some(idl_binding) = idl_binding { uwrite!( output, @@ -373,6 +421,8 @@ impl EsmBindgen { uwriteln!(output, "}} from '{specifier}';"); } } + + // For local bindings we can use a simpler direct assignment ImportBinding::Local(binding_local_names) => { let local_name = &binding_local_names[0]; if let Some(imports_object) = imports_object { @@ -391,10 +441,12 @@ impl EsmBindgen { } } - // render interface import member getters + // Render interface import member getters for (iface_local_name, iface_imports) in iface_imports { uwrite!(output, "const {{"); let mut first = true; + let mut generated_member_names = Vec::new(); + for (member_name, binding) in iface_imports { let ImportBinding::Local(binding_local_names) = binding else { continue; @@ -411,12 +463,26 @@ impl EsmBindgen { } else { uwrite!(output, "{member_name}: {local_name}"); } + generated_member_names.push((member_name, local_name)); } } if !first { output.push_str(" "); } 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) + for (member_name, local_name) in generated_member_names { + uwriteln!( + output, + r#" + if ({local_name} === undefined) {{ + throw new Error("unexpectedly undefined local import '{local_name}', was '{member_name}' available at instantiation?"); + }} + "#, + ); + } } } } diff --git a/crates/js-component-bindgen/src/function_bindgen.rs b/crates/js-component-bindgen/src/function_bindgen.rs index df2279a0..83ef7634 100644 --- a/crates/js-component-bindgen/src/function_bindgen.rs +++ b/crates/js-component-bindgen/src/function_bindgen.rs @@ -8,7 +8,7 @@ use wit_bindgen_core::abi::{Bindgen, Bitcast, Instruction}; use wit_component::StringEncoding; use wit_parser::abi::WasmType; use wit_parser::{ - Alignment, ArchitectureSize, Handle, Resolve, SizeAlign, Type, TypeDef, TypeDefKind, TypeId, + Alignment, ArchitectureSize, Handle, Resolve, SizeAlign, Type, TypeDefKind, TypeId, }; use crate::intrinsics::Intrinsic; @@ -20,7 +20,7 @@ use crate::intrinsics::p3::async_stream::AsyncStreamIntrinsic; use crate::intrinsics::p3::async_task::AsyncTaskIntrinsic; use crate::intrinsics::resource::ResourceIntrinsic; use crate::intrinsics::string::StringIntrinsic; -use crate::{get_thrown_type, source}; +use crate::{ManagesIntrinsics, get_thrown_type, source}; use crate::{uwrite, uwriteln}; /// Method of error handling @@ -228,6 +228,7 @@ impl FunctionBindgen<'_> { /// Write result assignment lines to output /// /// In general this either means writing preambles, for example that look like the following: + /// /// ```js /// const ret = /// ``` @@ -235,14 +236,22 @@ impl FunctionBindgen<'_> { /// ``` /// var [ ret0, ret1, ret2 ] = /// ``` - fn write_result_assignment(&mut self, amt: usize, results: &mut Vec) { - match amt { - 0 => {} - 1 => { + fn write_result_assignment(&mut self, amt: usize, results: &mut Vec, is_async: bool) { + // For async functions there is always a result returned and it's a single integer + // which indicates async state. This is a sort of "meta" result -- i.e. it shouldn't be counted + // as a regular function result but *should* be made available to the JS internal code. + if is_async { + uwrite!(self.src, "const ret = "); + return; + } + match (is_async, amt) { + (true, _) => {} + (false, 0) => {} + (false, 1) => { uwrite!(self.src, "const ret = "); results.push("ret".to_string()); } - n => { + (false, n) => { uwrite!(self.src, "var ["); for i in 0..n { if i > 0 { @@ -305,7 +314,13 @@ impl FunctionBindgen<'_> { } /// Start the current task - fn start_current_task(&mut self, instr: &Instruction, is_async: bool, fn_name: &str) { + fn start_current_task( + &mut self, + instr: &Instruction, + is_async: bool, + fn_name: &str, + callback_fn_name: Option, + ) { let prefix = match instr { Instruction::CallWasm { .. } => "_wasm_call_", Instruction::CallInterface { .. } => "_interface_call_", @@ -316,9 +331,20 @@ impl FunctionBindgen<'_> { let start_current_task_fn = self.intrinsic(Intrinsic::AsyncTask(AsyncTaskIntrinsic::StartCurrentTask)); let component_instance_idx = self.canon_opts.instance.as_u32(); + uwriteln!( self.src, - "const {prefix}currentTaskID = {start_current_task_fn}({component_instance_idx}, {is_async}, '{fn_name}');" + " + 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}', + }}); + ", + // NOTE: callback functions are missing on async imports that are host defined + callback_fn_name = callback_fn_name.unwrap_or_else(||"null".into()), ); } @@ -336,6 +362,13 @@ impl FunctionBindgen<'_> { } } +impl ManagesIntrinsics for FunctionBindgen<'_> { + /// Add an intrinsic, supplying it's name afterwards + fn add_intrinsic(&mut self, intrinsic: Intrinsic) { + self.intrinsic(intrinsic); + } +} + impl Bindgen for FunctionBindgen<'_> { type Operand = String; @@ -1212,13 +1245,37 @@ impl Bindgen for FunctionBindgen<'_> { has_post_return = self.post_return.is_some(), ); - // Inject machinery for starting an async 'current' task - self.start_current_task(inst, self.is_async, self.callee); + // Inject machinery for starting a 'current' task + self.start_current_task( + inst, + self.is_async, + self.callee, + self.canon_opts + .callback + .as_ref() + .map(|v| format!("callback_{}", v.as_u32())), + ); + + // NOTE: PrepareCall and AsyncStartCall *do* happen right before the guest->guest call + // The CallWasm actually returns BEFORE any of that runs. + // + // This means that AsyncStartCall *DOES* have to start it's own push loop for the subtask, + // and call the callee itself??? + + // TODO: we need to distinguish between a call from host and guest + // so we can know when to create subtasks and also when to DO subtask stuff? + + // TODO: trap if this component is already on the call stack (re-entrancy) + + // TODO(threads): start a thread + // TODO(threads): Task#enter needs to be called with the thread that is executing (inside thread_func) + // TODO(threads): thread_func will contain the actual call rather than attempting to execute immediately // Output result binding preamble (e.g. 'var ret =', 'var [ ret0, ret1] = exports...() ') let sig_results_length = sig.results.len(); - self.write_result_assignment(sig_results_length, results); + self.write_result_assignment(sig_results_length, results, self.is_async); + // Write the rest of the result asignment -- calling the callee function uwriteln!( self.src, "{maybe_async_await}{callee}({args});", @@ -1247,9 +1304,9 @@ impl Bindgen for FunctionBindgen<'_> { ); } - // If we're not dealing with an async call, we can immediately end the task - // after the call has completed. if !self.is_async { + // If we're not dealing with an async call, we can immediately end the task + // after the call has completed. self.end_current_task(); } } @@ -1264,11 +1321,19 @@ impl Bindgen for FunctionBindgen<'_> { async_ = async_.then_some("async").unwrap_or("sync"), ); - // Inject machinery for starting an async 'current' task - self.start_current_task(inst, *async_, &func.name); + // 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())), + ); let results_length = if func.result.is_none() { 0 } else { 1 }; - let maybe_async_await = if self.requires_async_porcelain { + let maybe_async_await = if self.requires_async_porcelain | async_ { "await " } else { "" @@ -1315,7 +1380,7 @@ impl Bindgen for FunctionBindgen<'_> { ); results.push("ret".to_string()); } else { - self.write_result_assignment(results_length, results); + self.write_result_assignment(results_length, results, self.is_async); uwriteln!(self.src, "{call};"); } @@ -1343,6 +1408,9 @@ impl Bindgen for FunctionBindgen<'_> { // TODO: if it was an async call, we may not be able to clear the borrows yet. // save them to the task/ensure they are added to the task's list of borrows? + // + // TODO: if there is a subtask, we must not clear borrows until subtask.deliverReturn + // is called. // After a high level call, we need to deactivate the component resource borrows. if self.clear_resource_borrows { @@ -1421,14 +1489,41 @@ impl Bindgen for FunctionBindgen<'_> { ) }; + assert!(!self.is_async, "async functions should use AsyncTaskReturn"); + + // // TODO: this shouldn't be here, handle via AsyncTaskReturn + // // see: https://github.com/bytecodealliance/wit-bindgen/pull/1414 + // if self.is_async { + // // Forward to handling of async task return + // let fn_name = format!("[task-return]{}", func.name); + // let params = { + // let mut container = Vec::new(); + // let mut flattened_types = FlatTypes::new(&mut container); + // for (_name, ty) in func.params.iter() { + // self.resolve.push_flat(ty, &mut flattened_types); + // } + // flattened_types.to_vec() + // }; + // self.emit( + // resolve, + // &Instruction::AsyncTaskReturn { + // name: &fn_name, + // params: ¶ms, + // }, + // operands, + // results, + // ); + // return; + // } + // Depending how many values are on the stack after returning, we must execute differently. // // In particular, if this function is async (distinct from whether async porcelain was necessary or not), // rather than simply executing the function we must return (or block for) the promise that was created // for the task. - match (self.is_async, stack_value_count) { + match stack_value_count { // (sync) Handle no result case - (_is_async @ false, 0) => { + 0 => { if let Some(f) = &self.post_return { uwriteln!( self.src, @@ -1439,7 +1534,7 @@ impl Bindgen for FunctionBindgen<'_> { } // (sync) Handle single `result` case - (_is_async @ false, 1) if self.err == ErrHandling::ThrowResultErr => { + 1 if self.err == ErrHandling::ThrowResultErr => { let component_err = self.intrinsic(Intrinsic::ComponentError); let op = &operands[0]; uwriteln!(self.src, "const retCopy = {op};"); @@ -1462,7 +1557,7 @@ impl Bindgen for FunctionBindgen<'_> { } // (sync) Handle all other cases (including single parameter non-result) - (_is_async @ false, stack_value_count) => { + stack_value_count => { let ret_val = match stack_value_count { 0 => unreachable!( "unexpectedly zero return values for synchronous return" @@ -1491,81 +1586,6 @@ impl Bindgen for FunctionBindgen<'_> { uwriteln!(self.src, "return {ret_val};",) } } - - // (async) some async functions will not put values on the stack - (_is_async @ true, 0) => {} - - // (async) handle return of valid async call (single parameter) - (_is_async @ true, 1) => { - // Given that this function was async lifted, regardless of whether we are allowed to use async - // porcelain or not, we must return a Promise that resolves to the result of this function - // - // If we are using async porcelain, then we can at the very least resolve the promise immediately - // and do the waiting "on our side", but if async porcelain is not enabled, then we must return a - // Promise and let the caller resolve it in their sync fasion however they can (i.e. hopefully off - // the main thread, in a loop somewhere). - // - // It is up to sync callers to resolve the returned Promise to a value, for now, - // we do not attempt to do any synchronous busy waiting until the - // - let component_instance_idx = self.canon_opts.instance.as_u32(); - let get_current_task_fn = self - .intrinsic(Intrinsic::AsyncTask(AsyncTaskIntrinsic::GetCurrentTask)); - let component_err = self.intrinsic(Intrinsic::ComponentError); - - // If the return value is a result, we attempt to extract the relevant value from inside - // or throw an error, in keeping with transpile's value handling rules - let mut return_res_js = "return taskRes;".into(); - if let Some(Type::Id(result_ty_id)) = func.result - && let Some(TypeDef { - kind: TypeDefKind::Result(_), - .. - }) = self.resolve.types.get(result_ty_id) - { - if self.requires_async_porcelain { - // If we're using async porcelain, then we have already resolved the promise - // to a value, and we can throw if it's an Result that contains an error - return_res_js = format!(" - if (taskRes.tag === 'err') {{ throw new {component_err}(taskRes.val); }} - return taskRes.val; - "); - } else { - // If we're not using async porcelain, then we're return a Promise, - // but rather than returning the promise of a Result object, we should - // return the value inside (or error) - return_res_js = format!(" - return taskRes.then((_taskRes) => {{ - if (_taskRes.tag === 'err') {{ throw new {component_err}(_taskRes.val); }} - return _taskRes.val; - }}); - "); - } - } - - uwriteln!( - self.src, - " - const taskMeta = {get_current_task_fn}({component_instance_idx}); - if (!taskMeta) {{ throw new Error('failed to find current task metadata'); }} - const task = taskMeta.task; - if (!task) {{ throw new Error('missing/invalid task in current task metadata'); }} - const taskRes = {maybe_async_await} task.completionPromise(); - {return_res_js} - ", - maybe_async_await = if self.requires_async_porcelain { - "await " - } else { - "" - } - ); - } - - // (async) more than one value on the stack is not expected - (_is_async @ true, _) => { - unreachable!( - "async functions must return no more than one single i32 result indicating async behavior" - ); - } } } @@ -2147,14 +2167,21 @@ impl Bindgen for FunctionBindgen<'_> { } // Instruction::AsyncTaskReturn does *not* correspond to an canonical `task.return`, - // but rather to a "return"/exit from an async function (e.g. pre-callback) + // but rather to a "return"/exit from an a lifted async function (e.g. pre-callback) // - // At this point, `ret` has already been declared as the original return value - // of the function that was called. + // To control the *real* `task.return` intrinsic: + // - `Intrinsic::TaskReturn` + // - `AsyncTaskIntrinsic::TaskReturn` + // + // This is simply the end of the async function definition (e.g. `CallWasm`) that has been + // lifted, which contains information about the async state. // // For an async function 'some-func', this instruction is triggered w/ the following `name`s: // - '[task-return]some-func' // + // At this point in code generation, the following things have already been set: + // - `ret`: the original function return value, via (i.e. via `CallWasm`/`CallInterface`) + // Instruction::AsyncTaskReturn { name, params } => { let debug_log_fn = self.intrinsic(Intrinsic::DebugLog); uwriteln!( @@ -2178,113 +2205,72 @@ impl Bindgen for FunctionBindgen<'_> { "async fn cannot have post_return specified (func {name})" ); - let get_current_task_fn = + // If we're dealing with an async call, then `ret` is actually the + // state of async behavior. + // + // The result *should* be a Promise that resolves to whatever the current task + // will eventually resolve to. + // + // NOTE: Regardless of whether async porcelain is required here, we want to return the result + // of the computation as a whole, not the current async state (which is what `ret` currently is). + // + // `ret` is only a Promise if we have async-lowered the function in question (e.g. via JSPI) + // + // ```ts + // type ret = number | Promise; + let async_driver_loop_fn = + self.intrinsic(Intrinsic::AsyncTask(AsyncTaskIntrinsic::DriverLoop)); + 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_idx = self.canon_opts.instance.as_u32(); + let component_instance_idx = self.canon_opts.instance.as_u32(); + let is_async_js = self.requires_async_porcelain | self.is_async; - let i32_typecheck = self.intrinsic(Intrinsic::TypeCheckValidI32); - let to_int32_fn = - self.intrinsic(Intrinsic::Conversion(ConversionIntrinsic::ToInt32)); - let unpack_callback_result_fn = self.intrinsic(Intrinsic::AsyncTask( - AsyncTaskIntrinsic::UnpackCallbackResult, - )); - // NOTE: callback fns are sometimes missing (e.g. when processing a `[task-return]some-fn`) - let callback_fn_name = self - .canon_opts - .callback - .map(|v| format!("callback_{}", v.as_u32())); - - // Generate the fn signatures for task function calls, - // since we may be using async porcelain or not for this function - let ( - task_fn_call_prefix, - task_yield_fn, - task_wait_for_event_fn, - task_poll_for_event_fn, - ) = if self.requires_async_porcelain { - ( - "await ", - "task.yield", - "task.waitForEvent", - "task.pollForEvent", - ) - } else { - ( - "", - "task.yieldSync", - "task.waitForEventSync", - "task.pollForEventSync", - ) - }; + // Resolve the promise that *would* have been returned via `WebAssembly.promising` + if self.requires_async_porcelain { + uwriteln!(self.src, "ret = await ret;"); + } + // TODO: if we're returning from an async subtask, we need to call onProgress *before* callback + // TODO: when this is done, we need to call subtask.resolve ?? + + // TODO: If we're returning from a call that was initiated by another component, + // then there will be a current subtask... We need to detect that and + // use the callback that is from there instead/information from it? + // + // If we're returning from a subtask, then we need to resolve the subtask! + + // Perform the reaction to async state uwriteln!( self.src, r#" - const retCopy = {first_op}; - if (retCopy !== undefined) {{ - if (!({i32_typecheck}(retCopy))) {{ throw new Error('invalid async return value [' + retCopy + '], not a number'); }} - if (retCopy < 0 || retCopy > 3) {{ - throw new Error('invalid async return value, outside callback code range'); - }} - }} - - const taskMeta = {get_current_task_fn}({component_idx}); - if (!taskMeta) {{ throw new Error('missing/invalid current task metadata'); }} - - const task = taskMeta.task; - if (!task) {{ throw new Error('missing/invalid current task in metadata'); }} - - let currentRes = retCopy; - let taskRes, eventCode, index, result; - if (currentRes !== undefined) {{ - while (true) {{ - let [code, waitableSetIdx] = {unpack_callback_result_fn}(currentRes); - switch (code) {{ - case 0: // EXIT - {debug_log_fn}('{prefix} [Instruction::AsyncTaskReturn] exit', {{ fn: '{name}' }}); - task.exit(); - return; - case 1: // YIELD - {debug_log_fn}('{prefix} [Instruction::AsyncTaskReturn] yield', {{ fn: '{name}' }}); - taskRes = {task_fn_call_prefix}{task_yield_fn}({{ isCancellable: true, forCallback: true }}); - break; - case 2: // WAIT for a given waitable set - {debug_log_fn}('{prefix} [Instruction::AsyncTaskReturn] waiting for event', {{ waitableSetIdx }}); - taskRes = {task_fn_call_prefix}{task_wait_for_event_fn}({{ isAsync: true, waitableSetIdx }}); - break; - case 3: // POLL - {debug_log_fn}('{prefix} [Instruction::AsyncTaskReturn] polling for event', {{ waitableSetIdx }}); - taskRes = {task_fn_call_prefix}{task_poll_for_event_fn}({{ isAsync: true, waitableSetIdx }}); - break; - default: - throw new Error('invalid async return value [' + retCopy + ']'); - }} - - {maybe_callback_call} - }} - }} - "#, - first_op = operands.first().map(|s| s.as_str()).unwrap_or("undefined"), - prefix = self.tracing_prefix, - maybe_callback_call = match callback_fn_name { - Some(fn_name) => format!(r#" - eventCode = taskRes[0]; - index = taskRes[1]; - result = taskRes[2]; - {debug_log_fn}('performing callback', {{ fn: "{fn_name}", eventCode, index, result }}); - currentRes = {fn_name}( - {to_int32_fn}(eventCode), - {to_int32_fn}(index), - {to_int32_fn}(result), - ); - "#), - None => "if (taskRes !== 0) {{ throw new Error('function with no callback returned a non-zero result'); }}".into(), - } + const componentState = {get_or_create_async_state_fn}({component_instance_idx}); + if (!componentState) {{ throw new Error('failed to lookup current component state'); }} + + const taskMeta = {current_task_get_fn}({component_instance_idx}); + if (!taskMeta) {{ throw new Error('failed to find current task metadata'); }} + + const task = taskMeta.task; + if (!task) {{ throw new Error('missing/invalid task in current task metadata'); }} + + new Promise((resolve, reject) => {{ + {async_driver_loop_fn}({{ + componentInstanceIdx: {component_instance_idx}, + componentState, + task, + fnName: '{name}', + isAsync: {is_async_js}, + callbackResult: ret, + resolve, + reject + }}); + }}); + + return task.completionPromise(); + "#, ); - - // Inject machinery for ending an async 'current' task - // which may return a result if necessary - self.end_current_task(); } Instruction::GuestDeallocate { .. } diff --git a/crates/js-component-bindgen/src/intrinsics/component.rs b/crates/js-component-bindgen/src/intrinsics/component.rs index 59382e4e..bb601616 100644 --- a/crates/js-component-bindgen/src/intrinsics/component.rs +++ b/crates/js-component-bindgen/src/intrinsics/component.rs @@ -51,6 +51,18 @@ 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, } impl ComponentIntrinsic { @@ -73,6 +85,7 @@ impl ComponentIntrinsic { Self::BackpressureInc => "backpressureInc", Self::BackpressureDec => "backpressureDec", Self::ComponentAsyncStateClass => "ComponentAsyncState", + Self::LowerImport => "_intrinsic_component_lowerImport", } } @@ -132,7 +145,7 @@ impl ComponentIntrinsic { let rep_table_class = Intrinsic::RepTableClass.name(); let debug_log_fn = Intrinsic::DebugLog.name(); output.push_str(&format!( - " + r#" class {class_name} {{ #callingAsyncImport = false; #syncImportWait = Promise.withResolvers(); @@ -141,9 +154,26 @@ impl ComponentIntrinsic { mayLeave = true; waitableSets = new {rep_table_class}(); waitables = new {rep_table_class}(); + subtasks = new {rep_table_class}(); #parkedTasks = new Map(); + #suspendedTasksByTaskID = new Map(); + #suspendedTaskIDs = []; + #taskResumerInterval = null; + + #pendingTasks = []; + + constructor(args) {{ + this.#taskResumerInterval = setInterval(() => {{ + try {{ + this.tick(); + }} catch (err) {{ + {debug_log_fn}('[{class_name}#taskResumer()] tick failed', {{ err }}); + }} + }}, 0); + }}; + callingSyncImport(val) {{ if (val === undefined) {{ return this.#callingAsyncImport; }} if (typeof val !== 'boolean') {{ throw new TypeError('invalid setting for async import'); }} @@ -242,8 +272,77 @@ impl ComponentIntrinsic { isExclusivelyLocked() {{ return this.#lock !== null; }} + #getSuspendedTaskMeta(taskID) {{ + return this.#suspendedTasksByTaskID.get(taskID); + }} + + #removeSuspendedTaskMeta(taskID) {{ + {debug_log_fn}('[{class_name}#removeSuspendedTaskMeta()] removing suspended task', {{ taskID }}); + const idx = this.#suspendedTaskIDs.findIndex(t => t && t.taskID === taskID); + const meta = this.#suspendedTasksByTaskID.get(taskID); + this.#suspendedTaskIDs[idx] = null; + this.#suspendedTasksByTaskID.delete(taskID); + return meta; + }} + + #addSuspendedTaskMeta(meta) {{ + if (!meta) {{ throw new Error('missing task meta'); }} + const taskID = meta.taskID; + this.#suspendedTasksByTaskID.set(taskID, meta); + this.#suspendedTaskIDs.push(taskID); + if (this.#suspendedTasksByTaskID.size < this.#suspendedTaskIDs.length - 10) {{ + this.#suspendedTaskIDs = this.#suspendedTaskIDs.filter(t => t !== null); + }} + }} + + suspendTask(args) {{ + // TODO(threads): readyFn is normally on the thread + const {{ task, readyFn }} = args; + const taskID = task.id(); + {debug_log_fn}('[{class_name}#suspendTask()]', {{ taskID }}); + + if (this.#getSuspendedTaskMeta(taskID)) {{ + throw new Error('task [' + taskID + '] already suspended'); + }} + + const {{ promise, resolve }} = Promise.withResolvers(); + this.#addSuspendedTaskMeta({{ + task, + taskID, + readyFn, + resume: () => {{ + {debug_log_fn}('[{class_name}#suspendTask()] resuming suspended task', {{ taskID }}); + // TODO(threads): it's thread cancellation we should be checking for below, not task + resolve(!task.isCancelled()); + }}, + }}); + + return promise; + }} + + resumeTaskByID(taskID) {{ + const meta = this.#removeSuspendedTaskMeta(taskID); + if (!meta) {{ return; }} + if (meta.taskID !== taskID) {{ throw new Error('task ID does not match'); }} + meta.resume(); + }} + + tick() {{ + for (const taskID of this.#suspendedTaskIDs.filter(t => t !== null)) {{ + const meta = this.#suspendedTasksByTaskID.get(taskID); + if (!meta || !meta.readyFn) {{ + throw new Error('missing/invalid task despite ID [' + taskID + '] being present'); + }} + if (!meta.readyFn()) {{ continue; }} + this.resumeTaskByID(taskID); + }} + }} + + addPendingTask(task) {{ + this.#pendingTasks.push(task); + }} }} - ", + "#, class_name = self.name(), )); } @@ -263,6 +362,22 @@ 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'); + }} + " + )); + } } } } diff --git a/crates/js-component-bindgen/src/intrinsics/mod.rs b/crates/js-component-bindgen/src/intrinsics/mod.rs index d2cc9280..e1c9a22e 100644 --- a/crates/js-component-bindgen/src/intrinsics/mod.rs +++ b/crates/js-component-bindgen/src/intrinsics/mod.rs @@ -308,6 +308,16 @@ pub fn render_intrinsics(args: RenderIntrinsicsArgs) -> Source { ]); } + if args + .intrinsics + .contains(&Intrinsic::AsyncTask(AsyncTaskIntrinsic::DriverLoop)) + { + args.intrinsics.extend([ + &Intrinsic::TypeCheckValidI32, + &Intrinsic::Conversion(ConversionIntrinsic::ToInt32), + ]); + } + if args .intrinsics .contains(&Intrinsic::Component(ComponentIntrinsic::BackpressureSet)) @@ -337,6 +347,14 @@ pub fn render_intrinsics(args: RenderIntrinsicsArgs) -> Source { ]); } + if args + .intrinsics + .contains(&Intrinsic::Waitable(WaitableIntrinsic::WaitableSetNew)) + { + args.intrinsics + .extend([&Intrinsic::Waitable(WaitableIntrinsic::WaitableSetClass)]); + } + if args.intrinsics.contains(&Intrinsic::Component( ComponentIntrinsic::GetOrCreateAsyncState, )) { @@ -681,12 +699,13 @@ pub fn render_intrinsics(args: RenderIntrinsicsArgs) -> Source { output.push_str(&format!( " const {name} = {{ - NONE: 'none', - TASK_CANCELLED: 'task-cancelled', - STREAM_READ: 'stream-read', - STREAM_WRITE: 'stream-write', - FUTURE_READ: 'future-read', - FUTURE_WRITE: 'future-write', + NONE: 0, + SUBTASK: 1, + STREAM_READ: 2, + STREAM_WRITE: 3, + FUTURE_READ: 4, + FUTURE_WRITE: 5, + TASK_CANCELLED: 6, }}; " )); diff --git a/crates/js-component-bindgen/src/intrinsics/p3/async_stream.rs b/crates/js-component-bindgen/src/intrinsics/p3/async_stream.rs index e7e2e298..d67417cd 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/async_stream.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/async_stream.rs @@ -353,7 +353,7 @@ impl AsyncStreamIntrinsic { {debug_log_fn}('[{stream_new_fn}()] args', {{ streamTypeRep, payloadTypeRep, componentIdx }}); if (!componentIdx) {{ - const task = {current_task_get_fn}(); + const task = {current_task_get_fn}(componentIdx); if (!task) {{ throw new Error('invalid/missing async task during stream.new'); }} componentIdx = task.componentIdx; }} 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 e9c01811..08cc9273 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs @@ -1,7 +1,10 @@ //! Intrinsics that represent helpers that implement async tasks use crate::{ - intrinsics::{Intrinsic, component::ComponentIntrinsic, p3::waitable::WaitableIntrinsic}, + intrinsics::{ + Intrinsic, component::ComponentIntrinsic, conversion::ConversionIntrinsic, + p3::waitable::WaitableIntrinsic, + }, source::Source, }; @@ -195,6 +198,37 @@ pub enum AsyncTaskIntrinsic { /// function unpackCallbackResult(callbackResult: i32): [i32, i32]; /// ``` UnpackCallbackResult, + + /// JS that contains the loop which drives a given async task to completion. + /// + /// This intrinsic is not a canon function but instead a reusable JS snippet + /// that controls + /// + /// The Canonical ABI pseudo-code equivalent be `thread_func(thread)` in `canon_lift` + /// though threads are not yet implemented. + /// + /// Normally, the async driver loop returns a Promise that resolves to the result + /// of the original async function that was called. + /// + /// See `Instruction::CallWasm` for example usage. + /// + /// ```ts + /// interface DriverLoopArgs { + /// componentState: ComponentAsyncState, + /// task: AsyncTask, + /// fnName: string, + /// callbackFnName: string, + /// isAsync: boolean, // whether using JSPI *or* lifted async function + /// callbackResult: number, // initial wasm call result that contains callback code and more metadata + /// // Normally, the driver loop is run in a separately executing Promise, + /// // so we ensure that the enclosing promise itself can eventually be resolved + /// resolve: () => void, + /// reject: () => void, + /// } + /// + /// function asyncDriverLoop(args: DriverLoopArgs): Promise; + /// ``` + DriverLoop, } impl AsyncTaskIntrinsic { @@ -205,7 +239,26 @@ impl AsyncTaskIntrinsic { /// Retrieve global names for this intrinsic pub fn get_global_names() -> impl IntoIterator { - ["taskReturn", "subtaskDrop"] + [ + "ASYNC_BLOCKED_CODE", + "ASYNC_CURRENT_COMPONENT_IDXS", + "ASYNC_CURRENT_TASK_IDS", + "ASYNC_TASKS_BY_COMPONENT_IDX", + "AsyncSubtask", + "AsyncTask", + "asyncYield", + "contextGet", + "contextSet", + "endCurrentTask", + "getCurrentTask", + "startCurrentTask", + "subtaskCancel", + "subtaskDrop", + "subtaskDrop", + "taskCancel", + "taskReturn", + "unpackCallbackResult", + ] } /// Get the name for the intrinsic @@ -228,6 +281,7 @@ impl AsyncTaskIntrinsic { Self::TaskReturn => "taskReturn", Self::Yield => "asyncYield", Self::UnpackCallbackResult => "unpackCallbackResult", + Self::DriverLoop => "_driverLoop", } } @@ -295,21 +349,18 @@ impl AsyncTaskIntrinsic { ")); } + // Equivalent of `task.return` Self::TaskReturn => { - // TODO(async): write results into provided memory, perform checks for task & result types - // see: https://github.com/WebAssembly/component-model/blob/main/design/mvp/CanonicalABI.md#-canon-taskreturn let debug_log_fn = Intrinsic::DebugLog.name(); let task_return_fn = Self::TaskReturn.name(); let current_task_get_fn = Self::GetCurrentTask.name(); output.push_str(&format!(" - function {task_return_fn}(componentIdx, useDirectParams, memory, callbackFnIdx, liftFns) {{ - const params = [...arguments].slice(5); + function {task_return_fn}(args) {{ + const {{ componentIdx, useDirectParams, memory, memoryIdx, callbackFnIdx, liftFns }} = args; + const params = [...arguments].slice(1); {debug_log_fn}('[{task_return_fn}()] args', {{ - componentIdx, - memory, - callbackFnIdx, - liftFns, + ...args, params, }}); @@ -319,6 +370,11 @@ impl AsyncTaskIntrinsic { const task = taskMeta.task; if (!taskMeta) {{ throw new Error('invalid/missing current task in metadata'); }} + const expectedMemoryIdx = task.getMemoryIdx(); + if (expectedMemoryIdx !== memoryIdx) {{ + throw new Error('task.return memory [' + memoryIdx + '] does not match task [' + expectedMemoryIdx + ']'); + }} + task.callbackFnIdx = callbackFnIdx; if (!memory && liftFns.length > 4) {{ @@ -447,32 +503,50 @@ impl AsyncTaskIntrinsic { let task_id_globals = Self::GlobalAsyncCurrentTaskIds.name(); let component_idx_globals = Self::GlobalAsyncCurrentComponentIdxs.name(); output.push_str(&format!( - " - let NEXT_TASK_ID = 0n; - function {fn_name}(componentIdx, isAsync, entryFnName) {{ - {debug_log_fn}('[{fn_name}()] args', {{ componentIdx, isAsync }}); + r#" + function {fn_name}(args) {{ + {debug_log_fn}('[{fn_name}()] args', args); + const {{ + componentIdx, + isAsync, + entryFnName, + parentSubtaskID, + callbackFnName, + getCallbackFn, + getParamsFn, + stringEncoding, + getCalleeParamsFn, + }} = args; if (componentIdx === undefined || componentIdx === null) {{ throw new Error('missing/invalid component instance index while starting task'); }} const tasks = {global_task_map}.get(componentIdx); - const nextId = ++NEXT_TASK_ID; - const newTask = new {task_class}({{ id: nextId, componentIdx, isAsync, entryFnName }}); - const newTaskMeta = {{ id: nextId, componentIdx, task: newTask }}; + const newTask = new {task_class}({{ + componentIdx, + isAsync, + entryFnName, + callbackFn: getCallbackFn ? getCallbackFn() : null, + callbackFnName, + stringEncoding, + getCalleeParamsFn, + }}); + const newTaskID = newTask.id(); + const newTaskMeta = {{ id: newTaskID, componentIdx, task: newTask }}; - {task_id_globals}.push(nextId); + {task_id_globals}.push(newTaskID); {component_idx_globals}.push(componentIdx); if (!tasks) {{ {global_task_map}.set(componentIdx, [newTaskMeta]); - return nextId; + return [newTask, newTaskID]; }} else {{ tasks.push(newTaskMeta); }} - return nextId; + return [newTask, newTaskID]; }} - ", + "#, fn_name = self.name(), )); } @@ -533,13 +607,16 @@ impl AsyncTaskIntrinsic { {task_id_globals}.pop(); {component_idx_globals}.pop(); - return tasks.pop(); + const taskMeta = tasks.pop(); + return taskMeta.task; }} ", fn_name = self.name() )); } + // NOTE: since threads are not yet supported, places that would have called out to threads instead run + // `immediate` -- i.e. `Thread#suspendUntil` becomes `AsyncTask#immediateSuspendUntil` Self::AsyncTaskClass => { let debug_log_fn = Intrinsic::DebugLog.name(); let get_or_create_async_state_fn = @@ -553,8 +630,10 @@ impl AsyncTaskIntrinsic { let coin_flip_fn = Intrinsic::CoinFlip.name(); // TODO: remove the public mutable members that are eagerly exposed for early impl - output.push_str(&format!(" + output.push_str(&format!(r#" class {task_class} {{ + static _ID = 0n; + static State = {{ INITIAL: 'initial', CANCELLED: 'cancelled', @@ -576,6 +655,18 @@ impl AsyncTaskIntrinsic { #entryFnName = null; #subtasks = []; #completionPromise = null; + #memoryIdx = null; + + #callbackFn = null; + #callbackFnName = null; + + #postReturnFn = null; + + #getCalleeParamsFn = null; + + #stringEncoding = null; + + #parentSubtask = null; cancelled = false; requested = false; @@ -590,12 +681,13 @@ impl AsyncTaskIntrinsic { constructor(opts) {{ - if (opts?.id === undefined) {{ throw new TypeError('missing task ID during task creation'); }} - this.#id = opts.id; + this.#id = ++{task_class}._ID; + if (opts?.componentIdx === undefined) {{ throw new TypeError('missing component id during task creation'); }} this.#componentIdx = opts.componentIdx; + this.#state = {task_class}.State.INITIAL; this.#isAsync = opts?.isAsync ?? false; this.#entryFnName = opts.entryFnName; @@ -611,6 +703,15 @@ impl AsyncTaskIntrinsic { // TODO: handle external facing cancellation (should likely be a rejection) resolveCompletionPromise(results); }} + + if (opts.callbackFn) {{ this.#callbackFn = opts.callbackFn; }} + if (opts.callbackFnName) {{ this.#callbackFnName = opts.callbackFnName; }} + + if (opts.getCalleeParamsFn) {{ this.#getCalleeParamsFn = opts.getCalleeParamsFn; }} + + if (opts.stringEncoding) {{ this.#stringEncoding = opts.stringEncoding; }} + + if (opts.parentSubtask) {{ this.#parentSubtask = opts.parentSubtask; }} }} taskState() {{ return this.#state.slice(); }} @@ -620,6 +721,45 @@ impl AsyncTaskIntrinsic { entryFnName() {{ return this.#entryFnName; }} completionPromise() {{ return this.#completionPromise; }} + setMemoryIdx(idx) {{ this.#memoryIdx = idx; }} + getMemoryIdx(idx) {{ return this.#memoryIdx; }} + + setParentSubtask(subtask) {{ + if (!subtask || !(subtask instanceof {subtask_class})) {{ return }} + if (this.#parentSubtask) {{ throw new Error('parent subtask can only be set once'); }} + this.#parentSubtask = subtask; + }} + + getParentSubtask() {{ return this.#parentSubtask; }} + + setPostReturnFn(f) {{ + if (!f) {{ return; }} + if (this.#postReturnFn) {{ throw new Error('postReturn fn can only be set once'); }} + this.#postReturnFn = f; + }} + + setCallbackFn(f, name) {{ + if (!f) {{ return; }} + if (this.#callbackFn) {{ throw new Error('callback fn can only be set once'); }} + this.#callbackFn = f; + this.#callbackFnName = name; + }} + + getCallbackFnName() {{ + if (!this.#callbackFnName) {{ return undefined; }} + return this.#callbackFnName; + }} + + runCallbackFn(...args) {{ + if (!this.#callbackFn) {{ throw new Error('on callback function has been set for task'); }} + return this.#callbackFn.apply(null, args); + }} + + getCalleeParams() {{ + if (!this.#getCalleeParamsFn) {{ throw new Error('missing/invalid getCalleeParamsFn'); }} + return this.#getCalleeParamsFn(); + }} + mayEnter(task) {{ const cstate = {get_or_create_async_state_fn}(this.#componentIdx); if (!cstate.backpressure) {{ @@ -676,52 +816,39 @@ impl AsyncTaskIntrinsic { return true; }} - async waitForEvent(opts) {{ - const {{ waitableSetRep, isAsync }} = opts; - {debug_log_fn}('[{task_class}#waitForEvent()] args', {{ taskID: this.#id, waitableSetRep, isAsync }}); + async waitUntil(opts) {{ + const {{ readyFn, waitableSetRep, cancellable }} = opts; + {debug_log_fn}('[{task_class}#waitUntil()] args', {{ taskID: this.#id, waitableSetRep, cancellable }}); - if (this.#isAsync !== isAsync) {{ - throw new Error('async waitForEvent called on non-async task'); - }} + const state = {get_or_create_async_state_fn}(this.#componentIdx); + const wset = state.waitableSets.get(waitableSetRep); - if (this.status === {task_class}.State.CANCEL_PENDING) {{ - this.#state = {task_class}.State.CANCEL_DELIVERED; - return {{ + let event; + + wset.incrementNumWaiting(); + + const keepGoing = await this.suspendUntil({{ + readyFn: () => {{ + return readyFn() && wset.hasPendingEvent(); + }}, + cancellable, + }}); + + if (keepGoing) {{ + event = wset.getPendingEvent(); + }} else {{ + event = {{ code: {event_code_enum}.TASK_CANCELLED, + index: 0, + result: 0, }}; }} - const state = {get_or_create_async_state_fn}(this.#componentIdx); - const waitableSet = state.waitableSets.get(waitableSetRep); - if (!waitableSet) {{ throw new Error('missing/invalid waitable set'); }} - - waitableSet.numWaiting += 1; - let event = null; - - while (event == null) {{ - const awaitable = new {awaitable_class}(waitableSet.getPendingEvent()); - const waited = await this.blockOn({{ awaitable, isAsync, isCancellable: true }}); - if (waited) {{ - if (this.#state !== {task_class}.State.INITIAL) {{ - throw new Error('task should be in initial state found [' + this.#state + ']'); - }} - this.#state = {task_class}.State.CANCELLED; - return {{ - code: {event_code_enum}.TASK_CANCELLED, - }}; - }} - - event = waitableSet.poll(); - }} + wset.decrementNumWaiting(); - waitableSet.numWaiting -= 1; return event; }} - waitForEventSync(opts) {{ - throw new Error('{task_class}#yieldSync() not implemented') - }} - async pollForEvent(opts) {{ const {{ waitableSetRep, isAsync }} = opts; {debug_log_fn}('[{task_class}#pollForEvent()] args', {{ taskID: this.#id, waitableSetRep, isAsync }}); @@ -733,10 +860,6 @@ impl AsyncTaskIntrinsic { throw new Error('{task_class}#pollForEvent() not implemented'); }} - pollForEventSync(opts) {{ - throw new Error('{task_class}#yieldSync() not implemented') - }} - async blockOn(opts) {{ const {{ awaitable, isCancellable, forCallback }} = opts; {debug_log_fn}('[{task_class}#blockOn()] args', {{ taskID: this.#id, awaitable, isCancellable, forCallback }}); @@ -833,55 +956,90 @@ impl AsyncTaskIntrinsic { throw new Error('AsyncTask#asyncOnBlock() not yet implemented'); }} - async yield(opts) {{ - const {{ isCancellable, forCallback }} = opts; - {debug_log_fn}('[{task_class}#yield()] args', {{ taskID: this.#id, isCancellable, forCallback }}); + async yieldUntil(opts) {{ + const {{ readyFn, cancellable }} = opts; + {debug_log_fn}('[{task_class}#yield()] args', {{ taskID: this.#id, cancellable }}); - if (isCancellable && this.status === {task_class}.State.CANCEL_PENDING) {{ - this.#state = {task_class}.State.CANCELLED; + const keepGoing = await this.suspendUntil({{ readyFn, cancellable }}); + if (!keepGoing) {{ return {{ code: {event_code_enum}.TASK_CANCELLED, - payload: [0, 0], + index: 0, + result: 0, }}; }} - // TODO: Awaitables need to *always* trigger the parking mechanism when they're done...? - // TODO: Component async state should remember which awaitables are done and work to clear tasks waiting + return {{ + code: {event_code_enum}.NONE, + index: 0, + result: 0, + }}; + }} - const blockResult = await this.blockOn({{ - awaitable: new {awaitable_class}(new Promise(resolve => setTimeout(resolve, 0))), - isCancellable, - forCallback, - }}); + async suspendUntil(opts) {{ + const {{ cancellable, readyFn }} = opts; + {debug_log_fn}('[{task_class}#suspendUntil()] args', {{ cancellable }}); - if (blockResult === {task_class}.BlockResult.CANCELLED) {{ - if (this.#state !== {task_class}.State.INITIAL) {{ - throw new Error('task should be in initial state found [' + this.#state + ']'); - }} - this.#state = {task_class}.State.CANCELLED; - return {{ - code: {event_code_enum}.TASK_CANCELLED, - payload: [0, 0], - }}; + const pendingCancelled = this.deliverPendingCancel({{ cancellable }}); + if (pendingCancelled) {{ return false; }} + + const completed = await this.immediateSuspendUntil({{ readyFn, cancellable }}); + return completed; + }} + + // TODO(threads): equivalent to thread.suspend_until() + async immediateSuspendUntil(opts) {{ + const {{ cancellable, readyFn }} = opts; + {debug_log_fn}('[{task_class}#immediateSuspendUntil()] args', {{ cancellable, readyFn }}); + + const ready = readyFn(); + if (ready && !{global_async_determinism} && {coin_flip_fn}()) {{ + return true; }} - return {{ - code: {event_code_enum}.NONE, - payload: [0, 0], - }}; + const cstate = {get_or_create_async_state_fn}(this.#componentIdx); + cstate.addPendingTask(this); + + const keepGoing = await this.immediateSuspend({{ cancellable, readyFn }}); + return keepGoing; + }} + + async immediateSuspend(opts) {{ // NOTE: equivalent to thread.suspend() + // TODO(threads): store readyFn on the thread + const {{ cancellable, readyFn }} = opts; + {debug_log_fn}('[{task_class}#immediateSuspend()] args', {{ cancellable, readyFn }}); + + const pendingCancelled = this.deliverPendingCancel({{ cancellable }}); + if (pendingCancelled) {{ return false; }} + + const cstate = {get_or_create_async_state_fn}(this.#componentIdx); + + const taskWait = await cstate.suspendTask({{ task: this, readyFn }}); + const keepGoing = await taskWait; + return keepGoing; }} - yieldSync(opts) {{ - throw new Error('{task_class}#yieldSync() not implemented') + deliverPendingCancel(opts) {{ + const {{ cancellable }} = opts; + {debug_log_fn}('[{task_class}#deliverPendingCancel()] args', {{ cancellable }}); + + if (cancellable && this.#state === {task_class}.State.PENDING_CANCEL) {{ + this.#state = Task.State.CANCEL_DELIVERED; + return true; + }} + + return false; }} + isCancelled() {{ return this.cancelled }} + cancel() {{ {debug_log_fn}('[{task_class}#cancel()] args', {{ }}); if (!this.taskState() !== {task_class}.State.CANCEL_DELIVERED) {{ throw new Error('invalid task state for cancellation'); }} if (this.borrowedHandles.length > 0) {{ throw new Error('task still has borrow handles'); }} - + this.cancelled = true; this.#onResolve(new Error('cancelled')); this.#state = {task_class}.State.RESOLVED; }} @@ -892,7 +1050,16 @@ impl AsyncTaskIntrinsic { throw new Error('task is already resolved'); }} if (this.borrowedHandles.length > 0) {{ throw new Error('task still has borrow handles'); }} - this.#onResolve(results.length === 1 ? results[0] : results); + switch (results.length) {{ + case 0: + this.#onResolve(undefined); + break; + case 1: + this.#onResolve(results[0]); + break; + default: + throw new Error('unexpected number of results'); + }} this.#state = {task_class}.State.RESOLVED; }} @@ -914,20 +1081,20 @@ impl AsyncTaskIntrinsic { }} state.inSyncExportCall = false; - this.startPendingTask(); - }} - - startPendingTask(args) {{ - {debug_log_fn}('[{task_class}#startPendingTask()] args', args); - throw new Error('{task_class}#startPendingTask() not implemented'); + if (!state.isExclusivelyLocked()) {{ + throw new Error('task should have been exclusively locked at end of execution'); + }} + state.exclusiveRelease(); }} createSubtask(args) {{ {debug_log_fn}('[{task_class}#createSubtask()] args', args); + const {{ componentIdx, memoryIdx, childTask }} = args; const newSubtask = new {subtask_class}({{ - componentIdx: this.componentIdx(), - taskID: this.id(), - memoryIdx: args?.memoryIdx, + componentIdx, + childTask, + parentTask: this, + memoryIdx, }}); this.#subtasks.push(newSubtask); return newSubtask; @@ -935,7 +1102,7 @@ impl AsyncTaskIntrinsic { currentSubtask() {{ {debug_log_fn}('[{task_class}#currentSubtask()]'); - if (this.#subtasks.length === 0) {{ throw new Error('no current subtask'); }} + if (this.#subtasks.length === 0) {{ return undefined; }} return this.#subtasks.at(-1); }} @@ -947,7 +1114,7 @@ impl AsyncTaskIntrinsic { return subtask; }} }} - ")); + "#)); } Self::AsyncSubtaskClass => { @@ -956,19 +1123,24 @@ impl AsyncTaskIntrinsic { let waitable_class = Intrinsic::Waitable(WaitableIntrinsic::WaitableClass).name(); let get_or_create_async_state_fn = Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState).name(); - output.push_str(&format!(" + output.push_str(&format!(r#" class {subtask_class} {{ + static _ID = 0n; + static State = {{ - STARTING: 'starting', - STARTED: 'started', - RETURNED: 'returned', - CANCELLED_BEFORE_STARTED: 'cancelled-before-started', - CANCELLED_BEFORE_RETURNED: 'cancelled-before-returned', + STARTING: 0, + STARTED: 1, + RETURNED: 2, + CANCELLED_BEFORE_STARTED: 3, + CANCELLED_BEFORE_RETURNED: 4, }}; + #id; #state = {subtask_class}.State.STARTING; #componentIdx; - #taskID; + + #parentTask; + #childTask = null; #dropped = false; #cancelRequested = false; @@ -981,12 +1153,23 @@ impl AsyncTaskIntrinsic { #waitableResolve = null; #waitableReject = null; + #callbackFn = null; + #callbackFnName = null; + + #postReturnFn = null; + #onProgressFn = null; + #pendingEventFn = null; + + #componentRep = null; + constructor(args) {{ if (!args.componentIdx) {{ throw new Error('missing componentIdx for subtask creation'); }} this.#componentIdx = args.componentIdx; - if (!args.taskID) {{ throw new Error('missing taskID for subtask creation'); }} - this.#taskID = args.taskID; + if (!args.parentTask) {{ throw new Error('missing parent task during subtask creation'); }} + this.#parentTask = args.parentTask; + + if (args.childTask) {{ this.#childTask = args.childTask; }} if (args.memoryIdx) {{ this.#memoryIdx = args.memoryIdx; }} @@ -1002,24 +1185,81 @@ impl AsyncTaskIntrinsic { throw new Error('invalid/missing async state for component instance [' + componentInstanceID + ']'); }} - this.#waitable = new {waitable_class}(promise); + this.#waitable = new {waitable_class}({{ promise, componentInstanceID: this.#componentIdx }}); this.#waitableRep = state.waitables.insert(this.#waitable); }} this.#lenders = []; + this.#id = ++{subtask_class}._ID; + }} + + id() {{ return this.#id; }} + parentTaskID() {{ return this.#parentTask?.id(); }} + childTaskID() {{ return this.#childTask?.id(); }} + + setCallbackFn(f, name) {{ + if (!f) {{ return; }} + if (this.#callbackFn) {{ throw new Error('callback fn can only be set once'); }} + this.#callbackFn = f; + this.#callbackFnName = name; + }} + + getCallbackFnName() {{ + if (!this.#callbackFn) {{ return undefined; }} + return this.#callbackFn.name; }} - getStateNumber() {{ - switch (this.#state) {{ - case 'starting': return 0; - case 'started': return 1; - case 'returned': return 2; - case 'cancelled-before-started': return 3; - case 'cancelled-before-returned': return 4; - default: throw new Error('unrecognized async subtask status [' + this.#state + ']'); + setPostReturnFn(f) {{ + if (!f) {{ return; }} + if (this.#postReturnFn) {{ throw new Error('postReturn fn can only be set once'); }} + this.#postReturnFn = f; + }} + + setOnProgressFn(f) {{ + if (this.#onProgressFn) {{ throw new Error('on progress fn can only be set once'); }} + this.#onProgressFn = f; + }} + + onStart(f) {{ + if (!this.#onProgressFn) {{ throw new Error('missing on progress function'); }} + this.#onProgressFn(); + this.#state = {subtask_class}.State.STARTED; + }} + + setPendingEventFn(fn) {{ + this.#waitable.setPendingEventFn(fn); + }} + + onResolve(value) {{ + if (!this.#onProgressFn) {{ throw new Error('missing on progress function'); }} + this.#onProgressFn(); + + if (value === null) {{ + if (this.#cancelRequested) {{ + throw new Error('cancel was not requested, but no value present at return'); + }} + + if (this.#state === {subtask_class}.State.STARTING) {{ + this.#state = Subtask.State.CANCELLED_BEFORE_STARTED; + }} else {{ + if (this.#state !== {subtask_class}.State.STARTED) {{ + throw new Error('cancelled subtask must have been started before cancellation'); + }} + this.#state = Subtask.State.CANCELLED_BEFORE_RETURNED; + }} + }} else {{ + if (this.#state !== {subtask_class}.State.STARTED) {{ + throw new Error('cancelled subtask must have been started before cancellation'); + }} + this.#state = Subtask.State.RETURNED; }} }} + setRep(rep) {{ this.#componentRep = rep; }} + + getStateNumber() {{ return this.#state; }} + getWaitableRep() {{ return this.#waitableRep; }} + waitableRep() {{ return this.#waitableRep; }} resolved() {{ @@ -1084,7 +1324,7 @@ impl AsyncTaskIntrinsic { return this.#waitableRep; }} }} - ")); + "#)); } Self::UnpackCallbackResult => { @@ -1101,12 +1341,163 @@ impl AsyncTaskIntrinsic { }} if (result < 0 || result >= 2**32) {{ throw new Error('invalid callback result'); }} // TODO: table max length check? - const waitableSetIdx = result >> 4; - return [eventCode, waitableSetIdx]; + const waitableSetRep = result >> 4; + return [eventCode, waitableSetRep]; }} ", )); } + + // TODO: This function likely needs to be a generator + // that first yields the task promise result, then tries to push resolution + Self::DriverLoop => { + let debug_log_fn = Intrinsic::DebugLog.name(); + let driver_loop_fn = Self::DriverLoop.name(); + let i32_typecheck = Intrinsic::TypeCheckValidI32.name(); + let to_int32_fn = Intrinsic::Conversion(ConversionIntrinsic::ToInt32).name(); + let unpack_callback_result_fn = Self::UnpackCallbackResult.name(); + + output.push_str(&format!(r#" + async function {driver_loop_fn}(args) {{ + {debug_log_fn}('[{driver_loop_fn}()] args', args); + const {{ + componentState, + task, + fnName, + isAsync, + resolve, + reject, + + callbackResult, + }} = args; + + const callbackFnName = task.getCallbackFnName(); + + // TODO: how can we know whether this is a Promise? (due to WebAssembly.promising, because async) + // BUT, attempting to await this promising with a host import that is async fails with + // 'trying to suspend JS frames' + callbackResult = await callbackResult; + + let callbackCode; + let waitableSetRep; + let unpacked; + if (callbackResult !== undefined) {{ + 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]; + }} else {{ + throw new Error('NO INTIIAL CALLBACK RESULT'); + }} + + let eventCode; + let index; + let result; + let asyncRes; + try {{ + while (true) {{ + if (callbackCode !== 0) {{ + componentState.exclusiveRelease(); + }} + + switch (callbackCode) {{ + case 0: // EXIT + {debug_log_fn}('[{driver_loop_fn}()] async exit indicated', {{ + fnName, + callbackFnName, + taskID: task.id() + }}); + task.exit(); + resolve(null); + return; + + case 1: // YIELD + {debug_log_fn}('[{driver_loop_fn}()] yield', {{ + fnName, + callbackFnName, + taskID: task.id() + }}); + asyncRes = await task.yieldUntil({{ + cancellable: true, + readyFn: () => !componentState.isExclusivelyLocked() + }}); + break; + + case 2: // WAIT for a given waitable set + {debug_log_fn}('[{driver_loop_fn}()] waiting for event', {{ + fnName, + callbackFnName, + taskID: task.id(), + waitableSetRep, + }}); + if (eventCode === 1 && waitableSetRep === task.currentSubtask().getWaitableRep()) {{ + task.currentSubtask().doTheThing(); + }} + asyncRes = await task.waitUntil({{ + readyFn: () => true, + waitableSetRep, + cancellable: true, + }}); + break; + + case 3: // POLL + {debug_log_fn}('[{driver_loop_fn}()] polling for event', {{ + fnName, + callbackFnName, + taskID: task.id(), + waitableSetRep, + }}); + asyncRes = await task.pollForEvent({{ isAsync: true, waitableSetRep }}); + break; + + default: + throw new Error('Unrecognized async function result [' + ret + ']'); + }} + + componentState.exclusiveLock(); + + eventCode = asyncRes.code; + index = asyncRes.index; + result = asyncRes.result; + asyncRes = null; + + {debug_log_fn}('[{driver_loop_fn}()] performing callback', {{ + fnName, + callbackFnName, + eventCode, + index, + result + }}); + + const callbackRes = task.runCallbackFn( + {to_int32_fn}(eventCode), + {to_int32_fn}(index), + {to_int32_fn}(result), + ); + unpacked = {unpack_callback_result_fn}(callbackRes); + callbackCode = unpacked[0]; + waitableSetRep = unpacked[1]; + }} + }} catch (err) {{ + {debug_log_fn}('[{driver_loop_fn}()] error while resolving in async driver loop', {{ + fnName, + callbackFnName, + eventCode, + index, + result, + err, + }}); + reject(err); + }} + }} + "#, + )); + } } } } diff --git a/crates/js-component-bindgen/src/intrinsics/p3/host.rs b/crates/js-component-bindgen/src/intrinsics/p3/host.rs index 3249dac8..ca1f5e88 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/host.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/host.rs @@ -32,7 +32,7 @@ pub enum HostIntrinsic { /// /// This intrinsic returns a combination of the initial call status and optionally /// the handle to a waitable that should be awaited until it's time to (if necessary), - /// packed into the same u64. + /// packed into the same u32. /// /// # Host Intrinsic implementation function /// @@ -40,10 +40,19 @@ pub enum HostIntrinsic { /// /// ```ts /// type i32 = number; + /// type u32 = number; /// type u64 = number; - /// function asyncStartCall(callbackIdx: i32, postReturnIdx: i32): u64; + /// type Args = { + /// postReturnIdx: number | null, + /// getPostReturnFn: () => function | null, + /// callbackIdx: number | null, + /// getCallbackFn: () => function | null, + /// }; + /// function asyncStartCall(args: Args, callee: function, paramCount: u32, resultCount: u32, flags: u32): u32; /// ``` /// + /// NOTE: args are gathered during Trampoline, and the rest of the arguments are fed in. + /// AsyncStartCall, /// Start of an sync call emitted by modules generated by wasmtime's @@ -107,15 +116,44 @@ impl HostIntrinsic { AsyncTaskIntrinsic::GlobalAsyncCurrentTaskIds.name(); let current_component_idx_globals = AsyncTaskIntrinsic::GlobalAsyncCurrentComponentIdxs.name(); - let get_or_create_async_state_fn = - Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState).name(); let current_task_get_fn = Intrinsic::AsyncTask(AsyncTaskIntrinsic::GetCurrentTask).name(); + let start_current_task = Intrinsic::AsyncTask(AsyncTaskIntrinsic::StartCurrentTask).name(); + // See: + // https://github.com/bytecodealliance/wasmtime/blob/e2f9ca6be1b06b5c5c9e78834d10b8132cea0c80/crates/wasmtime/src/runtime/component/concurrent.rs#L2033 + // https://github.com/bytecodealliance/wasmtime/blob/e2f9ca6be1b06b5c5c9e78834d10b8132cea0c80/crates/wasmtime/src/runtime/component/concurrent.rs#L3519 + // + // We don't have `resultCountOrMax`, that's used by wasmtime upstream + + // TODO: arguments for prepare came in like this: + // + // PREPARING! [ null, [Function: 4], [Function: 5], 4, 1, 1, 0, -1, 600n ] + // + // - everything to callee idx is right + // - taskReturnTypeidx is 1 (??) + // - stringEncoding is 0 is right, it's 0 according to rust + // - storagePtr -1 means no storage??? + // - storageLen is actually the parameter in this case, already processed. + // - this means we need to chop from there on if we're @ -1? need to get all args, up to max async + // - should probably check we don't ever get over max async params... + + // TODO: Finish implementing prepare properly output.push_str(&format!( " - function {prepare_call_fn}(memoryIdx) {{ + function {prepare_call_fn}( + memoryIdx, + startFn, + returnFn, + callerInstanceIdx, + calleeInstanceIdx, + taskReturnTypeIdx, + stringEncoding, + storagePtr, + storageLen, + ) {{ {debug_log_fn}('[{prepare_call_fn}()] args', {{ memoryIdx }}); + const argArray = [...arguments]; const taskMeta = {current_task_get_fn}({current_component_idx_globals}.at(-1), {current_async_task_id_globals}.at(-1)); if (!taskMeta) {{ throw new Error('invalid/missing current async task meta during prepare call'); }} @@ -123,13 +161,57 @@ impl HostIntrinsic { const task = taskMeta.task; if (!task) {{ throw new Error('unexpectedly missing task in task meta during prepare call'); }} - const state = {get_or_create_async_state_fn}(task.componentIdx()); - if (!state) {{ - throw new Error('invalid/missing async state for component instance [' + componentInstanceID + ']'); + if (task.componentIdx() !== callerInstanceIdx) {{ + throw new Error(`task component idx [${{ task.componentIdx() }}] differs from caller [${{ callerInstanceIdx }}]`); }} + const directParams = storagePtr === -1; + let getCalleeParamsFn; + if (directParams) {{ + const directParamsArr = argArray.slice(8); + getCalleeParamsFn = () => directParamsArr; + }} else {{ + if (memoryIdx === null) {{ throw new Error('memory index not supplied to prepare depsite indirect params being used'); }} + + // TODO: call startFn() which lifts parameters into given space + // + // TODO: if we're using indirect params/the max number of params, + // then the last param is actually a return pointer? + throw new Error(`indirect parameter loading not yet supported`); + }} + + let encoding; + switch (stringEncoding) {{ + case 0: + encoding = 'utf8'; + break; + case 1: + encoding = 'utf16'; + break; + case 2: + encoding = 'compact-utf16'; + break; + default: + throw new Error(`unrecognized string encoding enum [${{stringEncoding}}]`); + }} + + const [newTask, newTaskID] = {start_current_task}({{ + componentIdx: calleeInstanceIdx, + isAsync: true, // TODO: we don't know if this task corresponds to an async function or not? + getCalleeParamsFn, + stringEncoding, + }}); + const subtask = task.createSubtask({{ - memoryIdx, + componentIdx: task.componentIdx(), + parentTask: task, + childTask: newTask, + }}); + + newTask.setParentSubtask(subtask); + + newTask.completionPromise().then(() => {{ + // TODO: run return function when the task finishes and the return is ready to be saved }}); }} @@ -137,15 +219,11 @@ impl HostIntrinsic { )); } - // AsyncStartCall is called just before an async-lowered import (host/another component), - // is called from inside a component. + // AsyncStartCall is called just before an async-lowered import from component "A" + // is called from inside component "B" (both host->guest and guest->guest calls). // - // It's primary function is to actually start the Subtask that should have been prepared at - // this point via PrepareCall, and to return the intial code made available by the subtask - // which will prompt the caller to take action, depending on the output. - // - // For example, if the async lowered import is called and it immediately returns RETURNED, - // Then the caller will know that the callee has completed and can act accordingly. + // We don't need to do much here, because async `Task`s are created during execution of + // CallWasm/CallInterface, rather than here. // Self::AsyncStartCall => { let debug_log_fn = Intrinsic::DebugLog.name(); @@ -156,26 +234,101 @@ impl HostIntrinsic { AsyncTaskIntrinsic::GlobalAsyncCurrentComponentIdxs.name(); let current_task_get_fn = Intrinsic::AsyncTask(AsyncTaskIntrinsic::GetCurrentTask).name(); - - output.push_str(&format!(" - function {async_start_call_fn}(callbackIdx, postReturnIdx) {{ - {debug_log_fn}('[{async_start_call_fn}()] args', {{ callbackIdx, postReturnIdx }}); + let get_or_create_async_state_fn = + Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState).name(); + let async_event_code_enum = Intrinsic::AsyncEventCodeEnum.name(); + let async_driver_loop_fn = Intrinsic::AsyncTask(AsyncTaskIntrinsic::DriverLoop).name(); + output.push_str(&format!(r#" + function {async_start_call_fn}(args, callee, paramCount, resultCount, flags) {{ + const {{ getCallbackFn, callbackIdx, getPostReturnFn, postReturnIdx }} = args; + {debug_log_fn}('[{async_start_call_fn}()] args', args); const taskMeta = {current_task_get_fn}({current_component_idx_globals}.at(-1), {current_async_task_id_globals}.at(-1)); if (!taskMeta) {{ throw new Error('invalid/missing current async task meta during prepare call'); }} - const task = taskMeta.task; - if (!task) {{ throw new Error('unexpectedly missing task in task meta during prepare call'); }} + // NOTE: at this point we know the current task is the one that was started + // in PrepareCall, so we *should* be able to pop it back off and be left with + // the previous task + const preparedTask = taskMeta.task; + if (!preparedTask) {{ throw new Error('unexpectedly missing task in task meta during prepare call'); }} + + if (resultCount < 0 || resultCount > 1) {{ throw new Error('invalid/unsupported result count'); }} + if (resultCount === 1) {{ + // TODO: signal to the task that the last param is a result pointer + }} + + preparedTask.setCallbackFn(getCallbackFn(), 'callback_' + callbackIdx); + preparedTask.setPostReturnFn(getPostReturnFn()); + + const subtask = preparedTask.getParentSubtask(); + + if (resultCount < 0 || resultCount > 1) {{ throw new Error(`unsupported result count [${{ resultCount }}]`); }} - const subtask = task.currentSubtask(); - if (!subtask) {{ throw new Error('invalid/missing subtask during async start call'); }} + // TODO: handle paramCount + // TODO: handle resultCount + // TODO: parse flags + + const params = preparedTask.getCalleeParams(); + // TODO: double-check the result count expectation here (lifted vs core values? result ptr?) + const expectedParamCount = resultCount > 1 ? params.length - 1 : params.length; + if (paramCount !== expectedParamCount) {{ + throw new Error(`unexpected param count [${{ paramCount }}], expected [${{ expectedParamCount }}]`); + }} + + let callbackResult = callee.apply(null, params); + + // If a single call resolved the subtask, we can return immediately + if (subtask.resolved()) {{ + subtask.deliverResolve(); + return Subtask.State.RETURNED; + }} + + const subtaskState = subtask.getStateNumber(); + if (subtaskState < 0 || subtaskState > 2**5) {{ + throw new Error('invalid substack state, out of valid range'); + }} + + const callerComponentState = {get_or_create_async_state_fn}(subtask.componentIdx()); + const rep = callerComponentState.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(), + }} + }}); + }}); + + subtask.onStart(); + + new Promise(async (resolve, reject) => {{ + // TODO: result count space must be reserved in memory, will either + // be a callback code or nothing? + + newTask.completionPromise().then(v => {{ + subtask.onResolve(v); + }}); + + {async_driver_loop_fn}({{ + componentState, + task: newTask, + fnName: '', + isAsync: true, + callbackResult, + resolve, + reject + }}); + }}); - return Number(subtask.waitableRep()) << 4 | subtask.getStateNumber(); + return Number(subtask.waitableRep()) << 4 | subtaskState; }} - ")); + "#)); } - // TODO: implement Self::SyncStartCall => { let debug_log_fn = Intrinsic::DebugLog.name(); let sync_start_call_fn = Self::SyncStartCall.name(); @@ -183,6 +336,7 @@ impl HostIntrinsic { " function {sync_start_call_fn}(callbackIdx) {{ {debug_log_fn}('[{sync_start_call_fn}()] args', {{ callbackIdx }}); + throw new Error('synchronous start call not implemented!'); }} " )); diff --git a/crates/js-component-bindgen/src/intrinsics/p3/waitable.rs b/crates/js-component-bindgen/src/intrinsics/p3/waitable.rs index 59f3f495..79785b2b 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/waitable.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/waitable.rs @@ -171,9 +171,12 @@ impl WaitableIntrinsic { this.#componentInstanceID = componentInstanceID; }} - numWaitables() {{ return this.#waitable.length; }} + numWaitables() {{ return this.#waitables.length; }} numWaiting() {{ return this.#waiting; }} + incrementNumWaiting(n) {{ this.#waiting += n ?? 1; }} + decrementNumWaiting(n) {{ this.#waiting -= n ?? 1; }} + shuffleWaitables() {{ this.#waitables = this.#waitables .map(value => ({{ value, sort: Math.random() }})) @@ -181,6 +184,33 @@ impl WaitableIntrinsic { .map(({{ value }}) => value); }} + removeWaitable(waitable) {{ + const existing = this.#waitables.find(w => w === waitable); + if (!existing) {{ return undefined; }} + this.#waitables = this.#waitables.filter(w => w !== waitable); + return waitable; + }} + + addWaitable(waitable) {{ + this.removeWaitable(waitable); + this.#waitables.push(waitable); + }} + + hasPendingEvent() {{ + {debug_log_fn}('[{waitable_set_class}#hasPendingEvent()] args', {{ }}); + const waitable = this.#waitables.find(w => w.hasPendingEvent()); + return waitable !== undefined; + }} + + getPendingEvent() {{ + {debug_log_fn}('[{waitable_set_class}#getPendingEvent()] args', {{ }}); + for (const waitable of this.#waitables) {{ + if (!waitable.hasPendingEvent()) {{ continue; }} + return waitable.getPendingEvent(); + }} + throw new Error('no waitables had a pending event'); + }} + async poll() {{ {debug_log_fn}('[{waitable_set_class}#poll()] args', {{ }}); @@ -207,26 +237,31 @@ impl WaitableIntrinsic { let waitable_class = Self::WaitableClass.name(); let get_or_create_async_state_fn = Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState).name(); - // TODO: remove the public mutable members that are eagerly exposed for early impl output.push_str(&format!(" class {waitable_class} {{ #componentInstanceID; - #pendingEvent; + #pendingEventFn = null; #waitableSet; + #promise; - constructor(componentInstanceID) {{ + constructor({{ promise, componentInstanceID }}) {{ this.#componentInstanceID = componentInstanceID; + this.#promise = promise; }} hasPendingEvent() {{ - return !!this.#pendingEvent; + return this.#pendingEventFn !== null; + }} + + setPendingEventFn(fn) {{ + this.#pendingEventFn = fn; }} getPendingEvent() {{ {debug_log_fn}('[{waitable_class}#getPendingEvent()] args', {{ }}); - if (!this.#pendingEvent) {{ return null; }} - const e = this.#pendingEvent; - this.#pendingEvent = null; + if (this.#pendingEventFn === null) {{ return null; }} + const e = this.#pendingEventFn(); + this.#pendingEventFn = null; return e; }} @@ -251,11 +286,12 @@ impl WaitableIntrinsic { }} join(waitableSet) {{ - if (waitableSet) {{ - waitableSet.waitables = waitableSet.waitables.filter(w => w !== this); - waitableSet.waitables.push(this); + if (!waitableSet) {{ + this.#waitableSet = null; + return; }} - this.waitableSet = waitableSet; + waitableSet.addWaitable(this); + this.#waitableSet = waitableSet; }} }} ")); @@ -265,14 +301,15 @@ impl WaitableIntrinsic { let debug_log_fn = Intrinsic::DebugLog.name(); let get_or_create_async_state_fn = Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState).name(); + let waitable_set_class = Self::WaitableSetClass.name(); let waitable_set_new_fn = Self::WaitableSetNew.name(); output.push_str(&format!(" function {waitable_set_new_fn}(componentInstanceID) {{ {debug_log_fn}('[{waitable_set_new_fn}()] args', {{ componentInstanceID }}); const state = {get_or_create_async_state_fn}(componentInstanceID); if (!state) {{ throw new Error('invalid/missing async state for component instance [' + componentInstanceID + ']'); }} - const rep = state.waitableSets.insert({{ waitables: [] }}); - if (typeof rep !== 'number') {{ throw new Error('invalid/missing waitable set rep'); }} + const rep = state.waitableSets.insert(new {waitable_set_class}(componentInstanceID)); + if (typeof rep !== 'number') {{ throw new Error('invalid/missing waitable set rep [' + rep + ']'); }} return rep; }} ")); @@ -350,7 +387,9 @@ impl WaitableIntrinsic { {debug_log_fn}('[{remove_waitable_set_fn}()] args', {{ componentInstanceID, waitableSetRep }}); const ws = state.waitableSets.get(waitableSetRep); - if (!ws) {{ throw new Error('missing/invalid waitable set specified for removal'); }} + if (!ws) {{ + throw new Error('cannot remove waitable set: no set present with rep [' + waitableSetRep + ']'); + }} if (waitableSet.hasPendingEvent()) {{ throw new Error('waitable set cannot be removed with pending items remaining'); }} const waitableSet = state.waitableSets.get(waitableSetRep); @@ -371,8 +410,6 @@ impl WaitableIntrinsic { let waitable_join_fn = Self::WaitableJoin.name(); let get_or_create_async_state_fn = Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState).name(); - // let current_task_get_fn = - // Intrinsic::AsyncTask(AsyncTaskIntrinsic::GetCurrentTask).name(); output.push_str(&format!(" function {waitable_join_fn}(componentInstanceID, waitableRep, waitableSetRep) {{ {debug_log_fn}('[{waitable_join_fn}()] args', {{ componentInstanceID, waitableSetRep, waitableRep }}); diff --git a/crates/js-component-bindgen/src/lib.rs b/crates/js-component-bindgen/src/lib.rs index e82a1df8..b92d6b13 100644 --- a/crates/js-component-bindgen/src/lib.rs +++ b/crates/js-component-bindgen/src/lib.rs @@ -19,10 +19,12 @@ mod ts_bindgen; pub mod esm_bindgen; pub mod function_bindgen; -pub mod intrinsics; pub mod names; pub mod source; +pub mod intrinsics; +use intrinsics::Intrinsic; + use transpile_bindgen::transpile_bindgen; pub use transpile_bindgen::{AsyncMode, BindingsMode, InstantiationMode, TranspileOpts}; @@ -296,7 +298,8 @@ pub(crate) fn requires_async_porcelain( let name = match func { FunctionIdentifier::Fn(func) => func.name.as_str(), FunctionIdentifier::CanonFnName(name) => name, - }; + } + .trim_start_matches("[async]"); if async_funcs.contains(name) { return true; @@ -317,3 +320,9 @@ pub(crate) fn requires_async_porcelain( } false } + +/// Objects that can control the printing/setup of intrinsics (normally in some final codegen output) +trait ManagesIntrinsics { + /// Add an intrinsic, supplying it's name afterwards + fn add_intrinsic(&mut self, intrinsic: Intrinsic); +} diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index 3e5ca6e7..1a209b47 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -47,8 +47,8 @@ use crate::intrinsics::{ }; use crate::names::{LocalNames, is_js_reserved_word, maybe_quote_id, maybe_quote_member}; use crate::{ - FunctionIdentifier, core, get_thrown_type, is_async_fn, requires_async_porcelain, source, - uwrite, uwriteln, + FunctionIdentifier, ManagesIntrinsics, core, get_thrown_type, is_async_fn, + requires_async_porcelain, source, uwrite, uwriteln, }; /// Number of flat parameters allowed before spilling over to memory @@ -162,6 +162,9 @@ struct JsBindgen<'a> { /// List of all core Wasm exported functions (and if is async) referenced in /// `src` so far. + /// + /// The second boolean is true when async procelain is required *or* if the + /// export itself is async. all_core_exported_funcs: Vec<(String, bool)>, } @@ -189,6 +192,12 @@ struct JsFunctionBindgenArgs<'a> { is_async: bool, } +impl<'a> ManagesIntrinsics for JsBindgen<'a> { + fn add_intrinsic(&mut self, intrinsic: Intrinsic) { + self.intrinsic(intrinsic); + } +} + #[allow(clippy::too_many_arguments)] pub fn transpile_bindgen( name: &str, @@ -397,6 +406,7 @@ impl JsBindgen<'_> { ); } + // Render all imports let imports_object = if self.opts.instantiation.is_some() { Some("imports") } else { @@ -405,6 +415,7 @@ impl JsBindgen<'_> { self.esm_bindgen .render_imports(&mut output, imports_object, &mut self.local_names); + // Create instantiation code if self.opts.instantiation.is_some() { uwrite!(&mut self.src.js, "{}", &core_exported_funcs as &str); self.esm_bindgen.render_exports( @@ -570,6 +581,12 @@ struct Instantiator<'a, 'b> { PrimaryMap, } +impl<'a> ManagesIntrinsics for Instantiator<'a, '_> { + fn add_intrinsic(&mut self, intrinsic: Intrinsic) { + self.bindgen.intrinsic(intrinsic); + } +} + impl<'a> Instantiator<'a, '_> { fn initialize(&mut self) { // Populate reverse map from import and export names to world items @@ -1613,11 +1630,11 @@ impl<'a> Instantiator<'a, '_> { .intrinsic(Intrinsic::Host(HostIntrinsic::PrepareCall)); uwriteln!( self.src.js, - "const trampoline{i} = {prepare_call_fn}.bind(null, {});", - memory + "const trampoline{i} = {prepare_call_fn}.bind(null, {memory});", + memory = memory .map(|v| v.as_u32().to_string()) .unwrap_or_else(|| "null".into()), - ); + ) } Trampoline::SyncStartCall { callback } => { @@ -1633,38 +1650,133 @@ impl<'a> Instantiator<'a, '_> { ); } - // This actually starts a subtask for a from-component async import call + // This actually starts a Task (whose parent is a subtask generated during PrepareCall) + // for a from-component async import call Trampoline::AsyncStartCall { callback, post_return, } => { - // TODO: Need to create a subtask here - // AsyncStartCall means an async call originating from a Component? (either to component or host?) - // - // Subtask needs to also somehow write itself in as the current subtask? - // let async_start_call_fn = self .bindgen .intrinsic(Intrinsic::Host(HostIntrinsic::AsyncStartCall)); + let (callback_idx, callback_fn) = callback + .map(|v| (v.as_u32().to_string(), format!("callback_{}", v.as_u32()))) + .unwrap_or_else(|| ("null".into(), "null".into())); + let (post_return_idx, post_return_fn) = post_return + .map(|v| { + ( + v.as_u32().to_string(), + format!("post_return_{}", 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(null, {}, {});", - callback - .map(|v| v.as_u32().to_string()) - .unwrap_or_else(|| "null".into()), - post_return - .map(|v| v.as_u32().to_string()) - .unwrap_or_else(|| "null".into()), + "const trampoline{i} = {async_start_call_fn}.bind( + null, + {{ + postReturnIdx: {post_return_idx}, + getPostReturnFn: () => {post_return_fn}, + callbackIdx: {callback_idx}, + getCallbackFn: () => {callback_fn}, + getCallee: () => {callback_fn}, + }}, + );", ); } + // 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 _ = (index, lower_ty, options); - // TODO: implement (can't build without, empty does work) + let canon_opts = self + .component + .options + .get(*options) + .expect("failed to find options"); + + let fn_idx = index.as_u32(); + + let lower_import_fn = self + .bindgen + .intrinsic(Intrinsic::Component(ComponentIntrinsic::LowerImport)); + + let _ = (lower_ty, canon_opts); + + // 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! + + // 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(...) + // ``` + + + // 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. + + // NOTE: this means that start_call is a guest->guest *only* thing previously prepared + // In our case the only valid thign is going to + // + // The functionidx is useless it seems, trampoline idx *does* match though + + uwriteln!( + self.src.js, + "const trampoline_lower_{i} = {lower_import_fn}.bind( + null, + {{ + functionIdx: {fn_idx}, + }}, + );", + ); } Trampoline::AlwaysTrap => { @@ -1996,6 +2108,9 @@ impl<'a> Instantiator<'a, '_> { } let lift_fns_js = format!("[{}]", lift_fns.join(",")); + let memory_idx_js = memory + .map(|v| v.as_u32().to_string()) + .unwrap_or_else(|| "null".into()); let memory_js = memory .map(|idx| format!("memory{}", idx.as_u32())) .unwrap_or_else(|| "null".into()); @@ -2011,11 +2126,15 @@ impl<'a> Instantiator<'a, '_> { self.src.js, "const trampoline{i} = {task_return_fn}.bind( null, - {component_idx}, - {use_direct_params}, - {memory_js}, - {callback_fn_idx}, - {lift_fns_js}, + {{ + componentIdx: {component_idx}, + useDirectParams: {use_direct_params}, + memoryIdx: {memory_idx_js}, + memory: {memory_js}, + callbackFnIdx: {callback_fn_idx}, + liftFns: {lift_fns_js}, + isAsync: {async_}, + }}, );", ); } @@ -2216,6 +2335,7 @@ impl<'a> Instantiator<'a, '_> { let (import_name, _) = &self.component.import_types[*import_index]; let world_key = &self.imports[import_name]; + // Determine the name of the function let (func, func_name, iface_name) = match &self.resolve.worlds[self.world].imports[world_key] { WorldItem::Function(func) => { @@ -2234,6 +2354,7 @@ 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); @@ -2423,7 +2544,7 @@ impl<'a> Instantiator<'a, '_> { uwriteln!(self.src.js, ""); // Write new function ending - if requires_async_porcelain { + if requires_async_porcelain | is_async { uwriteln!(self.src.js, ");"); } else { uwriteln!(self.src.js, ""); @@ -2554,9 +2675,22 @@ impl<'a> Instantiator<'a, '_> { // Figure out the function name and callee (e.g. class for a given resource) to use let (import_name, binding_name) = match func.kind { - FunctionKind::Freestanding | FunctionKind::AsyncFreestanding => { - (func_name.to_lower_camel_case(), callee_name) - } + FunctionKind::Freestanding | FunctionKind::AsyncFreestanding => ( + // TODO: if we want to avoid the naming of 'async' (e.g. 'asyncSleepMillis' + // vs 'sleepMillis' which just *is* an imported async function).... + // + // We need to use the code below: + // + // func_name + // .strip_prefix("[async]") + // .unwrap_or(func_name) + // .to_lower_camel_case(), + // + // This has the potential to break a lot of downstream consumers who are expecting to + // provide 'async`, so it must be done before a breaking change. + func_name.to_lower_camel_case(), + callee_name, + ), FunctionKind::Method(tid) | FunctionKind::AsyncMethod(tid) @@ -3135,6 +3269,20 @@ impl<'a> Instantiator<'a, '_> { ); } + // Every call to a component export should create a new Task + let is_guest_top_level_export = matches!( + func.kind, + FunctionKind::Freestanding | FunctionKind::AsyncFreestanding + ) && matches!( + abi, + AbiVariant::GuestExport + | AbiVariant::GuestExportAsync + | AbiVariant::GuestExportAsyncStackful + ); + if is_guest_top_level_export { + // TODO: create a new Task for this export call + } + // Generate function body let mut f = FunctionBindgen { resource_map, @@ -3526,7 +3674,8 @@ impl<'a> Instantiator<'a, '_> { ); } - let maybe_async = if requires_async_porcelain { + let is_async = is_async_fn(func, options); + let maybe_async = if requires_async_porcelain || is_async { "async " } else { "" @@ -3545,6 +3694,12 @@ impl<'a> Instantiator<'a, '_> { uwriteln!(self.src.js, "let {local_name};"); self.bindgen .all_core_exported_funcs + // NOTE: this breaks because using WebAssembly.promising and trying to + // await JS from the host is a bug ("trying to suspend JS frames") + // + // We trigger this either with --async-exports *OR* by widening the check as below + // + // .push((core_export_fn.clone(), requires_async_porcelain || is_async)); .push((core_export_fn.clone(), requires_async_porcelain)); local_name } @@ -3651,7 +3806,7 @@ impl<'a> Instantiator<'a, '_> { remote_resource_map: export_remote_resource_map, abi: AbiVariant::GuestExport, requires_async_porcelain, - is_async: is_async_fn(func, options), + is_async, }); // End the function @@ -3796,8 +3951,8 @@ fn string_encoding_js_literal(val: &wasmtime_environ::component::StringEncoding) /// /// The intrinsic it guaranteed to be in scope once execution time because it wlil be used in the relevant branch. /// -fn gen_flat_lift_fn_js_expr( - bindgen: &mut JsBindgen<'_>, +pub fn gen_flat_lift_fn_js_expr( + intrinsic_mgr: &mut impl ManagesIntrinsics, component_types: &ComponentTypes, ty: &InterfaceType, canon_opts: &CanonicalOptions, @@ -3805,35 +3960,78 @@ fn gen_flat_lift_fn_js_expr( //let ty_abi = component_types.canonical_abi(ty); let string_encoding = canon_opts.string_encoding; match ty { - InterfaceType::Bool => bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatBool)), - InterfaceType::S8 => bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatS8)), - InterfaceType::U8 => bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatU8)), - InterfaceType::S16 => bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatS16)), - InterfaceType::U16 => bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatU16)), - InterfaceType::S32 => bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatS32)), - InterfaceType::U32 => bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatU32)), - InterfaceType::S64 => bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatS64)), - InterfaceType::U64 => bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatU64)), + InterfaceType::Bool => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatBool)); + Intrinsic::Lift(LiftIntrinsic::LiftFlatBool).name().into() + } + InterfaceType::S8 => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatS8)); + Intrinsic::Lift(LiftIntrinsic::LiftFlatS8).name().into() + } + InterfaceType::U8 => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatU8)); + Intrinsic::Lift(LiftIntrinsic::LiftFlatU8).name().into() + } + InterfaceType::S16 => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatS16)); + Intrinsic::Lift(LiftIntrinsic::LiftFlatS16).name().into() + } + InterfaceType::U16 => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatU16)); + Intrinsic::Lift(LiftIntrinsic::LiftFlatU16).name().into() + } + InterfaceType::S32 => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatS32)); + Intrinsic::Lift(LiftIntrinsic::LiftFlatS32).name().into() + } + InterfaceType::U32 => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatU32)); + Intrinsic::Lift(LiftIntrinsic::LiftFlatU32).name().into() + } + InterfaceType::S64 => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatS64)); + Intrinsic::Lift(LiftIntrinsic::LiftFlatS64).name().into() + } + InterfaceType::U64 => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatU64)); + Intrinsic::Lift(LiftIntrinsic::LiftFlatU64).name().into() + } InterfaceType::Float32 => { - bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatFloat32)) + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatFloat32)); + Intrinsic::Lift(LiftIntrinsic::LiftFlatFloat32) + .name() + .into() } InterfaceType::Float64 => { - bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatFloat64)) + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatFloat64)); + Intrinsic::Lift(LiftIntrinsic::LiftFlatFloat64) + .name() + .into() + } + InterfaceType::Char => { + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatChar)); + Intrinsic::Lift(LiftIntrinsic::LiftFlatChar).name().into() } - InterfaceType::Char => bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatChar)), InterfaceType::String => match string_encoding { wasmtime_environ::component::StringEncoding::Utf8 => { - bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatStringUtf8)) + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatStringUtf8)); + Intrinsic::Lift(LiftIntrinsic::LiftFlatStringUtf8) + .name() + .into() } wasmtime_environ::component::StringEncoding::Utf16 => { - bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatStringUtf16)) + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatStringUtf16)); + Intrinsic::Lift(LiftIntrinsic::LiftFlatStringUtf16) + .name() + .into() } wasmtime_environ::component::StringEncoding::CompactUtf16 => { todo!("latin1+utf8 not supported") } }, InterfaceType::Record(ty_idx) => { - let lift_fn = bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatRecord)); + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatRecord)); + let lift_fn = Intrinsic::Lift(LiftIntrinsic::LiftFlatRecord).name(); let record_ty = &component_types[*ty_idx]; let mut keys_and_lifts_expr = String::from("["); for f in &record_ty.fields { @@ -3843,7 +4041,7 @@ fn gen_flat_lift_fn_js_expr( keys_and_lifts_expr.push_str(&format!( "['{}', {}, {}],", f.name, - gen_flat_lift_fn_js_expr(bindgen, component_types, &f.ty, canon_opts), + gen_flat_lift_fn_js_expr(intrinsic_mgr, component_types, &f.ty, canon_opts), component_types.canonical_abi(ty).size32, )); } @@ -3851,7 +4049,8 @@ fn gen_flat_lift_fn_js_expr( format!("{lift_fn}({keys_and_lifts_expr})") } InterfaceType::Variant(ty_idx) => { - let lift_fn = bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatVariant)); + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatVariant)); + let lift_fn = Intrinsic::Lift(LiftIntrinsic::LiftFlatVariant).name(); let variant_ty = &component_types[*ty_idx]; let mut cases_and_lifts_expr = String::from("["); for (name, maybe_ty) in &variant_ty.cases { @@ -3861,7 +4060,7 @@ fn gen_flat_lift_fn_js_expr( maybe_ty .as_ref() .map(|ty| gen_flat_lift_fn_js_expr( - bindgen, + intrinsic_mgr, component_types, ty, canon_opts @@ -3875,25 +4074,31 @@ fn gen_flat_lift_fn_js_expr( )); } cases_and_lifts_expr.push(']'); - format!("{lift_fn}({cases_and_lifts_expr})",) + format!("{lift_fn}({cases_and_lifts_expr})") } InterfaceType::List(_ty_idx) => { - bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatList)) + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatList)); + Intrinsic::Lift(LiftIntrinsic::LiftFlatList).name().into() } InterfaceType::Tuple(_ty_idx) => { - bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatTuple)) + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatTuple)); + Intrinsic::Lift(LiftIntrinsic::LiftFlatTuple).name().into() } InterfaceType::Flags(_ty_idx) => { - bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatFlags)) + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatFlags)); + Intrinsic::Lift(LiftIntrinsic::LiftFlatFlags).name().into() } InterfaceType::Enum(_ty_idx) => { - bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatEnum)) + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatEnum)); + Intrinsic::Lift(LiftIntrinsic::LiftFlatEnum).name().into() } InterfaceType::Option(_ty_idx) => { - bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatOption)) + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatOption)); + Intrinsic::Lift(LiftIntrinsic::LiftFlatOption).name().into() } InterfaceType::Result(ty_idx) => { - let lift_fn = bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatResult)); + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatResult)); + let lift_fn = Intrinsic::Lift(LiftIntrinsic::LiftFlatResult).name(); let result_ty = &component_types[*ty_idx]; let mut cases_and_lifts_expr = String::from("["); cases_and_lifts_expr.push_str(&format!( @@ -3902,7 +4107,12 @@ fn gen_flat_lift_fn_js_expr( result_ty .ok .as_ref() - .map(|ty| gen_flat_lift_fn_js_expr(bindgen, component_types, ty, canon_opts)) + .map(|ty| gen_flat_lift_fn_js_expr( + intrinsic_mgr, + component_types, + ty, + canon_opts + )) .unwrap_or(String::from("null")), result_ty .ok @@ -3917,7 +4127,12 @@ fn gen_flat_lift_fn_js_expr( result_ty .err .as_ref() - .map(|ty| gen_flat_lift_fn_js_expr(bindgen, component_types, ty, canon_opts)) + .map(|ty| gen_flat_lift_fn_js_expr( + intrinsic_mgr, + component_types, + ty, + canon_opts + )) .unwrap_or(String::from("null")), result_ty .err @@ -3928,22 +4143,29 @@ fn gen_flat_lift_fn_js_expr( )); cases_and_lifts_expr.push(']'); - format!("{lift_fn}({cases_and_lifts_expr})",) + format!("{lift_fn}({cases_and_lifts_expr})") } InterfaceType::Own(_ty_idx) => { - bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatOwn)) + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatOwn)); + Intrinsic::Lift(LiftIntrinsic::LiftFlatOwn).name().into() } InterfaceType::Borrow(_ty_idx) => { - bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatOwn)) + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatOwn)); + Intrinsic::Lift(LiftIntrinsic::LiftFlatOwn).name().into() } InterfaceType::Future(_ty_idx) => { - bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatFuture)) + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatFuture)); + Intrinsic::Lift(LiftIntrinsic::LiftFlatFuture).name().into() } InterfaceType::Stream(_ty_idx) => { - bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatStream)) + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatStream)); + Intrinsic::Lift(LiftIntrinsic::LiftFlatStream).name().into() } InterfaceType::ErrorContext(_ty_idx) => { - bindgen.intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatErrorContext)) + intrinsic_mgr.add_intrinsic(Intrinsic::Lift(LiftIntrinsic::LiftFlatErrorContext)); + Intrinsic::Lift(LiftIntrinsic::LiftFlatErrorContext) + .name() + .into() } } } diff --git a/packages/jco/test/fixtures/components/p3/backpressure/async_backpressure_callee.component.wasm b/packages/jco/test/fixtures/components/p3/backpressure/async-backpressure-callee.wasm similarity index 100% rename from packages/jco/test/fixtures/components/p3/backpressure/async_backpressure_callee.component.wasm rename to packages/jco/test/fixtures/components/p3/backpressure/async-backpressure-callee.wasm diff --git a/packages/jco/test/fixtures/components/p3/backpressure/async_backpressure_caller.component.wasm b/packages/jco/test/fixtures/components/p3/backpressure/async-backpressure-caller.wasm similarity index 100% rename from packages/jco/test/fixtures/components/p3/backpressure/async_backpressure_caller.component.wasm rename to packages/jco/test/fixtures/components/p3/backpressure/async-backpressure-caller.wasm diff --git a/packages/jco/test/fixtures/components/p3/general/async_post_return_callee.component.wasm b/packages/jco/test/fixtures/components/p3/general/async-post-return-callee.wasm similarity index 100% rename from packages/jco/test/fixtures/components/p3/general/async_post_return_callee.component.wasm rename to packages/jco/test/fixtures/components/p3/general/async-post-return-callee.wasm diff --git a/packages/jco/test/fixtures/components/p3/general/async_post_return_caller.component.wasm b/packages/jco/test/fixtures/components/p3/general/async-post-return-caller.wasm similarity index 100% rename from packages/jco/test/fixtures/components/p3/general/async_post_return_caller.component.wasm rename to packages/jco/test/fixtures/components/p3/general/async-post-return-caller.wasm diff --git a/packages/jco/test/fixtures/components/p3/general/async-sleep-post-return-callee.wasm b/packages/jco/test/fixtures/components/p3/general/async-sleep-post-return-callee.wasm new file mode 100644 index 00000000..3e3a9a34 Binary files /dev/null and b/packages/jco/test/fixtures/components/p3/general/async-sleep-post-return-callee.wasm differ diff --git a/packages/jco/test/fixtures/components/p3/general/async-sleep-post-return-caller.wasm b/packages/jco/test/fixtures/components/p3/general/async-sleep-post-return-caller.wasm new file mode 100644 index 00000000..f9f02bff Binary files /dev/null and b/packages/jco/test/fixtures/components/p3/general/async-sleep-post-return-caller.wasm differ diff --git a/packages/jco/test/fixtures/modules/hello_stdout.component.wasm b/packages/jco/test/fixtures/modules/hello_stdout.component.wasm index 9f1167f9..52787e2a 100644 Binary files a/packages/jco/test/fixtures/modules/hello_stdout.component.wasm and b/packages/jco/test/fixtures/modules/hello_stdout.component.wasm differ diff --git a/packages/jco/test/p3/ported/wasmtime/component-async/backpressure.js b/packages/jco/test/p3/ported/wasmtime/component-async/backpressure.js new file mode 100644 index 00000000..758f02dd --- /dev/null +++ b/packages/jco/test/p3/ported/wasmtime/component-async/backpressure.js @@ -0,0 +1,37 @@ +import { join } from 'node:path'; + +import { suite, test } from 'vitest'; + +import { buildAndTranspile, composeCallerCallee, COMPONENT_FIXTURES_DIR } from "./common.js"; + +// These tests are ported from upstream wasmtime's component-async-tests +// +// In the upstream wasmtime repo, see: +// wasmtime/crates/misc/component-async-tests/tests/scenario/backpressure.rs +// +suite('backpressure scenario', () => { + test('caller & callee', async () => { + const callerPath = join( + COMPONENT_FIXTURES_DIR, + "p3/backpressure/async-backpressure-caller.wasm" + ); + const calleePath = join( + COMPONENT_FIXTURES_DIR, + "p3/backpressure/async-backpressure-callee.wasm" + ); + const componentPath = await composeCallerCallee({ + callerPath, + calleePath, + }); + + let cleanup; + try { + const res = await buildAndTranspile({ componentPath }); + const instance = res.instance; + cleanup = res.cleanup; + await instance['local:local/run'].asyncRun(); + } finally { + if (cleanup) { 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 404aa940..0e433472 100644 --- a/packages/jco/test/p3/ported/wasmtime/component-async/common.js +++ b/packages/jco/test/p3/ported/wasmtime/component-async/common.js @@ -1,16 +1,19 @@ +import { fileURLToPath, URL } from 'node:url'; import { exec as syncExec } from "node:child_process"; import { promisify } from "node:util"; import { resolve } from "node:path"; import { stat } from "node:fs/promises"; -import { assert } from "vitest"; import which from "which"; import { setupAsyncTest, getTmpDir } from '../../../../helpers.js'; -import { AsyncFunction } from '../../../../common.js'; const exec = promisify(syncExec); +export const COMPONENT_FIXTURES_DIR = fileURLToPath( + new URL('../../../../fixtures/components', import.meta.url) +); + /** Ensure that the given file path exists */ async function ensureFile(filePath) { if (!filePath) { throw new Error("missing componentPath"); } @@ -22,23 +25,18 @@ async function ensureFile(filePath) { } /** - * Run a single component test for a component that - * exports `local:local/run` (normally async) in the style of - * wasmtime component-async-tests - * - * This test will generally transpile the component and then run its' 'local:local/run' export - * - * @see https://github.com/bytecodealliance/wasmtime/blob/main/crates/misc/component-async-tests/tests/scenario/util.rs + * Build and transpile a component for use in a test * * @param {object} args * @param {string} args.componentPath - path to the wasm binary that should be tested + * @param {object} args.noCleanup - avoid cleaning up the async test * @param {object} args.transpile - options to control transpile * @param {object} args.transpile.extraArgs - extra arguments that should be used during transpilation (ex. `{minify: false}`) * @returns {Promise} A Promise that resolves when the test completes */ -export async function testComponent(args) { +export async function buildAndTranspile(args) { const componentPath = await ensureFile(args.componentPath); - const { esModule, cleanup } = await setupAsyncTest({ + const { esModule, cleanup, outputDir } = await setupAsyncTest({ asyncMode: 'jspi', component: { name: 'async-error-context', @@ -48,8 +46,8 @@ export async function testComponent(args) { jco: { transpile: { extraArgs: { + asyncExports: ['local:local/run#run'] , ...(args.transpile?.extraArgs || {}), - asyncExports: ['local:local/run#run'], }, }, }, @@ -60,19 +58,18 @@ export async function testComponent(args) { ); const instance = await esModule.instantiate( undefined, - new WASIShim().getImportObject() + { + ...new WASIShim().getImportObject(), + ...(args.instantiation?.imports ?? {}), + } ); - const runFn = instance['local:local/run'].asyncRun; - assert.strictEqual( - runFn instanceof AsyncFunction, - true, - 'local:local/run should be async' - ); - - await runFn(); - - await cleanup(); + return { + instance, + esModule, + cleanup, + outputDir, + }; } /** diff --git a/packages/jco/test/p3/ported/wasmtime/component-async/error-context.js b/packages/jco/test/p3/ported/wasmtime/component-async/error-context.js index 9e8f7a1b..75e67ae0 100644 --- a/packages/jco/test/p3/ported/wasmtime/component-async/error-context.js +++ b/packages/jco/test/p3/ported/wasmtime/component-async/error-context.js @@ -1,13 +1,8 @@ import { join } from 'node:path'; -import { fileURLToPath, URL } from 'node:url'; import { suite, test } from 'vitest'; -import { testComponent, composeCallerCallee } from "./common.js"; - -const COMPONENT_FIXTURES_DIR = fileURLToPath( - new URL('../../../../fixtures/components', import.meta.url) -); +import { buildAndTranspile, composeCallerCallee, COMPONENT_FIXTURES_DIR } from "./common.js"; // These tests are ported from upstream wasmtime's component-async-tests // @@ -20,7 +15,16 @@ suite('error-context scenario', () => { COMPONENT_FIXTURES_DIR, 'p3/error-context/async-error-context.wasm' ); - await testComponent({ componentPath }); + + let cleanup; + try { + const res = await buildAndTranspile({ componentPath }); + const instance = res.instance; + cleanup = res.cleanup; + await instance['local:local/run'].asyncRun(); + } finally { + if (cleanup) { await cleanup(); } + } }); test('caller & callee', async () => { @@ -36,6 +40,15 @@ suite('error-context scenario', () => { callerPath, calleePath, }); - await testComponent({ componentPath }); + + let cleanup; + try { + const res = await buildAndTranspile({ componentPath }); + cleanup = res.cleanup; + const instance = res.instance; + await instance['local:local/run'].asyncRun(); + } finally { + if (cleanup) { await cleanup(); } + } }); }); diff --git a/packages/jco/test/p3/ported/wasmtime/component-async/post-return.js b/packages/jco/test/p3/ported/wasmtime/component-async/post-return.js new file mode 100644 index 00000000..e770bc05 --- /dev/null +++ b/packages/jco/test/p3/ported/wasmtime/component-async/post-return.js @@ -0,0 +1,120 @@ +import { join } from 'node:path'; + +import { suite, test, expect, vi } from 'vitest'; + +import { buildAndTranspile, composeCallerCallee, COMPONENT_FIXTURES_DIR } from "./common.js"; + +// These tests are ported from upstream wasmtime's component-async-tests +// +// In the upstream wasmtime repo, see: +// wasmtime/crates/misc/component-async-tests/tests/scenario/post_return.rs +// +suite.skip('post-return scenario', () => { + test('caller & callee', async () => { + const callerPath = join( + COMPONENT_FIXTURES_DIR, + "p3/general/async-post-return-caller.wasm" + ); + const calleePath = join( + COMPONENT_FIXTURES_DIR, + "p3/general/async-post-return-callee.wasm" + ); + const componentPath = await composeCallerCallee({ + callerPath, + calleePath, + }); + + let cleanup; + try { + const res = await buildAndTranspile({ componentPath }); + const instance = res.instance; + cleanup = res.cleanup; + await instance['local:local/run'].asyncRun(); + } finally { + if (cleanup) { await cleanup(); } + } + }); +}); + +// These tests are ported from upstream wasmtime's component-async-tests +// +// In the upstream wasmtime repo, see: +// wasmtime/crates/misc/component-async-tests/tests/scenario/post_return.rs +// +suite('post-return async sleep scenario', () => { + test('caller & callee', async () => { + const callerPath = join( + COMPONENT_FIXTURES_DIR, + "p3/general/async-sleep-post-return-caller.wasm" + ); + const calleePath = join( + COMPONENT_FIXTURES_DIR, + "p3/general/async-sleep-post-return-callee.wasm" + ); + const componentPath = await composeCallerCallee({ + callerPath, + calleePath, + }); + + const waitTimeMs = 300; + const asyncSleepMillis = vi.fn(async (ms) => { + // NOTE: as written, the caller/callee manipulate (double) the original wait time before use + expect(ms).toStrictEqual(BigInt(waitTimeMs * 2)); + if (ms > BigInt(Number.MAX_SAFE_INTEGER) || ms < BigInt(Number.MIN_SAFE_INTEGER)) { + throw new Error('wait time value cannot be represented safely as a Number'); + } + await new Promise((resolve) => setTimeout(resolve, Number(ms))); + }); + + let cleanup; + try { + const res = await buildAndTranspile({ + componentPath, + noCleanup: true, + instantiation: { + imports: { + 'local:local/sleep': { + // WIT: + // + // ``` + // sleep-millis: async func(time-in-millis: u64); + // ``` + // see: wasmtime/crates/misc/component-async-tests/wit/test.wit + asyncSleepMillis, + } + } + }, + transpile: { + extraArgs: { + minify: false, + asyncImports: [ + // Provided by the host + 'local:local/sleep#sleep-millis', + ], + asyncExports: [ + // NOTE: Provided by the component, but *does* trigger calling + // of the host-provided async improt + 'local:local/sleep-post-return#run', + ], + } + }, + }); + const instance = res.instance; + cleanup = res.cleanup; + + const result = await instance['local:local/sleep-post-return'].asyncRun(waitTimeMs); + expect(result).toBeUndefined(); + + // Although the original async export call has finished, we expect that the spawned task + // that occurred during it to run to completion (and eventually call the import we provided), + // in the runtime itself. + await vi.waitFor( + () => expect(asyncSleepMillis).toHaveBeenCalled(), + { timeout: 5_000 }, + ); + + } finally { + if (cleanup) { await cleanup(); } + } + }); +}); diff --git a/packages/jco/test/p3/transpile.js b/packages/jco/test/p3/transpile.js index fbfc7edf..607901ae 100644 --- a/packages/jco/test/p3/transpile.js +++ b/packages/jco/test/p3/transpile.js @@ -9,8 +9,8 @@ import { transpile } from '../../src/api'; import { P3_COMPONENT_FIXTURES_DIR } from '../common.js'; const P3_FIXTURE_COMPONENTS = [ - 'backpressure/async_backpressure_callee.component.wasm', - 'backpressure/async_backpressure_caller.component.wasm', + 'backpressure/async-backpressure-callee.wasm', + 'backpressure/async-backpressure-caller.wasm', 'sockets/tcp/p3_sockets_tcp_states.component.wasm', 'sockets/tcp/p3_sockets_tcp_sample_application.component.wasm', @@ -33,9 +33,13 @@ const P3_FIXTURE_COMPONENTS = [ 'cli/p3_cli.component.wasm', - 'general/async_post_return_caller.component.wasm', + 'general/async-post-return-caller.wasm', + 'general/async-post-return-callee.wasm', + + 'general/async-sleep-post-return-caller.wasm', + 'general/async-sleep-post-return-callee.wasm', + 'general/async_borrowing_caller.component.wasm', - 'general/async_post_return_callee.component.wasm', 'general/async_borrowing_callee.component.wasm', 'general/async_intertask_communication.component.wasm', 'general/async_transmit_callee.component.wasm', @@ -52,8 +56,8 @@ const P3_FIXTURE_COMPONENTS = [ 'round-trip/async_round_trip_many_stackless.component.wasm', 'round-trip/async_round_trip_stackless_sync_import.component.wasm', - 'backpressure/async_backpressure_caller.component.wasm', - 'backpressure/async_backpressure_callee.component.wasm', + 'backpressure/async-backpressure-caller.wasm', + 'backpressure/async-backpressure-callee.wasm', 'http/p3_http_outbound_request_unknown_method.component.wasm', 'http/p3_http_outbound_request_invalid_dnsname.component.wasm',