diff --git a/crates/js-component-bindgen/src/function_bindgen.rs b/crates/js-component-bindgen/src/function_bindgen.rs index 1b7a1af3b..c84b8f651 100644 --- a/crates/js-component-bindgen/src/function_bindgen.rs +++ b/crates/js-component-bindgen/src/function_bindgen.rs @@ -260,30 +260,49 @@ impl FunctionBindgen<'_> { /// var [ ret0, ret1, ret2 ] = /// ``` /// + /// ```js + /// let ret; + /// ``` + /// /// # Arguments /// /// * `amt` - number of results /// * `results` - list of variables that will be returned /// - fn write_result_assignment(&mut self, amt: usize, results: &mut Vec) { + fn generate_result_assignment_lhs( + &mut self, + amt: usize, + results: &mut Vec, + is_async: bool, + ) -> String { + let mut s = String::new(); match amt { - 0 => uwrite!(self.src, "let ret;"), + 0 => { + // Async functions with no returns still return async code, + // which will be used as the initial callback result going into the async driver + if is_async { + uwrite!(s, "let ret = ") + } else { + uwrite!(s, "let ret;") + } + } 1 => { - uwrite!(self.src, "let ret = "); + uwrite!(s, "let ret = "); results.push("ret".to_string()); } n => { - uwrite!(self.src, "var ["); + uwrite!(s, "var ["); for i in 0..n { if i > 0 { - uwrite!(self.src, ", "); + uwrite!(s, ", "); } - uwrite!(self.src, "ret{}", i); + uwrite!(s, "ret{}", i); results.push(format!("ret{i}")); } - uwrite!(self.src, "] = "); + uwrite!(s, "] = "); } } + s } fn bitcast(&mut self, cast: &Bitcast, op: &str) -> String { @@ -340,6 +359,7 @@ impl FunctionBindgen<'_> { /// where appropriate. fn start_current_task(&mut self, instr: &Instruction) { let is_async = self.is_async; + let is_manual_async = self.requires_async_porcelain; let fn_name = self.callee; let err_handling = self.err.to_js_string(); let callback_fn_js = self @@ -377,7 +397,7 @@ impl FunctionBindgen<'_> { .map(mt => mt.task) .filter(t => !t.getParentSubtask()) .map(t => t.exitPromise()); - await Promise.all(taskPromises); + await Promise.allSettled(taskPromises); }} "#, ); @@ -389,6 +409,7 @@ impl FunctionBindgen<'_> { const [task, {prefix}currentTaskID] = {start_current_task_fn}({{ componentIdx: {component_instance_idx}, isAsync: {is_async}, + isManualAsync: {is_manual_async}, entryFnName: '{fn_name}', getCallbackFn: () => {callback_fn_js}, callbackFnName: '{callback_fn_js}', @@ -398,19 +419,6 @@ impl FunctionBindgen<'_> { "#, ); } - - /// End an an existing current task - /// - /// Optionally, ending the current task can also return the value - /// collected by the task, primarily relevant when dealing with - /// async lowered imports (in particular AsyncTaskReturn) that must return - /// collected values from the current task in question. - fn end_current_task(&mut self) { - let end_current_task_fn = - self.intrinsic(Intrinsic::AsyncTask(AsyncTaskIntrinsic::EndCurrentTask)); - let component_instance_idx = self.canon_opts.instance.as_u32(); - uwriteln!(self.src, "{end_current_task_fn}({component_instance_idx});",); - } } impl ManagesIntrinsics for FunctionBindgen<'_> { @@ -492,7 +500,9 @@ impl Bindgen for FunctionBindgen<'_> { Instruction::U32FromI32 => results.push(format!("{} >>> 0", operands[0])), - Instruction::U64FromI64 => results.push(format!("BigInt.asUintN(64, {})", operands[0])), + Instruction::U64FromI64 => { + results.push(format!("BigInt.asUintN(64, BigInt({}))", operands[0])) + } Instruction::S32FromI32 | Instruction::S64FromI64 => { results.push(operands.pop().unwrap()) @@ -1137,7 +1147,12 @@ impl Bindgen for FunctionBindgen<'_> { // Allocate space for the type in question uwriteln!( self.src, - "var ptr{tmp} = {realloc}(0, 0, {align}, len{tmp} * {size});", + "var ptr{tmp} = {realloc_call}(0, 0, {align}, len{tmp} * {size});", + realloc_call = if self.is_async { + format!("await {realloc}") + } else { + realloc.to_string() + }, ); // We may or may not be dealing with a buffer like object or a regular JS array, @@ -1205,12 +1220,21 @@ impl Bindgen for FunctionBindgen<'_> { self.encoding, StringEncoding::UTF8 | StringEncoding::UTF16 )); - let intrinsic = if self.encoding == StringEncoding::UTF16 { - Intrinsic::String(StringIntrinsic::Utf16Encode) - } else { - Intrinsic::String(StringIntrinsic::Utf8Encode) + + let encode_intrinsic = match (self.encoding, self.is_async) { + (StringEncoding::UTF16, true) => { + Intrinsic::String(StringIntrinsic::Utf16EncodeAsync) + } + (StringEncoding::UTF16, false) => { + Intrinsic::String(StringIntrinsic::Utf16Encode) + } + (StringEncoding::UTF8, true) => { + Intrinsic::String(StringIntrinsic::Utf8EncodeAsync) + } + (StringEncoding::UTF8, false) => Intrinsic::String(StringIntrinsic::Utf8Encode), + _ => unreachable!("unsupported encoding {}", self.encoding), }; - let encode = self.intrinsic(intrinsic); + let encode = self.intrinsic(encode_intrinsic); let tmp = self.tmp(); let memory = self.memory.as_ref().unwrap(); let str = String::from("cabi_realloc"); @@ -1277,7 +1301,12 @@ impl Bindgen for FunctionBindgen<'_> { let realloc = self.realloc.as_ref().unwrap(); uwriteln!( self.src, - "var {result} = {realloc}(0, 0, {align}, {len} * {size});" + "var {result} = {realloc_call}(0, 0, {align}, {len} * {size});", + realloc_call = if self.is_async { + format!("await {realloc}") + } else { + realloc.to_string() + }, ); // ... then consume the vector and use the block to lower the @@ -1318,8 +1347,6 @@ impl Bindgen for FunctionBindgen<'_> { Instruction::CallWasm { name, sig } => { let debug_log_fn = self.intrinsic(Intrinsic::DebugLog); - // let global_async_param_lower_class = - // self.intrinsic(Intrinsic::GlobalAsyncParamLowersClass); let has_post_return = self.post_return.is_some(); let is_async = self.is_async; uwriteln!( @@ -1363,52 +1390,25 @@ impl Bindgen for FunctionBindgen<'_> { }} "#, ); + } else { + uwriteln!(self.src, "const started = task.enterSync();",); } - // If we're async w/ params that have been lowered, we must lower the params - // - // TODO(fix): this isn't how to tell whether we need to do async lower... - // this seems to only be the case when doing a ListCanonLower - // if self.is_async - // && let Some(memory) = self.memory - // { - // let component_idx = self.canon_opts.instance.as_u32(); - // uwriteln!( - // self.src, - // r#" - // {{ - // const paramLowerFn = {global_async_param_lower_class}.lookup({{ - // componentIdx: {component_idx}, - // iface: '{iface_name}', - // fnName: '{callee}', - // }}); - // if (!paramLowerFn) {{ - // throw new Error(`missing async param lower function for generated export fn [{callee}]`); - // }} - // paramLowerFn({{ - // memory: {memory}, - // vals: [{params}], - // indirect: {indirect}, - // loweringParams: [{operands}], - // }}); - // }} - // "#, - // operands = operands.join(","), - // params = self.params.join(","), - // indirect = sig.indirect_params, - // callee = self.callee, - // iface_name = self.iface_name.unwrap_or("$root"), - // ); - // } + // Save the memory for this task, + // which will be used for any subtasks that might be spawned + if let Some(mem_idx) = self.canon_opts.memory() { + let idx = mem_idx.as_u32(); + uwriteln!(self.src, "task.setReturnMemoryIdx({idx});"); + uwriteln!(self.src, "task.setReturnMemory(memory{idx});"); + } // Output result binding preamble (e.g. 'var ret =', 'var [ ret0, ret1] = exports...() ') + // along with the code to perofrm the call let sig_results_length = sig.results.len(); - self.write_result_assignment(sig_results_length, results); - - // Write the rest of the result asignment -- calling the callee function + let s = self.generate_result_assignment_lhs(sig_results_length, results, is_async); uwriteln!( self.src, - "{maybe_async_await}{callee}({args});", + "{s} {maybe_async_await}{callee}({args});", callee = self.callee, args = operands.join(", "), maybe_async_await = if self.requires_async_porcelain { @@ -1432,15 +1432,9 @@ impl Bindgen for FunctionBindgen<'_> { } ); } - - 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(); - } } - // Call to an interface, usually but not always an externally imported interface + // Call to an imported interface (normally provided by the host) Instruction::CallInterface { func, async_ } => { let debug_log_fn = self.intrinsic(Intrinsic::DebugLog); let start_current_task_fn = self.intrinsic(Intrinsic::AsyncTask( @@ -1467,21 +1461,7 @@ impl Bindgen for FunctionBindgen<'_> { (self.callee.into(), operands.join(", ")) }; - uwriteln!(self.src, "let hostProvided = false;"); - match func.kind { - wit_parser::FunctionKind::Constructor(_) => { - let cls = fn_js.trim_start_matches("new "); - uwriteln!(self.src, "hostProvided = {cls}?._isHostProvided;"); - } - wit_parser::FunctionKind::Freestanding - | wit_parser::FunctionKind::AsyncFreestanding - | wit_parser::FunctionKind::Method(_) - | wit_parser::FunctionKind::AsyncMethod(_) - | wit_parser::FunctionKind::Static(_) - | wit_parser::FunctionKind::AsyncStatic(_) => { - uwriteln!(self.src, "hostProvided = {fn_js}?._isHostProvided;"); - } - } + uwriteln!(self.src, "let hostProvided = true;"); // Start the necessary subtasks and/or host task // @@ -1506,7 +1486,7 @@ impl Bindgen for FunctionBindgen<'_> { const createTask = () => {{ const results = {start_current_task_fn}({{ - componentIdx: {component_instance_idx}, + componentIdx: -1, // {component_instance_idx}, isAsync: {is_async}, entryFnName: '{fn_name}', getCallbackFn: () => {callback_fn_js}, @@ -1526,13 +1506,11 @@ impl Bindgen for FunctionBindgen<'_> { createTask(); - const isHostAsyncImport = hostProvided && {is_async}; - if (isHostAsyncImport) {{ + if (hostProvided) {{ subtask = parentTask.getLatestSubtask(); if (!subtask) {{ - throw new Error("Missing subtask for host import, has the import been lowered? (ensure asyncImports are set properly)"); + throw new Error(`Missing subtask (in parent task [${{parentTask.id()}}]) for host import, has the import been lowered? (ensure asyncImports are set properly)`); }} - subtask.setChildTask(task); task.setParentSubtask(subtask); }} }} @@ -1548,9 +1526,12 @@ impl Bindgen for FunctionBindgen<'_> { .unwrap_or_else(|| "null".into()), ); - let results_length = if func.result.is_none() { 0 } else { 1 }; let is_async = self.requires_async_porcelain || *async_; + // If we're async then we *know* that there is a result, even if the functoin doesn't have one + // at the CM level -- async functions always return + let fn_wasm_result_count = if func.result.is_none() { 0 } else { 1 }; + // If the task is async, do an explicit wait for backpressure before the call execution if is_async { uwriteln!( @@ -1566,6 +1547,8 @@ impl Bindgen for FunctionBindgen<'_> { }} "#, ); + } else { + uwriteln!(self.src, "const started = task.enterSync();",); } // Build the JS expression that calls the callee @@ -1578,8 +1561,12 @@ impl Bindgen for FunctionBindgen<'_> { // If configured to do *no* error handling at all or throw // error objects directly, we can simply perform the call ErrHandling::None | ErrHandling::ThrowResultErr => { - self.write_result_assignment(results_length, results); - uwriteln!(self.src, "{call};"); + let s = self.generate_result_assignment_lhs( + fn_wasm_result_count, + results, + is_async, + ); + uwriteln!(self.src, "{s}{call};"); } // If configured to force all thrown errors into result objects, // then we add a try/catch around the call @@ -1620,7 +1607,7 @@ impl Bindgen for FunctionBindgen<'_> { uwriteln!( self.src, "console.error(`{prefix} return {}`);", - if results_length > 0 || !results.is_empty() { + if fn_wasm_result_count > 0 || !results.is_empty() { format!("result=${{{to_result_string}(ret)}}") } else { "".to_string() @@ -1666,11 +1653,6 @@ impl Bindgen for FunctionBindgen<'_> { } self.clear_resource_borrows = false; } - - // For non-async calls, the current task can end immediately - if !async_ { - self.end_current_task(); - } } Instruction::Return { @@ -1697,19 +1679,21 @@ impl Bindgen for FunctionBindgen<'_> { let get_or_create_async_state_fn = self.intrinsic(Intrinsic::Component( ComponentIntrinsic::GetOrCreateAsyncState, )); - let gen_post_return_js = |(invocation_stmt, ret_stmt): (String, Option)| { - format!( - " + let gen_post_return_js = + |(post_return_call, ret_stmt): (String, Option)| { + format!( + r#" let cstate = {get_or_create_async_state_fn}({component_idx}); cstate.mayLeave = false; - {invocation_stmt} + {post_return_call} cstate.mayLeave = true; + task.exit(); {ret_stmt} - ", - component_idx = self.canon_opts.instance.as_u32(), - ret_stmt = ret_stmt.unwrap_or_default(), - ) - }; + "#, + component_idx = self.canon_opts.instance.as_u32(), + ret_stmt = ret_stmt.unwrap_or_default(), + ) + }; assert!(!self.is_async, "async functions should use AsyncTaskReturn"); @@ -1721,12 +1705,15 @@ impl Bindgen for FunctionBindgen<'_> { match stack_value_count { // (sync) Handle no result case 0 => { + uwriteln!(self.src, "task.resolve([ret]);"); if let Some(f) = &self.post_return { uwriteln!( self.src, - "{}", - gen_post_return_js((format!("{f}();"), None)) + "{post_return_js}", + post_return_js = gen_post_return_js((format!("{f}();"), None)), ); + } else { + uwriteln!(self.src, "task.exit();"); } } @@ -1734,22 +1721,28 @@ impl Bindgen for FunctionBindgen<'_> { 1 if self.err == ErrHandling::ThrowResultErr => { let component_err = self.intrinsic(Intrinsic::ComponentError); let op = &operands[0]; + uwriteln!(self.src, "const retCopy = {op};"); + uwriteln!(self.src, "task.resolve([retCopy.val]);"); + if let Some(f) = &self.post_return { uwriteln!( self.src, "{}", gen_post_return_js((format!("{f}(ret);"), None)) ); + } else { + uwriteln!(self.src, "task.exit();"); } + uwriteln!( self.src, - " - if (typeof retCopy === 'object' && retCopy.tag === 'err') {{ - throw new {component_err}(retCopy.val); - }} - return retCopy.val; - " + r#" + if (typeof retCopy === 'object' && retCopy.tag === 'err') {{ + throw new {component_err}(retCopy.val); + }} + return retCopy.val; + "# ); } @@ -1763,6 +1756,8 @@ impl Bindgen for FunctionBindgen<'_> { _ => format!("[{}]", operands.join(", ")), }; + uwriteln!(self.src, "task.resolve([{ret_val}]);"); + // Handle the post return if necessary if let Some(post_return_fn) = self.post_return { // In the case there is a post return function, we'll want to copy the value @@ -1775,12 +1770,12 @@ impl Bindgen for FunctionBindgen<'_> { // and pass a copy fo the result to the actual caller let post_return_js = gen_post_return_js(( format!("{post_return_fn}(ret);"), - Some("return retCopy;".into()), + Some(["return retCopy;"].join("\n")), )); - - uwriteln!(self.src, "{post_return_js}",); + uwriteln!(self.src, "{post_return_js}"); } else { - uwriteln!(self.src, "return {ret_val};",) + uwriteln!(self.src, "task.exit();"); + uwriteln!(self.src, "return {ret_val};") } } } @@ -1834,8 +1829,13 @@ impl Bindgen for FunctionBindgen<'_> { let ptr = format!("ptr{tmp}"); uwriteln!( self.src, - "var {ptr} = {realloc}(0, 0, {align}, {size});", + "var {ptr} = {realloc_call}(0, 0, {align}, {size});", align = align.align_wasm32(), + realloc_call = if self.is_async { + format!("await {realloc}") + } else { + realloc.to_string() + }, size = size.size_wasm32() ); results.push(ptr); @@ -1868,10 +1868,12 @@ impl Bindgen for FunctionBindgen<'_> { if !imported { let symbol_resource_handle = self.intrinsic(Intrinsic::SymbolResourceHandle); + uwriteln!( self.src, "var {rsc} = new.target === {local_name} ? this : Object.create({local_name}.prototype);" ); + if is_own { // Sending an own handle out to JS as a return value - set up finalizer and disposal. let empty_func = self @@ -1912,15 +1914,20 @@ impl Bindgen for FunctionBindgen<'_> { let symbol_resource_rep = self.intrinsic(Intrinsic::SymbolResourceRep); let symbol_resource_handle = self.intrinsic(Intrinsic::SymbolResourceHandle); - uwriteln!(self.src, - "var {rep} = handleTable{tid}[({handle} << 1) + 1] & ~{rsc_flag}; - var {rsc} = captureTable{rid}.get({rep}); - if (!{rsc}) {{ - {rsc} = Object.create({local_name}.prototype); - Object.defineProperty({rsc}, {symbol_resource_handle}, {{ writable: true, value: {handle} }}); - Object.defineProperty({rsc}, {symbol_resource_rep}, {{ writable: true, value: {rep} }}); - }}" - ); + + uwriteln!( + self.src, + r#" + var {rep} = handleTable{tid}[({handle} << 1) + 1] & ~{rsc_flag}; + var {rsc} = captureTable{rid}.get({rep}); + if (!{rsc}) {{ + {rsc} = Object.create({local_name}.prototype); + Object.defineProperty({rsc}, {symbol_resource_handle}, {{ writable: true, value: {handle} }}); + Object.defineProperty({rsc}, {symbol_resource_rep}, {{ writable: true, value: {rep} }}); + }} + "#, + ); + if is_own { // An own lifting is a transfer to JS, so existing own handle is implicitly dropped. uwriteln!( @@ -1965,12 +1972,13 @@ impl Bindgen for FunctionBindgen<'_> { "var {rsc} = repTable.get($resource_{prefix}rep${lower_camel}({handle})).rep;" ); uwrite!( - self.src, - "repTable.delete({handle}); - delete {rsc}[{symbol_resource_handle}]; - finalizationRegistry_export${prefix}{lower_camel}.unregister({rsc}); - " - ); + self.src, + r#" + repTable.delete({handle}); + delete {rsc}[{symbol_resource_handle}]; + finalizationRegistry_export${prefix}{lower_camel}.unregister({rsc}); + "# + ); } else { uwriteln!(self.src, "var {rsc} = repTable.get({handle}).rep;"); } @@ -1978,11 +1986,12 @@ impl Bindgen for FunctionBindgen<'_> { let upper_camel = resource_name.to_upper_camel_case(); uwrite!( - self.src, - "var {rsc} = new.target === import_{prefix}{upper_camel} ? this : Object.create(import_{prefix}{upper_camel}.prototype); - Object.defineProperty({rsc}, {symbol_resource_handle}, {{ writable: true, value: {handle} }}); - " - ); + self.src, + r#" + var {rsc} = new.target === import_{prefix}{upper_camel} ? this : Object.create(import_{prefix}{upper_camel}.prototype); + Object.defineProperty({rsc}, {symbol_resource_handle}, {{ writable: true, value: {handle} }}); + "# + ); uwriteln!( self.src, @@ -2191,7 +2200,7 @@ impl Bindgen for FunctionBindgen<'_> { // TODO: convert this return of the lifted Future: // // ``` - // return BigInt(writeEndIdx) << 32n | BigInt(readEndIdx); + // return BigInt(writeEndWaitableIdx) << 32n | BigInt(readEndWaitableIdx); // ``` // // Into a component-local Future instance @@ -2286,7 +2295,7 @@ impl Bindgen for FunctionBindgen<'_> { Instruction::StreamLower { .. } => { // TODO: convert this return of the lifted Future: // ``` - // return BigInt(writeEndIdx) << 32n | BigInt(readEndIdx); + // return BigInt(writeEndWaitableIdx) << 32n | BigInt(readEndWaitableIdx); // ``` // // Into a component-local Future instance @@ -2413,7 +2422,7 @@ impl Bindgen for FunctionBindgen<'_> { const {result_var} = {stream_new_from_lift_fn}({{ componentIdx: {component_idx}, streamTableIdx: {stream_table_idx}, - streamEndIdx: {arg_stream_end_idx}, + streamEndWaitableIdx: {arg_stream_end_idx}, payloadLiftFn, payloadTypeSize32: {payload_ty_size_js}, payloadLowerFn, @@ -2426,8 +2435,8 @@ impl Bindgen for FunctionBindgen<'_> { // Instruction::AsyncTaskReturn does *not* correspond to an canonical `task.return`, // but rather to a "return"/exit from an a lifted async function (e.g. pre-callback) // - // To control the *real* `task.return` intrinsic: - // - `Intrinsic::TaskReturn` + // To modify behavior of the `task.return` intrinsic, see: + // - `Trampoline::TaskReturn` // - `AsyncTaskIntrinsic::TaskReturn` // // This is simply the end of the async function definition (e.g. `CallWasm`) that has been @@ -2445,11 +2454,20 @@ impl Bindgen for FunctionBindgen<'_> { // Instruction::AsyncTaskReturn { name, params } => { let debug_log_fn = self.intrinsic(Intrinsic::DebugLog); + let component_instance_idx = self.canon_opts.instance.as_u32(); + let is_async_js = self.requires_async_porcelain | self.is_async; + let async_driver_loop_fn = + self.intrinsic(Intrinsic::AsyncTask(AsyncTaskIntrinsic::DriverLoop)); + let get_or_create_async_state_fn = self.intrinsic(Intrinsic::Component( + ComponentIntrinsic::GetOrCreateAsyncState, + )); + uwriteln!( self.src, "{debug_log_fn}('{prefix} [Instruction::AsyncTaskReturn]', {{ funcName: '{name}', paramCount: {param_count}, + componentIdx: {component_instance_idx}, postReturn: {post_return_present}, hostProvided, }});", @@ -2476,18 +2494,9 @@ impl Bindgen for FunctionBindgen<'_> { // // ```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 end_current_task_fn = - self.intrinsic(Intrinsic::AsyncTask(AsyncTaskIntrinsic::EndCurrentTask)); - - let component_instance_idx = self.canon_opts.instance.as_u32(); - let is_async_js = self.requires_async_porcelain | self.is_async; - - // NOTE: if the import was host provided we *already* have the result via + // ```ts + // + // If the import was host provided we *already* have the result via // JSPI and simply calling the host provided JS function -- there is no need // to drive the async loop as with an async import that came from a component. // @@ -2513,26 +2522,20 @@ impl Bindgen for FunctionBindgen<'_> { result: ret, }}) task.resolve([ret]); - {end_current_task_fn}({component_instance_idx}, task.id()); + task.exit(); return task.completionPromise(); }} - const currentSubtask = task.getLatestSubtask(); - if (currentSubtask && currentSubtask.isNotStarted()) {{ - {debug_log_fn}('[Instruction::AsyncTaskReturn] subtask not started at end of task run, starting it', {{ - task: task.id(), - subtask: currentSubtask?.id(), - result: ret, - }}) - currentSubtask.onStart(); - }} - const componentState = {get_or_create_async_state_fn}({component_instance_idx}); if (!componentState) {{ throw new Error('failed to lookup current component state'); }} new Promise(async (resolve, reject) => {{ try {{ - {debug_log_fn}("[Instruction::AsyncTaskReturn] starting driver loop", {{ fnName: '{name}' }}); + {debug_log_fn}("[Instruction::AsyncTaskReturn] starting driver loop", {{ + fnName: '{name}', + componentInstanceIdx: {component_instance_idx}, + taskID: task.id(), + }}); await {async_driver_loop_fn}({{ componentInstanceIdx: {component_instance_idx}, componentState, diff --git a/crates/js-component-bindgen/src/intrinsics/component.rs b/crates/js-component-bindgen/src/intrinsics/component.rs index ffb309e8b..012d8931b 100644 --- a/crates/js-component-bindgen/src/intrinsics/component.rs +++ b/crates/js-component-bindgen/src/intrinsics/component.rs @@ -1,7 +1,10 @@ //! Intrinsics that represent helpers that manage per-component state use crate::{ - intrinsics::{Intrinsic, p3::async_stream::AsyncStreamIntrinsic}, + intrinsics::{ + Intrinsic, + p3::{async_stream::AsyncStreamIntrinsic, waitable::WaitableIntrinsic}, + }, source::Source, }; @@ -115,14 +118,18 @@ impl ComponentIntrinsic { } Self::ComponentAsyncStateClass => { + let component_async_state_class = self.name(); let debug_log_fn = Intrinsic::DebugLog.name(); let rep_table_class = Intrinsic::RepTableClass.name(); let internal_stream_class = AsyncStreamIntrinsic::InternalStreamClass.name(); let global_stream_map = AsyncStreamIntrinsic::GlobalStreamMap.name(); + let global_stream_table_map = AsyncStreamIntrinsic::GlobalStreamTableMap.name(); + let waitable_class = Intrinsic::Waitable(WaitableIntrinsic::WaitableClass).name(); + let get_or_create_async_state_fn = Self::GetOrCreateAsyncState.name(); output.push_str(&format!( r#" - class {class_name} {{ + class {component_async_state_class} {{ static EVENT_HANDLER_EVENTS = [ 'backpressure-change' ]; #componentIdx; @@ -140,31 +147,25 @@ impl ComponentIntrinsic { #handlerMap = new Map(); #nextHandlerID = 0n; - #streams; - #tickLoop = null; #tickLoopInterval = null; mayLeave = true; - waitableSets; - waitables; + handles; subtasks; constructor(args) {{ this.#componentIdx = args.componentIdx; - this.waitableSets = new {rep_table_class}({{ target: `component [${{this.#componentIdx}}] waitable sets` }}); - this.waitables = new {rep_table_class}({{ target: `component [${{this.#componentIdx}}] waitable objects` }}); + this.handles = new {rep_table_class}({{ target: `component [${{this.#componentIdx}}] handles (waitable objects)` }}); this.subtasks = new {rep_table_class}({{ target: `component [${{this.#componentIdx}}] subtasks` }}); - this.#streams = new Map(); }}; componentIdx() {{ return this.#componentIdx; }} - streams() {{ return this.#streams; }} errored() {{ return this.#errored !== null; }} setErrored(err) {{ - {debug_log_fn}('[{class_name}#setErrored()] component errored', {{ err, componentIdx: this.#componentIdx }}); + {debug_log_fn}('[{component_async_state_class}#setErrored()] component errored', {{ err, componentIdx: this.#componentIdx }}); if (this.#errored) {{ return; }} if (!err) {{ err = new Error('error elswehere (see other component instance error)') @@ -233,7 +234,7 @@ impl ComponentIntrinsic { if (!event) {{ throw new Error("missing handler event"); }} if (!fn) {{ throw new Error("missing handler fn"); }} - if (!{class_name}.EVENT_HANDLER_EVENTS.includes(event)) {{ + if (!{component_async_state_class}.EVENT_HANDLER_EVENTS.includes(event)) {{ throw new Error(`unrecognized event handler [${{event}}]`); }} @@ -287,7 +288,7 @@ impl ComponentIntrinsic { const taskList = this.#parkedTasks.get(awaitableID); if (!taskList || taskList.length === 0) {{ - {debug_log_fn}('[{class_name}] no tasks waiting for awaitable', {{ awaitableID: awaitable.id() }}); + {debug_log_fn}('[{component_async_state_class}] no tasks waiting for awaitable', {{ awaitableID: awaitable.id() }}); return; }} @@ -301,18 +302,20 @@ impl ComponentIntrinsic { }} isExclusivelyLocked() {{ return this.#locked === true; }} - setLocked(locked) {{ this.#locked = locked; }} + setLocked(locked) {{ + this.#locked = locked; + }} // TODO(fix): we might want to check for pre-locked status here, we should be deterministically // going from locked -> unlocked and vice versa exclusiveLock() {{ - {debug_log_fn}('[{class_name}#exclusiveLock()]', {{ + {debug_log_fn}('[{component_async_state_class}#exclusiveLock()]', {{ locked: this.#locked, componentIdx: this.#componentIdx, }}); this.setLocked(true); }} exclusiveRelease() {{ - {debug_log_fn}('[{class_name}#exclusiveRelease()]', {{ + {debug_log_fn}('[{component_async_state_class}#exclusiveRelease()]', {{ locked: this.#locked, componentIdx: this.#componentIdx, }}); @@ -324,7 +327,7 @@ impl ComponentIntrinsic { }} #removeSuspendedTaskMeta(taskID) {{ - {debug_log_fn}('[{class_name}#removeSuspendedTaskMeta()] removing suspended task', {{ taskID }}); + {debug_log_fn}('[{component_async_state_class}#removeSuspendedTaskMeta()] removing suspended task', {{ taskID }}); const idx = this.#suspendedTaskIDs.findIndex(t => t === taskID); const meta = this.#suspendedTasksByTaskID.get(taskID); this.#suspendedTaskIDs[idx] = null; @@ -346,7 +349,7 @@ impl ComponentIntrinsic { suspendTask(args) {{ const {{ task, readyFn }} = args; const taskID = task.id(); - {debug_log_fn}('[{class_name}#suspendTask()]', {{ taskID }}); + {debug_log_fn}('[{component_async_state_class}#suspendTask()]', {{ taskID }}); if (this.#getSuspendedTaskMeta(taskID)) {{ throw new Error(`task [${{taskID}}] already suspended`); @@ -358,7 +361,7 @@ impl ComponentIntrinsic { taskID, readyFn, resume: () => {{ - {debug_log_fn}('[{class_name}#suspendTask()] resuming suspended task', {{ taskID }}); + {debug_log_fn}('[{component_async_state_class}#suspendTask()] resuming suspended task', {{ taskID }}); // TODO(threads): it's thread cancellation we should be checking for below, not task resolve(!task.isCancelled()); }}, @@ -377,20 +380,20 @@ impl ComponentIntrinsic { }} async runTickLoop() {{ - if (this.#tickLoop !== null) {{ await this.#tickLoop; }} - this.#tickLoop = new Promise(async (resolve) => {{ + if (this.#tickLoop !== null) {{ return; }} + this.#tickLoop = 1; + setTimeout(async () => {{ let done = this.tick(); while (!done) {{ - await new Promise((resolve) => setTimeout(resolve, 0)); + await new Promise((resolve) => setTimeout(resolve, 30)); done = this.tick(); }} this.#tickLoop = null; - resolve(); - }}); + }}, 10); }} tick() {{ - {debug_log_fn}('[{class_name}#tick()]', {{ suspendedTaskIDs: this.#suspendedTaskIDs }}); + // {debug_log_fn}('[{component_async_state_class}#tick()]', {{ suspendedTaskIDs: this.#suspendedTaskIDs }}); const resumableTasks = this.#suspendedTaskIDs.filter(t => t !== null); for (const taskID of resumableTasks) {{ const meta = this.#suspendedTasksByTaskID.get(taskID); @@ -407,92 +410,185 @@ impl ComponentIntrinsic { return this.#suspendedTaskIDs.filter(t => t !== null).length === 0; }} - addStreamEnd(args) {{ - {debug_log_fn}('[{class_name}#addStreamEnd()] args', args); + addStreamEndToTable(args) {{ + {debug_log_fn}('[{component_async_state_class}#addStreamEnd()] args', args); const {{ tableIdx, streamEnd }} = args; + if (typeof streamEnd === 'number') {{ throw new Error("INSERTING BAD STREAMEND"); }} - let tbl = this.#streams.get(tableIdx); - if (!tbl) {{ - tbl = new {rep_table_class}({{ target: `stream table (idx [${{tableIdx}}], component [${{this.#componentIdx}}])` }}); - this.#streams.set(tableIdx, tbl); + let {{ table, componentIdx }} = {global_stream_table_map}[tableIdx]; + if (componentIdx === undefined || !table) {{ + throw new Error(`invalid global stream table state for table [${{tableIdx}}]`); }} - // TODO(fix): streams are waitables so need to go there - const streamIdx = tbl.insert(streamEnd); - return streamIdx; + const handle = table.insert(streamEnd); + streamEnd.setHandle(handle); + streamEnd.setStreamTableIdx(tableIdx); + + const cstate = {get_or_create_async_state_fn}(componentIdx); + const waitableIdx = cstate.handles.insert(streamEnd); + streamEnd.setWaitableIdx(waitableIdx); + + {debug_log_fn}('[{component_async_state_class}#addStreamEnd()] added stream end', {{ + tableIdx, + table, + handle, + streamEnd, + destComponentIdx: componentIdx, + }}); + + return {{ handle, waitableIdx }}; + }} + + createWaitable(args) {{ + return new {waitable_class}({{ target: args?.target, }}); }} createStream(args) {{ - {debug_log_fn}('[{class_name}#createStream()] args', args); + {debug_log_fn}('[{component_async_state_class}#createStream()] args', args); const {{ tableIdx, elemMeta }} = args; if (tableIdx === undefined) {{ throw new Error("missing table idx while adding stream"); }} if (elemMeta === undefined) {{ throw new Error("missing element metadata while adding stream"); }} - let localStreamTable = this.#streams.get(tableIdx); + const {{ table: localStreamTable, componentIdx }} = {global_stream_table_map}[tableIdx]; if (!localStreamTable) {{ - localStreamTable = new {rep_table_class}({{ target: `component [${{this.#componentIdx}}] streams` }}); - this.#streams.set(tableIdx, localStreamTable); + throw new Error(`missing global stream table lookup for table [${{tableIdx}}] while creating stream`); }} + if (componentIdx !== this.#componentIdx) {{ + throw new Error('component idx mismatch while creating stream'); + }} + + const readWaitable = this.createWaitable(); + const writeWaitable = this.createWaitable(); const stream = new {internal_stream_class}({{ tableIdx, componentIdx: this.#componentIdx, elemMeta, - localStreamTable, - globalStreamMap: {global_stream_map}, + readWaitable, + writeWaitable, }}); - - const writeEndIdx = this.waitables.insert(stream.writeEnd()); - stream.setWriteEndWaitableIdx(writeEndIdx); - - const readEndIdx = this.waitables.insert(stream.readEnd()); - stream.setReadEndWaitableIdx(readEndIdx); - - return {{ writeEndIdx, readEndIdx }}; + stream.setGlobalStreamMapRep({global_stream_map}.insert(stream)); + + const writeEnd = stream.writeEnd(); + writeEnd.setWaitableIdx(this.handles.insert(writeEnd)); + writeEnd.setHandle(localStreamTable.insert(writeEnd)); + if (writeEnd.streamTableIdx() !== tableIdx) {{ throw new Error("unexpectedly mismatched stream table"); }} + + const writeEndWaitableIdx = writeEnd.waitableIdx(); + const writeEndHandle = writeEnd.handle(); + writeWaitable.setTarget(`waitable for stream write end (waitable [${{writeEndWaitableIdx}}])`); + writeEnd.setTarget(`stream write end (waitable [${{writeEndWaitableIdx}}])`); + + const readEnd = stream.readEnd(); + readEnd.setWaitableIdx(this.handles.insert(readEnd)); + readEnd.setHandle(localStreamTable.insert(readEnd)); + if (readEnd.streamTableIdx() !== tableIdx) {{ throw new Error("unexpectedly mismatched stream table"); }} + + const readEndWaitableIdx = readEnd.waitableIdx(); + const readEndHandle = readEnd.handle(); + readWaitable.setTarget(`waitable for read end (waitable [${{readEndWaitableIdx}}])`); + readEnd.setTarget(`stream read end (waitable [${{readEndWaitableIdx}}])`); + + return {{ + writeEndWaitableIdx, + writeEndHandle, + readEndWaitableIdx, + readEndHandle, + }}; }} getStreamEnd(args) {{ - {debug_log_fn}('[{class_name}#getStreamEnd()] args', args); - const {{ tableIdx, streamEndIdx }} = args; + {debug_log_fn}('[{component_async_state_class}#getStreamEnd()] args', args); + const {{ tableIdx, streamEndHandle, streamEndWaitableIdx }} = args; if (tableIdx === undefined) {{ throw new Error('missing table idx while getting stream end'); }} - if (streamEndIdx === undefined) {{ throw new Error('missing stream idx while getting stream end'); }} - const streamEnd = this.waitables.get(streamEndIdx); + const {{ table, componentIdx }} = {global_stream_table_map}[tableIdx]; + const cstate = {get_or_create_async_state_fn}(componentIdx); + + let streamEnd; + if (streamEndWaitableIdx !== undefined) {{ + streamEnd = cstate.handles.get(streamEndWaitableIdx); + }} else if (streamEndHandle !== undefined) {{ + if (!table) {{ throw new Error(`missing/invalid table [${{tableIdx}}] while getting stream end`); }} + streamEnd = table.get(streamEndHandle); + }} else {{ + throw new TypeError("must specify either waitable idx or handle to retrieve stream"); + }} + if (!streamEnd) {{ - throw new Error(`missing stream table [${{tableIdx}}] in component [${{this.#componentIdx}}] while getting stream`); + throw new Error(`missing stream end (tableIdx [${{tableIdx}}], handle [${{streamEndHandle}}], waitableIdx [${{streamEndWaitableIdx}}])`); }} - if (streamEnd.streamTableIdx() !== tableIdx) {{ + if (tableIdx && streamEnd.streamTableIdx() !== tableIdx) {{ throw new Error(`stream end table idx [${{streamEnd.streamTableIdx()}}] does not match [${{tableIdx}}]`); }} return streamEnd; }} - // TODO(fix): local/global stream table checks could be simplified/removed, if we centralize tracking - removeStreamEnd(args) {{ - {debug_log_fn}('[{class_name}#removeStreamEnd()] args', args); - const {{ tableIdx, streamEndIdx }} = args; + deleteStreamEnd(args) {{ + {debug_log_fn}('[{component_async_state_class}#deleteStreamEnd()] args', args); + const {{ tableIdx, streamEndWaitableIdx }} = args; if (tableIdx === undefined) {{ throw new Error("missing table idx while removing stream end"); }} - if (streamEndIdx === undefined) {{ throw new Error("missing stream idx while removing stream end"); }} + if (streamEndWaitableIdx === undefined) {{ throw new Error("missing stream idx while removing stream end"); }} - const streamEnd = this.waitables.get(streamEndIdx); + const {{ table, componentIdx }} = {global_stream_table_map}[tableIdx]; + const cstate = {get_or_create_async_state_fn}(componentIdx); + + const streamEnd = cstate.handles.get(streamEndWaitableIdx); if (!streamEnd) {{ - throw new Error(`missing stream table [${{tableIdx}}] in component [${{this.#componentIdx}}] while getting stream`); + throw new Error(`missing stream end [${{streamEndWaitableIdx}}] in component handles while deleting stream`); }} if (streamEnd.streamTableIdx() !== tableIdx) {{ throw new Error(`stream end table idx [${{streamEnd.streamTableIdx()}}] does not match [${{tableIdx}}]`); }} - const removed = this.waitables.remove(streamEnd.waitableIdx()); + let removed = cstate.handles.remove(streamEnd.waitableIdx()); + if (!removed) {{ + throw new Error(`failed to remove stream end [${{streamEndWaitableIdx}}] waitable obj in component [${{componentIdx}}]`); + }} + + removed = table.remove(streamEnd.handle()); + if (!removed) {{ + throw new Error(`failed to remove stream end with handle [${{streamEnd.handle()}}] from stream table [${{tableIdx}}] in component [${{componentIdx}}]`); + }} + + return streamEnd; + }} + + removeStreamEndFromTable(args) {{ + {debug_log_fn}('[{component_async_state_class}#removeStreamEndFromTable()] args', args); + + const {{ tableIdx, streamWaitableIdx }} = args; + if (tableIdx === undefined) {{ throw new Error("missing table idx while removing stream end"); }} + if (streamWaitableIdx === undefined) {{ + throw new Error("missing stream end waitable idx while removing stream end"); + }} + + const {{ table, componentIdx }} = {global_stream_table_map}[tableIdx]; + if (!table) {{ throw new Error(`missing/invalid table [${{tableIdx}}] while removing stream end`); }} + + const cstate = {get_or_create_async_state_fn}(componentIdx); + + const streamEnd = cstate.handles.get(streamWaitableIdx); + if (!streamEnd) {{ + throw new Error(`missing stream end (handle [${{streamWaitableIdx}}], table [${{tableIdx}}])`); + }} + const handle = streamEnd.handle(); + + let removed = cstate.handles.remove(streamWaitableIdx); + if (!removed) {{ + throw new Error(`failed to remove streamEnd from handles (waitable idx [${{streamWaitableIdx}}]), component [${{componentIdx}}])`); + }} + + removed = table.remove(handle); if (!removed) {{ - throw new Error(`failed to remove stream [${{streamEndIdx}}] waitable obj in component [${{this.#componentIdx}}] while removing stream end`); + throw new Error(`failed to remove streamEnd from table (handle [${{handle}}]), table [${{tableIdx}}], component [${{componentIdx}}])`); }} return streamEnd; }} }} "#, - class_name = self.name(), )); } diff --git a/crates/js-component-bindgen/src/intrinsics/lift.rs b/crates/js-component-bindgen/src/intrinsics/lift.rs index ba833e650..e5c1f9b6a 100644 --- a/crates/js-component-bindgen/src/intrinsics/lift.rs +++ b/crates/js-component-bindgen/src/intrinsics/lift.rs @@ -903,24 +903,36 @@ impl LiftIntrinsic { let external_stream_class = Intrinsic::AsyncStream(AsyncStreamIntrinsic::ExternalStreamClass).name(); let lift_flat_stream_fn = self.name(); + let global_stream_table_map = AsyncStreamIntrinsic::GlobalStreamTableMap.name(); + output.push_str(&format!(r#" - function {lift_flat_stream_fn}(componentTableIdx, ctx) {{ - {debug_log_fn}('[{lift_flat_stream_fn}()] args', {{ componentTableIdx, ctx }}); - const {{ memory, useDirectParams, params, componentIdx }} = ctx; + function {lift_flat_stream_fn}(streamTableIdx, ctx) {{ + {debug_log_fn}('[{lift_flat_stream_fn}()] args', {{ streamTableIdx, ctx }}); + const {{ memory, useDirectParams, params }} = ctx; - const streamEndIdx = params[0]; - if (!streamEndIdx) {{ throw new Error('missing stream idx'); }} + const {{ table, componentIdx }} = {global_stream_table_map}[streamTableIdx]; + if (componentIdx === undefined || !table) {{ + throw new Error(`invalid global stream table state for table [${{tableIdx}}]`); + }} const cstate = {get_or_create_async_state_fn}(componentIdx); if (!cstate) {{ throw new Error(`missing async state for component [${{componentIdx}}]`); }} - const streamEnd = cstate.getStreamEnd({{ tableIdx: componentTableIdx, streamEndIdx }}); + const streamEndWaitableIdx = params[0]; + if (!streamEndWaitableIdx) {{ throw new Error('missing stream idx'); }} + + const streamEnd = cstate.getStreamEnd({{ tableIdx: streamTableIdx, streamEndWaitableIdx }}); if (!streamEnd) {{ - throw new Error(`missing stream end [${{streamEndIdx}}] (table [${{componentTableIdx}}]) in component [${{componentIdx}}] during lift`); + throw new Error(`missing stream end [${{streamEndWaitableIdx}}] (table [${{streamTableIdx}}]) in component [${{componentIdx}}] during lift`); }} + // TODO: check for borrowed type + // TODO: check for readable only + // TODO: confirm shared type matches tyep for lift + // TODO: check for IDLE state + const stream = new {external_stream_class}({{ - hostStreamRep: streamEnd.streamRep(), + globalRep: streamEnd.globalStreamMapRep(), isReadable: streamEnd.isReadable(), isWritable: streamEnd.isWritable(), writeFn: (v) => {{ return streamEnd.write(v); }}, diff --git a/crates/js-component-bindgen/src/intrinsics/lower.rs b/crates/js-component-bindgen/src/intrinsics/lower.rs index c8c94001b..4dbb8a059 100644 --- a/crates/js-component-bindgen/src/intrinsics/lower.rs +++ b/crates/js-component-bindgen/src/intrinsics/lower.rs @@ -1,9 +1,9 @@ //! Intrinsics that represent helpers that enable Lower integration -use crate::{ - intrinsics::{Intrinsic, p3::error_context::ErrCtxIntrinsic, string::StringIntrinsic}, - source::Source, -}; +use crate::intrinsics::Intrinsic; +use crate::intrinsics::p3::{async_stream::AsyncStreamIntrinsic, error_context::ErrCtxIntrinsic}; +use crate::intrinsics::string::StringIntrinsic; +use crate::source::Source; use super::conversion::ConversionIntrinsic; @@ -708,12 +708,53 @@ impl LowerIntrinsic { Self::LowerFlatStream => { let debug_log_fn = Intrinsic::DebugLog.name(); - output.push_str(&format!(" - function _lowerFlatStream(size, memory, vals, storagePtr, storageLen) {{ - {debug_log_fn}('[_lowerFlatStream()] args', {{ size, memory, vals, storagePtr, storageLen }}); - throw new Error('flat lower for streams not yet implemented!'); + let global_stream_map = AsyncStreamIntrinsic::GlobalStreamMap.name(); + let external_stream_class = AsyncStreamIntrinsic::ExternalStreamClass.name(); + let internal_stream_class = AsyncStreamIntrinsic::InternalStreamClass.name(); + + // TODO: fix writable is getting dropped before it can be read!! + // We need to do some waiting? + // Last write should have been triggering the reader to progress... + // Then the last reads return all the data??? + + output.push_str(&format!( + r#" + function _lowerFlatStream(streamTableIdx, ctx) {{ + {debug_log_fn}('[_lowerFlatStream()] args', {{ streamTableIdx, ctx }}); + const {{ + memory, + realloc, + vals, + storagePtr: resultPtr, + }} = ctx; + + const externalStream = vals[0]; + if (!externalStream || !(externalStream instanceof {external_stream_class})) {{ + throw new Error("invalid external stream value"); + }} + + const globalRep = externalStream.globalRep(); + const internalStream = {global_stream_map}.get(globalRep); + if (!internalStream || !(internalStream instanceof {internal_stream_class})) {{ + throw new Error(`failed to find internal stream with rep [${{globalRep}}]`); + }} + + const readEnd = internalStream.readEnd(); + const waitableIdx = readEnd.waitableIdx(); + + // Write the idx of the waitable to memory (a waiting async task or caller) + if (resultPtr) {{ + new DataView(memory.buffer).setUint32(resultPtr, waitableIdx, true); + }} + + // TODO: if we flat lower another way (host -> guest async) we need to actually + // modify the guests table's afresh, we can't just use the global rep! + // (can detect this by whether the external stream has a rep or not) + + return waitableIdx }} - ")); + "# + )); } // When a component-model level error context is lowered, it contains the global error-context diff --git a/crates/js-component-bindgen/src/intrinsics/mod.rs b/crates/js-component-bindgen/src/intrinsics/mod.rs index b38497dfe..a4eba7d00 100644 --- a/crates/js-component-bindgen/src/intrinsics/mod.rs +++ b/crates/js-component-bindgen/src/intrinsics/mod.rs @@ -117,16 +117,9 @@ pub enum Intrinsic { /// Representations of objects stored in one of these tables is a u32 (0 is expected to be an invalid index). RepTableClass, - /// Class that wraps `Promise`s and other things that can be awaited so that we can - /// keep track of whether resolutions have happened - AwaitableClass, - /// Event codes used for async, as a JS enum AsyncEventCodeEnum, - /// Write an async event (e.g. result of waitable-set.wait) to linear memory - WriteAsyncEventToMemory, - // JS helper functions IsLE, ThrowInvalidBool, @@ -140,17 +133,14 @@ pub enum Intrinsic { /// handle, so this helper checks for that. IsBorrowedType, - /// Async lower functions that are saved by component instance - GlobalComponentAsyncLowersClass, + /// Tracking of component memories + GlobalComponentMemoryMap, - /// Param lowering functions saved by a component instance, interface and function - /// - /// This lookup is keyed by a combination of the component instance, interface - /// and generated JS function name where the lowering should be performed. - GlobalAsyncParamLowersClass, + /// Tracking of component memories + RegisterGlobalMemoryForComponent, /// Tracking of component memories - GlobalComponentMemoriesClass, + LookupMemoriesForComponent, } impl Intrinsic { @@ -185,40 +175,6 @@ impl Intrinsic { )); } - Intrinsic::AwaitableClass => { - output.push_str(&format!( - " - class {class_name} {{ - static _ID = 0n; - - #id; - #promise; - #resolved = false; - - constructor(promise) {{ - if (!promise) {{ - throw new TypeError('Awaitable must have an interior promise'); - }} - - if (!('then' in promise) || typeof promise.then !== 'function') {{ - throw new Error('missing/invalid promise'); - }} - promise.then(() => this.#resolved = true); - this.#promise = promise; - this.#id = ++{class_name}._ID; - }} - - id() {{ return this.#id; }} - - resolved() {{ return this.#resolved; }} - - then() {{ return this.#promise.then(...arguments); }} - }} - ", - class_name = self.name(), - )); - } - Intrinsic::ConstantI32Min => output.push_str(&format!( "const {const_name} = -2_147_483_648;\n", const_name = self.name() @@ -439,7 +395,7 @@ impl Intrinsic { const {fn_name} = (...args) => {{ if (!globalThis?.process?.env?.JCO_DEBUG) {{ return; }} console.debug(...args); - }} + }}; " )); } @@ -513,18 +469,20 @@ impl Intrinsic { #start; #ptr; - #capacity; - #copied = 0; + capacity; + processed = 0; - #data; // initial data (only filled out for host-owned) + #hostOnlyData; // initial data (only filled out for host-owned) + + target; constructor(args) {{ - if (args.capacity >= {managed_buffer_class}.MAX_LENGTH) {{ + if (args.capacity > {managed_buffer_class}.MAX_LENGTH) {{ throw new Error(`buffer size [${{args.capacity}}] greater than max length`); }} if (args.componentIdx === undefined) {{ throw new TypeError('missing/invalid component idx'); }} if (args.capacity === undefined) {{ throw new TypeError('missing/invalid capacity'); }} - if (args.elemMeta === undefined || typeof args.elemMeta.align32 !== 'number') {{ + if (!args.elemMeta || typeof args.elemMeta.align32 !== 'number') {{ throw new TypeError('missing/invalid element metadata'); }} @@ -536,22 +494,30 @@ impl Intrinsic { throw new TypeError('missing/invalid start ptr, depsite memory being present'); }} - if (args.start && args.start % args.elemMeta.align32 !== 0) {{ - throw new Error(`invalid alignment: type with 32bit alignment [${{this.#elemMeta.align32}}] at starting pointer [${{start}}]`); + if (!args.elemMeta.isNone && args.capacity > 0) {{ + if (args.start && args.start % args.elemMeta.align32 !== 0) {{ + throw new Error(`invalid alignment: type with 32bit alignment [${{args.elemMeta.align32}}] at starting pointer [${{args.start}}]`); + }} + // TODO: memory lenght bounds check }} this.#componentIdx = args.componentIdx; this.#memory = args.memory; this.#start = args.start; this.#ptr = this.#start; - this.#capacity = args.capacity; + this.capacity = args.capacity; this.#elemMeta = args.elemMeta; - this.#data = args.data; + this.#hostOnlyData = args.data; + this.target = args.target; }} - capacity() {{ return this.#capacity; }} - remainingCapacity() {{ return this.#capacity - this.#copied; }} - copied() {{ return this.#copied; }} + setTarget(tgt) {{ this.target = tgt; }} + + remaining() {{ + return this.capacity - this.processed; + }} + + componentIdx() {{ return this.#componentIdx; }} getElemMeta() {{ return this.#elemMeta; }} @@ -559,26 +525,30 @@ impl Intrinsic { read(count) {{ {debug_log_fn}('[{managed_buffer_class}#read()] args', {{ count }}); - const rc = this.remainingCapacity(); - if (count > rc) {{ - throw new Error(`cannot read [${{count}}] elements from [${{rc}}] managed buffer`); + if (count === undefined || count <= 0) {{ + throw new TypeError(`missing/invalid count [${{count}}]`); + }} + + const cap = this.capacity; + if (count > cap) {{ + throw new Error(`cannot read [${{count}}] elements from buffer with capacity [${{cap}}]`); }} let values = []; - if (this.#elemMeta.typeIdx === null) {{ + if (this.#elemMeta.isNone) {{ values = [...new Array(count)].map(() => null); }} else {{ if (this.isHostOwned()) {{ - const remainingItems = this.#data.slice(count); - values.push(...this.#data.slice(0, count)); - this.#data = remainingItems; + const remainingItems = this.#hostOnlyData.slice(count); + values.push(...this.#hostOnlyData.slice(0, count)); + this.#hostOnlyData = remainingItems; }} else {{ let currentCount = count; let startPtr = this.#ptr; let liftCtx = {{ storagePtr: startPtr, memory: this.#memory }}; if (currentCount < 0) {{ throw new Error('unexpectedly invalid count'); }} while (currentCount > 0) {{ - const [ value, _ctx ] = this.#elemMeta.liftFn(liftCtx) + const [value, _ctx] = this.#elemMeta.liftFn(liftCtx); values.push(value); currentCount -= 1; }} @@ -586,7 +556,7 @@ impl Intrinsic { }} }} - this.#copied += count; + this.processed += count; return values; }} @@ -594,18 +564,18 @@ impl Intrinsic { {debug_log_fn}('[{managed_buffer_class}#write()] args', {{ values }}); if (!Array.isArray(values)) {{ throw new TypeError('values input to write() must be an array'); }} - let rc = this.remainingCapacity(); + let rc = this.remaining(); if (values.length > rc) {{ throw new Error(`cannot write [${{values.length}}] elements to managed buffer with remaining capacity [${{rc}}]`); }} - if (this.#elemMeta.typeIdx === null) {{ + if (this.#elemMeta.isNone) {{ if (!values.every(v => v === null)) {{ throw new Error('non-null values in write() to unit managed buffer'); }} }} else {{ if (this.isHostOwned()) {{ - this.#data.push(...values); + this.#hostOnlyData.push(...values); }} else {{ let startPtr = this.#ptr; for (const v of values) {{ @@ -619,7 +589,7 @@ impl Intrinsic { }} }} - this.#copied += values.length; + this.processed += values.length; }} }} @@ -636,6 +606,7 @@ impl Intrinsic { #buffers = new Map(); #bufferIDs = new Map(); + // NOTE: componentIdx === -1 indicates the host getNextBufferID(componentIdx) {{ const current = this.#bufferIDs.get(componentIdx); if (current === undefined) {{ @@ -662,7 +633,7 @@ impl Intrinsic { if (args.start !== undefined && args.componentIdx === undefined) {{ throw new TypeError('missing/invalid component idx'); }} if (args.count === undefined) {{ throw new TypeError('missing/invalid obj count'); }} - if (args.elemMeta === undefined) {{ throw new TypeError('missing/invalid element metadata for use with managed buffer'); }} + if (!args.elemMeta) {{ throw new TypeError('missing/invalid element metadata for use with managed buffer'); }} const {{ componentIdx, data, start, count }} = args; @@ -678,6 +649,7 @@ impl Intrinsic { capacity: args.count, elemMeta: args.elemMeta, data: args.data, + target: args.target, }}); if (instanceBuffers.has(nextBufID)) {{ @@ -705,17 +677,6 @@ impl Intrinsic { )); } - Intrinsic::WriteAsyncEventToMemory => { - let debug_log_fn = Intrinsic::DebugLog.name(); - let write_async_event_to_memory_fn = Intrinsic::WriteAsyncEventToMemory.name(); - output.push_str(&format!(r#" - function {write_async_event_to_memory_fn}(memory, task, event, ptr) {{ - {debug_log_fn}('[{write_async_event_to_memory_fn}()] args', {{ memory, task, event, ptr }}); - throw new Error('{write_async_event_to_memory_fn}() not implemented'); - }} - "#)); - } - Intrinsic::RepTableClass => { let debug_log_fn = Intrinsic::DebugLog.name(); let rep_table_class = Intrinsic::RepTableClass.name(); @@ -736,17 +697,22 @@ impl Intrinsic { if (freeIdx === 0) {{ this.#data.push(val); this.#data.push(null); - return (this.#data.length >> 1) - 1; + const rep = (this.#data.length >> 1) - 1; + {debug_log_fn}('[{rep_table_class}#insert()] inserted', {{ val, target: this.target, rep }}); + return rep; }} this.#data[0] = this.#data[freeIdx << 1]; const placementIdx = freeIdx << 1; this.#data[placementIdx] = val; this.#data[placementIdx + 1] = null; + {debug_log_fn}('[{rep_table_class}#insert()] inserted', {{ val, target: this.target, rep: freeIdx }}); return freeIdx; }} get(rep) {{ {debug_log_fn}('[{rep_table_class}#get()] args', {{ rep, target: this.target }}); + if (rep === 0) {{ throw new Error('invalid resource rep during get, (cannot be 0)'); }} + const baseIdx = rep << 1; const val = this.#data[baseIdx]; return val; @@ -754,17 +720,19 @@ impl Intrinsic { contains(rep) {{ {debug_log_fn}('[{rep_table_class}#contains()] args', {{ rep, target: this.target }}); + if (rep === 0) {{ throw new Error('invalid resource rep during contains, (cannot be 0)'); }} + const baseIdx = rep << 1; return !!this.#data[baseIdx]; }} remove(rep) {{ {debug_log_fn}('[{rep_table_class}#remove()] args', {{ rep, target: this.target }}); + if (rep === 0) {{ throw new Error('invalid resource rep during remove, (cannot be 0)'); }} if (this.#data.length === 2) {{ throw new Error('invalid'); }} const baseIdx = rep << 1; const val = this.#data[baseIdx]; - if (val === 0) {{ throw new Error('invalid resource rep (cannot be 0)'); }} this.#data[baseIdx] = this.#data[0]; this.#data[0] = rep; @@ -780,133 +748,56 @@ impl Intrinsic { "#)); } - Intrinsic::GlobalAsyncParamLowersClass => { - let global_async_param_lowers_class = self.name(); + Intrinsic::GlobalComponentMemoryMap => { + let global_component_memory_map = Intrinsic::GlobalComponentMemoryMap.name(); output.push_str(&format!( - r#" - class {global_async_param_lowers_class} {{ - static map = new Map(); - - static generateKey(args) {{ - const {{ componentIdx, iface, fnName }} = args; - if (componentIdx === undefined) {{ throw new TypeError("missing component idx"); }} - if (iface === undefined) {{ throw new TypeError("missing iface name"); }} - if (fnName === undefined) {{ throw new TypeError("missing function name"); }} - return `${{componentIdx}}-${{iface}}-${{fnName}}`; - }} - - static define(args) {{ - const {{ componentIdx, iface, fnName, fn }} = args; - if (!fn) {{ throw new TypeError('missing function'); }} - const key = {global_async_param_lowers_class}.generateKey(args); - {global_async_param_lowers_class}.map.set(key, fn); - }} - - static lookup(args) {{ - const {{ componentIdx, iface, fnName }} = args; - const key = {global_async_param_lowers_class}.generateKey(args); - return {global_async_param_lowers_class}.map.get(key); - }} - }} - "# + "const {global_component_memory_map} = new Map();\n" )); } - Intrinsic::GlobalComponentAsyncLowersClass => { - let global_component_lowers_class = - Intrinsic::GlobalComponentAsyncLowersClass.name(); + Intrinsic::RegisterGlobalMemoryForComponent => { + let global_component_memory_map = Intrinsic::GlobalComponentMemoryMap.name(); + let register_global_component_memory = + Intrinsic::RegisterGlobalMemoryForComponent.name(); output.push_str(&format!( r#" - class {global_component_lowers_class} {{ - static map = new Map(); - - constructor() {{ throw new Error('{global_component_lowers_class} should not be constructed'); }} - - static define(args) {{ - const {{ componentIdx, qualifiedImportFn, fn }} = args; - let inner = {global_component_lowers_class}.map.get(componentIdx); - if (!inner) {{ - inner = new Map(); - {global_component_lowers_class}.map.set(componentIdx, inner); - }} - - inner.set(qualifiedImportFn, fn); - }} - - static lookup(componentIdx, qualifiedImportFn) {{ - let inner = {global_component_lowers_class}.map.get(componentIdx); - if (!inner) {{ - inner = new Map(); - {global_component_lowers_class}.map.set(componentIdx, inner); - }} - - const found = inner.get(qualifiedImportFn); - if (found) {{ return found; }} - - // In some cases, async lowers are *not* host provided, and - // but contain/will call an async function in the host. - // - // One such case is `stream.write`/`stream.read` trampolines which are - // actually re-exported through a patch up container *before* - // they call the relevant async host trampoline. - // - // So the path of execution from a component export would be: - // - // async guest export --> stream.write import (host wired) -> guest export (patch component) -> async host trampoline - // - // On top of all this, the trampoline that is eventually called is async, - // so we must await the patched guest export call. - // - if (qualifiedImportFn.includes("[stream-write-") || qualifiedImportFn.includes("[stream-read-")) {{ - return async (...args) => {{ - const [originalFn, ...params] = args; - return await originalFn(...params); - }}; - }} - - // All other cases can call the registered function directly - return (...args) => {{ - const [originalFn, ...params] = args; - return originalFn(...params); - }}; - }} - }} - "# - )); + function {register_global_component_memory}(args) {{ + const {{ componentIdx, memory, memoryIdx }} = args ?? {{}}; + if (componentIdx === undefined) {{ throw new TypeError('missing component idx'); }} + if (memory === undefined && memoryIdx === undefined) {{ throw new TypeError('missing both memory & memory idx'); }} + let inner = {global_component_memory_map}.get(componentIdx); + if (!inner) {{ + inner = []; + {global_component_memory_map}.set(componentIdx, inner); + }} + inner.push({{ memory, memoryIdx, componentIdx }}); + }} + "#) + ); } - Intrinsic::GlobalComponentMemoriesClass => { - let global_component_memories_class = - Intrinsic::GlobalComponentMemoriesClass.name(); + Intrinsic::LookupMemoriesForComponent => { + let global_component_memory_map = Intrinsic::GlobalComponentMemoryMap.name(); + let lookup_global_memories_for_component = + Intrinsic::LookupMemoriesForComponent.name(); output.push_str(&format!( r#" - class {global_component_memories_class} {{ - static map = new Map(); + function {lookup_global_memories_for_component}(args) {{ + const {{ componentIdx }} = args ?? {{}}; + if (args.componentIdx === undefined) {{ throw new TypeError("missing component idx"); }} - constructor() {{ throw new Error('{global_component_memories_class} should not be constructed'); }} + const metas = {global_component_memory_map}.get(componentIdx); + if (!metas) {{ return []; }} - static save(args) {{ - const {{ idx, componentIdx, memory }} = args; - let inner = {global_component_memories_class}.map.get(componentIdx); - if (!inner) {{ - inner = []; - {global_component_memories_class}.map.set(componentIdx, inner); - }} - inner.push({{ memory, idx }}); - }} + if (args.memoryIdx === undefined) {{ + return metas.map(meta => meta.memory); + }} - static getMemoriesForComponentIdx(componentIdx) {{ - const metas = {global_component_memories_class}.map.get(componentIdx); - return metas.map(meta => meta.memory); - }} - - static getMemory(componentIdx, idx) {{ - const metas = {global_component_memories_class}.map.get(componentIdx); - return metas.find(meta => meta.idx === idx)?.memory; - }} - }} - "# - )); + const meta = metas.map.find(meta => meta.componentIdx === componentIdx); + return meta?.memory; + }} + "#) + ); } } } @@ -950,12 +841,12 @@ pub struct RenderIntrinsicsArgs<'a> { } /// Intrinsics that should be rendered as early as possible -const EARLY_INTRINSICS: [Intrinsic; 21] = [ +const EARLY_INTRINSICS: [Intrinsic; 24] = [ Intrinsic::DebugLog, Intrinsic::GlobalAsyncDeterminism, - Intrinsic::GlobalComponentAsyncLowersClass, - Intrinsic::GlobalAsyncParamLowersClass, - Intrinsic::GlobalComponentMemoriesClass, + Intrinsic::GlobalComponentMemoryMap, + Intrinsic::LookupMemoriesForComponent, + Intrinsic::RegisterGlobalMemoryForComponent, Intrinsic::RepTableClass, Intrinsic::CoinFlip, Intrinsic::ScopeId, @@ -964,6 +855,7 @@ const EARLY_INTRINSICS: [Intrinsic; 21] = [ Intrinsic::TypeCheckValidI32, Intrinsic::TypeCheckAsyncFn, Intrinsic::AsyncFunctionCtor, + Intrinsic::AsyncTask(AsyncTaskIntrinsic::ClearCurrentTask), Intrinsic::AsyncTask(AsyncTaskIntrinsic::CurrentTaskMayBlock), Intrinsic::AsyncTask(AsyncTaskIntrinsic::GlobalAsyncCurrentTaskIds), Intrinsic::AsyncTask(AsyncTaskIntrinsic::GlobalAsyncCurrentComponentIdxs), @@ -972,6 +864,8 @@ const EARLY_INTRINSICS: [Intrinsic; 21] = [ Intrinsic::Host(HostIntrinsic::PrepareCall), Intrinsic::Host(HostIntrinsic::AsyncStartCall), Intrinsic::Host(HostIntrinsic::SyncStartCall), + Intrinsic::AsyncTask(AsyncTaskIntrinsic::AsyncSubtaskClass), + Intrinsic::Waitable(WaitableIntrinsic::WaitableClass), ]; /// Emits the intrinsic `i` to this file and then returns the name of the @@ -1121,7 +1015,6 @@ pub fn render_intrinsics(args: RenderIntrinsicsArgs) -> Source { &Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState), &Intrinsic::Component(ComponentIntrinsic::GlobalAsyncStateMap), &Intrinsic::RepTableClass, - &Intrinsic::AwaitableClass, &Intrinsic::AsyncTask(AsyncTaskIntrinsic::AsyncSubtaskClass), &Intrinsic::Waitable(WaitableIntrinsic::WaitableClass), ]); @@ -1143,6 +1036,14 @@ pub fn render_intrinsics(args: RenderIntrinsicsArgs) -> Source { .extend([&Intrinsic::Host(HostIntrinsic::StoreEventInComponentMemory)]); } + if args + .intrinsics + .contains(&Intrinsic::Waitable(WaitableIntrinsic::WaitableSetDrop)) + { + args.intrinsics + .extend([&Intrinsic::Waitable(WaitableIntrinsic::RemoveWaitableSet)]); + } + if args.intrinsics.contains(&Intrinsic::Component( ComponentIntrinsic::GetOrCreateAsyncState, )) { @@ -1152,6 +1053,14 @@ pub fn render_intrinsics(args: RenderIntrinsicsArgs) -> Source { ]); } + if args.intrinsics.contains(&Intrinsic::Component( + ComponentIntrinsic::ComponentAsyncStateClass, + )) { + args.intrinsics.extend([&Intrinsic::AsyncStream( + AsyncStreamIntrinsic::GlobalStreamMap, + )]); + } + if args .intrinsics .contains(&Intrinsic::Lift(LiftIntrinsic::LiftFlatResult)) @@ -1228,7 +1137,7 @@ pub fn render_intrinsics(args: RenderIntrinsicsArgs) -> Source { .contains(&Intrinsic::AsyncTask(AsyncTaskIntrinsic::GetCurrentTask)) || args .intrinsics - .contains(&Intrinsic::AsyncTask(AsyncTaskIntrinsic::EndCurrentTask)) + .contains(&Intrinsic::AsyncTask(AsyncTaskIntrinsic::ClearCurrentTask)) { args.intrinsics.extend([ &Intrinsic::AsyncTask(AsyncTaskIntrinsic::AsyncTaskClass), @@ -1242,6 +1151,7 @@ pub fn render_intrinsics(args: RenderIntrinsicsArgs) -> Source { { args.intrinsics.extend([ &Intrinsic::AsyncStream(AsyncStreamIntrinsic::GlobalStreamMap), + &Intrinsic::AsyncStream(AsyncStreamIntrinsic::GlobalStreamTableMap), &Intrinsic::AsyncStream(AsyncStreamIntrinsic::StreamWritableEndClass), &Intrinsic::AsyncStream(AsyncStreamIntrinsic::StreamReadableEndClass), ]); @@ -1263,6 +1173,7 @@ pub fn render_intrinsics(args: RenderIntrinsicsArgs) -> Source { )) { args.intrinsics.extend([ &Intrinsic::AsyncStream(AsyncStreamIntrinsic::GlobalStreamMap), + &Intrinsic::AsyncStream(AsyncStreamIntrinsic::GlobalStreamTableMap), &Intrinsic::AsyncStream(AsyncStreamIntrinsic::HostStreamClass), &Intrinsic::AsyncStream(AsyncStreamIntrinsic::ExternalStreamClass), ]); @@ -1348,7 +1259,6 @@ impl Intrinsic { "URL", "WebAssembly", "GlobalComponentMemories", - "GlobalComponentAsyncLowers", ]) } @@ -1404,11 +1314,12 @@ impl Intrinsic { // Async Intrinsic::GlobalAsyncDeterminism => "ASYNC_DETERMINISM", - Intrinsic::AwaitableClass => "Awaitable", Intrinsic::CoinFlip => "_coinFlip", - Intrinsic::GlobalComponentAsyncLowersClass => "GlobalComponentAsyncLowers", - Intrinsic::GlobalAsyncParamLowersClass => "GlobalAsyncParamLowers", - Intrinsic::GlobalComponentMemoriesClass => "GlobalComponentMemories", + + // Iteratively saved metadata + Intrinsic::GlobalComponentMemoryMap => "GLOBAL_COMPONENT_MEMORY_MAP", + Intrinsic::RegisterGlobalMemoryForComponent => "registerGlobalMemoryForComponent", + Intrinsic::LookupMemoriesForComponent => "lookupMemoriesForComponent", // Data structures Intrinsic::RepTableClass => "RepTable", @@ -1420,7 +1331,6 @@ impl Intrinsic { // Helpers for working with async state Intrinsic::AsyncEventCodeEnum => "ASYNC_EVENT_CODE", - Intrinsic::WriteAsyncEventToMemory => "writeAsyncEventToMemory", } } } diff --git a/crates/js-component-bindgen/src/intrinsics/p3/async_future.rs b/crates/js-component-bindgen/src/intrinsics/p3/async_future.rs index 444d19855..7371c2fee 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/async_future.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/async_future.rs @@ -225,7 +225,7 @@ impl AsyncFutureIntrinsic { }} elementTypeRep() {{ return this.#elementTypeRep; }} - isHostOwned() {{ return this.#componentIdx === null; }} + isHostOwned() {{ return this.#componentIdx === -1; }} }} ")); } @@ -291,16 +291,16 @@ impl AsyncFutureIntrinsic { if (!state.mayLeave) {{ throw new Error('component instance is not marked as may leave'); }} let future = new Promise(); - let writeEndIdx = {global_future_map}.insert(new {future_writable_end_class}({{ + let writeEndWaitableIdx = {global_future_map}.insert(new {future_writable_end_class}({{ isFuture: true, elementTypeRep, }})); - let readEndIdx = {global_future_map}.insert(new {future_readable_end_class}({{ + let readEndWaitableIdx = {global_future_map}.insert(new {future_readable_end_class}({{ isFuture: true, elementTypeRep, }})); - return BigInt(writeEndIdx) << 32n | BigInt(readEndIdx); + return BigInt(writeEndWaitableIdx) << 32n | BigInt(readEndWaitableIdx); }} ")); } @@ -367,7 +367,15 @@ impl AsyncFutureIntrinsic { if ({is_borrowed_type}(componentIdx, future.elementTypeRep())) {{ throw new Error('borrowed types not supported'); }} - const {{ id: bufferID }} = {global_buffer_mgr}.createBuffer({{ componentIdx, start, len, typeIdx, writable, readable }}); + const {{ id: bufferID }} = {global_buffer_mgr}.createBuffer({{ + componentIdx, + start, + len, + typeIdx, + writable, + readable, + target: `future read/write`, + }}); const processFn = (result) => {{ if (.remaining(bufferID) !== 0) {{ @@ -404,7 +412,7 @@ impl AsyncFutureIntrinsic { }} if (futureEnd.hasPendingEvent()) {{ - const {{ code, payload0: index, payload1 }} = futureEnd.getEvent(); + const {{ code, payload0: index, payload1 }} = futureEnd.getPendingEvent(); if (code !== eventCode || index != 1) {{ throw new Error('invalid event, does not match expected event code'); }} @@ -469,10 +477,10 @@ impl AsyncFutureIntrinsic { }} }} - const {{ code, payload0: index, payload1: payload }} = e.getEvent(); + const {{ code, payload0: index, payload1: payload }} = futureEnd.getPendingEvent(); if (futureEnd.isCopying()) {{ throw new Error('future end is still in copying state'); }} if (code !== {async_event_code_enum}) {{ throw new Error('unexpected event code [' + code + '], expected [' + {async_event_code_enum} + ']'); }} - if (index !== 1) {{ throw new Error('unexpected index, should be 1'); }} + if (index !== streamEndIdx) {{ throw new Error('index does not match stream end'); }} return payload; }} 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 fdfbec229..ae6f304da 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/async_stream.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/async_stream.rs @@ -1,7 +1,7 @@ //! Intrinsics that represent helpers that enable Stream integration use crate::{ - intrinsics::{Intrinsic, component::ComponentIntrinsic, p3::waitable::WaitableIntrinsic}, + intrinsics::{Intrinsic, component::ComponentIntrinsic}, source::Source, }; @@ -19,6 +19,22 @@ pub enum AsyncStreamIntrinsic { /// ``` GlobalStreamMap, + /// Global that stores stream tables + /// + /// Each component has a distinct stream table, + /// and operations like `stream-transfer` move streams + /// between table to represent ownership moving between + /// components. + /// + /// Note that stream ends themselves are *waitables*, and are stored + /// there, though references exist in the stream table map. + /// + /// ```ts + /// type u32 = number; + /// type GlobalStreamTableMap = Map>; + /// ``` + GlobalStreamTableMap, + /// The definition of the `StreamEnd` JS superclass StreamEndClass, @@ -230,6 +246,7 @@ impl AsyncStreamIntrinsic { pub fn name(&self) -> &'static str { match self { Self::GlobalStreamMap => "STREAMS", + Self::GlobalStreamTableMap => "STREAM_TABLES", Self::StreamEndClass => "StreamEnd", Self::InternalStreamClass => "InternalStream", Self::StreamWritableEndClass => "StreamWritableEnd", @@ -260,7 +277,7 @@ impl AsyncStreamIntrinsic { static CopyResult = {{ COMPLETED: 0, DROPPED: 1, - CANCELLED: 1, + CANCELLED: 2, }}; static CopyState = {{ @@ -295,21 +312,20 @@ impl AsyncStreamIntrinsic { if (!args.waitable) {{ throw new Error('missing/invalid waitable'); }} this.#tableIdx = args.tableIdx; - this.#componentIdx = args.componentIdx ??= null; this.#waitable = args.waitable; this.#onDrop = args.onDrop; this.target = args.target; }} - isHostOwned() {{ return this.#componentIdx === null; }} - tableIdx() {{ return this.#tableIdx; }} + idx() {{ return this.#idx; }} setIdx(idx) {{ this.#idx = idx; }} setTarget(tgt) {{ this.target = tgt; }} getWaitable() {{ return this.#waitable; }} + setWaitable(w) {{ this.#waitable = w; }} setCopyState(state) {{ this.#copyState = state; }} getCopyState() {{ return this.#copyState; }} @@ -359,17 +375,27 @@ impl AsyncStreamIntrinsic { isDropped() {{ return this.#dropped; }} drop() {{ - if (this.#dropped) {{ throw new Error('already dropped'); }} + {debug_log_fn}('[{stream_end_class}#drop()]', {{ + waitable: this.#waitable, + waitableinSet: this.#waitable.isInSet(), + componentIdx: this.#waitable.componentIdx(), + }}); + + if (this.#dropped) {{ + {debug_log_fn}('[{stream_end_class}#drop()] already dropped', {{ + waitable: this.#waitable, + waitableinSet: this.#waitable.isInSet(), + componentIdx: this.#waitable.componentIdx(), + }}); + return; + }} if (this.#waitable) {{ const w = this.#waitable; - this.#waitable = null; w.drop(); }} this.#dropped = true; - - if (this.#onDrop) {{ this.#onDrop() }} }} }} "# @@ -401,16 +427,20 @@ impl AsyncStreamIntrinsic { let copy_setup_impl = format!( r#" setupCopy(args) {{ - const {{ memory, ptr, count, eventCode }} = args; + const {{ memory, ptr, count, eventCode, componentIdx, skipStateCheck }} = args; if (eventCode === undefined) {{ throw new Error("missing/invalid event code"); }} let buffer = args.buffer; let bufferID = args.bufferID; - if (this.isCopying()) {{ - throw new Error('stream is currently undergoing a separate copy'); - }} - if (this.getCopyState() !== {stream_end_class}.CopyState.IDLE) {{ - throw new Error(`stream [${{streamEndIdx}}] (tableIdx [${{streamTableIdx}}], component [${{componentIdx}}]) is not in idle state`); + + // Only check invariants if we are *not* doing a follow-up/post-blocked read + if (!skipStateCheck) {{ + if (this.isCopying()) {{ + throw new Error('stream is currently undergoing a separate copy'); + }} + if (this.getCopyState() !== {stream_end_class}.CopyState.IDLE) {{ + throw new Error(`stream copy state is not idle`); + }} }} const elemMeta = this.getElemMeta(); @@ -419,8 +449,8 @@ impl AsyncStreamIntrinsic { // If we already have a managed buffer (likely host case), we can use that, otherwise we must // create a buffer (likely in the guest case) if (!buffer) {{ - const newBuffer = {global_buffer_manager}.createBuffer({{ - componentIdx: this.#componentIdx, + const newBufferMeta = {global_buffer_manager}.createBuffer({{ + componentIdx, memory, start: ptr, count, @@ -433,8 +463,9 @@ impl AsyncStreamIntrinsic { isWritable: this.isReadable(), elemMeta, }}); - bufferID = newBuffer.bufferID; - buffer = newBuffer.buffer; + bufferID = newBufferMeta.id; + buffer = newBufferMeta.buffer; + buffer.setTarget(`component [${{componentIdx}}] {end_class_name} buffer (id [${{bufferID}}], count [${{count}}], eventCode [${{eventCode}}])`); }} const streamEnd = this; @@ -450,46 +481,50 @@ impl AsyncStreamIntrinsic { if (result < 0 || result >= 16) {{ throw new Error(`unsupported stream copy result [${{result}}]`); }} - if (buffer.copied() >= {managed_buffer_class}.MAX_LENGTH) {{ - throw new Error(`buffer size [${{buf.length}}] greater than max length`); + if (buffer.processed >= {managed_buffer_class}.MAX_LENGTH) {{ + throw new Error(`processed count [${{buf.length}}] greater than max length`); }} if (buffer.length > 2**28) {{ throw new Error('buffer uses reserved space'); }} - const packedResult = (buffer.copied() << 4) | result; - return {{ code: eventCode, payload0: streamEnd.waitableIdx(), payload1: packedResult }}; + const packedResult = (Number(buffer.processed) << 4) | result; + const event = {{ code: eventCode, payload0: streamEnd.waitableIdx(), payload1: packedResult }}; + + return event; }}; - const onCopy = (reclaimBufferFn) => {{ + const onCopyFn = (reclaimBufferFn) => {{ streamEnd.setPendingEventFn(() => {{ return processFn({stream_end_class}.CopyResult.COMPLETED, reclaimBufferFn); }}); }}; - const onCopyDone = (result) => {{ + const onCopyDoneFn = (result) => {{ streamEnd.setPendingEventFn(() => {{ return processFn(result); }}); }}; - return {{ bufferID, buffer, onCopy, onCopyDone }}; + return {{ bufferID, buffer, onCopyFn, onCopyDoneFn }}; }} "# ); - let (inner_rw_fn_name, inner_rw_impl) = match self { + let (rw_fn_name, inner_rw_impl) = match self { // Internal implementation for writing to internal buffer after reading from a provided managed buffers + // + // This _write() function is primarily called by guests. Self::StreamWritableEndClass => ( - "_write", + "write", format!( r#" _write(args) {{ - const {{ buffer, onCopy, onCopyDone }} = args; + const {{ buffer, onCopyFn, onCopyDoneFn, componentIdx }} = args; if (!buffer) {{ throw new TypeError('missing/invalid buffer'); }} - if (!onCopy) {{ throw new TypeError("missing/invalid onCopy handler"); }} - if (!onCopyDone) {{ throw new TypeError("missing/invalid onCopyDone handler"); }} + if (!onCopyFn) {{ throw new TypeError("missing/invalid onCopy handler"); }} + if (!onCopyDoneFn) {{ throw new TypeError("missing/invalid onCopyDone handler"); }} if (!this.#pendingBufferMeta.buffer) {{ - this.setPendingBufferMeta({{ componentIdx: this.#componentIdx, buffer, onCopy, onCopyDone }}); + this.setPendingBufferMeta({{ componentIdx, buffer, onCopyFn, onCopyDoneFn }}); return; }} @@ -501,57 +536,71 @@ impl AsyncStreamIntrinsic { // If the buffer came from the same component that is currently doing the operation // we're doing a inter-component write, and only unit or numeric types are allowed - if (this.#pendingBufferMeta.componentIdx === this.#componentIdx && !this.#elemMeta.isNoneOrNumeric) {{ + const pendingElemIsNoneOrNumeric = pendingElemMeta.isNone || pendingElemMeta.isNumeric; + if (this.#pendingBufferMeta.componentIdx === buffer.componentIdx() && !pendingElemIsNoneOrNumeric) {{ throw new Error("trap: cannot stream non-numeric types within the same component (send)"); }} // If original capacities were zero, we're dealing with a unit stream, // a write to the unit stream is instantly copied without any work. - if (buffer.capacity() === 0 && this.#pendingBufferMeta.buffer.capacity() === 0) {{ - onCopyDone({stream_end_class}.CopyResult.COMPLETED); + if (buffer.capacity === 0 && this.#pendingBufferMeta.buffer.capacity === 0) {{ + onCopyDoneFn({stream_end_class}.CopyResult.COMPLETED); return; }} // If the internal buffer has no space left to take writes, // the write is complete, we must reset and wait for another read // to clear up space in the buffer. - if (this.#pendingBufferMeta.buffer.remainingCapacity() === 0) {{ + if (this.#pendingBufferMeta.buffer.remaining() === 0) {{ this.resetAndNotifyPending({stream_end_class}.CopyResult.COMPLETED); - this.setPendingBufferMeta({{ componentIdx, buffer, onCopy, onCopyDone }}); + this.setPendingBufferMeta({{ componentIdx, buffer, onCopyFn, onCopyDoneFn }}); return; }} - // If there is still remaining capacity in the incoming buffer, perform copy of values + // At this point it is implied that remaining is > 0, + // so if there is still remaining capacity in the incoming buffer, perform copy of values // to the internal buffer from the incoming buffer - if (buffer.remainingCapacity() > 0) {{ - const numElements = Math.min(buffer.remainingCapacity(), this.#pendingBufferMeta.buffer.remainingCapacity()); + let transferred = false; + if (buffer.remaining() > 0) {{ + const numElements = Math.min(buffer.remaining(), this.#pendingBufferMeta.buffer.remaining()); this.#pendingBufferMeta.buffer.write(buffer.read(numElements)); this.#pendingBufferMeta.onCopyFn(() => this.resetPendingBufferMeta()); + transferred = true; }} - onCopyDone({stream_end_class}.CopyResult.COMPLETED); + onCopyDoneFn({stream_end_class}.CopyResult.COMPLETED); + + // After successfully doing a guest write, we may need to + // notify a blocked/waiting host read that it can continue + // + // We notify the other end of the stream (likely held by the hsot) + // *after* the transfer (above) and book-keeping is done, if one occurred. + if (transferred) {{ this.#otherEndNotify(); }} }} "#, ), ), + // Internal implementation for reading from an internal buffer and writing to a provided managed buffer + // + // This _read() function is primarily called by guests. Self::StreamReadableEndClass => ( - "_read", + "read", format!( r#" _read(args) {{ - const {{ buffer, onCopyDone, onCopy }} = args; + const {{ buffer, onCopyDoneFn, onCopyFn, componentIdx }} = args; if (this.isDropped()) {{ - onCopyDone({stream_end_class}.CopyResult.DROPPED); + onCopyDoneFn({stream_end_class}.CopyResult.DROPPED); return; }} if (!this.#pendingBufferMeta.buffer) {{ this.setPendingBufferMeta({{ - componentIdx: this.#componentIdx, + componentIdx, buffer, - onCopy, - onCopyDone, + onCopyFn, + onCopyDoneFn, }}); return; }} @@ -564,24 +613,36 @@ impl AsyncStreamIntrinsic { // If the buffer came from the same component that is currently doing the operation // we're doing a inter-component read, and only unit or numeric types are allowed - if (this.#pendingBufferMeta.componentIdx === this.#componentIdx && !this.#elemMeta.isNoneOrNumeric) {{ + const pendingElemIsNoneOrNumeric = pendingElemMeta.isNone || pendingElemMeta.isNumeric; + if (this.#pendingBufferMeta.componentIdx === buffer.componentIdx() && !pendingElemIsNoneOrNumeric) {{ throw new Error("trap: cannot stream non-numeric types within the same component (read)"); }} - const pendingRemaining = this.#pendingBufferMeta.buffer.remainingCapacity(); + const pendingRemaining = this.#pendingBufferMeta.buffer.remaining(); + let transferred = false; if (pendingRemaining > 0) {{ - const bufferRemaining = buffer.remainingCapacity(); + const bufferRemaining = buffer.remaining(); if (bufferRemaining > 0) {{ const count = Math.min(pendingRemaining, bufferRemaining); buffer.write(this.#pendingBufferMeta.buffer.read(count)) this.#pendingBufferMeta.onCopyFn(() => this.resetPendingBufferMeta()); + transferred = true; }} - onCopyDone({stream_end_class}.CopyResult.COMPLETED); + + onCopyDoneFn({stream_end_class}.CopyResult.COMPLETED); + + // After successfully doing a guest read, we may need to + // notify a blocked/waiting host write that it can continue + // + // We must only notify the other end of the stream (likely held + // by the host) after a transfer (above) and book-keeping is done. + if (transferred) {{ this.#otherEndNotify(); }} + return; }} this.resetAndNotifyPending({stream_end_class}.CopyResult.COMPLETED); - this.setPendingBufferMeta({{ componentIdx: this.#componentIdx, buffer, onCopy, onCopyDone }}); + this.setPendingBufferMeta({{ componentIdx, buffer, onCopyFn, onCopyDoneFn }}); }} "#, ), @@ -600,7 +661,7 @@ impl AsyncStreamIntrinsic { let copy_impl = format!( r#" async copy(args) {{ - const {{ isAsync, memory, componentIdx, ptr, count, eventCode }} = args; + const {{ isAsync, memory, componentIdx, ptr, count, eventCode, initial }} = args; if (eventCode === undefined) {{ throw new TypeError('missing/invalid event code'); }} if (this.isDropped()) {{ @@ -612,26 +673,30 @@ impl AsyncStreamIntrinsic { return; }} - const {{ buffer, onCopy, onCopyDone }} = this.setupCopy({{ + const {{ buffer, onCopyFn, onCopyDoneFn }} = this.setupCopy({{ memory, eventCode, + componentIdx, ptr, count, buffer: args.buffer, bufferID: args.bufferID, + initial, }}); // Perform the read/write - this.{inner_rw_fn_name}({{ + this._{rw_fn_name}({{ buffer, - onCopy, - onCopyDone, + onCopyFn, + onCopyDoneFn, + componentIdx, }}); // If sync, wait forever but allow task to do other things if (!this.hasPendingEvent()) {{ if (isAsync) {{ this.setCopyState({stream_end_class}.CopyState.ASYNC_COPYING); + {debug_log_fn}('[{stream_end_class}#copy()] blocked'); return {async_blocked_const}; }} else {{ this.setCopyState({stream_end_class}.CopyState.SYNC_COPYING); @@ -644,9 +709,7 @@ impl AsyncStreamIntrinsic { const streamEnd = this; await task.suspendUntil({{ - readyFn: () => {{ - return streamEnd.hasPendingEvent(); - }} + readyFn: () => streamEnd.hasPendingEvent(), }}); }} }} @@ -659,7 +722,8 @@ impl AsyncStreamIntrinsic { const {{ code, payload0: index, payload1: payload }} = event; - if (code !== eventCode || index !== this.#getEndIdxFn() || payload === {async_blocked_const}) {{ + const waitableIdx = this.getWaitable().idx(); + if (code !== eventCode || index !== waitableIdx || payload === {async_blocked_const}) {{ const errMsg = "invalid event code/event during stream operation"; {debug_log_fn}(errMsg, {{ event, @@ -669,8 +733,8 @@ impl AsyncStreamIntrinsic { eventCode, codeDoesNotMatchEventCode: code !== eventCode, index, - internalEndIdx: this.#getEndIdxFn(), - indexDoesNotMatch: index !== this.#getEndIdxFn(), + internalEndIdx: waitableIdx, + indexDoesNotMatch: index !== waitableIdx, }}); throw new Error(errMsg); }} @@ -705,25 +769,87 @@ impl AsyncStreamIntrinsic { async write(v) {{ {debug_log_fn}('[{end_class_name}#write()] args', {{ v }}); - const {{ id: bufferID, buffer }} = {global_buffer_manager}.createBuffer({{ - componentIdx: null, // componentIdx of null indicates the host - count: 1, - isReadable: true, // we need to read from this buffer later - isWritable: false, - elemMeta: this.#elemMeta, - data: v, - }}); + // Wait for an existing write operation to end, if present, + // otherwise register this write for any future operations. + // + // NOTE: this complexity below is an attempt to sequence operations + // to ensure consecutive writes only wait on their direct predecessors, + // (i.e. write #3 must wait on write #2, *not* write #1) + // + let newResult = Promise.withResolvers(); + if (this.#result) {{ + try {{ + const p = this.#result.promise; + this.#result = newResult; + await p; + }} catch (err) {{ + {debug_log_fn}('[{end_class_name}#write()] error waiting for previous write', err); + // If the previous write we were waiting on errors for any reason, + // we can ignore it and attempt to continue with this write + // which may also fail for a similar reason + }} + }} else {{ + this.#result = newResult; + }} + const {{ promise, resolve, reject }} = newResult; - await this.copy({{ - isAsync: true, - count: 1, - bufferID, - buffer, - eventCode: {async_event_code_enum}.STREAM_WRITE, - }}); + const count = 1; + try {{ + const {{ id: bufferID, buffer }} = {global_buffer_manager}.createBuffer({{ + componentIdx: -1, + count, + isReadable: true, // we need to read from this buffer later + isWritable: false, + elemMeta: this.#elemMeta, + data: v, + }}); + buffer.setTarget(`host stream write buffer (id [${{bufferID}}], count [${{count}}], data len [${{v.length}}])`); + + let packedResult; + packedResult = await this.copy({{ + isAsync: true, + count, + bufferID, + buffer, + eventCode: {async_event_code_enum}.STREAM_WRITE, + componentIdx: -1, + }}); + + if (packedResult === {async_blocked_const}) {{ + // If the write was blocked, we can only make progress when + // the read side notifies us of a read, then we must attempt the copy again + + await this.#otherEndWait(); + + packedResult = await this.copy({{ + isAsync: true, + count, + bufferID, + buffer, + eventCode: {async_event_code_enum}.STREAM_WRITE, + skipStateCheck: true, + componentIdx: -1, + }}); + + if (packedResult === {async_blocked_const}) {{ + throw new Error("unexpected double block during write"); + }} + }} + + // If the write was not blocked, we can resolve right away + this.#result = null; + resolve(); + + }} catch (err) {{ + {debug_log_fn}('[{end_class_name}#write()] error', err); + reject(err); + }} + + return await promise; }} "# ), + // NOTE: Host stream reads typically take this path, via `ExternalStream` class's // `read()` function which calls the underlying stream end's `read()` // fn (below) via an anonymous function. @@ -732,30 +858,100 @@ impl AsyncStreamIntrinsic { async read() {{ {debug_log_fn}('[{end_class_name}#read()]'); - const {{ id: bufferID, buffer }} = {global_buffer_manager}.createBuffer({{ - componentIdx: null, // componentIdx of null indicates the host - count: 1, - isReadable: false, - isWritable: true, // we need to write out the pending buffer (if present) - elemMeta: this.#elemMeta, - data: [], - }}); + // Wait for an existing read operation to end, if present, + // otherwise register this read for any future operations. + // + // NOTE: this complexity below is an attempt to sequence operations + // to ensure consecutive reads only wait on their direct predecessors, + // (i.e. read #3 must wait on read #2, *not* read #1) + // + const newResult = Promise.withResolvers(); + if (this.#result) {{ + try {{ + const p = this.#result.promise; + this.#result = newResult; + await p; + }} catch (err) {{ + {debug_log_fn}('[{end_class_name}#read()] error waiting for previous read', err); + // If the previous write we were waiting on errors for any reason, + // we can ignore it and attempt to continue with this read + // which may also fail for a similar reason + }} + }} else {{ + this.#result = newResult; + }} + const {{ promise, resolve, reject }} = newResult; const count = 1; - const packedResult = await this.copy({{ - isAsync: true, - count, - bufferID, - buffer, - eventCode: {async_event_code_enum}.STREAM_READ, - }}); + try {{ + const {{ id: bufferID, buffer }} = {global_buffer_manager}.createBuffer({{ + componentIdx: -1, // componentIdx of -1 indicates the host + count, + isReadable: false, + isWritable: true, // we need to write out the pending buffer (if present) + elemMeta: this.#elemMeta, + data: [], + }}); + buffer.setTarget(`host stream read buffer (id [${{bufferID}}], count [${{count}}])`); + + let packedResult; + packedResult = await this.copy({{ + isAsync: true, + count, + bufferID, + buffer, + eventCode: {async_event_code_enum}.STREAM_READ, + componentIdx: -1, + }}); + + if (packedResult === {async_blocked_const}) {{ + // If the read was blocked, we can only make progress when + // the write side notifies us of a write, then we must attempt the copy again + + await this.#otherEndWait(); + + packedResult = await this.copy({{ + isAsync: true, + count, + bufferID, + buffer, + eventCode: {async_event_code_enum}.STREAM_READ, + skipStateCheck: true, + componentIdx: -1, + }}); - let copied = packedResult >> 4; - let result = packedResult & 0x000F; + if (packedResult === {async_blocked_const}) {{ + throw new Error("unexpected double block during read"); + }} + }} - const vs = buffer.read(); + let copied = packedResult >> 4; + let result = packedResult & 0x000F; + + // Due to async timing vagaries, it is possible to get to this point + // and have an event have come out from the copy despite the writer end + // being closed or the reader being otherwise done: + // + // - The current CopyState is done (indicating a CopyResult.DROPPED being received) + // - The current CopyResult is DROPPED + // + // These two cases often overlap + // + if (this.isDone() || result === {stream_end_class}.CopyResult.DROPPED) {{ + reject(new Error("read end is closed")); + }} - return count === 1 ? vs[0] : vs; + const vs = buffer.read(count); + const res = count === 1 ? vs[0] : vs; + this.#result = null; + resolve(res); + + }} catch (err) {{ + {debug_log_fn}('[{end_class_name}#read()] error', err); + reject(err); + }} + + return await promise; }} "# ), @@ -764,18 +960,21 @@ impl AsyncStreamIntrinsic { output.push_str(&format!(r#" class {end_class_name} extends {stream_end_class} {{ - #componentIdx; - #copying = false; #done = false; #elemMeta = null; #pendingBufferMeta = null; // held by both write and read ends - #getEndIdxFn; + #streamTableIdx; // table index that the stream is in (can change after a stream transfer) + #handle; // handle (index) inside the given table (can change after a stream transfer) - #streamRep; - #streamTableIdx; + #globalStreamMapRep; // internal stream (which has both ends) rep + + #result = null; + + #otherEndWait = null; + #otherEndNotify = null; constructor(args) {{ {debug_log_fn}('[{end_class_name}#constructor()] args', args); @@ -784,26 +983,34 @@ impl AsyncStreamIntrinsic { if (!args.elemMeta) {{ throw new Error('missing/invalid element meta'); }} this.#elemMeta = args.elemMeta; - if (args.componentIdx === undefined) {{ throw new Error('missing/invalid component idx'); }} - this.#componentIdx = args.componentIdx; - if (!args.pendingBufferMeta) {{ throw new Error('missing/invalid shared pending buffer meta'); }} this.#pendingBufferMeta = args.pendingBufferMeta; - if (!args.getEndIdxFn) {{ throw new Error('missing/invalid fn for getting table idx'); }} - this.#getEndIdxFn = args.getEndIdxFn; - - if (!args.streamRep) {{ throw new Error('missing/invalid rep for stream'); }} - this.#streamRep = args.streamRep; - if (args.tableIdx === undefined) {{ throw new Error('missing index for stream table idx'); }} this.#streamTableIdx = args.tableIdx; + + if (args.otherEndNotify === undefined) {{ throw new Error('missing fn for notification'); }} + this.#otherEndNotify = args.otherEndNotify; + + if (args.otherEndWait === undefined) {{ throw new Error('missing fn for awaiting notification'); }} + this.#otherEndWait = args.otherEndWait; }} - streamRep() {{ return this.#streamRep; }} streamTableIdx() {{ return this.#streamTableIdx; }} + setStreamTableIdx(idx) {{ this.#streamTableIdx = idx; }} + + handle() {{ return this.#handle; }} + setHandle(h) {{ this.#handle = h; }} - waitableIdx() {{ return this.#getEndIdxFn(); }} + globalStreamMapRep() {{ return this.#globalStreamMapRep; }} + setGlobalStreamMapRep(rep) {{ this.#globalStreamMapRep = rep; }} + + waitableIdx() {{ return this.getWaitable().idx(); }} + setWaitableIdx(idx) {{ + const w = this.getWaitable(); + w.setIdx(idx); + w.setTarget(`waitable for {rw_fn_name} end (waitable [${{idx}}])`); + }} getElemMeta() {{ return {{...this.#elemMeta}}; }} @@ -811,6 +1018,7 @@ impl AsyncStreamIntrinsic { isDone() {{ this.getCopyState() === {stream_end_class}.CopyState.DONE; }} isCompleted() {{ this.getCopyState() === {stream_end_class}.CopyState.COMPLETED; }} + isDropped() {{ this.getCopyState() === {stream_end_class}.CopyState.DROPPED; }} {action_impl} {inner_rw_impl} @@ -818,17 +1026,19 @@ impl AsyncStreamIntrinsic { {copy_impl} setPendingBufferMeta(args) {{ - const {{ componentIdx, buffer, onCopy, onCopyDone }} = args; + const {{ componentIdx, buffer, onCopyFn, onCopyDoneFn }} = args; this.#pendingBufferMeta.componentIdx = componentIdx; this.#pendingBufferMeta.buffer = buffer; - this.#pendingBufferMeta.onCopyFn = onCopy; - this.#pendingBufferMeta.onCopyDoneFn = onCopyDone; + this.#pendingBufferMeta.onCopyFn = onCopyFn; + this.#pendingBufferMeta.onCopyDoneFn = onCopyDoneFn; }} resetPendingBufferMeta() {{ - this.setPendingBufferMeta({{ componentIdx: null, buffer: null, onCopy: null, onCopyDone: null }}); + this.setPendingBufferMeta({{ componentIdx: null, buffer: null, onCopyFn: null, onCopyDoneFn: null }}); }} + getPendingBufferMeta() {{ return this.#pendingBufferMeta; }} + resetAndNotifyPending(result) {{ const f = this.#pendingBufferMeta.onCopyDoneFn; this.resetPendingBufferMeta(); @@ -841,12 +1051,11 @@ impl AsyncStreamIntrinsic { }} drop() {{ - if (this.#copying) {{ throw new Error('cannot drop while copying'); }} - + {debug_log_fn}('[{stream_end_class}#drop()]'); + if (this.isDropped()) {{ return; }} if (this.#pendingBufferMeta) {{ this.resetAndNotifyPending({stream_end_class}.CopyResult.DROPPED); }} - super.drop(); }} }} @@ -855,17 +1064,13 @@ impl AsyncStreamIntrinsic { Self::InternalStreamClass => { let debug_log_fn = Intrinsic::DebugLog.name(); - let internal_stream_class = self.name(); + let internal_stream_class_name = self.name(); let read_end_class = Self::StreamReadableEndClass.name(); let write_end_class = Self::StreamWritableEndClass.name(); - let waitable_class = Intrinsic::Waitable(WaitableIntrinsic::WaitableClass).name(); + output.push_str(&format!( r#" - class {internal_stream_class} {{ - #rep; - #idx; - #componentIdx; - + class {internal_stream_class_name} {{ #readEnd; #readEndWaitableIdx; #readEndDropped; @@ -874,109 +1079,82 @@ impl AsyncStreamIntrinsic { #writeEndWaitableIdx; #writeEndDropped; - #pendingBufferMeta = {{}}; + #pendingBufferMeta = {{}}; // shared between read/write ends #elemMeta; - #localStreamTable; - #globalStreamMap; + #globalStreamMapRep; + + #readWaitPromise = null; + #writeWaitPromise = null; constructor(args) {{ - if (typeof args.componentIdx !== 'number') {{ throw new Error('missing/invalid component idx'); }} + {debug_log_fn}('[{internal_stream_class_name}#constructor()] args', args); if (!args.elemMeta) {{ throw new Error('missing/invalid stream element metadata'); }} if (args.tableIdx === undefined) {{ throw new Error('missing/invalid stream table idx'); }} - const {{ tableIdx, componentIdx, elemMeta, globalStreamMap, localStreamTable }} = args; + if (!args.readWaitable) {{ throw new Error('missing/invalid read waitable'); }} + if (!args.writeWaitable) {{ throw new Error('missing/invalid write waitable'); }} + const {{ tableIdx, elemMeta, readWaitable, writeWaitable, }} = args; - this.#componentIdx = args.componentIdx; this.#elemMeta = elemMeta; - if (args.globalStreamMap) {{ - this.#globalStreamMap = args.globalStreamMap; - this.#rep = globalStreamMap.insert(this); - }} - if (args.localStreamTable) {{ - this.#localStreamTable = args.localStreamTable; - this.#idx = localStreamTable.insert(this); - }} + const writeNotify = () => {{ + if (this.#writeWaitPromise === null) {{ return; }} + const resolve = this.#writeWaitPromise.resolve; + this.#writeWaitPromise = null; + resolve(); + }}; + const writeWait = () => {{ + if (this.#writeWaitPromise === null) {{ + this.#writeWaitPromise = Promise.withResolvers(); + }} + return this.#writeWaitPromise.promise; + }}; this.#readEnd = new {read_end_class}({{ - componentIdx, tableIdx, elemMeta: this.#elemMeta, pendingBufferMeta: this.#pendingBufferMeta, - streamRep: this.#rep, - getEndIdxFn: () => this.#readEndWaitableIdx, - target: `stream read end (global rep [${{this.#rep}}])`, - waitable: new {waitable_class}({{ - componentIdx, - target: `stream read end (stream local idx [${{this.#idx}}], global rep [${{this.#rep}}])` - }}), - onDrop: () => {{ - this.#readEndDropped = true; - if (this.#readEndDropped && this.#readEndDropped) {{ - {debug_log_fn}('[{internal_stream_class}] triggering drop due to end drops', {{ - globalRep: this.#rep, - streamTableIdx: this.#idx, - readEndWaitableIdx: this.#readEndWaitableIdx, - writeEndWaitableIdx: this.#writeEndWaitableIdx, - }}); - this.drop(); - }} - }}, + target: "stream read end (@ init)", + waitable: readWaitable, + otherEndWait: writeWait, + otherEndNotify: writeNotify, }}); + const readNotify = () => {{ + if (this.#readWaitPromise === null) {{ return; }} + const resolve = this.#readWaitPromise.resolve; + this.#readWaitPromise = null; + resolve(); + }}; + const readWait = () => {{ + if (this.#readWaitPromise === null) {{ + this.#readWaitPromise = Promise.withResolvers(); + }} + return this.#readWaitPromise.promise; + }}; + this.#writeEnd = new {write_end_class}({{ - componentIdx, tableIdx, elemMeta: this.#elemMeta, pendingBufferMeta: this.#pendingBufferMeta, - getEndIdxFn: () => this.#writeEndWaitableIdx, - streamRep: this.#rep, - target: `stream write end (global rep [${{this.#rep}}])`, - waitable: new {waitable_class}({{ - componentIdx, - target: `stream write end (stream local idx [${{this.#rep}}], global rep [${{this.#rep}}])` - }}), - onDrop: () => {{ - this.#writeEndDropped = true; - // TODO(fix): racy - if (this.#readEndDropped && this.#writeEndDropped) {{ - {debug_log_fn}('[{internal_stream_class}] triggering drop due to end drops', {{ - globalRep: this.#rep, - streamTableIdx: this.#idx, - readEndWaitableIdx: this.#readEndWaitableIdx, - writeEndWaitableIdx: this.#writeEndWaitableIdx, - }}); - this.drop(); - }} - }}, + target: "stream write end (@ init)", + waitable: writeWaitable, + otherEndWait: readWait, + otherEndNotify: readNotify, }}); }} - idx() {{ return this.#idx; }} - rep() {{ return this.#rep; }} + elemMeta() {{ return this.#elemMeta; }} - readEnd() {{ return this.#readEnd; }} - setReadEndWaitableIdx(idx) {{ this.#readEndWaitableIdx = idx; }} + globalStreamMapRep() {{ return this.#globalStreamMapRep; }} + setGlobalStreamMapRep(rep) {{ + this.#globalStreamMapRep = rep; + this.#readEnd.setGlobalStreamMapRep(rep); + this.#writeEnd.setGlobalStreamMapRep(rep); + }} + readEnd() {{ return this.#readEnd; }} writeEnd() {{ return this.#writeEnd; }} - setWriteEndWaitableIdx(idx) {{ this.#writeEndWaitableIdx = idx; }} - - drop() {{ - {debug_log_fn}('[{internal_stream_class}#drop()]'); - if (this.#globalStreamMap) {{ - const removed = this.#globalStreamMap.remove(this.#rep); - if (!removed) {{ - throw new Error(`failed to remove stream [${{this.#rep}}] in global stream map (component [${{this.#componentIdx}}] while removing stream end`); - }} - }} - - if (this.#localStreamTable) {{ - let removed = this.#localStreamTable.remove(this.#idx); - if (!removed) {{ - throw new Error(`failed to remove stream [${{streamEnd.waitableIdx()}}] in internal table (table [${{tableIdx}}]), component [${{this.#componentIdx}}] while removing stream end`); - }} - }} - }} }} "# )); @@ -1051,7 +1229,7 @@ impl AsyncStreamIntrinsic { return new {external_stream_class}({{ isReadable: streamEnd.isReadable(), isWritable: streamEnd.isWritable(), - hostStreamRep: this.#rep, + globalRep: this.#rep, readFn: async () => {{ return await streamEnd.read(); }}, @@ -1082,7 +1260,7 @@ impl AsyncStreamIntrinsic { output.push_str(&format!( r#" class {external_stream_class_name} {{ - #hostStreamRep = null; + #globalRep = null; #isReadable; #isWritable; #writeFn; @@ -1090,8 +1268,8 @@ impl AsyncStreamIntrinsic { constructor(args) {{ {debug_log_fn}('[{external_stream_class_name}#constructor()] args', args); - if (args.hostStreamRep === undefined) {{ throw new TypeError("missing host stream rep"); }} - this.#hostStreamRep = args.hostStreamRep; + if (args.globalRep === undefined) {{ throw new TypeError("missing host stream rep"); }} + this.#globalRep = args.globalRep; if (args.isReadable === undefined) {{ throw new TypeError("missing readable setting"); }} this.#isReadable = args.isReadable; @@ -1106,7 +1284,7 @@ impl AsyncStreamIntrinsic { this.#readFn = args.readFn; }} - isConnectedToHost() {{ return hostStreamRep === null; }} + globalRep() {{ return this.#globalRep; }} async next() {{ {debug_log_fn}('[{external_stream_class_name}#next()]'); @@ -1141,6 +1319,15 @@ impl AsyncStreamIntrinsic { )); } + Self::GlobalStreamTableMap => { + let global_stream_table_map = Self::GlobalStreamTableMap.name(); + output.push_str(&format!( + r#" + const {global_stream_table_map} = {{}}; + "# + )); + } + // TODO: allow customizable stream functionality (user should be able to specify a lib/import for a 'stream()' function // (this will enable using p3-shim explicitly or any other implementation) // @@ -1177,12 +1364,25 @@ impl AsyncStreamIntrinsic { throw new Error('component instance is not marked as may leave during stream.new'); }} - const {{ writeEndIdx, readEndIdx }} = cstate.createStream({{ + const {{ writeEndWaitableIdx, readEndWaitableIdx, writeEndHandle, readEndHandle }} = cstate.createStream({{ tableIdx: streamTableIdx, elemMeta, }}); - return (BigInt(writeEndIdx) << 32n) | BigInt(readEndIdx); + {debug_log_fn}('[{stream_new_fn}()] created stream ends', {{ + writeEnd: {{ + waitableIdx: writeEndWaitableIdx, + handle: writeEndHandle, + }}, + readEnd: {{ + waitableIdx: readEndWaitableIdx, + handle: readEndHandle, + }}, + streamTableIdx, + callerComponentIdx, + }}); + + return (BigInt(writeEndWaitableIdx) << 32n) | BigInt(readEndWaitableIdx); }} "#)); } @@ -1236,6 +1436,7 @@ impl AsyncStreamIntrinsic { let get_or_create_async_state_fn = Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState).name(); let async_event_code_enum = Intrinsic::AsyncEventCodeEnum.name(); + let managed_buffer_class = Intrinsic::ManagedBufferClass.name(); let (event_code, stream_end_class) = match self { Self::StreamWrite => ( format!("{async_event_code_enum}.STREAM_WRITE"), @@ -1251,11 +1452,11 @@ impl AsyncStreamIntrinsic { output.push_str(&format!(r#" async function {stream_op_fn}( ctx, - streamEndIdx, + streamEndWaitableIdx, ptr, count, ) {{ - {debug_log_fn}('[{stream_op_fn}()] args', {{ ctx, streamEndIdx, ptr, count }}); + {debug_log_fn}('[{stream_op_fn}()] args', {{ ctx, streamEndWaitableIdx, ptr, count }}); const {{ componentIdx, memoryIdx, @@ -1269,33 +1470,46 @@ impl AsyncStreamIntrinsic { if (componentIdx === undefined) {{ throw new TypeError("missing/invalid component idx"); }} if (streamTableIdx === undefined) {{ throw new TypeError("missing/invalid stream table idx"); }} - if (streamEndIdx === undefined) {{ throw new TypeError("missing/invalid stream end idx"); }} + if (streamEndWaitableIdx === undefined) {{ throw new TypeError("missing/invalid stream end idx"); }} + + // NOTE: it is possible for count to come in as *negative*, when + // conveying incredibly large amounts like the max u32 size. Components + // may choose to attempt to take as much data as possible, but the buffer + // size is smaller than the max u32. + // + // Due to JS handling the value as 2s complement, the `resultCountOrAsync` ends up being: + // (a) -1 as u32 max size + // (b) -2 as u32 max size - 1 + // (c) x + // + if (count < 0) {{ count = {managed_buffer_class}.MAX_LENGTH; }} const cstate = {get_or_create_async_state_fn}(componentIdx); if (!cstate.mayLeave) {{ throw new Error('component instance is not marked as may leave'); }} // TODO(fix): check for may block & async - const streamEnd = cstate.getStreamEnd({{ tableIdx: streamTableIdx, streamEndIdx }}); + const streamEnd = cstate.getStreamEnd({{ tableIdx: streamTableIdx, streamEndWaitableIdx }}); if (!streamEnd) {{ - throw new Error(`missing stream end [${{streamEndIdx}}] (table [${{streamTableIdx}}], component [${{componentIdx}}])`); + throw new Error(`missing stream end [${{streamEndWaitableIdx}}] (table [${{streamTableIdx}}], component [${{componentIdx}}])`); }} if (!(streamEnd instanceof {stream_end_class})) {{ throw new Error('invalid stream type, expected {stream_end_class}'); }} - if (streamEnd.tableIdx() !== streamTableIdx) {{ - throw new Error(`stream end table idx [${{streamEnd.getStreamTableIdx()}}] != operation table idx [${{streamTableIdx}}]`); + if (streamEnd.streamTableIdx() !== streamTableIdx) {{ + throw new Error(`stream end table idx [${{streamEnd.streamTableIdx()}}] != operation table idx [${{streamTableIdx}}]`); }} - const res = await streamEnd.copy({{ + const result = await streamEnd.copy({{ isAsync, memory: getMemoryFn(), ptr, count, eventCode: {event_code}, + componentIdx, }}); - return res; + return result; }} "#)); } @@ -1303,7 +1517,6 @@ impl AsyncStreamIntrinsic { Self::StreamCancelRead | Self::StreamCancelWrite => { let debug_log_fn = Intrinsic::DebugLog.name(); let stream_cancel_fn = self.name(); - let global_stream_map = Self::GlobalStreamMap.name(); let async_blocked_const = Intrinsic::AsyncTask(AsyncTaskIntrinsic::AsyncBlockedConstant).name(); let is_cancel_write = matches!(self, Self::StreamCancelWrite); @@ -1317,51 +1530,55 @@ impl AsyncStreamIntrinsic { } else { Self::StreamReadableEndClass.name() }; + let current_task_get_fn = + Intrinsic::AsyncTask(AsyncTaskIntrinsic::GetCurrentTask).name(); let get_or_create_async_state_fn = Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState).name(); - output.push_str(&format!(" - async function {stream_cancel_fn}( - streamIdx, - isAsync, - streamEndIdx, - ) {{ - {debug_log_fn}('[{stream_cancel_fn}()] args', {{ - streamIdx, - isAsync, - streamEndIdx, - }}); + output.push_str(&format!(r#" + async function {stream_cancel_fn}(ctx, streamEndWaitableIdx) {{ + {debug_log_fn}('[{stream_cancel_fn}()] args', {{ ctx, streamEndWaitableIdx }}); + const {{ streamTableIdx, isAsync, componentIdx }} = ctx; - const state = {get_or_create_async_state_fn}(componentIdx); - if (!state.mayLeave) {{ throw new Error('component instance is not marked as may leave'); }} + const cstate = {get_or_create_async_state_fn}(componentIdx); + if (!cstate.mayLeave) {{ throw new Error('component instance is not marked as may leave'); }} - const streamEnd = {global_stream_map}.get(streamEndIdx); - if (!streamEnd) {{ throw new Error('missing stream end with idx [' + streamEndIdx + ']'); }} + const streamEnd = cstate.getStreamEnd({{ streamEndWaitableIdx, tableIdx: streamTableIdx }}); + if (!streamEnd) {{ throw new Error('missing stream end with idx [' + streamEndWaitableIdx + ']'); }} if (!(streamEnd instanceof {stream_end_class})) {{ throw new Error('invalid stream end, expected value of type [{stream_end_class}]'); }} - if (streamEnd.elementTypeRep() !== stream.elementTypeRep()) {{ - throw new Error('stream type [' + stream.elementTypeRep() + '], does not match stream end type [' + streamEnd.elementTypeRep() + ']'); - }} - if (!streamEnd.isCopying()) {{ throw new Error('stream end is not copying, cannot cancel'); }} + streamEnd.setCopyState({stream_end_class}.CopyState.CANCELLING_COPY); + if (!streamEnd.hasPendingEvent()) {{ - if (!streamEnd.hasPendingEvent()) {{ - if (!isAsync) {{ - await task.blockOn({{ promise: streamEnd.waitable, isAsync: false }}); - }} else {{ - return {async_blocked_const}; + + streamEnd.cancel(); + + if (!streamEnd.hasPendingEvent()) {{ + if (isAsync) {{ return {async_blocked_const}; }} + + const taskMeta = {current_task_get_fn}(componentIdx); + if (!taskMeta) {{ throw new Error('missing current task metadata while doing stream transfer'); }} + const task = taskMeta.task; + if (!task) {{ throw new Error('missing task while doing stream transfer'); }} + await task.suspendUntil({{ readyFn: () => streamEnd.hasPendingEvent() }}); }} - }} }} - const {{ code, payload0: index, payload1: payload }} = e.getEvent(); - if (streamEnd.isCopying()) {{ throw new Error('stream end is still in copying state'); }} - if (code !== {event_code_enum}) {{ throw new Error('unexpected event code [' + code + '], expected [' + {event_code_enum} + ']'); }} - if (index !== 1) {{ throw new Error('unexpected index, should be 1'); }} + const event = streamEnd.getPendingEvent(); + const {{ code, payload0: index, payload1: payload }} = event; + if (streamEnd.isCopying()) {{ + throw new Error(`stream end (idx [${{streamEndWaitableIdx}}]) is still in copying state`); + }} + if (code !== {event_code_enum}) {{ + throw new Error(`unexpected event code [${{code}}], expected [{event_code_enum}]`); + }} + if (index !== streamEnd.waitableIdx()) {{ throw new Error('event index does not match stream end'); }} + {debug_log_fn}('[{stream_cancel_fn}()] successful cancel', {{ ctx, streamEndWaitableIdx, streamEnd, event }}); return payload; }} - ")); + "#)); } // NOTE: as writable drops are called from guests, they may happen *after* @@ -1381,8 +1598,8 @@ impl AsyncStreamIntrinsic { let get_or_create_async_state_fn = Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState).name(); output.push_str(&format!(r#" - function {stream_drop_fn}(ctx, streamEndIdx) {{ - {debug_log_fn}('[{stream_drop_fn}()] args', {{ ctx, streamEndIdx }}); + function {stream_drop_fn}(ctx, streamEndWaitableIdx) {{ + {debug_log_fn}('[{stream_drop_fn}()] args', {{ ctx, streamEndWaitableIdx }}); const {{ streamTableIdx, componentIdx }} = ctx; const task = {current_task_get_fn}(componentIdx); @@ -1391,9 +1608,9 @@ impl AsyncStreamIntrinsic { const cstate = {get_or_create_async_state_fn}(componentIdx); if (!cstate) {{ throw new Error(`missing component state for component idx [${{componentIdx}}]`); }} - const streamEnd = cstate.removeStreamEnd({{ tableIdx: streamTableIdx, streamEndIdx }}); + const streamEnd = cstate.deleteStreamEnd({{ tableIdx: streamTableIdx, streamEndWaitableIdx }}); if (!streamEnd) {{ - throw new Error(`missing stream [${{streamEndIdx}}] (table [${{streamTableIdx}}], component [${{componentIdx}}])`); + throw new Error(`missing stream (waitable [${{streamEndWaitableIdx}}], table [${{streamTableIdx}}], component [${{componentIdx}}])`); }} if (!(streamEnd instanceof {stream_end_class})) {{ @@ -1410,8 +1627,6 @@ impl AsyncStreamIntrinsic { let stream_transfer_fn = self.name(); let current_component_idx_globals = AsyncTaskIntrinsic::GlobalAsyncCurrentComponentIdxs.name(); - let current_async_task_id_globals = - AsyncTaskIntrinsic::GlobalAsyncCurrentTaskIds.name(); let current_task_get_fn = AsyncTaskIntrinsic::GetCurrentTask.name(); let get_or_create_async_state_fn = Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState).name(); @@ -1419,38 +1634,56 @@ impl AsyncStreamIntrinsic { output.push_str(&format!( r#" function {stream_transfer_fn}( - srcStreamEndIdx, + srcStreamWaitableIdx, srcTableIdx, destTableIdx, ) {{ + const componentIdx = {current_component_idx_globals}.at(-1); {debug_log_fn}('[{stream_transfer_fn}()] args', {{ - srcStreamEndIdx, + srcStreamWaitableIdx, srcTableIdx, destTableIdx, + componentIdx, }}); - const taskMeta = {current_task_get_fn}( - {current_component_idx_globals}.at(-1), - {current_async_task_id_globals}.at(-1) - ); + const taskMeta = {current_task_get_fn}(componentIdx); if (!taskMeta) {{ throw new Error('missing current task metadata while doing stream transfer'); }} const task = taskMeta.task; if (!task) {{ throw new Error('missing task while doing stream transfer'); }} + if (componentIdx !== task.componentIdx()) {{ + throw new Error("task component ID should match current component ID"); + }} - const componentIdx = task.componentIdx(); const cstate = {get_or_create_async_state_fn}(componentIdx); if (!cstate) {{ throw new Error(`unexpectedly missing async state for component [${{componentIdx}}]`); }} - const streamEnd = cstate.removeStreamEnd({{ tableIdx: srcTableIdx, streamEndIdx: srcStreamEndIdx }}); - if (!streamEnd.isReadable()) {{ throw new Error("writable stream ends cannot be moved"); }} + const streamEnd = cstate.removeStreamEndFromTable({{ tableIdx: srcTableIdx, streamWaitableIdx: srcStreamWaitableIdx }}); + if (!streamEnd.isReadable()) {{ + throw new Error("writable stream ends cannot be moved"); + }} if (streamEnd.isDone()) {{ throw new Error('readable ends cannot be moved once writable ends are dropped'); }} - const streamEndIdx = cstate.addStreamEnd({{ tableIdx: destTableIdx, streamEnd }}); + const {{ handle, waitableIdx }} = cstate.addStreamEndToTable({{ tableIdx: destTableIdx, streamEnd }}); + streamEnd.setTarget(`stream read end (waitable [${{waitableIdx}}])`); + + {debug_log_fn}('[{stream_transfer_fn}()] successfully transferred', {{ + dest: {{ + streamEndHandle: handle, + streamEndWaitableIdx: waitableIdx, + tableIdx: destTableIdx, + }}, + src: {{ + streamEndWaitableIdx: srcStreamWaitableIdx, + tableIdx: srcTableIdx, + }}, + componentIdx, + }}); + + return waitableIdx; - return streamEndIdx; }} "# )); 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 def088ef0..4d55c2329 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs @@ -128,7 +128,7 @@ pub enum AsyncTaskIntrinsic { CreateNewCurrentTask, /// Function that stops the current task - EndCurrentTask, + ClearCurrentTask, /// Global that stores the current task for a given invocation. /// @@ -260,29 +260,28 @@ impl AsyncTaskIntrinsic { /// Retrieve global names for this intrinsic pub fn get_global_names() -> impl IntoIterator { [ - "ASYNC_BLOCKED_CODE", - "ASYNC_CURRENT_COMPONENT_IDXS", - "ASYNC_CURRENT_TASK_IDS", - "ASYNC_TASKS_BY_COMPONENT_IDX", - "CURRENT_TASK_MAY_BLOCK", - "AsyncSubtask", - "AsyncTask", - "asyncYield", - "contextGet", - "contextSet", - "endCurrentTask", - "getCurrentTask", - "createNewCurrentTask", - "subtaskCancel", - "subtaskDrop", - "subtaskDrop", - "taskCancel", - "taskReturn", - "unpackCallbackResult", - "_driverLoop", - "_lowerImport", - "_symmetricSyncGuestCallEnter", - "_symmetricSyncGuestCallExit", + Self::CurrentTaskMayBlock.name(), + Self::AsyncBlockedConstant.name(), + Self::AsyncSubtaskClass.name(), + Self::AsyncTaskClass.name(), + Self::ContextGet.name(), + Self::ContextSet.name(), + Self::GetCurrentTask.name(), + Self::CreateNewCurrentTask.name(), + Self::ClearCurrentTask.name(), + Self::GlobalAsyncCurrentTaskMap.name(), + Self::GlobalAsyncCurrentTaskIds.name(), + Self::GlobalAsyncCurrentComponentIdxs.name(), + Self::SubtaskCancel.name(), + Self::SubtaskDrop.name(), + Self::TaskCancel.name(), + Self::TaskReturn.name(), + Self::Yield.name(), + Self::UnpackCallbackResult.name(), + Self::DriverLoop.name(), + Self::LowerImport.name(), + Self::EnterSymmetricSyncGuestCall.name(), + Self::ExitSymmetricSyncGuestCall.name(), ] } @@ -297,7 +296,7 @@ impl AsyncTaskIntrinsic { Self::ContextSet => "contextSet", Self::GetCurrentTask => "getCurrentTask", Self::CreateNewCurrentTask => "createNewCurrentTask", - Self::EndCurrentTask => "endCurrentTask", + Self::ClearCurrentTask => "clearCurrentTask", Self::GlobalAsyncCurrentTaskMap => "ASYNC_TASKS_BY_COMPONENT_IDX", Self::GlobalAsyncCurrentTaskIds => "ASYNC_CURRENT_TASK_IDS", Self::GlobalAsyncCurrentComponentIdxs => "ASYNC_CURRENT_COMPONENT_IDXS", @@ -350,18 +349,17 @@ impl AsyncTaskIntrinsic { let current_component_idx_globals = Self::GlobalAsyncCurrentComponentIdxs.name(); let type_check_i32 = Intrinsic::TypeCheckValidI32.name(); output.push_str(&format!(r#" - function {context_set_fn}(slot, value) {{ + function {context_set_fn}(ctx, value) {{ + const {{ componentIdx, slot }} = ctx; + if (componentIdx === undefined) {{ throw new TypeError("missing component idx"); }} + if (slot === undefined) {{ throw new TypeError("missing slot"); }} + if (!({type_check_i32}(value))) {{ throw new Error('invalid value for context set (not valid i32)'); }} - const taskMeta = {current_task_get_fn}({current_component_idx_globals}.at(-1), {current_async_task_id_globals}.at(-1)); + const taskMeta = {current_task_get_fn}(componentIdx); if (!taskMeta) {{ throw new Error('failed to retrieve current task'); }} let task = taskMeta.task; if (!task) {{ throw new Error('invalid/missing current task in metadata while setting context'); }} - // TODO(threads): context has been moved to be stored on the thread, not the task. - // Until threads are implemented, we simulate a task with only one thread by storing - // the thread state on the topmost task - task = task.getRootTask(); - {debug_log_fn}('[{context_set_fn}()] args', {{ _globals: {{ {current_component_idx_globals}, {current_async_task_id_globals} }}, slot, @@ -384,17 +382,16 @@ impl AsyncTaskIntrinsic { let current_async_task_id_globals = Self::GlobalAsyncCurrentTaskIds.name(); let current_component_idx_globals = Self::GlobalAsyncCurrentComponentIdxs.name(); output.push_str(&format!(r#" - function {context_get_fn}(slot) {{ - const taskMeta = {current_task_get_fn}({current_component_idx_globals}.at(-1), {current_async_task_id_globals}.at(-1)); + function {context_get_fn}(ctx) {{ + const {{ componentIdx, slot }} = ctx; + if (componentIdx === undefined) {{ throw new TypeError("missing component idx"); }} + if (slot === undefined) {{ throw new TypeError("missing slot"); }} + + const taskMeta = {current_task_get_fn}(componentIdx); if (!taskMeta) {{ throw new Error('failed to retrieve current task metadata'); }} let task = taskMeta.task; if (!task) {{ throw new Error('invalid/missing current task in metadata while getting context'); }} - // TODO(threads): context has been moved to be stored on the thread, not the task. - // Until threads are implemented, we simulate a task with only one thread by storing - // the thread state on the topmost task - task = task.getRootTask(); - {debug_log_fn}('[{context_get_fn}()] args', {{ _globals: {{ {current_component_idx_globals}, {current_async_task_id_globals} }}, slot, @@ -415,12 +412,18 @@ impl AsyncTaskIntrinsic { let debug_log_fn = Intrinsic::DebugLog.name(); let task_return_fn = Self::TaskReturn.name(); let current_task_get_fn = Self::GetCurrentTask.name(); - let get_or_create_async_state_fn = - Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState).name(); output.push_str(&format!(r#" function {task_return_fn}(ctx) {{ - const {{ componentIdx, useDirectParams, getMemoryFn, memoryIdx, callbackFnIdx, liftFns }} = ctx; + const {{ + componentIdx, + useDirectParams, + getMemoryFn, + memoryIdx, + callbackFnIdx, + liftFns, + lowerFns + }} = ctx; const params = [...arguments].slice(1); const memory = getMemoryFn(); @@ -429,6 +432,7 @@ impl AsyncTaskIntrinsic { callbackFnIdx, memoryIdx, liftFns, + lowerFns, params, }}); @@ -450,32 +454,13 @@ impl AsyncTaskIntrinsic { throw new Error('memory must be present if more than max async flat lifts are performed'); }} - // If we are in a subtask, and have a fused helper function provided to use - // via PrepareCall, we can use that function rather than performing lifting manually. - // - // See also documentation on `HostIntrinsic::PrepareCall` - const returnFn = task.getParentSubtask()?.getCallMetadata()?.returnFn; - if (returnFn) {{ - const returnFnArgs = [...params]; - returnFn.apply(null, returnFnArgs); - // NOTE: as the return function will directly perform the lifting/lowering - // we cannot know what the task itself would return, without reading it out - // of where it *would* end up. - // - // TODO(fix): add an explicit enum/detectable result which indicates that lift/lower - // is happening via helper fns, to avoid mistaking for legitimate empty responses. - // - task.resolve([]); - return; - }} - let liftCtx = {{ memory, useDirectParams, params, componentIdx }}; if (!useDirectParams) {{ liftCtx.storagePtr = params[0]; liftCtx.storageLen = params[1]; }} - const results = []; + const liftedResults = []; {debug_log_fn}('[{task_return_fn}()] lifting results out of memory', {{ liftCtx }}); for (const liftFn of liftFns) {{ if (liftCtx.storageLen !== undefined && liftCtx.storageLen <= 0) {{ @@ -483,21 +468,25 @@ impl AsyncTaskIntrinsic { }} const [ val, newLiftCtx ] = liftFn(liftCtx); liftCtx = newLiftCtx; - results.push(val); + liftedResults.push(val); }} - // Register an on-resolve handler that runs a tick loop - task.registerOnResolveHandler(() => {{ - // Trigger ticking for any suspended tasks - const cstate = {get_or_create_async_state_fn}(task.componentIdx()); - cstate.runTickLoop(); - }}); - - // NOTE: during fused guest->guest calls, we have will never get here, - // as the lift has the fused helper function may have already performed the relevant - // lifting/lowering. + // If we are in a subtask, and have a fused helper function provided to use + // via PrepareCall, we can use that function rather than performing lifting manually. // - task.resolve(results); + // See also documentation on `HostIntrinsic::PrepareCall` + const subtaskCallMetadata = task.getParentSubtask()?.getCallMetadata() + const returnFn = subtaskCallMetadata?.returnFn; + if (returnFn) {{ + const returnFnArgs = [...params]; + returnFn.apply(null, returnFnArgs); + + // If we have a return fn, we're doing a guest->guest async call, + // so we should save the lowers (they are not available during prepare/asyncstartcall) + subtaskCallMetadata.lowers = lowerFns; + }} + + task.resolve(liftedResults); }} "#)); } @@ -592,6 +581,7 @@ impl AsyncTaskIntrinsic { let global_task_map = Self::GlobalAsyncCurrentTaskMap.name(); let task_id_globals = Self::GlobalAsyncCurrentTaskIds.name(); let component_idx_globals = Self::GlobalAsyncCurrentComponentIdxs.name(); + output.push_str(&format!( r#" function {fn_name}(args) {{ @@ -599,6 +589,7 @@ impl AsyncTaskIntrinsic { const {{ componentIdx, isAsync, + isManualAsync, entryFnName, parentSubtaskID, callbackFnName, @@ -613,12 +604,13 @@ impl AsyncTaskIntrinsic { if (componentIdx === undefined || componentIdx === null) {{ throw new Error('missing/invalid component instance index while starting task'); }} - const taskMetas = {global_task_map}.get(componentIdx); + let taskMetas = {global_task_map}.get(componentIdx); const callbackFn = getCallbackFn ? getCallbackFn() : null; const newTask = new {task_class}({{ componentIdx, isAsync, + isManualAsync, entryFnName, callbackFn, callbackFnName, @@ -631,10 +623,12 @@ impl AsyncTaskIntrinsic { const newTaskID = newTask.id(); const newTaskMeta = {{ id: newTaskID, componentIdx, task: newTask }}; + // NOTE: do not track host tasks {task_id_globals}.push(newTaskID); {component_idx_globals}.push(componentIdx); if (!taskMetas) {{ + taskMetas = [newTaskMeta]; {global_task_map}.set(componentIdx, [newTaskMeta]); }} else {{ taskMetas.push(newTaskMeta); @@ -650,32 +644,40 @@ impl AsyncTaskIntrinsic { // Debug log for this is disabled since it is fairly noisy Self::GetCurrentTask => { let global_task_map = Self::GlobalAsyncCurrentTaskMap.name(); + let current_component_idx_globals = + AsyncTaskIntrinsic::GlobalAsyncCurrentComponentIdxs.name(); output.push_str(&format!( - " + r#" function {fn_name}(componentIdx) {{ + let usedGlobal = false; if (componentIdx === undefined || componentIdx === null) {{ - throw new Error('missing/invalid component instance index [' + componentIdx + '] while getting current task'); + componentIdx = {current_component_idx_globals}.at(-1); + usedGlobal = true; }} - const tasks = {global_task_map}.get(componentIdx); - if (tasks === undefined) {{ return undefined; }} - if (tasks.length === 0) {{ return undefined; }} - return tasks[tasks.length - 1]; + + const taskMetas = {global_task_map}.get(componentIdx); + if (taskMetas === undefined || taskMetas.length === 0) {{ return undefined; }} + + const taskMeta = taskMetas[taskMetas.length - 1]; + if (!taskMeta || !taskMeta.task) {{ return undefined; }} + + return taskMeta; }} - ", + "#, fn_name = self.name(), )); } - Self::EndCurrentTask => { + Self::ClearCurrentTask => { let debug_log_fn = Intrinsic::DebugLog.name(); let global_task_map = Self::GlobalAsyncCurrentTaskMap.name(); let task_id_globals = Self::GlobalAsyncCurrentTaskIds.name(); let component_idx_globals = Self::GlobalAsyncCurrentComponentIdxs.name(); + let fn_name = self.name(); output.push_str(&format!( - " + r#" function {fn_name}(componentIdx, taskID) {{ componentIdx ??= {component_idx_globals}.at(-1); - taskID ??= {task_id_globals}.at(-1); {debug_log_fn}('[{fn_name}()] args', {{ componentIdx, taskID }}); if (componentIdx === undefined || componentIdx === null) {{ @@ -687,7 +689,7 @@ impl AsyncTaskIntrinsic { throw new Error('missing/invalid tasks for component instance while ending task'); }} if (tasks.length == 0) {{ - throw new Error('no current task(s) for component instance while ending task'); + throw new Error(`no current tasks for component instance [${{componentIdx}}] while ending task`); }} if (taskID) {{ @@ -704,8 +706,7 @@ impl AsyncTaskIntrinsic { const taskMeta = tasks.pop(); return taskMeta.task; }} - ", - fn_name = self.name() + "#, )); } @@ -719,9 +720,11 @@ impl AsyncTaskIntrinsic { let task_class = Self::AsyncTaskClass.name(); let subtask_class = Self::AsyncSubtaskClass.name(); - let awaitable_class = Intrinsic::AwaitableClass.name(); let global_async_determinism = Intrinsic::GlobalAsyncDeterminism.name(); let coin_flip_fn = Intrinsic::CoinFlip.name(); + let waitable_class = Intrinsic::Waitable(WaitableIntrinsic::WaitableClass).name(); + let clear_current_task_fn = + Intrinsic::AsyncTask(AsyncTaskIntrinsic::ClearCurrentTask).name(); output.push_str(&format!(r#" class {task_class} {{ @@ -744,6 +747,7 @@ impl AsyncTaskIntrinsic { #componentIdx; #state; #isAsync; + #isManualAsync; #entryFnName = null; #subtasks = []; @@ -754,6 +758,7 @@ impl AsyncTaskIntrinsic { #onExitHandlers = []; #memoryIdx = null; + #memory = null; #callbackFn = null; #callbackFnName = null; @@ -776,6 +781,7 @@ impl AsyncTaskIntrinsic { #returnLowerFns = null; #entered = false; + #exited = false; cancelled = false; requested = false; @@ -798,6 +804,7 @@ impl AsyncTaskIntrinsic { this.#state = {task_class}.State.INITIAL; this.#isAsync = opts?.isAsync ?? false; + this.#isManualAsync = opts?.isManualAsync ?? false; this.#entryFnName = opts.entryFnName; const {{ @@ -839,7 +846,6 @@ impl AsyncTaskIntrinsic { taskState() {{ return this.#state; }} id() {{ return this.#id; }} componentIdx() {{ return this.#componentIdx; }} - isAsync() {{ return this.#isAsync; }} entryFnName() {{ return this.#entryFnName; }} completionPromise() {{ return this.#completionPromise; }} @@ -852,8 +858,17 @@ impl AsyncTaskIntrinsic { hasCallback() {{ return this.#callbackFn !== null; }} - setReturnMemoryIdx(idx) {{ this.#memoryIdx = idx; }} getReturnMemoryIdx() {{ return this.#memoryIdx; }} + setReturnMemoryIdx(idx) {{ + if (idx === null) {{ return; }} + this.#memoryIdx = idx; + }} + + getReturnMemory() {{ return this.#memory; }} + setReturnMemory(m) {{ + if (m === null) {{ return; }} + this.#memory = m; + }} setReturnLowerFns(fns) {{ this.#returnLowerFns = fns; }} getReturnLowerFns() {{ return this.#returnLowerFns; }} @@ -906,6 +921,8 @@ impl AsyncTaskIntrinsic { return this.#getCalleeParamsFn(); }} + mayBlock() {{ return this.isAsync() || this.isResolved() }} + mayEnter(task) {{ const cstate = {get_or_create_async_state_fn}(this.#componentIdx); if (cstate.hasBackpressure()) {{ @@ -924,21 +941,42 @@ impl AsyncTaskIntrinsic { return true; }} + enterSync() {{ + if (this.needsExclusiveLock()) {{ + const cstate = {get_or_create_async_state_fn}(this.#componentIdx); + cstate.exclusiveLock(); + }} + return true; + }} + async enter(opts) {{ - {debug_log_fn}('[{task_class}#enter()] args', {{ taskID: this.#id }}); + {debug_log_fn}('[{task_class}#enter()] args', {{ + taskID: this.#id, + componentIdx: this.#componentIdx, + subtaskID: this.getParentSubtask()?.id(), + }}); if (this.#entered) {{ throw new Error(`task with ID [${{this.#id}}] should not be entered twice`); }} + const cstate = {get_or_create_async_state_fn}(this.#componentIdx); + // If a task is either synchronous or host-provided (e.g. a host import, whether sync or async) // then we can avoid component-relevant tracking and immediately enter if (this.isSync() || opts?.isHost) {{ this.#entered = true; + + // TODO(breaking): remove once manually-spccifying async fns is removed + // It is currently possible for an actually sync export to be specified + // as async via JSPI + if (this.#isManualAsync) {{ + if (this.needsExclusiveLock()) {{ cstate.exclusiveLock(); }} + }} + return this.#entered; }} - const cstate = {get_or_create_async_state_fn}(this.#componentIdx); if (cstate.hasBackpressure()) {{ cstate.addBackpressureWaiter(); @@ -961,22 +999,20 @@ impl AsyncTaskIntrinsic { return this.#entered; }} - isRunning() {{ - return this.#state !== {task_class}.State.RESOLVED; - }} + isRunning() {{ return this.#state !== {task_class}.State.RESOLVED; }} + isResolved() {{ return this.#state === {task_class}.State.RESOLVED; }} async waitUntil(opts) {{ const {{ readyFn, waitableSetRep, cancellable }} = opts; {debug_log_fn}('[{task_class}#waitUntil()] args', {{ taskID: this.#id, waitableSetRep, cancellable }}); const state = {get_or_create_async_state_fn}(this.#componentIdx); - const wset = state.waitableSets.get(waitableSetRep); + const wset = state.handles.get(waitableSetRep); let event; wset.incrementNumWaiting(); - // const pendingEventWaitID = wset.registerPendingEventWait(); const keepGoing = await this.suspendUntil({{ readyFn: () => {{ const hasPendingEvent = wset.hasPendingEvent(); @@ -1001,36 +1037,6 @@ impl AsyncTaskIntrinsic { return event; }} - async onBlock(awaitable) {{ - {debug_log_fn}('[{task_class}#onBlock()] args', {{ taskID: this.#id, awaitable }}); - if (!(awaitable instanceof {awaitable_class})) {{ - throw new Error('invalid awaitable during onBlock'); - }} - - // Build a promise that this task can await on which resolves when it is awoken - const {{ promise, resolve, reject }} = promiseWithResolvers(); - this.awaitableResume = () => {{ - {debug_log_fn}('[{task_class}] resuming after onBlock', {{ taskID: this.#id }}); - resolve(); - }}; - this.awaitableCancel = (err) => {{ - {debug_log_fn}('[{task_class}] rejecting after onBlock', {{ taskID: this.#id, err }}); - reject(err); - }}; - - // Park this task/execution to be handled later - const state = {get_or_create_async_state_fn}(this.#componentIdx); - state.parkTaskOnAwaitable({{ awaitable, task: this }}); - - try {{ - await promise; - return {task_class}.BlockResult.NOT_CANCELLED; - }} catch (err) {{ - // rejection means task cancellation - return {task_class}.BlockResult.CANCELLED; - }} - }} - async yieldUntil(opts) {{ const {{ readyFn, cancellable }} = opts; {debug_log_fn}('[{task_class}#yieldUntil()] args', {{ taskID: this.#id, cancellable }}); @@ -1132,6 +1138,10 @@ impl AsyncTaskIntrinsic { }}); this.#postReturnFn(); }} + + if (this.#parentSubtask) {{ + this.#parentSubtask.onResolve(taskValue); + }} }} registerOnResolveHandler(f) {{ @@ -1140,9 +1150,10 @@ impl AsyncTaskIntrinsic { resolve(results) {{ {debug_log_fn}('[{task_class}#resolve()] args', {{ - results, componentIdx: this.#componentIdx, taskID: this.#id, + entryFnName: this.entryFnName(), + callbackFnName: this.#callbackFnName, }}); if (this.#state === {task_class}.State.RESOLVED) {{ @@ -1168,6 +1179,8 @@ impl AsyncTaskIntrinsic { taskID: this.#id, }}); + if (this.#exited) {{ throw new Error("task has already exited"); }} + if (this.#state !== {task_class}.State.RESOLVED) {{ // TODO(fix): only fused, manually specified post returns seem to break this invariant, // as the TaskReturn trampoline is not activated it seems. @@ -1193,12 +1206,9 @@ impl AsyncTaskIntrinsic { const state = {get_or_create_async_state_fn}(this.#componentIdx); if (!state) {{ throw new Error('missing async state for component [' + this.#componentIdx + ']'); }} - if (!this.#isAsync && !state.inSyncExportCall) {{ - throw new Error('sync task must be run from components known to be in a sync export call'); - }} - state.inSyncExportCall = false; - if (this.needsExclusiveLock() && !state.isExclusivelyLocked()) {{ + // Exempt the host from exclusive lock check + if (this.#componentIdx !== -1 && this.needsExclusiveLock() && !state.isExclusivelyLocked()) {{ throw new Error(`task [${{this.#id}}] exit: component [${{this.#componentIdx}}] should have been exclusively locked`); }} @@ -1212,24 +1222,50 @@ impl AsyncTaskIntrinsic { throw err; }} }} + + this.#exited = true; + {clear_current_task_fn}(this.#componentIdx); }} - needsExclusiveLock() {{ return this.#needsExclusiveLock; }} + needsExclusiveLock() {{ + return !this.#isAsync || this.hasCallback(); + }} createSubtask(args) {{ {debug_log_fn}('[{task_class}#createSubtask()] args', args); - const {{ componentIdx, childTask, callMetadata }} = args; + const {{ componentIdx, childTask, callMetadata, fnName, isAsync, isManualAsync }} = args; + + const cstate = {get_or_create_async_state_fn}(this.#componentIdx); + if (!cstate) {{ + throw new Error(`invalid/missing async state for component idx [${{componentIdx}}]`); + }} + + const waitable = new {waitable_class}({{ + componentIdx: this.#componentIdx, + target: `subtask (internal ID [${{this.#id}}])`, + }}); + const newSubtask = new {subtask_class}({{ componentIdx, childTask, parentTask: this, callMetadata, + isAsync, + isManualAsync, + fnName, + waitable, }}); this.#subtasks.push(newSubtask); + newSubtask.setTarget(`subtask (internal ID [${{newSubtask.id()}}], waitable [${{waitable.idx()}}], component [${{componentIdx}}])`); + waitable.setIdx(cstate.handles.insert(newSubtask)); + waitable.setTarget(`waitable for subtask (id [${{waitable.idx()}}], subtask internal ID [${{newSubtask.id()}}])`); + return newSubtask; }} - getLatestSubtask() {{ return this.#subtasks.at(-1); }} + getLatestSubtask() {{ + return this.#subtasks.at(-1); + }} currentSubtask() {{ {debug_log_fn}('[{task_class}#currentSubtask()]'); @@ -1237,11 +1273,9 @@ impl AsyncTaskIntrinsic { return this.#subtasks.at(-1); }} - endCurrentSubtask() {{ - {debug_log_fn}('[{task_class}#endCurrentSubtask()]'); + removeSubtask(subtask) {{ if (this.#subtasks.length === 0) {{ throw new Error('cannot end current subtask: no current subtask'); }} - const subtask = this.#subtasks.pop(); - subtask.drop(); + this.#subtasks = this.#subtasks.filter(t => t !== subtask); return subtask; }} }} @@ -1251,9 +1285,10 @@ impl AsyncTaskIntrinsic { Self::AsyncSubtaskClass => { let debug_log_fn = Intrinsic::DebugLog.name(); let subtask_class = Self::AsyncSubtaskClass.name(); - let waitable_class = Intrinsic::Waitable(WaitableIntrinsic::WaitableClass).name(); let get_or_create_async_state_fn = Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState).name(); + let lookup_memories_for_component = Intrinsic::LookupMemoriesForComponent.name(); + output.push_str(&format!(r#" class {subtask_class} {{ static _ID = 0n; @@ -1280,9 +1315,6 @@ impl AsyncTaskIntrinsic { #lenders = null; #waitable = null; - #waitableRep = null; - #waitableResolve = null; - #waitableReject = null; #callbackFn = null; #callbackFnName = null; @@ -1295,7 +1327,18 @@ impl AsyncTaskIntrinsic { #callMetadata = {{}}; + #resolved = false; + #onResolveHandlers = []; + #onStartHandlers = []; + + #result = null; + #resultSet = false; + + fnName; + target; + isAsync; + isManualAsync; constructor(args) {{ if (typeof args.componentIdx !== 'number') {{ @@ -1304,6 +1347,7 @@ impl AsyncTaskIntrinsic { this.#componentIdx = args.componentIdx; this.#id = ++{subtask_class}._ID; + this.fnName = args.fnName; if (!args.parentTask) {{ throw new Error('missing parent task during subtask creation'); }} this.#parentTask = args.parentTask; @@ -1312,28 +1356,16 @@ impl AsyncTaskIntrinsic { if (args.memoryIdx) {{ this.#memoryIdx = args.memoryIdx; }} - if (args.waitable) {{ - this.#waitable = args.waitable; - this.#waitable.setTarget(`subtask (internal ID [${{this.#id}}])`); - }} else {{ - const state = {get_or_create_async_state_fn}(this.#componentIdx); - if (!state) {{ - throw new Error('invalid/missing async state for component instance [' + componentIdx + ']'); - }} - - this.#waitable = new {waitable_class}({{ - componentIdx: this.#componentIdx, - target: `subtask (internal ID [${{this.#id}}])`, - }}); - this.#waitableResolve = () => this.#waitable.resolve(); - this.#waitableReject = () => this.#waitable.reject(); - - this.#waitableRep = state.waitables.insert(this.#waitable); - }} + if (!args.waitable) {{ throw new Error("missing/invalid waitable"); }} + this.#waitable = args.waitable; this.#lenders = []; if (args.callMetadata) {{ this.#callMetadata = args.callMetadata; }} + + if (args.target) {{ this.target = args.target; }} + if (args.isAsync) {{ this.isAsync = args.isAsync; }} + if (args.isManualAsync) {{ this.isManualAsync = args.isManualAsync; }} }} id() {{ return this.#id; }} @@ -1341,6 +1373,25 @@ impl AsyncTaskIntrinsic { childTaskID() {{ return this.#childTask?.id(); }} state() {{ return this.#state; }} + waitable() {{ return this.#waitable; }} + + join() {{ return this.#waitable.join(...arguments); }} + getPendingEvent() {{ return this.#waitable.getPendingEvent(...arguments); }} + hasPendingEvent() {{ return this.#waitable.hasPendingEvent(...arguments); }} + setPendingEvent() {{ return this.#waitable.setPendingEvent(...arguments); }} + + setTarget(tgt) {{ this.target = tgt; }} + + getResult() {{ + if (!this.#resultSet) {{ throw new Error("subtask result has not been set") }} + return this.#result; + }} + setResult(v) {{ + if (this.#resultSet) {{ throw new Error("subtask result has already been set"); }} + this.#result = v; + this.#resultSet = true; + }} + componentIdx() {{ return this.#componentIdx; }} setChildTask(t) {{ @@ -1379,19 +1430,23 @@ impl AsyncTaskIntrinsic { return this.#state == {subtask_class}.State.STARTING; }} + registerOnStartHandler(f) {{ + this.#onStartHandlers.push(f); + }} + onStart(args) {{ - if (!this.#onProgressFn) {{ throw new Error('missing on progress function'); }} {debug_log_fn}('[{subtask_class}#onStart()] args', {{ componentIdx: this.#componentIdx, taskID: this.#id, parentTaskID: this.parentTaskID(), + fnName: this.fnName, }}); - this.#onProgressFn(); - - let result; + if (this.#onProgressFn) {{ this.#onProgressFn(); }} this.#state = {subtask_class}.State.STARTED; + let result; + // If we have been provided a helper start function as a result of // component fusion performed by wasmtime tooling, then we can call that helper and lifts/lowers will // be performed for us. @@ -1420,13 +1475,18 @@ impl AsyncTaskIntrinsic { {debug_log_fn}('[{subtask_class}#onResolve()] args', {{ componentIdx: this.#componentIdx, subtaskID: this.#id, + isAsync: this.isAsync, childTaskID: this.childTaskID(), parentTaskID: this.parentTaskID(), - subtaskValue, + parentTaskFnName: this.#parentTask?.entryFnName(), + fnName: this.fnName, }}); - if (!this.#onProgressFn) {{ throw new Error('missing on progress function'); }} - this.#onProgressFn(); + if (this.#resolved) {{ + throw new Error('subtask has already been resolved'); + }} + + if (this.#onProgressFn) {{ this.#onProgressFn(); }} if (subtaskValue === null) {{ if (this.#cancelRequested) {{ @@ -1437,17 +1497,19 @@ impl AsyncTaskIntrinsic { 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'); + throw new Error('resolved subtask must have been started before completion'); }} this.#state = {subtask_class}.State.CANCELLED_BEFORE_RETURNED; }} }} else {{ if (this.#state !== {subtask_class}.State.STARTED) {{ - throw new Error('cancelled subtask must have been started before cancellation'); + throw new Error('resolved subtask must have been started before completion'); }} this.#state = {subtask_class}.State.RETURNED; }} + this.setResult(subtaskValue); + for (const f of this.#onResolveHandlers) {{ try {{ f(subtaskValue); @@ -1457,14 +1519,34 @@ impl AsyncTaskIntrinsic { }} }} + const callMetadata = this.getCallMetadata(); + + // TODO(fix): we should be able to easily have the caller's meomry + // to lower into here, but it's not present in PrepareCall + const memory = callMetadata.memory ?? this.#parentTask?.getReturnMemory() ?? {lookup_memories_for_component}({{ componentIdx: this.#parentTask?.componentIdx() }})[0]; + if (this.isAsync && callMetadata.resultPtr && memory) {{ + const {{ resultPtr, realloc }} = callMetadata; + const lowers = callMetadata.lowers; // may have been updated in task.return of the child + if (lowers.length === 0) {{ return; }} + lowers[0]({{ + componentIdx: this.#componentIdx, + memory, + realloc, + vals: [subtaskValue], + storagePtr: resultPtr, + }}); + }} + + this.#resolved = true; + this.#parentTask.removeSubtask(this); }} setRep(rep) {{ this.#componentRep = rep; }} getStateNumber() {{ return this.#state; }} - getWaitableRep() {{ return this.#waitableRep; }} + isReturned() {{ return this.#state === {subtask_class}.State.RETURNED; }} - waitableRep() {{ return this.#waitableRep; }} + waitableRep() {{ return this.#waitable.idx(); }} getCallMetadata() {{ return this.#callMetadata; }} @@ -1503,8 +1585,8 @@ impl AsyncTaskIntrinsic { resolveDelivered: this.resolveDelivered(), }}); - const canDeliverResolve = !this.resolveDelivered() && this.resolved(); - if (!canDeliverResolve) {{ + const cannotDeliverResolve = this.resolveDelivered() || !this.resolved(); + if (cannotDeliverResolve) {{ throw new Error('subtask cannot deliver resolution twice, and the subtask must be resolved'); }} @@ -1525,19 +1607,10 @@ impl AsyncTaskIntrinsic { drop() {{ {debug_log_fn}('[{subtask_class}#drop()] args', {{ }}); + if (!this.#waitable) {{ throw new Error('missing/invalid inner waitable'); }} if (!this.resolveDelivered()) {{ throw new Error('cannot drop subtask before resolve is delivered'); }} - if (!this.#waitable) {{ throw new Error('missing/invalid waitable'); }} - - const state = this.#getComponentState(); - const waitable = state.waitables.remove(this.#waitableRep); - - if (waitable !== this.#waitable) {{ - throw new Error('unexpectedly different waitable from removed rep'); - }} - waitable.drop(); - this.#dropped = true; }} @@ -1552,7 +1625,7 @@ impl AsyncTaskIntrinsic { getWaitableHandleIdx() {{ {debug_log_fn}('[{subtask_class}#getWaitableHandleIdx()] args', {{ }}); if (!this.#waitable) {{ throw new Error('missing/invalid waitable'); }} - return this.#waitableRep; + return this.waitableRep(); }} }} "#)); @@ -1577,16 +1650,14 @@ impl AsyncTaskIntrinsic { )); } - // 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(); - let error_all_component_states_fn = - Intrinsic::Component(ComponentIntrinsic::ComponentStateSetAllError).name(); + let get_or_create_async_state_fn = + Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState).name(); output.push_str(&format!(r#" async function {driver_loop_fn}(args) {{ @@ -1604,46 +1675,14 @@ impl AsyncTaskIntrinsic { const callbackFnName = task.getCallbackFnName(); const componentIdx = task.componentIdx(); - try {{ - callbackResult = await callbackResult; - }} catch (err) {{ - err.componentIdx = componentIdx; - - componentState.setErrored(err); - {error_all_component_states_fn}(); - - reject(err); - task.resolve([]); - return; + if (callbackResult instanceof Promise) {{ + throw new Error("callbackResult should be a value, not a promise"); }} - // TODO(fix): callbackResult should not ever be undefined, *unless* - // we are calling it on a function that was not async to begin with?... - // - // In practice, the callback of `[async]run` returns undefined. - // if (callbackResult === undefined) {{ - {debug_log_fn}('[{driver_loop_fn}()] early exit due to undefined callback result', {{ - taskID: task.id(), - subtaskID: task.currentSubtask()?.id(), - parentTaskID: task.currentSubtask()?.parentTaskID(), - fnName, - callbackResult - }}); - resolve(null); - task.resolve([]); - return; + throw new Error("callback result should never be undefined"); }} - // TODO: the callback result here IS a number, - // because task-return was called with a host function. - // - // Is our job here to resolve the task promise w/ the results? - // - // - // Or maybe to take the results, lower them back in for the component - // that did this call (possibly) to use?? - let callbackCode; let waitableSetRep; let unpacked; @@ -1670,9 +1709,7 @@ impl AsyncTaskIntrinsic { let asyncRes; try {{ while (true) {{ - if (callbackCode !== 0) {{ - componentState.exclusiveRelease(); - }} + if (callbackCode !== 0) {{ componentState.exclusiveRelease(); }} switch (callbackCode) {{ case 0: // EXIT @@ -1707,12 +1744,14 @@ impl AsyncTaskIntrinsic { break; case 2: // WAIT for a given waitable set + const cstate = {get_or_create_async_state_fn}(componentIdx); {debug_log_fn}('[{driver_loop_fn}()] waiting for event', {{ fnName, componentIdx, callbackFnName, taskID: task.id(), waitableSetRep, + waitableSetTargets: cstate.handles.get(waitableSetRep).targets(), }}); asyncRes = await task.waitUntil({{ readyFn: () => !componentState.isExclusivelyLocked(), @@ -1764,6 +1803,9 @@ impl AsyncTaskIntrinsic { waitableSetRep = unpacked[1]; {debug_log_fn}('[{driver_loop_fn}()] callback result unpacked', {{ + fnName, + componentIdx, + callbackFnName, callbackRes, callbackCode, waitableSetRep, @@ -1778,14 +1820,6 @@ impl AsyncTaskIntrinsic { result, err, }}); - console.error('[{driver_loop_fn}()] error during async driver loop', {{ - fnName, - callbackFnName, - eventCode, - index, - result, - err, - }}); reject(err); }} }} @@ -1819,92 +1853,157 @@ impl AsyncTaskIntrinsic { output.push_str(&format!( r#" - function {lower_import_fn}(args, exportFn) {{ - const params = [...arguments].slice(2); - {debug_log_fn}('[{lower_import_fn}()] args', {{ args, params, exportFn }}); + function {lower_import_fn}(args) {{ + const params = [...arguments].slice(1); + {debug_log_fn}('[{lower_import_fn}()] args', {{ args, params }}); const {{ functionIdx, componentIdx, isAsync, + isManualAsync, paramLiftFns, resultLowerFns, + funcTypeIsAsync, metadata, memoryIdx, getMemoryFn, getReallocFn, + importFn, }} = args; - const parentTaskMeta = {current_task_get_fn}(componentIdx); - const parentTask = parentTaskMeta?.task; - if (!parentTask) {{ throw new Error('missing parent task during lower of import'); }} + const taskMeta = {current_task_get_fn}(componentIdx); + if (!taskMeta) {{ + throw new Error(`missing current task while lowering import (fn [${{importFn.fnName}}])`); + }} + + const task = taskMeta?.task; + if (!task) {{ throw new Error("missing/invalid task"); }} const cstate = {get_or_create_async_state_fn}(componentIdx); + // TODO: re-enable this check -- postReturn can call imports though, + // and that breaks things. + // + // if (!cstate.mayLeave) {{ + // throw new Error(`cannot leave instance [${{componentIdx}}]`); + // }} - const subtask = parentTask.createSubtask({{ + if (!task.mayBlock() && funcTypeIsAsync && !isAsync) {{ + throw new Error("non async exports cannot synchronously call async functions"); + }} + + // If there is an existing task, this should be part of a subtask + const memory = getMemoryFn(); + const subtask = task.createSubtask({{ componentIdx, - parentTask, + parentTask: task, + fnName: importFn.fnName, + isAsync, + isManualAsync, callMetadata: {{ memoryIdx, - memory: getMemoryFn(), + memory, realloc: getReallocFn(), resultPtr: params[0], + lowers: resultLowerFns, }} }}); - parentTask.setReturnMemoryIdx(memoryIdx); - + task.setReturnMemoryIdx(memoryIdx); + task.setReturnMemory(getMemoryFn()); const rep = cstate.subtasks.insert(subtask); subtask.setRep(rep); + subtask.onStart(); + + // If dealing with a sync lowered sync function, we can directly return results + // + // TODO(breaking): remove once we get rid of manual async import specification, + // as func types cannot be detected in that case only (and we don't need that w/ p3) + if (!isManualAsync && !isAsync && !funcTypeIsAsync) {{ + const res = importFn(...params); + // TODO(breaking): remove once we get rid of manual async import specification, + // as func types cannot be detected in that case only (and we don't need that w/ p3) + if (!funcTypeIsAsync && !subtask.isReturned()) {{ + throw new Error('post-execution subtasks must either be async or returned'); + }} + return subtask.getResult(); + }} + + // Sync-lowered async functions requires async behavior because the callee *can* block, + // but this call must *act* synchronously and return immediately with the result + // (i.e. not returning until the work is done) + // + // TODO(breaking): remove checking for manual async specification here, once we can go p3-only + // + if (!isManualAsync && !isAsync && funcTypeIsAsync) {{ + const {{ promise, resolve }} = new Promise(); + queueMicrotask(async () => {{ + if (!subtask.isResolved()) {{ + await task.suspendUntil({{ readyFn: () => task.isResolved() }}); + }} + resolve(subtask.getResult()); + }}); + return promise; + }} + + // NOTE: at this point we know that we are working with an async lowered import + + const subtaskState = subtask.getStateNumber(); + if (subtaskState < 0 || subtaskState > 2**5) {{ + throw new Error('invalid subtask state, out of valid range'); + }} + subtask.setOnProgressFn(() => {{ subtask.setPendingEventFn(() => {{ if (subtask.resolved()) {{ subtask.deliverResolve(); }} - return {{ + const event = {{ code: {async_event_code_enum}.SUBTASK, payload0: rep, payload1: subtask.getStateNumber(), }} + return event; }}); }}); - // Set up a handler on subtask completion to lower results from the call into the caller's memory region. - subtask.registerOnResolveHandler((res) => {{ - {debug_log_fn}('[{lower_import_fn}()] handling subtask result', {{ res, subtaskID: subtask.id() }}); - const {{ memory, resultPtr, realloc }} = subtask.getCallMetadata(); - if (resultLowerFns.length === 0) {{ return; }} - resultLowerFns[0]({{ componentIdx, memory, realloc, vals: [res], storagePtr: resultPtr }}); - }}); - const subtaskState = subtask.getStateNumber(); - if (subtaskState < 0 || subtaskState > 2**5) {{ - throw new Error('invalid subtask state, out of valid range'); + let manualAsyncResult; + if (isManualAsync) {{ + manualAsyncResult = Promise.withResolvers(); }} // NOTE: we must wait a bit before calling the export function, // to ensure the subtask state is not modified before the lower call return - // - // TODO: we should trigger via subtask state changing, rather than a static wait? - setTimeout(async () => {{ + queueMicrotask(async () => {{ try {{ - {debug_log_fn}('[{lower_import_fn}()] calling lowered import', {{ exportFn, params }}); - exportFn.apply(null, params); - - const task = subtask.getChildTask(); - task.registerOnResolveHandler((res) => {{ - {debug_log_fn}('[{lower_import_fn}()] cascading subtask completion', {{ - childTaskID: task.id(), - subtaskID: subtask.id(), - parentTaskID: parentTask.id(), - }}); + {debug_log_fn}('[{lower_import_fn}()] calling lowered import', {{ importFn, params }}); + const res = await importFn(...params); - subtask.onResolve(res); - - cstate.tick(); - }}); + // If the import was manually made async, we need to grab the result of + // the task execution and return that instead via a promise + // + // TODO(breaking): remove once manually specified async is removed + // we have to do more work to actually return the result to the caller + // + if (isManualAsync) {{ + manualAsyncResult.resolve(subtask.getResult()); + }} }} catch (err) {{ console.error("post-lower import fn error:", err); + manualAsyncResult.reject(new Error()); throw err; }} - }}, 100); + }}); + + // This is a hack to maintain backwards compatibility with + // manually-specified async imports, used in wasm exports that are + // not actually async (but are specified as so). + // + // This is not normal p3 sync behavior but instead anticipating that + // the caller that is doing manual async will be waiting for a promise that + // resolves to the *actual* result. + // + // TODO(breaking): remove once manually specified async is removed + // + if (isManualAsync) {{ return manualAsyncResult.promise; }} return Number(subtask.waitableRep()) << 4 | subtaskState; }} @@ -1912,16 +2011,24 @@ impl AsyncTaskIntrinsic { )); } - // TODO: convert logic to perform task stack management here explicitly. - // // This call receives the following params: // - caller instance // - callee async (0 for sync) // - callee instance // + // Note that symmetric guest calls are may be called before lowered import calls as well. + // + // TODO: convert logic to perform task stack management here explicitly. + // Self::EnterSymmetricSyncGuestCall => { let debug_log_fn = Intrinsic::DebugLog.name(); let enter_symmetric_sync_guest_call_fn = self.name(); + + let get_current_task_fn = + Intrinsic::AsyncTask(AsyncTaskIntrinsic::GetCurrentTask).name(); + let create_new_current_task_fn = + Intrinsic::AsyncTask(AsyncTaskIntrinsic::CreateNewCurrentTask).name(); + output.push_str(&format!( r#" function {enter_symmetric_sync_guest_call_fn}(callerComponentIdx, calleeIsAsync, calleeComponentIdx) {{ @@ -1930,6 +2037,27 @@ impl AsyncTaskIntrinsic { calleeIsAsync, calleeComponentIdx }}); + + if (calleeIsAsync) {{ throw new Error('symmetric sync guest->guest call should not be async'); }} + + const callerTaskMeta = {get_current_task_fn}(callerComponentIdx); + if (!callerTaskMeta) {{ throw new Error('missing current caller task metadata'); }} + const callerTask = callerTaskMeta.task; + if (!callerTask) {{ throw new Error('missing current caller task'); }} + + const [newTask, newTaskID] = {create_new_current_task_fn}({{ + componentIdx: calleeComponentIdx, + isAsync: !!calleeIsAsync, + entryFnName: 'task/' + callerTask.id() + '/new-sync-guest-task', + }}); + + // TODO: if the next task is a guest fn that is *not* a lowered import, + // we need to create a subtask for the task that *will* be greated to use + const subtask = callerTask.createSubtask({{ + componentIdx: callerComponentIdx, + parentTask: callerTask, + isAsync: !!calleeIsAsync, + }}); }} "#, )); diff --git a/crates/js-component-bindgen/src/intrinsics/p3/host.rs b/crates/js-component-bindgen/src/intrinsics/p3/host.rs index bd6152c63..2ea227c01 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/host.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/host.rs @@ -141,15 +141,16 @@ impl HostIntrinsic { callerInstanceIdx, calleeInstanceIdx, taskReturnTypeIdx, - isCalleeAsyncInt, + calleeIsAsyncInt, stringEncoding, resultCountOrAsync, ) {{ {debug_log_fn}('[{prepare_call_fn}()]', {{ + memoryIdx, callerInstanceIdx, calleeInstanceIdx, taskReturnTypeIdx, - isCalleeAsyncInt, + calleeIsAsyncInt, stringEncoding, resultCountOrAsync, }}); @@ -193,12 +194,13 @@ impl HostIntrinsic { let getCalleeParamsFn; let resultPtr = null; + let directParamsArr; if (hasResultPointer) {{ - const directParamsArr = argArray.slice(11); + directParamsArr = argArray.slice(10, argArray.length - 1); getCalleeParamsFn = () => directParamsArr; - resultPtr = argArray[10]; + resultPtr = argArray[argArray.length - 1]; }} else {{ - const directParamsArr = argArray.slice(10); + directParamsArr = argArray.slice(10); getCalleeParamsFn = () => directParamsArr; }} @@ -217,11 +219,11 @@ impl HostIntrinsic { throw new Error(`unrecognized string encoding enum [${{stringEncoding}}]`); }} + const calleeIsAsync = calleeIsAsyncInt !== 0; const [newTask, newTaskID] = {create_new_current_task_fn}({{ componentIdx: calleeInstanceIdx, - isAsync: isCalleeAsyncInt !== 0, + isAsync: calleeIsAsync, getCalleeParamsFn, - // TODO: find a way to pass the import name through here entryFnName: 'task/' + currentCallerTask.id() + '/new-prepare-task', stringEncoding, }}); @@ -230,19 +232,21 @@ impl HostIntrinsic { componentIdx: callerInstanceIdx, parentTask: currentCallerTask, childTask: newTask, + isAsync: calleeIsAsync, callMetadata: {{ - memory: getMemoryFn(), + getMemoryFn, memoryIdx, resultPtr, returnFn, startFn, }} }}); - newTask.setParentSubtask(subtask); + // NOTE: This isn't really a return memory idx for the caller, it's for checking // against the task.return (which will be called from the callee) newTask.setReturnMemoryIdx(memoryIdx); + newTask.setReturnMemory(getMemoryFn()); }} "# )); @@ -257,8 +261,6 @@ impl HostIntrinsic { Self::AsyncStartCall => { let debug_log_fn = Intrinsic::DebugLog.name(); let async_start_call_fn = Self::AsyncStartCall.name(); - let current_async_task_id_globals = - AsyncTaskIntrinsic::GlobalAsyncCurrentTaskIds.name(); let current_component_idx_globals = AsyncTaskIntrinsic::GlobalAsyncCurrentComponentIdxs.name(); let current_task_get_fn = @@ -268,19 +270,17 @@ impl HostIntrinsic { let async_event_code_enum = Intrinsic::AsyncEventCodeEnum.name(); let async_driver_loop_fn = Intrinsic::AsyncTask(AsyncTaskIntrinsic::DriverLoop).name(); - let subtask_class = - Intrinsic::AsyncTask(AsyncTaskIntrinsic::AsyncSubtaskClass).name(); - let global_component_memories_class = - Intrinsic::GlobalComponentMemoriesClass.name(); + let lookup_memories_for_component_fn = Intrinsic::LookupMemoriesForComponent.name(); // TODO: lower here for non-zero param count // https://github.com/bytecodealliance/wasmtime/blob/69ef9afc11a2846248c9e94affca0223dbd033fc/crates/wasmtime/src/runtime/component/concurrent.rs#L1775 output.push_str(&format!(r#" function {async_start_call_fn}(args, callee, paramCount, resultCount, flags) {{ + const componentIdx = {current_component_idx_globals}.at(-1); const {{ getCallbackFn, callbackIdx, getPostReturnFn, postReturnIdx }} = args; - {debug_log_fn}('[{async_start_call_fn}()] args', args); + {debug_log_fn}('[{async_start_call_fn}()] args', {{ args, componentIdx }}); - const taskMeta = {current_task_get_fn}({current_component_idx_globals}.at(-1), {current_async_task_id_globals}.at(-1)); + const taskMeta = {current_task_get_fn}(componentIdx); if (!taskMeta) {{ throw new Error('invalid/missing current async task meta during prepare call'); }} const argArray = [...arguments]; @@ -307,22 +307,6 @@ impl HostIntrinsic { throw new Error(`unexpected callee param count [${{ params.length }}], {async_start_call_fn} invocation expected [${{ paramCount }}]`); }} - subtask.setOnProgressFn(() => {{ - subtask.setPendingEventFn(() => {{ - if (subtask.resolved()) {{ subtask.deliverResolve(); }} - return {{ - code: {async_event_code_enum}.SUBTASK, - payload0: rep, - payload1: subtask.getStateNumber(), - }} - }}); - }}); - - const subtaskState = subtask.getStateNumber(); - if (subtaskState < 0 || subtaskState > 2**5) {{ - throw new Error('invalid subtask state, out of valid range'); - }} - const callerComponentState = {get_or_create_async_state_fn}(subtask.componentIdx()); const rep = callerComponentState.subtasks.insert(subtask); subtask.setRep(rep); @@ -336,6 +320,7 @@ impl HostIntrinsic { // lowering manually, as fused modules provider helper functions that can subtask.registerOnResolveHandler((res) => {{ {debug_log_fn}('[{async_start_call_fn}()] handling subtask result', {{ res, subtaskID: subtask.id() }}); + let subtaskCallMeta = subtask.getCallMetadata(); // NOTE: in the case of guest -> guest async calls, there may be no memory/realloc present, @@ -355,7 +340,9 @@ impl HostIntrinsic { // and the result will be delivered (lift/lowered) via helper function if (subtaskCallMeta.returnFn) {{ {debug_log_fn}('[{async_start_call_fn}()] return function present while handling subtask result, returning early (skipping lower)'); - return; + if (subtaskCallMeta.resultPtr === undefined) {{ throw new Error("unexpectedly missing result pointer"); }} + // NOTE: returnFn was already run in task.return + return; }} // If there is no where to lower the results, exit early @@ -366,9 +353,9 @@ impl HostIntrinsic { let callerMemory; if (callerMemoryIdx) {{ - callerMemory = {global_component_memories_class}.getMemory(callerComponentIdx, callerMemoryIdx); + callerMemory = {lookup_memories_for_component_fn}({{ componentIdx: callerComponentIdx, memoryIdx: callerMemoryIdx }}); }} else {{ - const callerMemories = {global_component_memories_class}.getMemoriesForComponentIdx(callerComponentIdx); + const callerMemories = {lookup_memories_for_component_fn}({{ componentIdx: callerComponentIdx }}); if (callerMemories.length != 1) {{ throw new Error(`unsupported amount of caller memories`); }} callerMemory = callerMemories[0]; }} @@ -396,90 +383,46 @@ impl HostIntrinsic { }}); - // Build call params - const subtaskCallMeta = subtask.getCallMetadata(); - let startFnParams = []; - let calleeParams = []; - if (subtaskCallMeta.startFn && subtaskCallMeta.resultPtr) {{ - // If we're using a fused component start fn and a result pointer is present, - // then we need to pass the result pointer and other params to the start fn - startFnParams.push(subtaskCallMeta.resultPtr, ...params); - }} else {{ - // if not we need to pass params to the callee instead - startFnParams.push(...params); - calleeParams.push(...params); - }} - - preparedTask.registerOnResolveHandler((res) => {{ - {debug_log_fn}('[{async_start_call_fn}()] signaling subtask completion due to task completion', {{ - childTaskID: preparedTask.id(), - subtaskID: subtask.id(), - parentTaskID: subtask.getParentTask().id(), + subtask.setOnProgressFn(() => {{ + subtask.setPendingEventFn(() => {{ + if (subtask.resolved()) {{ subtask.deliverResolve(); }} + const event = {{ + code: {async_event_code_enum}.SUBTASK, + payload0: rep, + payload1: subtask.getStateNumber(), + }}; + return event; }}); - subtask.onResolve(res); - }}); - - // TODO(fix): start fns sometimes produce results, how should they be used? - // the result should theoretically be used for flat lowering, but fused components do - // this automatically! - subtask.onStart({{ startFnParams }}); - - {debug_log_fn}("[{async_start_call_fn}()] initial call", {{ - task: preparedTask.id(), - subtaskID: subtask.id(), - calleeFnName: callee.name, - }}); - - const callbackResult = callee.apply(null, calleeParams); - - {debug_log_fn}("[{async_start_call_fn}()] after initial call", {{ - task: preparedTask.id(), - subtaskID: subtask.id(), - calleeFnName: callee.name, }}); - const doSubtaskResolve = () => {{ - subtask.deliverResolve(); - }}; - - // If a single call resolved the subtask and there is no backpressure in the guest, - // we can return immediately - if (subtask.resolved() && !calleeBackpressure) {{ - {debug_log_fn}("[{async_start_call_fn}()] instantly resolved", {{ - calleeComponentIdx: preparedTask.componentIdx(), - task: preparedTask.id(), - subtaskID: subtask.id(), - callerComponentIdx: subtask.componentIdx(), - }}); - - // If a fused component return function was specified for the subtask, - // we've likely already called it during resolution of the task. - // - // In this case, we do not want to actually return 2 AKA "RETURNED", - // but the normal started task state, because the fused component expects to get - // the waitable + the original subtask state (0 AKA "STARTING") - // - if (subtask.getCallMetadata().returnFn) {{ - return Number(subtask.waitableRep()) << 4 | subtaskState; - }} - - doSubtaskResolve(); - return {subtask_class}.State.RETURNED; - }} - // Start the (event) driver loop that will resolve the task - new Promise(async (resolve, reject) => {{ - if (subtask.resolved() && calleeBackpressure) {{ - await calleeComponentState.waitForBackpressure(); - - {debug_log_fn}("[{async_start_call_fn}()] instantly resolved after cleared backpressure", {{ - calleeComponentIdx: preparedTask.componentIdx(), - task: preparedTask.id(), - subtaskID: subtask.id(), - callerComponentIdx: subtask.componentIdx(), - }}); - return; - }} + queueMicrotask(async () => {{ + // // Suspend the created task until the callee is no longer locked + // await calleeComponentState.suspendTask({{ + // task: preparedTask, + // readyFn: () => !calleeComponentState.isExclusivelyLocked(), + // }}); + + const startResults = subtask.onStart({{ startFnParams: params }}); + + const jspiCallee = WebAssembly.promising(callee); + let callbackResult; + callbackResult = await jspiCallee.apply(null, params); + + // if (subtask.resolved()) {{ + // if (calleeBackpressure) {{ await calleeComponentState.waitForBackpressure(); }} + + // {debug_log_fn}("[{async_start_call_fn}()] resolved after cleared backpressure", {{ + // calleeComponentIdx: preparedTask.componentIdx(), + // task: preparedTask.id(), + // subtaskID: subtask.id(), + // callerComponentIdx: subtask.componentIdx(), + // }}); + + // console.log("DELIVERING FROM THE BACKRPESSURE CHECK"); + // subtask.deliverResolve(); + // return; + // }} const started = await preparedTask.enter(); if (!started) {{ @@ -491,20 +434,27 @@ impl HostIntrinsic { return; }} - // TODO: retrieve/pass along actual fn name the callback corresponds to - // (at least something like `_callback`) - const fnName = [ - '', - ].join(""); + let fnName = callbackFn.fnName; + if (!fnName) {{ + fnName = [ + '', + ].join(""); + }} try {{ - {debug_log_fn}("[{async_start_call_fn}()] starting driver loop", {{ fnName, componentIdx: preparedTask.componentIdx(), }}); + {debug_log_fn}("[{async_start_call_fn}()] starting driver loop", {{ + fnName, + componentIdx: preparedTask.componentIdx(), + subtaskID: subtask.id(), + parentTaskID: subtask.parentTaskID(), + }}); + await {async_driver_loop_fn}({{ componentState: calleeComponentState, task: preparedTask, @@ -515,11 +465,24 @@ impl HostIntrinsic { reject }}); }} catch (err) {{ + console.error("[AsyncStartCall] drive loop call failure", {{ err }}); {debug_log_fn}("[AsyncStartCall] drive loop call failure", {{ err }}); }} }}); + const subtaskState = subtask.getStateNumber(); + if (subtaskState < 0 || subtaskState > 2**5) {{ + throw new Error('invalid subtask state, out of valid range'); + }} + + {debug_log_fn}('[{async_start_call_fn}()] returning subtask rep & state', {{ + subtask: {{ + rep: subtask.waitableRep(), + state: subtaskState, + }} + }}); + return Number(subtask.waitableRep()) << 4 | subtaskState; }} "#)); @@ -546,12 +509,19 @@ impl HostIntrinsic { function {store_event_in_component_memory_fn}(args) {{ {debug_log_fn}('[{store_event_in_component_memory_fn}()] args', args); const {{ memory, ptr, event }} = args; + if (!memory) {{ throw new Error('unexpectedly missing memory'); }} if (ptr === undefined || ptr === null) {{ throw new Error('unexpectedly missing pointer'); }} if (!event) {{ throw new Error('event object missing'); }} + if (event.code === undefined) {{ throw new Error('invalid event object, missing code'); }} + if (event.payload0 === undefined) {{ throw new Error('invalid event object, missing payload0'); }} + if (event.payload1 === undefined) {{ throw new Error('invalid event object, missing payload1'); }} + const dv = new DataView(memory.buffer); - dv.setUint32(ptr, event.index, true); - dv.setUint32(ptr + 4, event.result, true); + dv.setUint32(ptr, event.payload0, true); + dv.setUint32(ptr + 4, event.payload1, true); + + return event.code; }} "# )); diff --git a/crates/js-component-bindgen/src/intrinsics/p3/waitable.rs b/crates/js-component-bindgen/src/intrinsics/p3/waitable.rs index 65383c4cb..1affa690e 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/waitable.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/waitable.rs @@ -160,8 +160,12 @@ impl WaitableIntrinsic { #pendingEvent = null; #waiting = 0; + target; + constructor(componentIdx) {{ + if (componentIdx === undefined) {{ throw new TypeError("missing/invalid component idx"); }} this.#componentIdx = componentIdx; + this.target = `component [${{this.#componentIdx}}] waitable set`; }} componentIdx() {{ return this.#componentIdx; }} @@ -172,6 +176,10 @@ impl WaitableIntrinsic { incrementNumWaiting(n) {{ this.#waiting += n ?? 1; }} decrementNumWaiting(n) {{ this.#waiting -= n ?? 1; }} + targets() {{ return this.#waitables.map(w => w.target); }} + + setTarget(tgt) {{ this.target = tgt; }} + shuffleWaitables() {{ this.#waitables = this.#waitables .map(value => ({{ value, sort: Math.random() }})) @@ -207,7 +215,12 @@ impl WaitableIntrinsic { }}); for (const waitable of this.#waitables) {{ if (!waitable.hasPendingEvent()) {{ continue; }} - return waitable.getPendingEvent(); + const event = waitable.getPendingEvent(); + {debug_log_fn}('[{waitable_set_class}#getPendingEvent()] found pending event', {{ + waitable, + event, + }}); + return event; }} throw new Error('no waitables had a pending event'); }} @@ -230,7 +243,9 @@ impl WaitableIntrinsic { #resolve; #reject; - #waitableSet; + #waitableSet = null; + + #idx = null; // to component-global waitables target; @@ -242,7 +257,13 @@ impl WaitableIntrinsic { }} componentIdx() {{ return this.#componentIdx; }} - isInSet() {{ return this.#waitableSet !== undefined; }} + isInSet() {{ return this.#waitableSet !== null; }} + + idx() {{ return this.#idx; }} + setIdx(idx) {{ + if (idx === 0) {{ throw new Error("waitable idx cannot be zero"); }} + this.#idx = idx; + }} setTarget(tgt) {{ this.target = tgt; }} @@ -258,19 +279,19 @@ impl WaitableIntrinsic { promise() {{ return this.#promise; }} hasPendingEvent() {{ - {debug_log_fn}('[{waitable_class}#hasPendingEvent()]', {{ - componentIdx: this.#componentIdx, - waitable: this, - waitableSet: this.#waitableSet, - hasPendingEvent: this.#pendingEventFn !== null, - }}); + // {debug_log_fn}('[{waitable_class}#hasPendingEvent()]', {{ + // componentIdx: this.#componentIdx, + // waitable: this, + // waitableSet: this.#waitableSet, + // hasPendingEvent: this.#pendingEventFn !== null, + // }}); return this.#pendingEventFn !== null; }} setPendingEventFn(fn) {{ {debug_log_fn}('[{waitable_class}#setPendingEvent()] args', {{ waitable: this, - waitableSet: this.#waitableSet, + inSet: this.#waitableSet, }}); this.#pendingEventFn = fn; }} @@ -278,7 +299,7 @@ impl WaitableIntrinsic { getPendingEvent() {{ {debug_log_fn}('[{waitable_class}#getPendingEvent()] args', {{ waitable: this, - waitableSet: this.#waitableSet, + inSet: this.#waitableSet, hasPendingEvent: this.#pendingEventFn !== null, }}); if (this.#pendingEventFn === null) {{ return null; }} @@ -328,12 +349,14 @@ impl WaitableIntrinsic { output.push_str(&format!(r#" function {waitable_set_new_fn}(componentIdx) {{ {debug_log_fn}('[{waitable_set_new_fn}()] args', {{ componentIdx }}); + const state = {get_or_create_async_state_fn}(componentIdx); - if (!state) {{ - throw new Error(`invalid/missing async state for component idx [${{componentIdx}}]`); - }} - const rep = state.waitableSets.insert(new {waitable_set_class}(componentIdx)); - if (typeof rep !== 'number') {{ throw new Error('invalid/missing waitable set rep [' + rep + ']'); }} + if (!state) {{throw new Error(`missing async state for component idx [${{componentIdx}}]`); }} + + const rep = state.handles.insert(new {waitable_set_class}(componentIdx)); + if (typeof rep !== 'number') {{ throw new Error(`invalid/missing waitable set rep [${{rep}}]`); }} + + {debug_log_fn}('[{waitable_set_new_fn}()] created waitable set', {{ componentIdx, rep }}); return rep; }} "#)); @@ -344,10 +367,11 @@ impl WaitableIntrinsic { let waitable_set_wait_fn = Self::WaitableSetWait.name(); let current_task_get_fn = Intrinsic::AsyncTask(AsyncTaskIntrinsic::GetCurrentTask).name(); - let write_async_event_to_memory_fn = Intrinsic::WriteAsyncEventToMemory.name(); + let store_event_in_component_memory_fn = + Intrinsic::Host(HostIntrinsic::StoreEventInComponentMemory).name(); output.push_str(&format!(r#" async function {waitable_set_wait_fn}(ctx, waitableSetRep, resultPtr) {{ - {debug_log_fn}('[{waitable_set_wait_fn}()] args', {{ args, waitableSetRep, resultPtr }}); + {debug_log_fn}('[{waitable_set_wait_fn}()] args', {{ ctx, waitableSetRep, resultPtr }}); const {{ componentIdx, isAsync, @@ -366,7 +390,7 @@ impl WaitableIntrinsic { }} const event = await task.waitForEvent({{ waitableSetRep, isAsync }}); - {write_async_event_to_memory_fn}(memory, task, event, resultPtr); + return {store_event_in_component_memory_fn}(memory, task, event, resultPtr); }} "#)); } @@ -382,14 +406,13 @@ impl WaitableIntrinsic { HostIntrinsic::StoreEventInComponentMemory.name(); let async_event_code_enum = Intrinsic::AsyncEventCodeEnum.name(); output.push_str(&format!(r#" - async function {waitable_set_poll_fn}(ctx, waitableSetRep, resultPtr) {{ + function {waitable_set_poll_fn}(ctx, waitableSetRep, resultPtr) {{ const {{ componentIdx, memoryIdx, getMemoryFn, isAsync, isCancellable }} = ctx; {debug_log_fn}('[{waitable_set_poll_fn}()] args', {{ componentIdx, memoryIdx, waitableSetRep, resultPtr, - resultPtr }}); const taskMeta = {current_task_get_fn}(componentIdx); @@ -406,13 +429,13 @@ impl WaitableIntrinsic { }} const cstate = {get_or_create_async_state_fn}(task.componentIdx()); - const wset = cstate.waitableSets.get(waitableSetRep); + const wset = cstate.handles.get(waitableSetRep); if (!wset) {{ throw new Error(`missing waitable set [${{waitableSetRep}}] in component [${{componentIdx}}]`); }} let event; - const cancelDelivered = task.deliverPendingCancel({{ cancelalble: isCancellable }}); + const cancelDelivered = task.deliverPendingCancel({{ cancellable: isCancellable }}); if (cancelDelivered) {{ event = {{ code: {async_event_code_enum}.TASK_CANCELLED, payload0: 0, payload1: 0 }}; }} else if (!wset.hasPendingEvent()) {{ @@ -421,7 +444,8 @@ impl WaitableIntrinsic { event = wset.getPendingEvent(); }} - return {store_event_in_component_memory_fn}({{ event, ptr: resultPtr, memory: getMemoryFn() }}); + const eventCode = {store_event_in_component_memory_fn}({{ event, ptr: resultPtr, memory: getMemoryFn() }}); + return eventCode; }} "#)); } @@ -433,8 +457,7 @@ impl WaitableIntrinsic { Intrinsic::AsyncTask(AsyncTaskIntrinsic::GetCurrentTask).name(); let get_or_create_async_state_fn = Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState).name(); - let remove_waitable_set_fn = - Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState).name(); + let remove_waitable_set_fn = Self::RemoveWaitableSet.name(); output.push_str(&format!(" function {waitable_set_drop_fn}(componentIdx, waitableSetRep) {{ {debug_log_fn}('[{waitable_set_drop_fn}()] args', {{ componentIdx, waitableSetRep }}); @@ -448,7 +471,7 @@ impl WaitableIntrinsic { const state = {get_or_create_async_state_fn}(componentIdx); if (!state.mayLeave) {{ throw new Error('component instance is not marked as may leave, cannot be cancelled'); }} - {remove_waitable_set_fn}({{ state, waitableSetRep, task }}); + {remove_waitable_set_fn}({{ state, waitableSetRep }}); }} ")); } @@ -456,27 +479,32 @@ impl WaitableIntrinsic { Self::RemoveWaitableSet => { let debug_log_fn = Intrinsic::DebugLog.name(); let remove_waitable_set_fn = Self::RemoveWaitableSet.name(); - output.push_str(&format!(" - function {remove_waitable_set_fn}({{ state, waitableSetRep, task }}) {{ - {debug_log_fn}('[{remove_waitable_set_fn}()] args', {{ componentIdx, waitableSetRep }}); + output.push_str(&format!(r#" + function {remove_waitable_set_fn}(args) {{ + {debug_log_fn}('[{remove_waitable_set_fn}()] args', args); + const {{ state, waitableSetRep }} = args; + if (!state) {{ throw new TypeError("missing component state"); }} + if (!waitableSetRep) {{ throw new TypeError("missing component waitableSetRep"); }} - const ws = state.waitableSets.get(waitableSetRep); + const ws = state.handles.get(waitableSetRep); 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'); }} + if (ws.hasPendingEvent()) {{ + throw new Error('waitable set cannot be removed with pending items remaining'); + }} - const waitableSet = state.waitableSets.get(waitableSetRep); - if (waitableSet.numWaitables() > 0) {{ + const waitableSet = state.handles.get(waitableSetRep); + if (ws.numWaitables() > 0) {{ throw new Error('waitable set still contains waitables'); }} - if (waitableSet.numWaiting() > 0) {{ + if (ws.numWaiting() > 0) {{ throw new Error('waitable set still has other tasks waiting on it'); }} - state.waitableSets.remove(waitableSetRep); + state.handles.remove(waitableSetRep); }} - ")); + "#)); } Self::WaitableJoin => { @@ -490,22 +518,26 @@ impl WaitableIntrinsic { const state = {get_or_create_async_state_fn}(componentIdx); if (!state) {{ - throw new Error('invalid/missing async state for component instance [' + componentIdx + ']'); + throw new Error(`invalid/missing async state for component instance [${{componentIdx}}]`); }} if (!state.mayLeave) {{ throw new Error('component instance is not marked as may leave, cannot join waitable'); }} - const waitableObj = state.waitables.get(waitableRep); + const waitableObj = state.handles.get(waitableRep); if (!waitableObj) {{ throw new Error(`missing waitable obj (rep [${{waitableRep}}]), component idx [${{componentIdx}}])`); }} - const waitable = typeof waitableObj.getWaitable === 'function' ? waitableObj.getWaitable() : waitableObj; + const waitable = waitableObj.getWaitable ? waitableObj.getWaitable() : waitableObj; + if (!waitable.join) {{ + console.error("WAITABLE", {{ waitable, componentIdx, waitableRep, waitableSetRep }}); + throw new Error("invalid waitable object, does not have join()"); + }} - const waitableSet = waitableSetRep === 0 ? null : state.waitableSets.get(waitableSetRep); + const waitableSet = waitableSetRep === 0 ? null : state.handles.get(waitableSetRep); if (waitableSetRep !== 0 && !waitableSet) {{ - throw new Error('failed to find waitable set [' + waitableSetRep + '] in component instance [' + componentIdx + ']'); + throw new Error(`missing waitable set [${{waitableSetRep}}] in component idx [${{componentIdx}}]`); }} waitable.join(waitableSet); diff --git a/crates/js-component-bindgen/src/intrinsics/string.rs b/crates/js-component-bindgen/src/intrinsics/string.rs index b5fd00a30..92e68c89f 100644 --- a/crates/js-component-bindgen/src/intrinsics/string.rs +++ b/crates/js-component-bindgen/src/intrinsics/string.rs @@ -8,14 +8,24 @@ use crate::{intrinsics::Intrinsic, source::Source}; #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] pub enum StringIntrinsic { Utf16Decoder, + Utf16Encode, + + Utf16EncodeAsync, + /// UTF8 Decoder (a JS `TextDecoder`) GlobalTextDecoderUtf8, + /// UTF8 Encoder (a JS `TextEncoder`) GlobalTextEncoderUtf8, + /// Encode a single string to memory Utf8Encode, + + Utf8EncodeAsync, + ValidateGuestChar, + ValidateHostChar, } @@ -30,9 +40,11 @@ impl StringIntrinsic { [ Self::Utf16Decoder.name(), Self::Utf16Encode.name(), + Self::Utf16EncodeAsync.name(), Self::GlobalTextDecoderUtf8.name(), Self::GlobalTextEncoderUtf8.name(), Self::Utf8Encode.name(), + Self::Utf8EncodeAsync.name(), Self::ValidateGuestChar.name(), Self::ValidateHostChar.name(), ] @@ -43,9 +55,11 @@ impl StringIntrinsic { match self { Self::Utf16Decoder => "utf16Decoder", Self::Utf16Encode => "_utf16AllocateAndEncode", + Self::Utf16EncodeAsync => "_utf16AllocateAndEncodeAsync", Self::GlobalTextDecoderUtf8 => "TEXT_DECODER_UTF8", Self::GlobalTextEncoderUtf8 => "TEXT_ENCODER_UTF8", Self::Utf8Encode => "_utf8AllocateAndEncode", + Self::Utf8EncodeAsync => "_utf8AllocateAndEncodeAsync", Self::ValidateGuestChar => "validateGuestChar", Self::ValidateHostChar => "validateHostChar", } @@ -57,14 +71,19 @@ impl StringIntrinsic { match self { Self::Utf16Decoder => uwriteln!(output, "const {name} = new TextDecoder('utf-16');"), - Self::Utf16Encode => { + Self::Utf16Encode | Self::Utf16EncodeAsync => { let is_le = Intrinsic::IsLE.name(); + let (fn_preamble, realloc_call) = match self { + Self::Utf16Encode => ("", "realloc"), + Self::Utf16EncodeAsync => ("async ", "await realloc"), + _ => unreachable!("unexpected intrinsic"), + }; uwriteln!( output, r#" - function {name}(str, realloc, memory) {{ + {fn_preamble}function {name}(str, realloc, memory) {{ const len = str.length; - const ptr = realloc(0, 0, 2, len * 2); + const ptr = {realloc_call}(0, 0, 2, len * 2); const out = new Uint16Array(memory.buffer, ptr, len); let i = 0; if ({is_le}) {{ @@ -84,20 +103,26 @@ impl StringIntrinsic { Self::GlobalTextDecoderUtf8 => uwriteln!(output, "const {name} = new TextDecoder();"), Self::GlobalTextEncoderUtf8 => uwriteln!(output, "const {name} = new TextEncoder();"), - Self::Utf8Encode => { + Self::Utf8Encode | Self::Utf8EncodeAsync => { let encoder = Self::GlobalTextEncoderUtf8.name(); + let (fn_preamble, realloc_call) = match self { + Self::Utf8Encode => ("", "realloc"), + Self::Utf8EncodeAsync => ("async ", "await realloc"), + _ => unreachable!("unexpected intrinsic"), + }; uwriteln!( output, r#" - function {name}(s, realloc, memory) {{ + {fn_preamble}function {name}(s, realloc, memory) {{ if (typeof s !== 'string') {{ throw new TypeError('expected a string, received [' + typeof s + ']'); }} if (s.length === 0) {{ return {{ ptr: 1, len: 0 }}; }} let buf = {encoder}.encode(s); - let ptr = realloc(0, 0, 1, buf.length); + let ptr = {realloc_call}(0, 0, 1, buf.length); new Uint8Array(memory.buffer).set(buf, ptr); - return {{ ptr, len: buf.length, codepoints: [...s].length }}; + const res = {{ ptr, len: buf.length, codepoints: [...s].length }}; + return res; }} "# ); diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index e4bbf55eb..0a003487c 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -13,7 +13,7 @@ use wasmtime_environ::component::{ CoreDef, CoreExport, Export, ExportItem, FixedEncoding, GlobalInitializer, InstantiateModule, InterfaceType, LinearMemoryOptions, LoweredIndex, ResourceIndex, RuntimeComponentInstanceIndex, RuntimeImportIndex, RuntimeInstanceIndex, StaticModuleIndex, Trampoline, TrampolineIndex, - TypeDef, TypeFuncIndex, TypeResourceTableIndex, + TypeDef, TypeFuncIndex, TypeResourceTableIndex, TypeStreamTableIndex, }; use wasmtime_environ::component::{ ExportIndex, ExtractCallback, NameMap, NameMapNoIntern, Transcode, @@ -234,6 +234,14 @@ pub fn transpile_bindgen( ); bindgen.core_module_cnt = modules.len(); + // Generate mapping of stream tables to components that are related + let mut stream_tables = BTreeMap::new(); + for idx in 0..component.component.num_stream_tables { + let stream_table_idx = TypeStreamTableIndex::from_u32(idx as u32); + let stream_table_ty = &types[stream_table_idx]; + stream_tables.insert(stream_table_idx, stream_table_ty.instance); + } + // Bindings are generated when the `instantiate` method is called on the // Instantiator structure created below let mut instantiator = Instantiator { @@ -268,7 +276,7 @@ pub fn transpile_bindgen( exports_resource_types: Default::default(), resources_initialized: BTreeMap::new(), resource_tables_initialized: BTreeMap::new(), - init_host_async_import_lookup: BTreeMap::new(), + stream_tables, }; instantiator.sizes.fill(resolve); instantiator.initialize(); @@ -333,6 +341,8 @@ impl JsBindgen<'_> { "{local_name} = WebAssembly.promising({core_export_fn});", ); } else { + // TODO: run may be sync lifted, but it COULD call an async lowered function! + uwriteln!(core_exported_funcs, "{local_name} = {core_export_fn};",); } } @@ -586,14 +596,8 @@ struct Instantiator<'a, 'b> { lowering_options: PrimaryMap, - /// Temporary storage for async host lowered functions that must be wired during - /// component intializer processsing - /// - /// This lookup depends on initializer processing order -- it expects that - /// module instantiations come first, then are quickly followed by relevant import lowerings - /// such that the lookup is filled and emptied to bridge a module and the lowered imports it - /// uses. - init_host_async_import_lookup: BTreeMap, + // Mapping of stream table indices to component indices + stream_tables: BTreeMap, } impl<'a> ManagesIntrinsics for Instantiator<'a, '_> { @@ -751,6 +755,18 @@ impl<'a> Instantiator<'a, '_> { } } + // Set up global stream map, which is used by intrinsics like stream.transfer + let global_stream_table_map = + Intrinsic::AsyncStream(AsyncStreamIntrinsic::GlobalStreamTableMap).name(); + let rep_table_class = Intrinsic::RepTableClass.name(); + for (table_idx, component_idx) in self.stream_tables.iter() { + self.src.js.push_str(&format!( + "{global_stream_table_map}[{}] = {{ componentIdx: {}, table: new {rep_table_class}() }};\n", + table_idx.as_u32(), + component_idx.as_u32(), + )); + } + // Process global initializers // // The order of initialization is unfortunately quite fragile. @@ -1231,7 +1247,8 @@ impl<'a> Instantiator<'a, '_> { flat_count_js, lift_fn_js, lower_fn_js, - is_none_or_numeric_type_js, + is_none_js, + is_numeric_type_js, is_borrow_js, is_async_value_js, ) = match stream_ty.payload { @@ -1242,7 +1259,8 @@ impl<'a> Instantiator<'a, '_> { "0".into(), "null".into(), "null".into(), - "true".into(), + "true", + "false".into(), "false".into(), "false".into(), ), @@ -1267,6 +1285,7 @@ impl<'a> Instantiator<'a, '_> { &ty, &wasmtime_environ::component::StringEncoding::Utf8, ), + "false", format!( "{}", matches!( @@ -1300,7 +1319,8 @@ impl<'a> Instantiator<'a, '_> { liftFn: {lift_fn_js}, lowerFn: {lower_fn_js}, typeIdx: {stream_ty_idx_js}, - isNoneOrNumeric: {is_none_or_numeric_type_js}, + isNone: {is_none_js}, + isNumeric: {is_numeric_type_js}, isBorrowed: {is_borrow_js}, isAsyncValue: {is_async_value_js}, flatCount: {flat_count_js}, @@ -1339,9 +1359,13 @@ impl<'a> Instantiator<'a, '_> { unreachable!("missing/invalid data model for options during stream.read") }; let memory_idx = memory.expect("missing memory idx for stream.read").as_u32(); - let realloc_idx = realloc - .map(|v| v.as_u32().to_string()) - .unwrap_or_else(|| "null".into()); + let (realloc_idx, get_realloc_fn_js) = match realloc { + Some(v) => { + let v = v.as_u32().to_string(); + (v.to_string(), format!("() => realloc{v}")) + } + None => ("null".into(), "null".into()), + }; let component_instance_id = instance.as_u32(); let string_encoding = string_encoding_js_literal(string_encoding); @@ -1350,21 +1374,34 @@ impl<'a> Instantiator<'a, '_> { .bindgen .intrinsic(Intrinsic::AsyncStream(AsyncStreamIntrinsic::StreamRead)); + // PrepareCall for an async call is sometimes missing memories, + // so we augment and save here, knowing that any stream.write/read operation + // that uses a memory is indicative of that component's memory + let register_global_memory_for_component_fn = + Intrinsic::RegisterGlobalMemoryForComponent.name(); + uwriteln!( + self.src.js_init, + r#"{register_global_memory_for_component_fn}({{ + componentIdx: {component_instance_id}, + memory: memory{memory_idx}, + }});"# + ); + uwriteln!( self.src.js, - r#"const trampoline{i} = {stream_read_fn}.bind( + r#"const trampoline{i} = new WebAssembly.Suspending({stream_read_fn}.bind( null, {{ componentIdx: {component_instance_id}, memoryIdx: {memory_idx}, getMemoryFn: () => memory{memory_idx}, reallocIdx: {realloc_idx}, - getReallocFn: () => realloc{realloc_idx}, + getReallocFn: {get_realloc_fn_js}, stringEncoding: {string_encoding}, isAsync: {async_}, streamTableIdx: {stream_table_idx}, }} - ); + )); "#, ); } @@ -1405,7 +1442,7 @@ impl<'a> Instantiator<'a, '_> { let v = v.as_u32().to_string(); (v.to_string(), format!("() => realloc{v}")) } - None => ("null".into(), "null".into()), + None => ("null".into(), "() => null".into()), }; let string_encoding = string_encoding_js_literal(string_encoding); @@ -1414,6 +1451,19 @@ impl<'a> Instantiator<'a, '_> { .bindgen .intrinsic(Intrinsic::AsyncStream(AsyncStreamIntrinsic::StreamWrite)); + // PrepareCall for an async call is sometimes missing memories, + // so we augment and save here, knowing that any stream.write/read operation + // that uses a memory is indicative of that component's memory + let register_global_memory_for_component_fn = + Intrinsic::RegisterGlobalMemoryForComponent.name(); + uwriteln!( + self.src.js_init, + r#"{register_global_memory_for_component_fn}({{ + componentIdx: {component_instance_id}, + memory: memory{memory_idx}, + }});"# + ); + uwriteln!( self.src.js, r#" @@ -1434,52 +1484,56 @@ impl<'a> Instantiator<'a, '_> { ); } - Trampoline::StreamCancelRead { ty, async_, .. } => { - let stream_cancel_read_fn = self.bindgen.intrinsic(Intrinsic::AsyncStream( - AsyncStreamIntrinsic::StreamCancelRead, - )); - uwriteln!( - self.src.js, - "const trampoline{i} = {stream_cancel_read_fn}.bind(null, {stream_idx}, {async_});\n", - stream_idx = ty.as_u32(), - ); - } - - Trampoline::StreamCancelWrite { ty, async_, .. } => { - let stream_cancel_write_fn = self.bindgen.intrinsic(Intrinsic::AsyncStream( - AsyncStreamIntrinsic::StreamCancelWrite, - )); - uwriteln!( - self.src.js, - "const trampoline{i} = {stream_cancel_write_fn}.bind(null, {stream_idx}, {async_});\n", - stream_idx = ty.as_u32(), - ); + Trampoline::StreamCancelRead { + instance, + ty, + async_, } + | Trampoline::StreamCancelWrite { + instance, + ty, + async_, + } => { + let stream_cancel_fn = match trampoline { + Trampoline::StreamCancelRead { .. } => self.bindgen.intrinsic( + Intrinsic::AsyncStream(AsyncStreamIntrinsic::StreamCancelRead), + ), + Trampoline::StreamCancelWrite { .. } => self.bindgen.intrinsic( + Intrinsic::AsyncStream(AsyncStreamIntrinsic::StreamCancelWrite), + ), + _ => unreachable!("unexpected trampoline"), + }; - Trampoline::StreamDropReadable { ty, instance } => { - let stream_drop_readable_fn = self.bindgen.intrinsic(Intrinsic::AsyncStream( - AsyncStreamIntrinsic::StreamDropReadable, - )); - let stream_idx = ty.as_u32(); - let instance_idx = instance.as_u32(); + let stream_table_idx = ty.as_u32(); + let component_idx = instance.as_u32(); uwriteln!( self.src.js, - "const trampoline{i} = {stream_drop_readable_fn}.bind(null, {{ - streamTableIdx: {stream_idx}, - componentIdx: {instance_idx}, - }});\n", + r#" + const trampoline{i} = new WebAssembly.Suspending({stream_cancel_fn}.bind(null, {{ + streamTableIdx: {stream_table_idx}, + isAsync: {async_}, + componentIdx: {component_idx}, + }})); + "#, ); } - Trampoline::StreamDropWritable { ty, instance } => { - let stream_drop_writable_fn = self.bindgen.intrinsic(Intrinsic::AsyncStream( - AsyncStreamIntrinsic::StreamDropWritable, - )); + Trampoline::StreamDropReadable { ty, instance } + | Trampoline::StreamDropWritable { ty, instance } => { + let intrinsic_fn = match trampoline { + Trampoline::StreamDropReadable { .. } => self.bindgen.intrinsic( + Intrinsic::AsyncStream(AsyncStreamIntrinsic::StreamDropReadable), + ), + Trampoline::StreamDropWritable { .. } => self.bindgen.intrinsic( + Intrinsic::AsyncStream(AsyncStreamIntrinsic::StreamDropWritable), + ), + _ => unreachable!("unexpected trampoline"), + }; let stream_idx = ty.as_u32(); let instance_idx = instance.as_u32(); uwriteln!( self.src.js, - "const trampoline{i} = {stream_drop_writable_fn}.bind(null, {{ + "const trampoline{i} = {intrinsic_fn}.bind(null, {{ streamTableIdx: {stream_idx}, componentIdx: {instance_idx}, }});\n", @@ -1889,37 +1943,34 @@ impl<'a> Instantiator<'a, '_> { getPostReturnFn: () => {post_return_fn}, callbackIdx: {callback_idx}, getCallbackFn: () => {callback_fn}, - getCallee: () => {callback_fn}, }}, );", ); } Trampoline::LowerImport { - index, + index: _, lower_ty, options, } => { let lower_import_fn = self .bindgen .intrinsic(Intrinsic::AsyncTask(AsyncTaskIntrinsic::LowerImport)); - let global_component_lowers_class = self - .bindgen - .intrinsic(Intrinsic::GlobalComponentAsyncLowersClass); let canon_opts = self .component .options .get(*options) .expect("failed to find options"); - let fn_idx = index.as_u32(); + // TODO(fix): remove Global lowers, should enable using just exports[x] to export[y] call + // TODO(fix): promising for the run (*as well as exports*) + // TODO(fix): delete all asyncImports/exports + // TODO(opt): opt-in sync import let component_idx = canon_opts.instance.as_u32(); let is_async = canon_opts.async_; - let cancellable = canon_opts.cancellable; - // TODO: is there *anything* that connects this lower import trampoline - // and the earlier module instantiation?? + let cancellable = canon_opts.cancellable; let func_ty = self.types.index(*lower_ty); @@ -1973,38 +2024,45 @@ impl<'a> Instantiator<'a, '_> { memory_exprs.unwrap_or_else(|| ("null".into(), "() => null".into())); let realloc_expr_js = realloc_expr_js.unwrap_or_else(|| "() => null".into()); - // NOTE: we make this lowering trampoline identifiable by two things: - // - component idx - // - type index of exported function (in the relevant component) - // - // This is required to be able to wire up the [async-lower] import - // that will be put on the glue/shim module (via which the host wires up trampolines). - uwriteln!( - self.src.js, - r#" - {global_component_lowers_class}.define({{ - componentIdx: lowered_import_{fn_idx}_metadata.moduleIdx, - qualifiedImportFn: lowered_import_{fn_idx}_metadata.qualifiedImportFn, - fn: {lower_import_fn}.bind( - null, - {{ - trampolineIdx: {i}, - componentIdx: {component_idx}, - isAsync: {is_async}, - paramLiftFns: {param_lift_fns_js}, - metadata: lowered_import_{fn_idx}_metadata, - resultLowerFns: {result_lower_fns_js}, - getCallbackFn: {get_callback_fn_js}, - getPostReturnFn: {get_post_return_fn_js}, - isCancellable: {cancellable}, - memoryIdx: {memory_idx_js}, - getMemoryFn: {memory_expr_js}, - getReallocFn: {realloc_expr_js}, - }}, - ), - }}); - "#, + // Build the lower import call that will wrap the actual trampoline + let call = format!( + r#"{lower_import_fn}.bind( + null, + {{ + trampolineIdx: {i}, + componentIdx: {component_idx}, + isAsync: {is_async}, + isManualAsync: _trampoline{i}.manuallyAsync, + paramLiftFns: {param_lift_fns_js}, + resultLowerFns: {result_lower_fns_js}, + funcTypeIsAsync: {func_ty_async}, + getCallbackFn: {get_callback_fn_js}, + getPostReturnFn: {get_post_return_fn_js}, + isCancellable: {cancellable}, + memoryIdx: {memory_idx_js}, + getMemoryFn: {memory_expr_js}, + getReallocFn: {realloc_expr_js}, + importFn: _trampoline{i}, + }}, + )"#, + func_ty_async = func_ty.async_, ); + + // NOTE: For Trampoline::LowerImport, the trampoline index is actually already defined, + // but we *redefine* it to call the lower import function first. + if is_async { + uwriteln!( + self.src.js, + "const trampoline{i} = new WebAssembly.Suspending({call});" + ); + } else { + // TODO(breaking): once manually specifying async imports is removed, + // we can avoid the second check below. + uwriteln!( + self.src.js, + "const trampoline{i} = _trampoline{i}.manuallyAsync ? new WebAssembly.Suspending({call}) : {call};" + ); + } } Trampoline::Transcoder { @@ -2238,23 +2296,35 @@ impl<'a> Instantiator<'a, '_> { ); } - Trampoline::ContextSet { slot, .. } => { + Trampoline::ContextSet { instance, slot, .. } => { let context_set_fn = self .bindgen .intrinsic(Intrinsic::AsyncTask(AsyncTaskIntrinsic::ContextSet)); + let component_idx = instance.as_u32(); uwriteln!( self.src.js, - "const trampoline{i} = {context_set_fn}.bind(null, {slot});" + r#" + const trampoline{i} = {context_set_fn}.bind(null, {{ + componentIdx: {component_idx}, + slot: {slot}, + }}); + "# ); } - Trampoline::ContextGet { slot, .. } => { + Trampoline::ContextGet { instance, slot } => { let context_get_fn = self .bindgen .intrinsic(Intrinsic::AsyncTask(AsyncTaskIntrinsic::ContextGet)); + let component_idx = instance.as_u32(); uwriteln!( self.src.js, - "const trampoline{i} = {context_get_fn}.bind(null, {slot});" + r#" + const trampoline{i} = {context_get_fn}.bind(null, {{ + componentIdx: {component_idx}, + slot: {slot}, + }}); + "# ); } @@ -2330,6 +2400,22 @@ impl<'a> Instantiator<'a, '_> { } let lift_fns_js = format!("[{}]", lift_fns.join(",")); + // Build up a list of all the lowering functions that will be needed for the types + // that are actually being passed through task.return + // + // This is usually only necessary if this task is part of a guest->guest async call + // (i.e. via prepare & async start call) + let mut lower_fns: Vec = Vec::with_capacity(result_types.len()); + for result_ty in result_types { + lower_fns.push(gen_flat_lower_fn_js_expr( + self.bindgen, + self.types, + result_ty, + &canon_opts.string_encoding, + )); + } + let lower_fns_js = format!("[{}]", lower_fns.join(",")); + let get_memory_fn_js = memory .map(|idx| format!("() => memory{}", idx.as_u32())) .unwrap_or_else(|| "() => null".into()); @@ -2355,6 +2441,7 @@ impl<'a> Instantiator<'a, '_> { memoryIdx: {memory_idx_js}, callbackFnIdx: {callback_fn_idx}, liftFns: {lift_fns_js}, + lowerFns: {lower_fns_js}, }}, );", ); @@ -2411,7 +2498,9 @@ impl<'a> Instantiator<'a, '_> { ); uwriteln!( self.src.js, - "const trampoline{i} = {enter_symmetric_sync_guest_call_fn};\n", + r#" + const trampoline{i} = {enter_symmetric_sync_guest_call_fn}; + "#, ); } @@ -2451,7 +2540,10 @@ impl<'a> Instantiator<'a, '_> { // every callback *could* do stream.write, but many may not. uwriteln!( self.src.js_init, - "callback_{callback_idx} = WebAssembly.promising({core_def});" + r#" + callback_{callback_idx} = WebAssembly.promising({core_def}); + callback_{callback_idx}.fnName = "{core_def}"; + "# ); } @@ -2466,74 +2558,59 @@ impl<'a> Instantiator<'a, '_> { }, GlobalInitializer::LowerImport { index, import } => { - let fn_idx = index.as_u32(); - let (import_index, path) = &self.component.imports[*import]; - let (import_iface, _type_def) = &self.component.import_types[*import_index]; - - let qualified_import_fn = match &path[..] { - // Likely a bare name like `[async]foo` which becomes 'foo' - [] => format!("$root#{}", import_iface.trim_start_matches("[async]")), - // Fully qualified function name `ns:pkg/iface#[async]foo` which becomes `ns:pkg/iface#foo` - [fn_name] => { - format!("{import_iface}#{}", fn_name.trim_start_matches("[async]")) - } - _ => unimplemented!( - "multi-part import paths ({path:?}) not supported (iface: '{import_iface}') -- only single function names are allowed" - ), - }; - - let maybe_module_idx = self - .init_host_async_import_lookup - .remove(&qualified_import_fn) - .map(|x| x.as_u32().to_string()) - .unwrap_or_else(|| "null".into()); - - uwriteln!( - self.src.js, - r#" - let lowered_import_{fn_idx}_metadata = {{ - qualifiedImportFn: '{qualified_import_fn}', - moduleIdx: {maybe_module_idx}, - }}; - "#, - ); - self.lower_import(*index, *import); } GlobalInitializer::ExtractMemory(m) => { - let component_idx = m.export.instance.as_u32(); let def = self.core_export_var_name(&m.export); let idx = m.index.as_u32(); - let global_component_memories_class = - Intrinsic::GlobalComponentMemoriesClass.name(); uwriteln!(self.src.js, "let memory{idx};"); uwriteln!(self.src.js_init, "memory{idx} = {def};"); - uwriteln!( - self.src.js_init, - "{global_component_memories_class}.save({{ idx: {idx}, componentIdx: {component_idx}, memory: memory{idx} }});" - ); } GlobalInitializer::ExtractRealloc(r) => { let def = self.core_def(&r.def); let idx = r.index.as_u32(); uwriteln!(self.src.js, "let realloc{idx};"); + uwriteln!(self.src.js, "let realloc{idx}Async;"); uwriteln!(self.src.js_init, "realloc{idx} = {def};",); + // NOTE: sometimes we may be fed a realloc that isn't a webassembly function at all + // but has instead been converted to JS (see 'flavorful' test in test/runtime.js') + uwriteln!( + self.src.js_init, + r#" + try {{ + realloc{idx}Async = WebAssembly.promising({def}); + }} catch(err) {{ + realloc{idx}Async = {def}; + }} + "# + ); } GlobalInitializer::ExtractPostReturn(p) => { let def = self.core_def(&p.def); let idx = p.index.as_u32(); uwriteln!(self.src.js, "let postReturn{idx};"); + uwriteln!(self.src.js, "let postReturn{idx}Async;"); uwriteln!(self.src.js_init, "postReturn{idx} = {def};"); + // NOTE: sometimes we may be fed a post return fn that isn't a webassembly function + // at all but has instead been converted to JS (see 'flavorful' test in test/runtime.js) + uwriteln!( + self.src.js_init, + r#" + try {{ + postReturn{idx}Async = WebAssembly.promising({def}); + }} catch(err) {{ + postReturn{idx}Async = {def}; + }} + "# + ); } GlobalInitializer::Resource(_) => {} - GlobalInitializer::ExtractTable(extract_table) => { - let _ = extract_table; - } + GlobalInitializer::ExtractTable(_) => {} } } @@ -2544,93 +2621,7 @@ impl<'a> Instantiator<'a, '_> { // differences between Wasmtime's and JS's embedding API. let mut import_obj = BTreeMap::new(); for (module, name, arg) in self.modules[module_idx].imports(args) { - // By default, async-lower gets patched to the relevant export function directly, - // but it *should* be patched to the lower import intrinsic, as subtask rep/state - // needs to be returned to the calling component. - // - // The LowerImport intrinsic will take care of calling the original export, - // which will likely trigger an `Instruction::CallInterface for the actual import - // that was called. - // - let def = if name.starts_with("[async-lower]") - && let core::AugmentedImport::CoreDef(CoreDef::Export(CoreExport { - item: ExportItem::Index(EntityIndex::Function(_)), - .. - })) = arg - { - let global_lowers_class = Intrinsic::GlobalComponentAsyncLowersClass.name(); - let key = name - .trim_start_matches("[async-lower]") - .trim_start_matches("[async]"); - - // For host imports that are asynchronous, we must wrap with WebAssembly.promising, - // as the callee will be a host function (that will suspend). - // - // Note that there is also a separate case where the import *is* from another component, - // and we do not want to call that as a promise, because it will return an async return code - // and follow the normal p3 async component call pattern. - // - let full_async_import_key = format!("{module}#{key}"); - - let mut exec_fn = self.augmented_import_def(&arg); - - // NOTE: Regardless of whether we're using a host import or not, we are likely *not* - // actually wrapping a host javascript function below -- rather, the host import is - // called via a trip a trip *through* an exporting component table. - // - // The host import is imported, passed through an indirect call in a component, - // (see $wit-component.shims, $wit-component.fixups modules in generated transpile output) - // then it is called from the component that is actually performing the call.. - // - let mut import_js = format!( - "{global_lowers_class}.lookup({}, '{full_async_import_key}')?.bind(null, {exec_fn})", - module_idx.as_u32(), - ); - - let is_known_iface_import = self.async_imports.contains(&full_async_import_key); - let is_known_root_import = self - .async_imports - .contains(full_async_import_key.trim_start_matches("$root#")); - let is_known_import = is_known_iface_import || is_known_root_import; - let is_stream_op = full_async_import_key.contains("[stream-write-") - || full_async_import_key.contains("[stream-read-"); - - if is_known_import { - // For root imports or imports that don't need any special handling, - // we can look up and use the async import function after saving the import key for later - - // Save the import for later to match it with it's lower import initializer - self.init_host_async_import_lookup - .insert(full_async_import_key.clone(), module_idx); - exec_fn = format!("WebAssembly.promising({exec_fn})"); - import_js = format!( - "{global_lowers_class}.lookup({}, '{full_async_import_key}')?.bind(null, {exec_fn})", - module_idx.as_u32(), - ); - } else if is_stream_op { - // stream.{write,read} are always handled by async functions when patched - // - // We must treat them like functions that call host async imports because - // the call that will do the stream operation is piped through a patchup component - // - // wasm export -> wasm import -> host-provided trampoline - // - // In this case, we know that the trampoline that eventually gets called - // will be an async host function (`streamWrite`/`streamRead`), which will - // be wrapped in `WebAssembly.Suspending` (as the terminal fn is known, no need to save it) - exec_fn = format!("WebAssembly.promising({exec_fn})"); - import_js = format!( - "new WebAssembly.Suspending({global_lowers_class}.lookup({}, '{full_async_import_key}').bind(null, {exec_fn}))", - module_idx.as_u32(), - ); - } - - import_js - } else { - // All other imports can be connected directly to the relevant export - self.augmented_import_def(&arg) - }; - + let def = self.augmented_import_def(&arg); let dst = import_obj.entry(module).or_insert(BTreeMap::new()); let prev = dst.insert(name, def); assert!( @@ -2898,33 +2889,38 @@ impl<'a> Instantiator<'a, '_> { .params .len(); - // Generate the JS trampoline function + // Generate the JS trampoline function for a bound import let trampoline_idx = trampoline.as_u32(); match self.bindgen.opts.import_bindings { None | Some(BindingsMode::Js) | Some(BindingsMode::Hybrid) => { - // Write out function declaration start - if requires_async_porcelain || is_async { - // If an import is either an async host import (i.e. JSPI powered) - // or a guest async lifted import from another component, - // use WebAssembly.Suspending to allow suspending this component + // TODO(breaking): remove as we do not not need to manually specify async imports anymore in P3 w/ native coloring + if is_async | requires_async_porcelain { + // NOTE: for async imports that will go through Trampoline::LowerImport, + // we prefix the raw import with '_' as it will later be used in the + // definition of trampoline{i} which will actually be fed into + // unbundled modules uwrite!( self.src.js, - "\nconst trampoline{trampoline_idx} = new WebAssembly.Suspending(async function", + "\nconst _trampoline{trampoline_idx} = async function" ); } else { - // If the import is not async in any way, use a regular trampoline - uwrite!(self.src.js, "\nfunction trampoline{trampoline_idx}"); + uwrite!( + self.src.js, + "\nconst _trampoline{trampoline_idx} = function" + ); } + let iface_name = if import_name.is_empty() { + None + } else { + Some(import_name.to_string()) + }; + // Write out the function (brace + body + brace) self.bindgen(JsFunctionBindgenArgs { nparams, call_type, - iface_name: if import_name.is_empty() { - None - } else { - Some(import_name) - }, + iface_name: iface_name.as_deref(), callee: &callee_name, opts: options, func, @@ -2935,11 +2931,18 @@ impl<'a> Instantiator<'a, '_> { }); uwriteln!(self.src.js, ""); - // Write new function ending - if requires_async_porcelain | is_async { - uwriteln!(self.src.js, ");"); - } else { - uwriteln!(self.src.js, ""); + uwriteln!( + self.src.js, + "_trampoline{trampoline_idx}.fnName = '{}#{callee_name}';", + iface_name.unwrap_or_default(), + ); + + // TODO(breaking): remove once support for manually specified async imports is removed + if requires_async_porcelain { + uwriteln!( + self.src.js, + "_trampoline{trampoline_idx}.manuallyAsync = true;" + ); } } @@ -3067,22 +3070,9 @@ 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 => ( - // 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::Freestanding | FunctionKind::AsyncFreestanding => { + (func_name.to_lower_camel_case(), callee_name) + } FunctionKind::Method(tid) | FunctionKind::AsyncMethod(tid) @@ -3356,7 +3346,7 @@ impl<'a> Instantiator<'a, '_> { } /// Connect resources that are defined at the type levels in `wit-parser` - /// to their types as defined in `wamstime-environ` + /// to their types as defined in `wasmtime-environ` /// /// The types that are connected here are stored in the `resource_map` for /// use later. @@ -3569,15 +3559,33 @@ impl<'a> Instantiator<'a, '_> { { ( memory.map(|idx| format!("memory{}", idx.as_u32())), - realloc.map(|idx| format!("realloc{}", idx.as_u32())), + realloc.map(|idx| { + format!( + "realloc{}{}", + idx.as_u32(), + if is_async { + "Async" + } else { + Default::default() + } + ) + }), ) } else { (None, None) }; - let post_return = opts - .post_return - .map(|idx| format!("postReturn{}", idx.as_u32())); + let post_return = opts.post_return.map(|idx| { + format!( + "postReturn{}{}", + idx.as_u32(), + if is_async { + "Async" + } else { + Default::default() + } + ) + }); let tracing_prefix = format!( "[iface=\"{}\", function=\"{}\"]", @@ -4058,13 +4066,10 @@ 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)); + // TODO(breaking): remove requires_async_porcelain once support + // for manual async import specification is removed, as p3 has + // built-in function async coloring + .push((core_export_fn.clone(), is_async | requires_async_porcelain)); local_name } }; @@ -4075,72 +4080,6 @@ impl<'a> Instantiator<'a, '_> { Some(export_name) }; - // TODO: re-enable when needed for more complex objects - // - // // Output the lift and lowering functions for the fn - // // - // // We do this here mostly to avoid introducing a breaking change at the FunctionBindgen - // // level, and to encourage re-use of param lowering. - // // - // // TODO(breaking): pass `Function` reference along to `FunctionBindgen` and generate *and* lookup lower there - // // - // // This function will be called early in execution of generated functions that correspond - // // to an async export, to lower parameters that were passed by JS into the component's memory. - // // - // if is_async { - // let component_idx = options.instance.as_u32(); - // let global_async_param_lower_class = { - // self.add_intrinsic(Intrinsic::GlobalAsyncParamLowersClass); - // Intrinsic::GlobalAsyncParamLowersClass.name() - // }; - - // // Get the interface-level types for the parameters to the function - // // in case we need to do async lowering - // let func_ty = &self.types[*func_ty_idx]; - // let param_types = func_ty.params; - // let func_param_iface_types = &self.types[param_types].types; - - // // Generate lowering functions for params - // let lower_fns_list_js = gen_flat_lower_fn_list_js_expr( - // self, - // self.types, - // func_param_iface_types, - // &options.string_encoding, - // ); - - // // TODO(fix): add tests for indirect param (> 16 args) - // // - // // We *should* be able to just jump to the memory location in question and then - // // start doing the reading. - // self.src.js(&format!( - // r#" - // {global_async_param_lower_class}.define({{ - // componentIdx: '{component_idx}', - // iface: '{iface}', - // fnName: '{callee}', - // fn: (args) => {{ - // const {{ loweringParams, vals, memory, indirect }} = args; - // if (!memory) {{ throw new TypeError("missing memory for async param lower"); }} - // if (indirect === undefined) {{ throw new TypeError("missing memory for async param lower"); }} - // if (indirect) {{ throw new Error("indirect aysnc param lowers not yet supported"); }} - - // const lowerFns = {lower_fns_list_js}; - // const lowerCtx = {{ - // params: loweringParams, - // vals, - // memory, - // componentIdx: {component_idx}, - // useDirectParams: !indirect, - // }}; - - // for (const lowerFn of lowerFns) {{ lowerFn(lowerCtx); }} - // }}, - // }}); - // "#, - // iface = iface_name.map(|v| v.as_str()).unwrap_or( "$root"), - // )); - // } - // Write function preamble (everything up to the `(` in `function (...`) match func.kind { FunctionKind::Freestanding => { diff --git a/packages/jco/src/cmd/transpile.js b/packages/jco/src/cmd/transpile.js index c4950caa9..421a3ae07 100644 --- a/packages/jco/src/cmd/transpile.js +++ b/packages/jco/src/cmd/transpile.js @@ -1,9 +1,9 @@ /* global Buffer */ import { extname, basename, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; import { minify } from "terser"; -import { fileURLToPath } from "node:url"; import { optimizeComponent } from "./opt.js"; diff --git a/packages/jco/test/api.js b/packages/jco/test/api.js index fab21c123..773199cc7 100644 --- a/packages/jco/test/api.js +++ b/packages/jco/test/api.js @@ -2,7 +2,7 @@ import { fileURLToPath } from "node:url"; import { platform } from "node:process"; import { readFile } from "node:fs/promises"; -import { suite, test, assert, beforeAll } from "vitest"; +import { suite, test, assert, beforeAll, expect } from "vitest"; import { transpile, @@ -23,7 +23,8 @@ const isWindows = platform === "win32"; // - (2025/02/04) increased due to incoming implementations of async and new flush impl // - (2025/08/07) increased due to async task implementations, refactors // - (2025/12/16) increased due to more async task impl -const FLAVORFUL_WASM_TRANSPILED_CODE_CHAR_LIMIT = 100_000; +// - (2026/03/09) increased due to more async task impl +const FLAVORFUL_WASM_TRANSPILED_CODE_CHAR_LIMIT = 150_000; suite("API", () => { let flavorfulWasmBytes; @@ -239,6 +240,6 @@ suite("API", () => { assert.strictEqual(imports.length, 4); assert.strictEqual(exports.length, 3); assert.deepStrictEqual(exports[0], ["test", "instance"]); - assert.ok(files[name + ".js"].length < FLAVORFUL_WASM_TRANSPILED_CODE_CHAR_LIMIT); + expect(files[name + ".js"].length).lessThan(FLAVORFUL_WASM_TRANSPILED_CODE_CHAR_LIMIT); }); }); diff --git a/packages/jco/test/fixtures/modules/hello_stdout.component.wasm b/packages/jco/test/fixtures/modules/hello_stdout.component.wasm index d96692d1a..aa020ac24 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/jspi.js b/packages/jco/test/jspi.js index 3855b20ad..e24ff52be 100644 --- a/packages/jco/test/jspi.js +++ b/packages/jco/test/jspi.js @@ -101,8 +101,9 @@ suite("Host Import Async (JSPI)", () => { } const tmpDir = await getTmpDir(); - const outDir = resolve(tmpDir, "out-component-dir"); + const outputDir = resolve(tmpDir, "out-component-dir"); const outFile = resolve(tmpDir, "out-component-file"); + await mkdir(outputDir, { recursive: true }); const modulesDir = resolve(tmpDir, "node_modules", "@bytecodealliance"); await mkdir(modulesDir, { recursive: true }); @@ -116,6 +117,7 @@ suite("Host Import Async (JSPI)", () => { asyncMode: "jspi", component: { name: "async_call", + outputDir, path: resolve("test/fixtures/components/simple-nested.component.wasm"), imports: { "calvinrp:test-async-funcs/hello": { @@ -126,6 +128,7 @@ suite("Host Import Async (JSPI)", () => { jco: { transpile: { extraArgs: { + // minify: false, asyncImports: ["calvinrp:test-async-funcs/hello#hello-world"], asyncExports: ["hello-world"], }, @@ -143,7 +146,7 @@ suite("Host Import Async (JSPI)", () => { await cleanup(); try { - await rm(outDir, { recursive: true }); + await rm(outputDir, { recursive: true }); await rm(outFile); } catch {} }); 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 index 926f491d9..a9cb1c5bb 100644 --- a/packages/jco/test/p3/ported/wasmtime/component-async/post-return.js +++ b/packages/jco/test/p3/ported/wasmtime/component-async/post-return.js @@ -1,6 +1,6 @@ import { join } from "node:path"; -import { suite, test, expect, vi } from "vitest"; +import { suite, test, assert, expect, vi } from "vitest"; import { buildAndTranspile, composeCallerCallee, COMPONENT_FIXTURES_DIR } from "./common.js"; @@ -108,7 +108,9 @@ suite("post-return async sleep scenario", () => { } }); - test("caller & callee", async () => { + // NOTE: this test was changed upstream + // see: https://github.com/bytecodealliance/wasmtime/pull/12567 + test.only("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({ @@ -117,9 +119,15 @@ suite("post-return async sleep scenario", () => { }); const waitTimeMs = 300; + const observedValues = []; + const expectedValues = [BigInt(waitTimeMs * 2), BigInt(waitTimeMs * 2), BigInt(waitTimeMs)]; + + // NOTE: sleepMillis is called three times -- twice by the guest then once by the host const sleepMillis = vi.fn(async (ms) => { // NOTE: as written, the caller/callee manipulate (double) the original wait time before use - expect(ms).toStrictEqual(BigInt(waitTimeMs * 2)); + assert(expectedValues.includes(ms)); + observedValues.push(ms); + 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"); } @@ -169,6 +177,9 @@ suite("post-return async sleep scenario", () => { // that occurred during it to run to completion (and eventually call the import we provided), // in the runtime itself. await vi.waitFor(() => expect(sleepMillis).toHaveBeenCalled(), { timeout: 5_000 }); + + assert.lengthOf(observedValues, 3, "sleepMillis was not called three times"); + assert.sameMembers(observedValues, expectedValues, "all expected values were not observed"); } finally { if (cleanup) { await cleanup(); diff --git a/packages/jco/test/p3/stream.js b/packages/jco/test/p3/stream.js index cd717a14b..c8345eedc 100644 --- a/packages/jco/test/p3/stream.js +++ b/packages/jco/test/p3/stream.js @@ -1,6 +1,6 @@ import { join } from "node:path"; -import { suite, test, assert, expect } from "vitest"; +import { suite, test, assert, expect, vi } from "vitest"; import { setupAsyncTest } from "../helpers.js"; import { AsyncFunction, LOCAL_TEST_COMPONENTS_DIR } from "../common.js"; @@ -18,7 +18,7 @@ suite("Stream (WASI P3)", () => { jco: { transpile: { extraArgs: { - // minify: false, + minify: false, asyncExports: [ "jco:test-components/get-stream-async#get-stream-u32", "jco:test-components/get-stream-async#get-stream-s32", @@ -57,30 +57,19 @@ suite("Stream (WASI P3)", () => { vals = [11, 22, 33]; stream = await instance["jco:test-components/get-stream-async"].getStreamU32(vals); - assert.equal(vals[0], await stream.next(), "first u32 read is incorrect"); - assert.equal(vals[1], await stream.next(), "second u32 read is incorrect"); - assert.equal(vals[2], await stream.next(), "third u32 read is incorrect"); + await checkStreamValues(stream, vals, "u32"); - // TODO(fix): re-enable this test, once we wait for writes and reject after drop()/closure of writer - // - // // The fourth read should error, as the writer should have been dropped after writing three values. - // // - // // If the writer is dropped while the host attempts a read, the reader should error - // await expect(vi.waitUntil( - // async () => { - // await stream.next(); - // return true; // we should never get here, as an error should occur - // }, - // { timeout: 500, interval: 0 }, - // )).rejects.toThrowError(/dropped/); + vals = [true, false]; + stream = await instance["jco:test-components/get-stream-async"].getStreamBool(vals); + await checkStreamValues(stream, vals, "u32"); vals = [-11, -22, -33]; stream = await instance["jco:test-components/get-stream-async"].getStreamS32(vals); - assert.equal(vals[0], await stream.next()); - assert.equal(vals[1], await stream.next()); - assert.equal(vals[2], await stream.next()); + await checkStreamValues(stream, vals, "u32"); // TODO(fix): attempting to stream non-null/numeric values traps, and the component cannot currently recover + // TODO(fix): this is *not* a inter-component non-null/numeric value send! + // // vals = [true]; // stream = await instance["jco:test-components/get-stream-async"].getStreamBool(vals); // await expect(() => stream.next()).rejects.toThrowError(/cannot stream non-numeric types within the same component/); @@ -102,32 +91,26 @@ suite("Stream (WASI P3)", () => { // assert.equal(vals[3], await stream.next()); // assert.equal(vals[4], await stream.next()); + // TODO(fix): we're stuck waiting for the previous task to complete?? It's waiting for waitable set? + // read end needs to be dropped at some point, and it's not! vals = [0, 100, 65535]; stream = await instance["jco:test-components/get-stream-async"].getStreamU16(vals); - assert.equal(vals[0], await stream.next()); - assert.equal(vals[1], await stream.next()); - assert.equal(vals[2], await stream.next()); + await checkStreamValues(stream, vals, "u32"); // TODO(fix): under/overflowing values hang vals = [-32_768, 0, 32_767]; stream = await instance["jco:test-components/get-stream-async"].getStreamS16(vals); - assert.equal(vals[0], await stream.next()); - assert.equal(vals[1], await stream.next()); - assert.equal(vals[2], await stream.next()); + await checkStreamValues(stream, vals, "u32"); // TODO(fix): under/overflowing values hang vals = [0n, 100n, 65535n]; stream = await instance["jco:test-components/get-stream-async"].getStreamU64(vals); - assert.equal(vals[0], await stream.next()); - assert.equal(vals[1], await stream.next()); - assert.equal(vals[2], await stream.next()); + await checkStreamValues(stream, vals, "u32"); // TODO(fix): under/overflowing values hang vals = [-32_768n, 0n, 32_767n]; stream = await instance["jco:test-components/get-stream-async"].getStreamS64(vals); - assert.equal(vals[0], await stream.next()); - assert.equal(vals[1], await stream.next()); - assert.equal(vals[2], await stream.next()); + await checkStreamValues(stream, vals, "u32"); // TODO(fix): under/overflowing values hang // // TODO(fix): add better edge cases @@ -154,41 +137,25 @@ suite("Stream (WASI P3)", () => { await cleanup(); }); +}); - test("stream (tx, traps)", async () => { - const name = "stream-tx"; - const { esModule, cleanup } = await setupAsyncTest({ - asyncMode: "jspi", - component: { - name, - path: join(LOCAL_TEST_COMPONENTS_DIR, `${name}.wasm`), - skipInstantiation: true, - }, - jco: { - transpile: { - extraArgs: { - // minify: false, - asyncExports: ["jco:test-components/get-stream-async#get-stream-bool"], - }, - }, +async function checkStreamValues(stream, vals, typeName) { + for (const [idx, expected] of vals.entries()) { + assert.equal(expected, await stream.next(), `${typeName} [${idx}] read is incorrect`); + } + + // If we get this far, the fourth read will do one of the following: + // - time out (hung during wait for writer that will never come) + // - report write end was dropped during the read (guest finished writing) + // - read end is fully closed after write + // + await expect( + vi.waitUntil( + async () => { + await stream.next(); + return true; // we should never get here, as we s error should occur }, - }); - - const { WASIShim } = await import("@bytecodealliance/preview2-shim/instantiation"); - const instance = await esModule.instantiate(undefined, new WASIShim().getImportObject()); - - let vals; - let stream; - - assert.instanceOf(instance["jco:test-components/get-stream-async"].getStreamBool, AsyncFunction); - - // TODO(fix): fix this should *not* trap, as the buffer is ont he host side/already passed outside the component - vals = [true]; - stream = await instance["jco:test-components/get-stream-async"].getStreamBool(vals); - await expect(() => stream.next()).rejects.toThrowError( - /cannot stream non-numeric types within the same component/, - ); - - await cleanup(); - }); -}); + { timeout: 500, interval: 0 }, + ), + ).rejects.toThrowError(/timed out|read end is closed|write end dropped during read/i); +} diff --git a/packages/jco/test/runtime.js b/packages/jco/test/runtime.js index 299cfe0f4..4f6b21897 100644 --- a/packages/jco/test/runtime.js +++ b/packages/jco/test/runtime.js @@ -34,6 +34,7 @@ const CODEGEN_TRANSPILE_DEPS = { resource_borrow_simple: ["resource_borrow_simple/resource_borrow_simple.js"], }; +// NOTE: if you find this test failing despite code changes, you may need to clear the test/output folder suite("Runtime", async () => { await tsGenerationPromise(); @@ -62,7 +63,6 @@ suite("Runtime", async () => { for (const [fixtureName, testName] of runtimes) { test.concurrent(testName, async () => { // Perform transpilation on deps where necessary - if (CODEGEN_TRANSPILE_DEPS[testName]) { for (const filename of CODEGEN_TRANSPILE_DEPS[testName]) { // Skip files that have already been generated diff --git a/packages/jco/test/vitest.lts.ts b/packages/jco/test/vitest.lts.ts index 736319dbd..927c10296 100644 --- a/packages/jco/test/vitest.lts.ts +++ b/packages/jco/test/vitest.lts.ts @@ -2,7 +2,7 @@ import { availableParallelism } from "node:os"; import { defineConfig } from "vitest/config"; -const DEFAULT_TIMEOUT_MS = 1000 * 60 * 10; // 10m +const DEFAULT_TIMEOUT_MS = 1000 * 60 * 1; // 1m const REPORTERS = process.env.GITHUB_ACTIONS ? ["verbose", "github-actions"] : ["verbose"]; diff --git a/packages/jco/test/vitest.ts b/packages/jco/test/vitest.ts index 41716f505..5a88fafee 100644 --- a/packages/jco/test/vitest.ts +++ b/packages/jco/test/vitest.ts @@ -2,7 +2,7 @@ import { availableParallelism } from "node:os"; import { defineConfig } from "vitest/config"; -const DEFAULT_TIMEOUT_MS = 1000 * 60 * 10; // 10m +const DEFAULT_TIMEOUT_MS = 1000 * 60 * 1; // 1m const REPORTERS = process.env.GITHUB_ACTIONS ? ["verbose", "github-actions"] : ["verbose"];