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..684bbfd6 100644 --- a/crates/js-component-bindgen/src/function_bindgen.rs +++ b/crates/js-component-bindgen/src/function_bindgen.rs @@ -6,9 +6,9 @@ use heck::{ToLowerCamelCase, ToUpperCamelCase}; use wasmtime_environ::component::{CanonicalOptions, ResourceIndex, TypeResourceTableIndex}; use wit_bindgen_core::abi::{Bindgen, Bitcast, Instruction}; use wit_component::StringEncoding; -use wit_parser::abi::WasmType; +use wit_parser::abi::{FlatTypes, 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 { @@ -336,6 +345,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; @@ -1215,9 +1231,15 @@ impl Bindgen for FunctionBindgen<'_> { // Inject machinery for starting an async 'current' task self.start_current_task(inst, self.is_async, self.callee); + // 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); uwriteln!( self.src, @@ -1231,6 +1253,13 @@ impl Bindgen for FunctionBindgen<'_> { } ); + uwriteln!( + self.src, + "console.log('===> DEBUG' , {{ callee: '{}', ret, isAsync: {} }});", + self.callee, + self.is_async, + ); // TODO: REMOVE + // Print post-return if tracing is enabled if self.tracing_enabled { let prefix = self.tracing_prefix; @@ -1247,9 +1276,23 @@ 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 self.is_async { + // If we're doing async, set up the variables that will be required eventually for + // the async task to make progress + assert!( + self.canon_opts.callback.is_some(), + "callback should be present for CallWasm on async functions" + ); + let callback_fn_name = self + .canon_opts + .callback + .map(|v| format!("callback_{}", v.as_u32())) + .expect(&format!("missing callback for async function {name}")); + uwriteln!(self.src, "const callbackFn = {callback_fn_name};"); + uwriteln!(self.src, "const callbackFnName = '{callback_fn_name}';"); + } else { + // If we're not dealing with an async call, we can immediately end the task + // after the call has completed. self.end_current_task(); } } @@ -1268,7 +1311,7 @@ impl Bindgen for FunctionBindgen<'_> { self.start_current_task(inst, *async_, &func.name); 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 +1358,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};"); } @@ -1421,14 +1464,39 @@ impl Bindgen for FunctionBindgen<'_> { ) }; + // 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 +1507,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 +1530,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 +1559,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 +2140,19 @@ 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. + // This is simply the end of the async function 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`) + // - `callbackFn`: the CPS style function that should be called to drive progress + // - `callbackFnName`: the name of the callback fn itself (usually "callback_") + // Instruction::AsyncTaskReturn { name, params } => { let debug_log_fn = self.intrinsic(Intrinsic::DebugLog); uwriteln!( @@ -2178,113 +2176,65 @@ impl Bindgen for FunctionBindgen<'_> { "async fn cannot have post_return specified (func {name})" ); + // 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 get_current_task_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;"); + } + // 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 = {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'); }} + + new Promise((resolve, reject) => {{ + {async_driver_loop_fn}({{ + componentInstanceIdx: {component_instance_idx}, + componentState, + task, + fnName: '{name}', + callbackFnName, + callbackFn, + 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..37774c76 100644 --- a/crates/js-component-bindgen/src/intrinsics/component.rs +++ b/crates/js-component-bindgen/src/intrinsics/component.rs @@ -144,6 +144,20 @@ impl ComponentIntrinsic { #parkedTasks = new Map(); + #suspendedTasksByTaskID = new Map(); + #suspendedTaskIDs = []; + #taskResumerInterval = null; + + constructor(args) {{ + this.#taskResumerInterval = setInterval(() => {{ + try {{ + this.resumeNextTask(); + }} catch (err) {{ + {debug_log_fn}('[{class_name}#taskResumer()] failed to resume next task', {{ err }}); + }} + }}, 10); + }}; + callingSyncImport(val) {{ if (val === undefined) {{ return this.#callingAsyncImport; }} if (typeof val !== 'boolean') {{ throw new TypeError('invalid setting for async import'); }} @@ -242,6 +256,57 @@ impl ComponentIntrinsic { isExclusivelyLocked() {{ return this.#lock !== null; }} + #getSuspendedTaskMeta(taskID) {{ + return this.#suspendedTasksByTaskID.get(taskID); + }} + + #removeSuspendedTaskMeta(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) {{ + const {{ task }} = args; + const taskID = task.id(); + + if (this.#getSuspendedTaskMeta(taskID)) {{ + throw new Error('task [' + taskID + '] already suspended'); + }} + + const {{ promise, resolve }} = Promise.withResolvers(); + this.#addSuspendedTaskMeta({{ resolve, taskID }}); + + 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.resolve(null); + }} + + resumeNextTask() {{ + const taskID = this.#suspendedTaskIDs.find(t => t !== null); + if (!taskID) {{ return; }} + this.resumeTaskByID(taskID); + }} }} ", class_name = self.name(), diff --git a/crates/js-component-bindgen/src/intrinsics/mod.rs b/crates/js-component-bindgen/src/intrinsics/mod.rs index d2cc9280..b17245a7 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, )) { 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..de697de3 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,38 @@ 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 { + /// componentInstanceIdx: number, + /// 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 +240,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 +282,7 @@ impl AsyncTaskIntrinsic { Self::TaskReturn => "taskReturn", Self::Yield => "asyncYield", Self::UnpackCallbackResult => "unpackCallbackResult", + Self::DriverLoop => "_driverLoop", } } @@ -296,20 +351,16 @@ impl AsyncTaskIntrinsic { } 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, callbackFnIdx, liftFns }} = args; + const params = [...arguments].slice(1); {debug_log_fn}('[{task_return_fn}()] args', {{ - componentIdx, - memory, - callbackFnIdx, - liftFns, + ...args, params, }}); @@ -540,6 +591,8 @@ impl AsyncTaskIntrinsic { )); } + // 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 = @@ -693,7 +746,10 @@ impl AsyncTaskIntrinsic { 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'); }} + if (!waitableSet) {{ + console.log('waitable sets?', state.waitableSets); + throw new Error('cannot wait for waitable set: no set for rep [' + waitableSetRep + ']'); + }} waitableSet.numWaiting += 1; let event = null; @@ -718,10 +774,6 @@ impl AsyncTaskIntrinsic { 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 +785,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,46 +881,72 @@ 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 {{ readyFunc, 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({{ readyFunc, 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, + }}; + }} + + async suspendUntil(opts) {{ + const {{ cancellable, readyFunc }} = opts; + {debug_log_fn}('[{task_class}#suspendUntil()] args', {{ cancellable }}); - const blockResult = await this.blockOn({{ - awaitable: new {awaitable_class}(new Promise(resolve => setTimeout(resolve, 0))), - isCancellable, - forCallback, - }}); + const pendingCancelled = this.deliverPendingCancel({{ cancellable }}); + if (pendingCancelled) {{ return false; }} - 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 completed = await this.immediateSuspendUntil({{ readyFunc, cancellable }}); + return completed; + }} + + async immediateSuspendUntil(opts) {{ // NOTE: equivalent to thread.suspend_until() + const {{ cancellable, readyFunc }} = opts; + {debug_log_fn}('[{task_class}#immediateSuspendUntil()] args', {{ cancellable, readyFunc }}); + + const ready = readyFunc(); + if (ready && !{global_async_determinism} && {coin_flip_fn}()) {{ + return true; }} - return {{ - code: {event_code_enum}.NONE, - payload: [0, 0], - }}; + const pendingCancelled = await this.immediateSuspend({{ cancellable }}); + return pendingCancelled; + }} + + async immediateSuspend(opts) {{ // NOTE: equivalent to thread.suspend() + const {{ cancellable }} = opts; + {debug_log_fn}('[{task_class}#immediateSuspend()] args', {{ cancellable }}); + + const pendingCancelled = this.deliverPendingCancel({{ cancellable }}); + if (pendingCancelled) {{ return false; }} + + const cstate = {get_or_create_async_state_fn}(this.#componentIdx); + + await cstate.suspendTask({{ task: this }}); }} - 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; }} cancel() {{ @@ -892,6 +966,7 @@ impl AsyncTaskIntrinsic { throw new Error('task is already resolved'); }} if (this.borrowedHandles.length > 0) {{ throw new Error('task still has borrow handles'); }} + console.log('subtasks?', {{ subtasks: this.#subtasks }} ); this.#onResolve(results.length === 1 ? results[0] : results); this.#state = {task_class}.State.RESOLVED; }} @@ -914,12 +989,10 @@ 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) {{ @@ -1101,8 +1174,138 @@ 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!(" + async function {driver_loop_fn}(args) {{ + {debug_log_fn}('[{driver_loop_fn}()] args', args); + const {{ + componentInstanceIdx, + componentState, + task, + fnName, + callbackFnName, + callbackFn, + isAsync, + resolve, + reject, + + callbackResult, + }} = args; + + 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'); + }} + }} + let [callbackCode, waitableSetRep] = {unpack_callback_result_fn}(callbackResult); + + 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, + readyFunc: () => !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, + }}); + asyncRes = await task.waitForEvent({{ isAsync: true, waitableSetRep }}); + 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; + + {debug_log_fn}('[{driver_loop_fn}()] performing callback', {{ + fnName, + callbackFnName, + eventCode, + index, + result + }}); + + const callbackRes = callbackFn( + {to_int32_fn}(eventCode), + {to_int32_fn}(index), + {to_int32_fn}(result), + ); + const 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/waitable.rs b/crates/js-component-bindgen/src/intrinsics/p3/waitable.rs index 59f3f495..d46e4977 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/waitable.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/waitable.rs @@ -171,7 +171,7 @@ impl WaitableIntrinsic { this.#componentInstanceID = componentInstanceID; }} - numWaitables() {{ return this.#waitable.length; }} + numWaitables() {{ return this.#waitables.length; }} numWaiting() {{ return this.#waiting; }} shuffleWaitables() {{ @@ -181,6 +181,35 @@ 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', {{ }}); + this.shuffleWaitables(); + for (const waitable of this.#waitables) {{ + if (!waitable.hasPendingEvent()) {{ continue; }} + return waitable.getPendingEvent(); + }} + console.log('waitables?', this.#waitables); + throw new Error('no waitables had a pending event'); + }} + async poll() {{ {debug_log_fn}('[{waitable_set_class}#poll()] args', {{ }}); @@ -222,6 +251,10 @@ impl WaitableIntrinsic { return !!this.#pendingEvent; }} + setPendingEvent(event) {{ + this.#pendingEvent = event; + }} + getPendingEvent() {{ {debug_log_fn}('[{waitable_class}#getPendingEvent()] args', {{ }}); if (!this.#pendingEvent) {{ return null; }} @@ -251,11 +284,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 +299,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 +385,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); diff --git a/crates/js-component-bindgen/src/lib.rs b/crates/js-component-bindgen/src/lib.rs index e82a1df8..2b873446 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}; @@ -317,3 +319,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..f0058ec2 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 @@ -2011,11 +2028,14 @@ 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}, + memory: {memory_js}, + callbackFnIdx: {callback_fn_idx}, + liftFns: {lift_fns_js}, + isAsync: {async_}, + }}, );", ); } @@ -2216,6 +2236,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 +2255,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 +2445,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 +2576,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) @@ -2577,6 +2612,17 @@ impl<'a> Instantiator<'a, '_> { } }; + // // AT THIS POINT, ITS WRONG: + // // + // // GENERATED IMPORT NAME: asyncSleepMillis (kind: AsyncFreestanding), (func_name [async]sleep-millis) + // // + // eprintln!( + // "GENERATED IMPORT NAME: {import_name} (kind: {:?}), (func_name {}, lower cameled ? {})", + // func.kind, + // func_name, + // func_name.to_lower_camel_case() + // ); + self.ensure_import( import_specifier, iface_name, @@ -3526,7 +3572,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 { "" @@ -3651,7 +3698,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 +3843,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 +3852,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 +3933,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 +3941,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 +3952,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 +3966,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 +3999,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 +4019,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 +4035,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..9b9c419f 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', @@ -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..fb17128a --- /dev/null +++ b/packages/jco/test/p3/ported/wasmtime/component-async/post-return.js @@ -0,0 +1,104 @@ +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) => { + expect(ms).toStrictEqual(waitTimeMs); + await new Promise((resolve) => setTimeout(resolve, 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, + } + }, + }); + const instance = res.instance; + cleanup = res.cleanup; + console.log("OUTPUT DIR:", res.outputDir); + + const result = await instance['local:local/sleep-post-return'].asyncRun(waitTimeMs); + + // TODO: fix: sleep-post-return is running but *does not* call the import properly, + // likely because the task is not actually the thing being returned -- the promise is + // returning early? + + expect(asyncSleepMillis).toHaveBeenCalled(); + } 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',