From 3639126e2e898360e8d4d8b210c4081deaf90c94 Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Fri, 27 Feb 2026 08:44:54 +0900 Subject: [PATCH 01/15] fix(bindgen): host read on stream, determinism, expand stream tests This commit fixes some issues with host reads on streams to ensure that reads initiated by hosts automatically retry after being blocked, fixes some bugs that were exposed by determinism changes. This commit does not yet *fully* enumerate the types we want to test under streams, but re-enabling tests from here should be dramatically more straight forward. --- .../src/function_bindgen.rs | 10 +- .../src/intrinsics/component.rs | 4 +- .../src/intrinsics/mod.rs | 52 +-- .../src/intrinsics/p3/async_future.rs | 10 +- .../src/intrinsics/p3/async_stream.rs | 368 ++++++++++++++---- .../src/intrinsics/p3/async_task.rs | 43 +- .../src/intrinsics/p3/host.rs | 11 +- .../src/intrinsics/p3/waitable.rs | 52 ++- .../src/transpile_bindgen.rs | 16 + .../modules/hello_stdout.component.wasm | Bin 2167250 -> 2167250 bytes .../wasmtime/component-async/post-return.js | 24 +- packages/jco/test/p3/stream.js | 103 ++--- 12 files changed, 474 insertions(+), 219 deletions(-) diff --git a/crates/js-component-bindgen/src/function_bindgen.rs b/crates/js-component-bindgen/src/function_bindgen.rs index 1b7a1af3b..22eb52773 100644 --- a/crates/js-component-bindgen/src/function_bindgen.rs +++ b/crates/js-component-bindgen/src/function_bindgen.rs @@ -377,7 +377,7 @@ impl FunctionBindgen<'_> { .map(mt => mt.task) .filter(t => !t.getParentSubtask()) .map(t => t.exitPromise()); - await Promise.all(taskPromises); + await Promise.allSettled(taskPromises); }} "#, ); @@ -1530,7 +1530,13 @@ impl Bindgen for FunctionBindgen<'_> { if (isHostAsyncImport) {{ subtask = parentTask.getLatestSubtask(); if (!subtask) {{ - throw new Error("Missing subtask for host import, has the import been lowered? (ensure asyncImports are set properly)"); + console.log("MISSING SUBTASK", {{ + entryFnName: '{fn_name}', + parentTask, + parentTaskID: parentTask.id(), + parentTaskComponent: parentTask.componentIdx(), + }}); + throw new Error(`Missing subtask (in parent task [${{parentTask.id()}}]) for async host import, has the import been lowered? (ensure asyncImports are set properly)`); }} subtask.setChildTask(task); task.setParentSubtask(subtask); diff --git a/crates/js-component-bindgen/src/intrinsics/component.rs b/crates/js-component-bindgen/src/intrinsics/component.rs index ffb309e8b..ecb7be196 100644 --- a/crates/js-component-bindgen/src/intrinsics/component.rs +++ b/crates/js-component-bindgen/src/intrinsics/component.rs @@ -301,7 +301,9 @@ 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() {{ diff --git a/crates/js-component-bindgen/src/intrinsics/mod.rs b/crates/js-component-bindgen/src/intrinsics/mod.rs index b38497dfe..0df09ca66 100644 --- a/crates/js-component-bindgen/src/intrinsics/mod.rs +++ b/crates/js-component-bindgen/src/intrinsics/mod.rs @@ -124,9 +124,6 @@ pub enum Intrinsic { /// 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, @@ -514,10 +511,12 @@ impl Intrinsic { #start; #ptr; #capacity; - #copied = 0; + #processed = 0; #data; // initial data (only filled out for host-owned) + target; + constructor(args) {{ if (args.capacity >= {managed_buffer_class}.MAX_LENGTH) {{ throw new Error(`buffer size [${{args.capacity}}] greater than max length`); @@ -547,11 +546,16 @@ impl Intrinsic { this.#capacity = args.capacity; this.#elemMeta = args.elemMeta; this.#data = args.data; + this.target = args.target; }} + setTarget(tgt) {{ this.target = tgt; }} + capacity() {{ return this.#capacity; }} - remainingCapacity() {{ return this.#capacity - this.#copied; }} - copied() {{ return this.#copied; }} + remaining() {{ return this.#capacity - this.#processed; }} + processed() {{ return this.#processed; }} + + componentIdx() {{ return this.#componentIdx; }} getElemMeta() {{ return this.#elemMeta; }} @@ -559,9 +563,11 @@ 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) {{ throw new TypeError("missing/undefined count"); }} + + const cap = this.capacity(); + if (count > cap) {{ + throw new Error(`cannot read [${{count}}] elements from buffer with capacity [${{cap}}]`); }} let values = []; @@ -586,7 +592,7 @@ impl Intrinsic { }} }} - this.#copied += count; + this.#processed += count; return values; }} @@ -594,7 +600,7 @@ 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}}]`); }} @@ -619,7 +625,7 @@ impl Intrinsic { }} }} - this.#copied += values.length; + this.#processed += values.length; }} }} @@ -636,6 +642,7 @@ impl Intrinsic { #buffers = new Map(); #bufferIDs = new Map(); + // NOTE: componentIdx === null indicates the host getNextBufferID(componentIdx) {{ const current = this.#bufferIDs.get(componentIdx); if (current === undefined) {{ @@ -678,6 +685,7 @@ impl Intrinsic { capacity: args.count, elemMeta: args.elemMeta, data: args.data, + target: args.target, }}); if (instanceBuffers.has(nextBufID)) {{ @@ -705,17 +713,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(); @@ -1143,6 +1140,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, )) { @@ -1420,7 +1425,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..e22ab922c 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/async_future.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/async_future.rs @@ -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) {{ 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..2ef3d5746 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/async_stream.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/async_stream.rs @@ -401,16 +401,20 @@ impl AsyncStreamIntrinsic { let copy_setup_impl = format!( r#" setupCopy(args) {{ - const {{ memory, ptr, count, eventCode }} = args; + const {{ memory, ptr, count, eventCode, 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 [${{streamEndIdx}}] (tableIdx [${{streamTableIdx}}], component [${{componentIdx}}]) is not in idle state`); + }} }} const elemMeta = this.getElemMeta(); @@ -419,7 +423,7 @@ 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({{ + const newBufferMeta = {global_buffer_manager}.createBuffer({{ componentIdx: this.#componentIdx, memory, start: ptr, @@ -433,8 +437,9 @@ impl AsyncStreamIntrinsic { isWritable: this.isReadable(), elemMeta, }}); - bufferID = newBuffer.bufferID; - buffer = newBuffer.buffer; + bufferID = newBufferMeta.id; + buffer = newBufferMeta.buffer; + buffer.setTarget(`component [${{this.#componentIdx}}] {end_class_name} buffer (id [${{bufferID}}], count [${{count}}], eventCode [${{eventCode}}])`); }} const streamEnd = this; @@ -450,46 +455,48 @@ impl AsyncStreamIntrinsic { if (result < 0 || result >= 16) {{ throw new Error(`unsupported stream copy result [${{result}}]`); }} - if (buffer.copied() >= {managed_buffer_class}.MAX_LENGTH) {{ + if (buffer.processed() >= {managed_buffer_class}.MAX_LENGTH) {{ throw new Error(`buffer size [${{buf.length}}] greater than max length`); }} if (buffer.length > 2**28) {{ throw new Error('buffer uses reserved space'); }} - const packedResult = (buffer.copied() << 4) | result; + const packedResult = (buffer.processed() << 4) | result; return {{ code: eventCode, payload0: streamEnd.waitableIdx(), payload1: packedResult }}; }}; - 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 { // 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", format!( r#" _write(args) {{ - const {{ buffer, onCopy, onCopyDone }} = args; + const {{ buffer, onCopyFn, onCopyDoneFn }} = 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: this.#componentIdx, buffer, onCopyFn, onCopyDoneFn }}); return; }} @@ -501,48 +508,61 @@ 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) {{ + if (this.#pendingBufferMeta.componentIdx === buffer.componentIdx() && !pendingElemMeta.isNoneOrNumeric) {{ 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); + 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: this.#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", format!( r#" _read(args) {{ - const {{ buffer, onCopyDone, onCopy }} = args; + const {{ buffer, onCopyDoneFn, onCopyFn }} = args; if (this.isDropped()) {{ - onCopyDone({stream_end_class}.CopyResult.DROPPED); + onCopyDoneFn({stream_end_class}.CopyResult.DROPPED); return; }} @@ -550,8 +570,8 @@ impl AsyncStreamIntrinsic { this.setPendingBufferMeta({{ componentIdx: this.#componentIdx, buffer, - onCopy, - onCopyDone, + onCopyFn, + onCopyDoneFn, }}); return; }} @@ -564,24 +584,37 @@ 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) {{ + if (this.#pendingBufferMeta.componentIdx === buffer.componentIdx() && !pendingElemMeta.isNoneOrNumeric) {{ 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: this.#componentIdx, buffer, onCopyFn, onCopyDoneFn }}); + + }} "#, ), @@ -600,7 +633,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,20 +645,21 @@ impl AsyncStreamIntrinsic { return; }} - const {{ buffer, onCopy, onCopyDone }} = this.setupCopy({{ + const {{ buffer, onCopyFn, onCopyDoneFn }} = this.setupCopy({{ memory, eventCode, ptr, count, buffer: args.buffer, bufferID: args.bufferID, + initial, }}); // Perform the read/write this.{inner_rw_fn_name}({{ buffer, - onCopy, - onCopyDone, + onCopyFn, + onCopyDoneFn, }}); // If sync, wait forever but allow task to do other things @@ -705,25 +739,85 @@ 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; + + try {{ + 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, + }}); + buffer.setTarget(`host stream write buffer (id [${{bufferID}}], count [${{count}}], data len [${{v.length}}])`); + + let packedResult; + packedResult = await this.copy({{ + isAsync: true, + count: 1, + bufferID, + buffer, + eventCode: {async_event_code_enum}.STREAM_WRITE, + }}); + + 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: 1, + bufferID, + buffer, + eventCode: {async_event_code_enum}.STREAM_WRITE, + skipStateCheck: true, + }}); - await this.copy({{ - isAsync: true, - count: 1, - bufferID, - buffer, - eventCode: {async_event_code_enum}.STREAM_WRITE, - }}); + 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); + console.error('[{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 +826,99 @@ 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: null, // componentIdx of null 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, + }}); + + 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, + }}); + + if (packedResult === {async_blocked_const}) {{ + throw new Error("unexpected double block during 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")); + }} - let copied = packedResult >> 4; - let result = packedResult & 0x000F; + const vs = buffer.read(count); + const res = count === 1 ? vs[0] : vs; + this.#result = null; + resolve(res); - const vs = buffer.read(); + }} catch (err) {{ + {debug_log_fn}('[{end_class_name}#read()] error', err); + console.error('[{end_class_name}#read()] error', err); + reject(err); + }} - return count === 1 ? vs[0] : vs; + return await promise; }} "# ), @@ -777,6 +940,11 @@ impl AsyncStreamIntrinsic { #streamRep; #streamTableIdx; + #result = null; + + #otherEndWait = null; + #otherEndNotify = null; + constructor(args) {{ {debug_log_fn}('[{end_class_name}#constructor()] args', args); super(args); @@ -798,6 +966,12 @@ impl AsyncStreamIntrinsic { 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; }} @@ -811,6 +985,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,15 +993,15 @@ 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 }}); }} resetAndNotifyPending(result) {{ @@ -842,11 +1017,9 @@ impl AsyncStreamIntrinsic { drop() {{ if (this.#copying) {{ throw new Error('cannot drop while copying'); }} - if (this.#pendingBufferMeta) {{ this.resetAndNotifyPending({stream_end_class}.CopyResult.DROPPED); }} - super.drop(); }} }} @@ -880,6 +1053,9 @@ impl AsyncStreamIntrinsic { #localStreamTable; #globalStreamMap; + #readWaitPromise = null; + #writeWaitPromise = null; + constructor(args) {{ if (typeof args.componentIdx !== 'number') {{ throw new Error('missing/invalid component idx'); }} if (!args.elemMeta) {{ throw new Error('missing/invalid stream element metadata'); }} @@ -898,6 +1074,19 @@ impl AsyncStreamIntrinsic { 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, @@ -922,8 +1111,23 @@ impl AsyncStreamIntrinsic { this.drop(); }} }}, + 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, @@ -949,6 +1153,8 @@ impl AsyncStreamIntrinsic { this.drop(); }} }}, + otherEndWait: readWait, + otherEndNotify: readNotify, }}); }} @@ -1287,7 +1493,7 @@ impl AsyncStreamIntrinsic { throw new Error(`stream end table idx [${{streamEnd.getStreamTableIdx()}}] != operation table idx [${{streamTableIdx}}]`); }} - const res = await streamEnd.copy({{ + const packedResult = await streamEnd.copy({{ isAsync, memory: getMemoryFn(), ptr, @@ -1295,7 +1501,7 @@ impl AsyncStreamIntrinsic { eventCode: {event_code}, }}); - return res; + return packedResult; }} "#)); } 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..b6d86c29e 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs @@ -357,11 +357,6 @@ impl AsyncTaskIntrinsic { 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, @@ -390,11 +385,6 @@ impl AsyncTaskIntrinsic { 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, @@ -976,7 +966,6 @@ impl AsyncTaskIntrinsic { wset.incrementNumWaiting(); - // const pendingEventWaitID = wset.registerPendingEventWait(); const keepGoing = await this.suspendUntil({{ readyFn: () => {{ const hasPendingEvent = wset.hasPendingEvent(); @@ -1296,6 +1285,7 @@ impl AsyncTaskIntrinsic { #callMetadata = {{}}; #onResolveHandlers = []; + #onStartHandlers = []; constructor(args) {{ if (typeof args.componentIdx !== 'number') {{ @@ -1379,6 +1369,10 @@ 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', {{ @@ -1405,6 +1399,15 @@ impl AsyncTaskIntrinsic { result = this.#callMetadata.startFn.apply(null, startFnArgs); }} + // for (const f of this.#onStartHandlers) {{ + // try {{ + // f({{ subtask: this }}); + // }} catch (err) {{ + // console.error("error during subtask on start handler", err); + // throw err; + // }} + // }} + return result; }} @@ -1850,6 +1853,11 @@ impl AsyncTaskIntrinsic { resultPtr: params[0], }} }}); + console.log("CREATED SUBTASK!", {{ + memoryIdx, + subtaskID: subtask.id(), + parentTaskID: parentTask.id(), + }}); parentTask.setReturnMemoryIdx(memoryIdx); const rep = cstate.subtasks.insert(subtask); @@ -1881,30 +1889,31 @@ impl AsyncTaskIntrinsic { // 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 () => {{ try {{ {debug_log_fn}('[{lower_import_fn}()] calling lowered import', {{ exportFn, params }}); - exportFn.apply(null, params); + await exportFn.apply(null, params); const task = subtask.getChildTask(); + if (!task) {{ + throw new Error("missing child task for subtask while preparing subtask resolution"); + }} + task.registerOnResolveHandler((res) => {{ {debug_log_fn}('[{lower_import_fn}()] cascading subtask completion', {{ childTaskID: task.id(), subtaskID: subtask.id(), parentTaskID: parentTask.id(), }}); - subtask.onResolve(res); - cstate.tick(); }}); + }} catch (err) {{ console.error("post-lower import fn error:", err); throw err; }} - }}, 100); + }}, 0); return Number(subtask.waitableRep()) << 4 | subtaskState; }} diff --git a/crates/js-component-bindgen/src/intrinsics/p3/host.rs b/crates/js-component-bindgen/src/intrinsics/p3/host.rs index bd6152c63..47713b283 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/host.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/host.rs @@ -546,12 +546,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..d35dd35ee 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() }})) @@ -230,7 +238,7 @@ impl WaitableIntrinsic { #resolve; #reject; - #waitableSet; + #waitableSet = null; target; @@ -242,7 +250,7 @@ impl WaitableIntrinsic { }} componentIdx() {{ return this.#componentIdx; }} - isInSet() {{ return this.#waitableSet !== undefined; }} + isInSet() {{ return this.#waitableSet !== null; }} setTarget(tgt) {{ this.target = tgt; }} @@ -312,6 +320,7 @@ impl WaitableIntrinsic { throw new Error('waitables with pending events cannot be dropped'); }} this.join(null); + // TODO: remove the waitable set }} }} @@ -344,10 +353,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 +376,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 +392,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); @@ -412,7 +421,7 @@ impl WaitableIntrinsic { }} 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 +430,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 +443,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 +457,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 +465,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); 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) {{ + 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); }} - ")); + "#)); } Self::WaitableJoin => { diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index e4bbf55eb..c184e70ad 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -1914,6 +1914,19 @@ impl<'a> Instantiator<'a, '_> { let fn_idx = index.as_u32(); + // TODO: work backwards from host + let (options, trampoline_idx, lowered_fn_idx) = self.lowering_options[*index]; + + // LoweredIndex -> RuntimeImportIndex -> Naming + + // 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; @@ -2470,6 +2483,9 @@ impl<'a> Instantiator<'a, '_> { let (import_index, path) = &self.component.imports[*import]; let (import_iface, _type_def) = &self.component.import_types[*import_index]; + // RuntimeImportIndex -> some string that we'll use to lookup + // import_iface + 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]")), diff --git a/packages/jco/test/fixtures/modules/hello_stdout.component.wasm b/packages/jco/test/fixtures/modules/hello_stdout.component.wasm index d96692d1a5fc69f572c11eec5e27bf2aaf40f6b2..aa020ac24276496c7b693561d300b4172d4e9ecf 100644 GIT binary patch delta 106 zcmcb#xP|c&5I3|iwlK9Yx3ILZwy?FZw{Wy@ws5s@xA3&^w(zy^w+OTdwg|Ndw}`Ze zwurTew@9=|wn()|x5%`}w#c=}Z&BFwlg(7mknt|#bi*GC#?wFlR9Mao6xcrZmx7Tv Ghy?&=kt3r3 delta 106 zcmcb#xP|c&5I3|iwlK9Yx3ILZwy?FZw{Wy@ws5s@xA3&^w(zy^w+OTdwg|Ndw}`Ze zwurTew@9=|wn()|x5%`}w#c=}Z&BFwlg&iWfblNlbi*GC#?wFlR9Mao6xcrZmx7Tv Ghy?&=J0qO{ 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..9cbdaea0b 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"); } @@ -146,7 +154,7 @@ suite("post-return async sleep scenario", () => { }, transpile: { extraArgs: { - // minify: false, + minify: false, asyncImports: [ // Host-provided async imports must be marked as such "local:local/sleep#sleep-millis", @@ -169,6 +177,14 @@ 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); +} From 3f7e9698428f16016b142b929cb64d5e9ef63234 Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Tue, 3 Mar 2026 04:00:40 +0900 Subject: [PATCH 02/15] refactor(bindgen): remove global async lowers --- .../src/intrinsics/mod.rs | 73 +------- .../src/intrinsics/p3/async_task.rs | 9 +- .../src/transpile_bindgen.rs | 173 ++++-------------- .../wasmtime/component-async/post-return.js | 1 + 4 files changed, 44 insertions(+), 212 deletions(-) diff --git a/crates/js-component-bindgen/src/intrinsics/mod.rs b/crates/js-component-bindgen/src/intrinsics/mod.rs index 0df09ca66..e19f7caac 100644 --- a/crates/js-component-bindgen/src/intrinsics/mod.rs +++ b/crates/js-component-bindgen/src/intrinsics/mod.rs @@ -137,9 +137,6 @@ pub enum Intrinsic { /// handle, so this helper checks for that. IsBorrowedType, - /// Async lower functions that are saved by component instance - GlobalComponentAsyncLowersClass, - /// Param lowering functions saved by a component instance, interface and function /// /// This lookup is keyed by a combination of the component instance, interface @@ -809,69 +806,6 @@ impl Intrinsic { )); } - Intrinsic::GlobalComponentAsyncLowersClass => { - let global_component_lowers_class = - Intrinsic::GlobalComponentAsyncLowersClass.name(); - output.push_str(&format!( - r#" - class {global_component_lowers_class} {{ - static map = new Map(); - - constructor() {{ throw new Error('{global_component_lowers_class} should not be constructed'); }} - - static define(args) {{ - const {{ componentIdx, 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); - }}; - }} - }} - "# - )); - } - Intrinsic::GlobalComponentMemoriesClass => { let global_component_memories_class = Intrinsic::GlobalComponentMemoriesClass.name(); @@ -947,10 +881,9 @@ pub struct RenderIntrinsicsArgs<'a> { } /// Intrinsics that should be rendered as early as possible -const EARLY_INTRINSICS: [Intrinsic; 21] = [ +const EARLY_INTRINSICS: [Intrinsic; 22] = [ Intrinsic::DebugLog, Intrinsic::GlobalAsyncDeterminism, - Intrinsic::GlobalComponentAsyncLowersClass, Intrinsic::GlobalAsyncParamLowersClass, Intrinsic::GlobalComponentMemoriesClass, Intrinsic::RepTableClass, @@ -969,6 +902,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 @@ -1353,7 +1288,6 @@ impl Intrinsic { "URL", "WebAssembly", "GlobalComponentMemories", - "GlobalComponentAsyncLowers", ]) } @@ -1411,7 +1345,6 @@ impl Intrinsic { Intrinsic::GlobalAsyncDeterminism => "ASYNC_DETERMINISM", Intrinsic::AwaitableClass => "Awaitable", Intrinsic::CoinFlip => "_coinFlip", - Intrinsic::GlobalComponentAsyncLowersClass => "GlobalComponentAsyncLowers", Intrinsic::GlobalAsyncParamLowersClass => "GlobalAsyncParamLowers", Intrinsic::GlobalComponentMemoriesClass => "GlobalComponentMemories", 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 b6d86c29e..2d8b74120 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs @@ -1822,9 +1822,9 @@ impl AsyncTaskIntrinsic { output.push_str(&format!( r#" - function {lower_import_fn}(args, exportFn) {{ + function {lower_import_fn}(args) {{ const params = [...arguments].slice(2); - {debug_log_fn}('[{lower_import_fn}()] args', {{ args, params, exportFn }}); + {debug_log_fn}('[{lower_import_fn}()] args', args); const {{ functionIdx, componentIdx, @@ -1835,6 +1835,7 @@ impl AsyncTaskIntrinsic { memoryIdx, getMemoryFn, getReallocFn, + importFn, }} = args; const parentTaskMeta = {current_task_get_fn}(componentIdx); @@ -1891,8 +1892,8 @@ impl AsyncTaskIntrinsic { // to ensure the subtask state is not modified before the lower call return setTimeout(async () => {{ try {{ - {debug_log_fn}('[{lower_import_fn}()] calling lowered import', {{ exportFn, params }}); - await exportFn.apply(null, params); + {debug_log_fn}('[{lower_import_fn}()] calling lowered import', {{ importFn, params }}); + await importFn.apply(null, params); const task = subtask.getChildTask(); if (!task) {{ diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index c184e70ad..1fa071938 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -1903,9 +1903,6 @@ impl<'a> Instantiator<'a, '_> { 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 @@ -1914,10 +1911,9 @@ impl<'a> Instantiator<'a, '_> { let fn_idx = index.as_u32(); - // TODO: work backwards from host - let (options, trampoline_idx, lowered_fn_idx) = self.lowering_options[*index]; - - // LoweredIndex -> RuntimeImportIndex -> Naming + // TODO: generate lifts for the parameters? + let (_options, lowered_fn_trampoline_idx, _lowered_fn_type_idx) = + self.lowering_options[*index]; // TODO(fix): remove Global lowers, should enable using just exports[x] to export[y] call @@ -1986,36 +1982,29 @@ 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). + // NOTE: For Trampoline::LowerImport, the trampoline index is actually already defined, + // but we *redefine* it to call the lower import function first. 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}, - }}, - ), - }}); + const trampoline{i} = new WebAssembly.Suspending({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}, + importFn: _trampoline{i}, + }}, + )); "#, ); } @@ -2560,93 +2549,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!( @@ -2914,24 +2817,25 @@ 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 + if is_async { + // 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" + ); } - // Write out the function (brace + body + brace) self.bindgen(JsFunctionBindgenArgs { nparams, @@ -2950,13 +2854,6 @@ impl<'a> Instantiator<'a, '_> { is_async, }); uwriteln!(self.src.js, ""); - - // Write new function ending - if requires_async_porcelain | is_async { - uwriteln!(self.src.js, ");"); - } else { - uwriteln!(self.src.js, ""); - } } Some(BindingsMode::Optimized) | Some(BindingsMode::DirectOptimized) => { 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 9cbdaea0b..1d88f3776 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 @@ -169,6 +169,7 @@ suite("post-return async sleep scenario", () => { }); const instance = res.instance; cleanup = res.cleanup; + console.log("OUTPUT DIR", res.outputDir); const result = await instance["local:local/sleep-post-return"].run(waitTimeMs); expect(result).toBeUndefined(); From 52330dda175c53368549e1ed9768e421c817e7ea Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Tue, 3 Mar 2026 04:30:46 +0900 Subject: [PATCH 03/15] fix(bindgen): rework use of WebAssembly.promising [breaking] --- .../src/function_bindgen.rs | 2 +- .../src/transpile_bindgen.rs | 114 +----------------- 2 files changed, 5 insertions(+), 111 deletions(-) diff --git a/crates/js-component-bindgen/src/function_bindgen.rs b/crates/js-component-bindgen/src/function_bindgen.rs index 22eb52773..f14addcf5 100644 --- a/crates/js-component-bindgen/src/function_bindgen.rs +++ b/crates/js-component-bindgen/src/function_bindgen.rs @@ -492,7 +492,7 @@ 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()) diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index 1fa071938..0989d667b 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -327,14 +327,10 @@ impl JsBindgen<'_> { for (core_export_fn, is_async) in self.all_core_exported_funcs.iter() { let local_name = self.local_names.get(core_export_fn); - if *is_async { - uwriteln!( - core_exported_funcs, - "{local_name} = WebAssembly.promising({core_export_fn});", - ); - } else { - uwriteln!(core_exported_funcs, "{local_name} = {core_export_fn};",); - } + uwriteln!( + core_exported_funcs, + "{local_name} = WebAssembly.promising({core_export_fn});", + ); } // adds a default implementation of `getCoreModule` @@ -1994,7 +1990,6 @@ impl<'a> Instantiator<'a, '_> { 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}, @@ -2468,41 +2463,6 @@ 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]; - - // RuntimeImportIndex -> some string that we'll use to lookup - // import_iface + 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); } @@ -3988,72 +3948,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 => { From 65202d03e1ccc6b4be84286b01fc420135176dc1 Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Tue, 3 Mar 2026 22:25:42 +0900 Subject: [PATCH 04/15] fix(bindgen): fix async task and stream impl bugs --- .../src/function_bindgen.rs | 367 +++++------ .../src/intrinsics/component.rs | 220 +++++-- .../src/intrinsics/lift.rs | 28 +- .../src/intrinsics/lower.rs | 59 +- .../src/intrinsics/mod.rs | 121 ++-- .../src/intrinsics/p3/async_future.rs | 14 +- .../src/intrinsics/p3/async_stream.rs | 449 ++++++------- .../src/intrinsics/p3/async_task.rs | 594 +++++++++--------- .../src/intrinsics/p3/host.rs | 208 +++--- .../src/intrinsics/p3/waitable.rs | 66 +- .../src/intrinsics/string.rs | 36 +- .../src/transpile_bindgen.rs | 342 ++++++---- .../wasmtime/component-async/post-return.js | 3 +- 13 files changed, 1402 insertions(+), 1105 deletions(-) diff --git a/crates/js-component-bindgen/src/function_bindgen.rs b/crates/js-component-bindgen/src/function_bindgen.rs index f14addcf5..e8f45aba4 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 { @@ -398,19 +417,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 +498,9 @@ impl Bindgen for FunctionBindgen<'_> { Instruction::U32FromI32 => results.push(format!("{} >>> 0", operands[0])), - Instruction::U64FromI64 => results.push(format!("BigInt.asUintN(64, BigInt({}))", operands[0])), + Instruction::U64FromI64 => { + results.push(format!("BigInt.asUintN(64, BigInt({}))", operands[0])) + } Instruction::S32FromI32 | Instruction::S64FromI64 => { results.push(operands.pop().unwrap()) @@ -1137,7 +1145,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 { + format!("{realloc}") + }, ); // We may or may not be dealing with a buffer like object or a regular JS array, @@ -1205,12 +1218,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 +1299,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 { + format!("{realloc}") + }, ); // ... then consume the vector and use the block to lower the @@ -1318,8 +1345,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 +1388,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 +1430,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 +1459,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 +1484,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,19 +1504,11 @@ impl Bindgen for FunctionBindgen<'_> { createTask(); - const isHostAsyncImport = hostProvided && {is_async}; - if (isHostAsyncImport) {{ + if (hostProvided) {{ subtask = parentTask.getLatestSubtask(); if (!subtask) {{ - console.log("MISSING SUBTASK", {{ - entryFnName: '{fn_name}', - parentTask, - parentTaskID: parentTask.id(), - parentTaskComponent: parentTask.componentIdx(), - }}); - throw new Error(`Missing subtask (in parent task [${{parentTask.id()}}]) for async 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); }} }} @@ -1554,9 +1524,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!( @@ -1572,6 +1545,8 @@ impl Bindgen for FunctionBindgen<'_> { }} "#, ); + } else { + uwriteln!(self.src, "const started = task.enterSync();",); } // Build the JS expression that calls the callee @@ -1584,8 +1559,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 @@ -1626,7 +1605,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() @@ -1672,11 +1651,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 { @@ -1703,19 +1677,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"); @@ -1727,12 +1703,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();"); } } @@ -1740,22 +1719,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; + "# ); } @@ -1769,6 +1754,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 @@ -1781,12 +1768,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};") } } } @@ -1840,8 +1827,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 { + format!("{realloc}") + }, size = size.size_wasm32() ); results.push(ptr); @@ -1874,10 +1866,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 @@ -1918,15 +1912,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!( @@ -1971,12 +1970,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;"); } @@ -1984,11 +1984,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, @@ -2197,7 +2198,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 @@ -2292,7 +2293,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 @@ -2419,7 +2420,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, @@ -2432,8 +2433,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 @@ -2451,11 +2452,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, }});", @@ -2482,18 +2492,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. // @@ -2519,26 +2520,30 @@ 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 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 ecb7be196..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; }} @@ -307,14 +308,14 @@ impl ComponentIntrinsic { // 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, }}); @@ -326,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; @@ -348,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`); @@ -360,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()); }}, @@ -379,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); @@ -409,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 e19f7caac..4d023d925 100644 --- a/crates/js-component-bindgen/src/intrinsics/mod.rs +++ b/crates/js-component-bindgen/src/intrinsics/mod.rs @@ -117,10 +117,6 @@ 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, @@ -179,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() @@ -507,20 +469,20 @@ impl Intrinsic { #start; #ptr; - #capacity; - #processed = 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'); }} @@ -532,25 +494,28 @@ 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; }} setTarget(tgt) {{ this.target = tgt; }} - capacity() {{ return this.#capacity; }} - remaining() {{ return this.#capacity - this.#processed; }} - processed() {{ return this.#processed; }} + remaining() {{ + return this.capacity - this.processed; + }} componentIdx() {{ return this.#componentIdx; }} @@ -560,28 +525,30 @@ impl Intrinsic { read(count) {{ {debug_log_fn}('[{managed_buffer_class}#read()] args', {{ count }}); - if (count === undefined) {{ throw new TypeError("missing/undefined count"); }} + if (count === undefined || count <= 0) {{ + throw new TypeError(`missing/invalid count [${{count}}]`); + }} - const cap = this.capacity(); + 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; }} @@ -589,7 +556,7 @@ impl Intrinsic { }} }} - this.#processed += count; + this.processed += count; return values; }} @@ -602,13 +569,13 @@ impl Intrinsic { 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) {{ @@ -622,7 +589,7 @@ impl Intrinsic { }} }} - this.#processed += values.length; + this.processed += values.length; }} }} @@ -639,7 +606,7 @@ impl Intrinsic { #buffers = new Map(); #bufferIDs = new Map(); - // NOTE: componentIdx === null indicates the host + // NOTE: componentIdx === -1 indicates the host getNextBufferID(componentIdx) {{ const current = this.#bufferIDs.get(componentIdx); if (current === undefined) {{ @@ -666,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; @@ -730,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; @@ -748,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; @@ -881,7 +855,7 @@ pub struct RenderIntrinsicsArgs<'a> { } /// Intrinsics that should be rendered as early as possible -const EARLY_INTRINSICS: [Intrinsic; 22] = [ +const EARLY_INTRINSICS: [Intrinsic; 23] = [ Intrinsic::DebugLog, Intrinsic::GlobalAsyncDeterminism, Intrinsic::GlobalAsyncParamLowersClass, @@ -894,6 +868,7 @@ const EARLY_INTRINSICS: [Intrinsic; 22] = [ Intrinsic::TypeCheckValidI32, Intrinsic::TypeCheckAsyncFn, Intrinsic::AsyncFunctionCtor, + Intrinsic::AsyncTask(AsyncTaskIntrinsic::ClearCurrentTask), Intrinsic::AsyncTask(AsyncTaskIntrinsic::CurrentTaskMayBlock), Intrinsic::AsyncTask(AsyncTaskIntrinsic::GlobalAsyncCurrentTaskIds), Intrinsic::AsyncTask(AsyncTaskIntrinsic::GlobalAsyncCurrentComponentIdxs), @@ -1053,7 +1028,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), ]); @@ -1092,6 +1066,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)) @@ -1168,7 +1150,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), @@ -1182,6 +1164,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), ]); @@ -1203,6 +1186,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), ]); @@ -1343,7 +1327,6 @@ impl Intrinsic { // Async Intrinsic::GlobalAsyncDeterminism => "ASYNC_DETERMINISM", - Intrinsic::AwaitableClass => "Awaitable", Intrinsic::CoinFlip => "_coinFlip", Intrinsic::GlobalAsyncParamLowersClass => "GlobalAsyncParamLowers", Intrinsic::GlobalComponentMemoriesClass => "GlobalComponentMemories", 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 e22ab922c..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); }} ")); } @@ -412,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'); }} @@ -477,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 2ef3d5746..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,7 +427,7 @@ impl AsyncStreamIntrinsic { let copy_setup_impl = format!( r#" setupCopy(args) {{ - const {{ memory, ptr, count, eventCode, skipStateCheck }} = args; + const {{ memory, ptr, count, eventCode, componentIdx, skipStateCheck }} = args; if (eventCode === undefined) {{ throw new Error("missing/invalid event code"); }} let buffer = args.buffer; @@ -413,7 +439,7 @@ impl AsyncStreamIntrinsic { 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`); + throw new Error(`stream copy state is not idle`); }} }} @@ -424,7 +450,7 @@ impl AsyncStreamIntrinsic { // create a buffer (likely in the guest case) if (!buffer) {{ const newBufferMeta = {global_buffer_manager}.createBuffer({{ - componentIdx: this.#componentIdx, + componentIdx, memory, start: ptr, count, @@ -439,7 +465,7 @@ impl AsyncStreamIntrinsic { }}); bufferID = newBufferMeta.id; buffer = newBufferMeta.buffer; - buffer.setTarget(`component [${{this.#componentIdx}}] {end_class_name} buffer (id [${{bufferID}}], count [${{count}}], eventCode [${{eventCode}}])`); + buffer.setTarget(`component [${{componentIdx}}] {end_class_name} buffer (id [${{bufferID}}], count [${{count}}], eventCode [${{eventCode}}])`); }} const streamEnd = this; @@ -455,13 +481,15 @@ impl AsyncStreamIntrinsic { if (result < 0 || result >= 16) {{ throw new Error(`unsupported stream copy result [${{result}}]`); }} - if (buffer.processed() >= {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.processed() << 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 onCopyFn = (reclaimBufferFn) => {{ @@ -481,22 +509,22 @@ impl AsyncStreamIntrinsic { "# ); - 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, onCopyFn, onCopyDoneFn }} = args; + const {{ buffer, onCopyFn, onCopyDoneFn, componentIdx }} = args; if (!buffer) {{ throw new TypeError('missing/invalid buffer'); }} 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, onCopyFn, onCopyDoneFn }}); + this.setPendingBufferMeta({{ componentIdx, buffer, onCopyFn, onCopyDoneFn }}); return; }} @@ -508,13 +536,14 @@ 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 === buffer.componentIdx() && !pendingElemMeta.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) {{ + if (buffer.capacity === 0 && this.#pendingBufferMeta.buffer.capacity === 0) {{ onCopyDoneFn({stream_end_class}.CopyResult.COMPLETED); return; }} @@ -524,7 +553,7 @@ impl AsyncStreamIntrinsic { // to clear up space in the buffer. if (this.#pendingBufferMeta.buffer.remaining() === 0) {{ this.resetAndNotifyPending({stream_end_class}.CopyResult.COMPLETED); - this.setPendingBufferMeta({{ componentIdx: this.#componentIdx, buffer, onCopyFn, onCopyDoneFn }}); + this.setPendingBufferMeta({{ componentIdx, buffer, onCopyFn, onCopyDoneFn }}); return; }} @@ -556,11 +585,11 @@ impl AsyncStreamIntrinsic { // // This _read() function is primarily called by guests. Self::StreamReadableEndClass => ( - "_read", + "read", format!( r#" _read(args) {{ - const {{ buffer, onCopyDoneFn, onCopyFn }} = args; + const {{ buffer, onCopyDoneFn, onCopyFn, componentIdx }} = args; if (this.isDropped()) {{ onCopyDoneFn({stream_end_class}.CopyResult.DROPPED); return; @@ -568,7 +597,7 @@ impl AsyncStreamIntrinsic { if (!this.#pendingBufferMeta.buffer) {{ this.setPendingBufferMeta({{ - componentIdx: this.#componentIdx, + componentIdx, buffer, onCopyFn, onCopyDoneFn, @@ -584,7 +613,8 @@ 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 === buffer.componentIdx() && !pendingElemMeta.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)"); }} @@ -612,9 +642,7 @@ impl AsyncStreamIntrinsic { }} this.resetAndNotifyPending({stream_end_class}.CopyResult.COMPLETED); - this.setPendingBufferMeta({{ componentIdx: this.#componentIdx, buffer, onCopyFn, onCopyDoneFn }}); - - + this.setPendingBufferMeta({{ componentIdx, buffer, onCopyFn, onCopyDoneFn }}); }} "#, ), @@ -648,6 +676,7 @@ impl AsyncStreamIntrinsic { const {{ buffer, onCopyFn, onCopyDoneFn }} = this.setupCopy({{ memory, eventCode, + componentIdx, ptr, count, buffer: args.buffer, @@ -656,16 +685,18 @@ impl AsyncStreamIntrinsic { }}); // Perform the read/write - this.{inner_rw_fn_name}({{ + this._{rw_fn_name}({{ buffer, 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); @@ -678,9 +709,7 @@ impl AsyncStreamIntrinsic { const streamEnd = this; await task.suspendUntil({{ - readyFn: () => {{ - return streamEnd.hasPendingEvent(); - }} + readyFn: () => streamEnd.hasPendingEvent(), }}); }} }} @@ -693,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, @@ -703,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); }} @@ -763,10 +793,11 @@ impl AsyncStreamIntrinsic { }} const {{ promise, resolve, reject }} = newResult; + const count = 1; try {{ const {{ id: bufferID, buffer }} = {global_buffer_manager}.createBuffer({{ - componentIdx: null, // componentIdx of null indicates the host - count: 1, + componentIdx: -1, + count, isReadable: true, // we need to read from this buffer later isWritable: false, elemMeta: this.#elemMeta, @@ -777,10 +808,11 @@ impl AsyncStreamIntrinsic { let packedResult; packedResult = await this.copy({{ isAsync: true, - count: 1, + count, bufferID, buffer, eventCode: {async_event_code_enum}.STREAM_WRITE, + componentIdx: -1, }}); if (packedResult === {async_blocked_const}) {{ @@ -791,11 +823,12 @@ impl AsyncStreamIntrinsic { packedResult = await this.copy({{ isAsync: true, - count: 1, + count, bufferID, buffer, eventCode: {async_event_code_enum}.STREAM_WRITE, skipStateCheck: true, + componentIdx: -1, }}); if (packedResult === {async_blocked_const}) {{ @@ -809,7 +842,6 @@ impl AsyncStreamIntrinsic { }} catch (err) {{ {debug_log_fn}('[{end_class_name}#write()] error', err); - console.error('[{end_class_name}#write()] error', err); reject(err); }} @@ -853,7 +885,7 @@ impl AsyncStreamIntrinsic { const count = 1; try {{ const {{ id: bufferID, buffer }} = {global_buffer_manager}.createBuffer({{ - componentIdx: null, // componentIdx of null indicates the host + componentIdx: -1, // componentIdx of -1 indicates the host count, isReadable: false, isWritable: true, // we need to write out the pending buffer (if present) @@ -869,6 +901,7 @@ impl AsyncStreamIntrinsic { bufferID, buffer, eventCode: {async_event_code_enum}.STREAM_READ, + componentIdx: -1, }}); if (packedResult === {async_blocked_const}) {{ @@ -884,6 +917,7 @@ impl AsyncStreamIntrinsic { buffer, eventCode: {async_event_code_enum}.STREAM_READ, skipStateCheck: true, + componentIdx: -1, }}); if (packedResult === {async_blocked_const}) {{ @@ -914,7 +948,6 @@ impl AsyncStreamIntrinsic { }} catch (err) {{ {debug_log_fn}('[{end_class_name}#read()] error', err); - console.error('[{end_class_name}#read()] error', err); reject(err); }} @@ -927,18 +960,16 @@ 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; @@ -952,18 +983,9 @@ 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; @@ -974,10 +996,21 @@ impl AsyncStreamIntrinsic { 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; }} + + globalStreamMapRep() {{ return this.#globalStreamMapRep; }} + setGlobalStreamMapRep(rep) {{ this.#globalStreamMapRep = rep; }} - waitableIdx() {{ return this.#getEndIdxFn(); }} + 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}}; }} @@ -1004,6 +1037,8 @@ impl AsyncStreamIntrinsic { this.setPendingBufferMeta({{ componentIdx: null, buffer: null, onCopyFn: null, onCopyDoneFn: null }}); }} + getPendingBufferMeta() {{ return this.#pendingBufferMeta; }} + resetAndNotifyPending(result) {{ const f = this.#pendingBufferMeta.onCopyDoneFn; this.resetPendingBufferMeta(); @@ -1016,7 +1051,8 @@ 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); }} @@ -1028,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; @@ -1047,33 +1079,24 @@ 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; @@ -1088,29 +1111,11 @@ impl AsyncStreamIntrinsic { }}; 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, }}); @@ -1129,60 +1134,27 @@ impl AsyncStreamIntrinsic { }}; 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`); - }} - }} - }} }} "# )); @@ -1257,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(); }}, @@ -1288,7 +1260,7 @@ impl AsyncStreamIntrinsic { output.push_str(&format!( r#" class {external_stream_class_name} {{ - #hostStreamRep = null; + #globalRep = null; #isReadable; #isWritable; #writeFn; @@ -1296,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; @@ -1312,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()]'); @@ -1347,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) // @@ -1383,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); }} "#)); } @@ -1442,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"), @@ -1457,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, @@ -1475,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 packedResult = await streamEnd.copy({{ + const result = await streamEnd.copy({{ isAsync, memory: getMemoryFn(), ptr, count, eventCode: {event_code}, + componentIdx, }}); - return packedResult; + return result; }} "#)); } @@ -1509,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); @@ -1523,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* @@ -1587,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); @@ -1597,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})) {{ @@ -1616,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(); @@ -1625,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 2d8b74120..8bc2286b0 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,9 +349,13 @@ 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'); }} @@ -379,8 +382,12 @@ 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'); }} @@ -405,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(); @@ -419,6 +432,7 @@ impl AsyncTaskIntrinsic { callbackFnIdx, memoryIdx, liftFns, + lowerFns, params, }}); @@ -440,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) {{ @@ -473,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); }} "#)); } @@ -582,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) {{ @@ -603,7 +603,7 @@ 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}({{ @@ -621,10 +621,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); @@ -640,32 +642,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) {{ @@ -677,7 +687,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) {{ @@ -694,8 +704,7 @@ impl AsyncTaskIntrinsic { const taskMeta = tasks.pop(); return taskMeta.task; }} - ", - fn_name = self.name() + "#, )); } @@ -709,9 +718,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 +755,7 @@ impl AsyncTaskIntrinsic { #onExitHandlers = []; #memoryIdx = null; + #memory = null; #callbackFn = null; #callbackFnName = null; @@ -766,6 +778,7 @@ impl AsyncTaskIntrinsic { #returnLowerFns = null; #entered = false; + #exited = false; cancelled = false; requested = false; @@ -829,7 +842,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; }} @@ -842,8 +854,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; }} @@ -896,6 +917,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()) {{ @@ -914,8 +937,20 @@ 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`); @@ -951,16 +986,15 @@ 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; @@ -990,36 +1024,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 }}); @@ -1121,6 +1125,10 @@ impl AsyncTaskIntrinsic { }}); this.#postReturnFn(); }} + + if (this.#parentSubtask) {{ + this.#parentSubtask.onResolve(taskValue); + }} }} registerOnResolveHandler(f) {{ @@ -1129,9 +1137,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) {{ @@ -1157,6 +1166,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. @@ -1182,10 +1193,6 @@ 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()) {{ throw new Error(`task [${{this.#id}}] exit: component [${{this.#componentIdx}}] should have been exclusively locked`); @@ -1201,24 +1208,47 @@ 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 }} = 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, + 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()]'); @@ -1226,11 +1256,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; }} }} @@ -1240,9 +1268,11 @@ 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 global_component_memories_class = + Intrinsic::GlobalComponentMemoriesClass.name(); + output.push_str(&format!(r#" class {subtask_class} {{ static _ID = 0n; @@ -1269,9 +1299,6 @@ impl AsyncTaskIntrinsic { #lenders = null; #waitable = null; - #waitableRep = null; - #waitableResolve = null; - #waitableReject = null; #callbackFn = null; #callbackFnName = null; @@ -1284,9 +1311,18 @@ impl AsyncTaskIntrinsic { #callMetadata = {{}}; + #resolved = false; + #onResolveHandlers = []; #onStartHandlers = []; + #result = null; + #resultSet = false; + + fnName; + target; + isAsync; + constructor(args) {{ if (typeof args.componentIdx !== 'number') {{ throw new Error('invalid componentIdx for subtask creation'); @@ -1294,6 +1330,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; @@ -1302,28 +1339,15 @@ 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; }} }} id() {{ return this.#id; }} @@ -1331,6 +1355,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) {{ @@ -1374,18 +1417,18 @@ impl AsyncTaskIntrinsic { }} 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. @@ -1399,15 +1442,6 @@ impl AsyncTaskIntrinsic { result = this.#callMetadata.startFn.apply(null, startFnArgs); }} - // for (const f of this.#onStartHandlers) {{ - // try {{ - // f({{ subtask: this }}); - // }} catch (err) {{ - // console.error("error during subtask on start handler", err); - // throw err; - // }} - // }} - return result; }} @@ -1423,13 +1457,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) {{ @@ -1440,17 +1479,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); @@ -1460,14 +1501,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() ?? {global_component_memories_class}.getMemoriesForComponentIdx(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; }} @@ -1506,8 +1567,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'); }} @@ -1528,19 +1589,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; }} @@ -1555,7 +1607,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(); }} }} "#)); @@ -1580,16 +1632,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) {{ @@ -1607,46 +1657,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; @@ -1673,9 +1691,7 @@ impl AsyncTaskIntrinsic { let asyncRes; try {{ while (true) {{ - if (callbackCode !== 0) {{ - componentState.exclusiveRelease(); - }} + if (callbackCode !== 0) {{ componentState.exclusiveRelease(); }} switch (callbackCode) {{ case 0: // EXIT @@ -1710,12 +1726,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(), @@ -1767,6 +1785,9 @@ impl AsyncTaskIntrinsic { waitableSetRep = unpacked[1]; {debug_log_fn}('[{driver_loop_fn}()] callback result unpacked', {{ + fnName, + componentIdx, + callbackFnName, callbackRes, callbackCode, waitableSetRep, @@ -1781,14 +1802,6 @@ impl AsyncTaskIntrinsic { result, err, }}); - console.error('[{driver_loop_fn}()] error during async driver loop', {{ - fnName, - callbackFnName, - eventCode, - index, - result, - err, - }}); reject(err); }} }} @@ -1823,14 +1836,15 @@ impl AsyncTaskIntrinsic { output.push_str(&format!( r#" function {lower_import_fn}(args) {{ - const params = [...arguments].slice(2); - {debug_log_fn}('[{lower_import_fn}()] args', args); + const params = [...arguments].slice(1); + {debug_log_fn}('[{lower_import_fn}()] args', {{ args, params }}); const {{ functionIdx, componentIdx, isAsync, paramLiftFns, resultLowerFns, + funcTypeIsAsync, metadata, memoryIdx, getMemoryFn, @@ -1838,83 +1852,101 @@ impl AsyncTaskIntrinsic { 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}}]`); + // }} + + if (!task.mayBlock() && funcTypeIsAsync && !isAsync) {{ + throw new Error("non async exports cannot synchronously call async functions"); + }} - const subtask = parentTask.createSubtask({{ + // 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, callMetadata: {{ memoryIdx, - memory: getMemoryFn(), + memory, realloc: getReallocFn(), resultPtr: params[0], + lowers: resultLowerFns, }} }}); - console.log("CREATED SUBTASK!", {{ - memoryIdx, - subtaskID: subtask.id(), - parentTaskID: parentTask.id(), - }}); - 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 + if (!isAsync && !funcTypeIsAsync) {{ + const res = importFn(...params); + 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) + if (!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'); - }} - // NOTE: we must wait a bit before calling the export function, // to ensure the subtask state is not modified before the lower call return - setTimeout(async () => {{ + queueMicrotask(async () => {{ try {{ {debug_log_fn}('[{lower_import_fn}()] calling lowered import', {{ importFn, params }}); - await importFn.apply(null, params); - - const task = subtask.getChildTask(); - if (!task) {{ - throw new Error("missing child task for subtask while preparing subtask resolution"); - }} - - task.registerOnResolveHandler((res) => {{ - {debug_log_fn}('[{lower_import_fn}()] cascading subtask completion', {{ - childTaskID: task.id(), - subtaskID: subtask.id(), - parentTaskID: parentTask.id(), - }}); - subtask.onResolve(res); - cstate.tick(); - }}); - + await importFn(...params); }} catch (err) {{ console.error("post-lower import fn error:", err); throw err; }} - }}, 0); + }}); return Number(subtask.waitableRep()) << 4 | subtaskState; }} diff --git a/crates/js-component-bindgen/src/intrinsics/p3/host.rs b/crates/js-component-bindgen/src/intrinsics/p3/host.rs index 47713b283..594cd8587 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/host.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/host.rs @@ -146,6 +146,7 @@ impl HostIntrinsic { resultCountOrAsync, ) {{ {debug_log_fn}('[{prepare_call_fn}()]', {{ + memoryIdx, callerInstanceIdx, calleeInstanceIdx, taskReturnTypeIdx, @@ -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 isCalleeAsync = isCalleeAsyncInt !== 0; const [newTask, newTaskID] = {create_new_current_task_fn}({{ componentIdx: calleeInstanceIdx, - isAsync: isCalleeAsyncInt !== 0, + isAsync: isCalleeAsync, 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: isCalleeAsync, 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,8 +270,6 @@ 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(); @@ -277,10 +277,11 @@ impl HostIntrinsic { // 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 +308,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 +321,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 +341,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 @@ -396,90 +384,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 +435,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 +466,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; }} "#)); diff --git a/crates/js-component-bindgen/src/intrinsics/p3/waitable.rs b/crates/js-component-bindgen/src/intrinsics/p3/waitable.rs index d35dd35ee..1affa690e 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/waitable.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/waitable.rs @@ -215,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'); }} @@ -240,6 +245,8 @@ impl WaitableIntrinsic { #waitableSet = null; + #idx = null; // to component-global waitables + target; constructor(args) {{ @@ -252,6 +259,12 @@ impl WaitableIntrinsic { componentIdx() {{ return this.#componentIdx; }} 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; }} #resetPromise() {{ @@ -266,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; }} @@ -286,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; }} @@ -320,7 +333,6 @@ impl WaitableIntrinsic { throw new Error('waitables with pending events cannot be dropped'); }} this.join(null); - // TODO: remove the waitable set }} }} @@ -337,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; }} "#)); @@ -415,7 +429,7 @@ 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}}]`); }} @@ -472,7 +486,7 @@ impl WaitableIntrinsic { 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 + ']'); }} @@ -480,7 +494,7 @@ impl WaitableIntrinsic { throw new Error('waitable set cannot be removed with pending items remaining'); }} - const waitableSet = state.waitableSets.get(waitableSetRep); + const waitableSet = state.handles.get(waitableSetRep); if (ws.numWaitables() > 0) {{ throw new Error('waitable set still contains waitables'); }} @@ -488,7 +502,7 @@ impl WaitableIntrinsic { throw new Error('waitable set still has other tasks waiting on it'); }} - state.waitableSets.remove(waitableSetRep); + state.handles.remove(waitableSetRep); }} "#)); } @@ -504,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..1cea41a3a 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,18 +103,23 @@ 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 }}; }} diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index 0989d667b..a2fef176f 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(); @@ -327,10 +335,16 @@ impl JsBindgen<'_> { for (core_export_fn, is_async) in self.all_core_exported_funcs.iter() { let local_name = self.local_names.get(core_export_fn); - uwriteln!( - core_exported_funcs, - "{local_name} = WebAssembly.promising({core_export_fn});", - ); + if *is_async { + uwriteln!( + core_exported_funcs, + "{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};",); + } } // adds a default implementation of `getCoreModule` @@ -582,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, '_> { @@ -747,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. @@ -1227,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 { @@ -1241,6 +1262,7 @@ impl<'a> Instantiator<'a, '_> { "true".into(), "false".into(), "false".into(), + "false".into(), ), // If there is a payload, generate relevant lift/lower and other metadata Some(ty) => ( @@ -1263,6 +1285,7 @@ impl<'a> Instantiator<'a, '_> { &ty, &wasmtime_environ::component::StringEncoding::Utf8, ), + "false", format!( "{}", matches!( @@ -1296,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}, @@ -1335,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); @@ -1346,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 global_component_memories_class = + Intrinsic::GlobalComponentMemoriesClass.name(); + uwriteln!( + self.src.js_init, + r#"{global_component_memories_class}.save({{ + 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}, }} - ); + )); "#, ); } @@ -1401,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); @@ -1410,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 global_component_memories_class = + Intrinsic::GlobalComponentMemoriesClass.name(); + uwriteln!( + self.src.js_init, + r#"{global_component_memories_class}.save({{ + componentIdx: {component_instance_id}, + memory: memory{memory_idx}, + }});"# + ); + uwriteln!( self.src.js, r#" @@ -1430,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 intrinsic_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({intrinsic_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", @@ -1885,14 +1943,13 @@ impl<'a> Instantiator<'a, '_> { getPostReturnFn: () => {post_return_fn}, callbackIdx: {callback_idx}, getCallbackFn: () => {callback_fn}, - getCallee: () => {callback_fn}, }}, );", ); } Trampoline::LowerImport { - index, + index: _, lower_ty, options, } => { @@ -1905,12 +1962,6 @@ impl<'a> Instantiator<'a, '_> { .get(*options) .expect("failed to find options"); - let fn_idx = index.as_u32(); - - // TODO: generate lifts for the parameters? - let (_options, lowered_fn_trampoline_idx, _lowered_fn_type_idx) = - self.lowering_options[*index]; - // 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*) @@ -1978,12 +2029,9 @@ 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: For Trampoline::LowerImport, the trampoline index is actually already defined, - // but we *redefine* it to call the lower import function first. - uwriteln!( - self.src.js, - r#" - const trampoline{i} = new WebAssembly.Suspending({lower_import_fn}.bind( + // Build the lower import call that will wrap the actual trampoline + let call = format!( + r#"{lower_import_fn}.bind( null, {{ trampolineIdx: {i}, @@ -1991,6 +2039,7 @@ impl<'a> Instantiator<'a, '_> { isAsync: {is_async}, 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}, @@ -1999,9 +2048,20 @@ impl<'a> Instantiator<'a, '_> { 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 { + uwriteln!(self.src.js, "const trampoline{i} = {call};"); + } } Trampoline::Transcoder { @@ -2235,23 +2295,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}, + }}); + "# ); } @@ -2327,6 +2399,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()); @@ -2352,6 +2440,7 @@ impl<'a> Instantiator<'a, '_> { memoryIdx: {memory_idx_js}, callbackFnIdx: {callback_fn_idx}, liftFns: {lift_fns_js}, + lowerFns: {lower_fns_js}, }}, );", ); @@ -2448,7 +2537,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}"; + "# ); } @@ -2467,38 +2559,44 @@ impl<'a> Instantiator<'a, '_> { } 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;"); + // NOTE: we can't maintain sync and async reallocs because + // simply calling a sync host import requires waiting. + // + // We don't know if the host fn is async so we have to + // assume it is. uwriteln!(self.src.js_init, "realloc{idx} = {def};",); + uwriteln!( + self.src.js_init, + "realloc{idx}Async = WebAssembly.promising({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}Async = WebAssembly.promising({def});" + ); uwriteln!(self.src.js_init, "postReturn{idx} = {def};"); } GlobalInitializer::Resource(_) => {} - GlobalInitializer::ExtractTable(extract_table) => { - let _ = extract_table; - } + GlobalInitializer::ExtractTable(_) => {} } } @@ -2796,15 +2894,18 @@ impl<'a> Instantiator<'a, '_> { "\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, @@ -2814,6 +2915,12 @@ impl<'a> Instantiator<'a, '_> { is_async, }); uwriteln!(self.src.js, ""); + + uwriteln!( + self.src.js, + "_trampoline{trampoline_idx}.fnName = '{}#{callee_name}';", + iface_name.unwrap_or_default(), + ); } Some(BindingsMode::Optimized) | Some(BindingsMode::DirectOptimized) => { @@ -2940,22 +3047,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) @@ -3229,7 +3323,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. @@ -3442,15 +3536,25 @@ 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(), + is_async.then_some("Async").unwrap_or_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(), + is_async.then_some("Async").unwrap_or_default() + ) + }); let tracing_prefix = format!( "[iface=\"{}\", function=\"{}\"]", @@ -3931,13 +4035,7 @@ 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)); + .push((core_export_fn.clone(), is_async)); local_name } }; 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 1d88f3776..688842e5f 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 @@ -154,7 +154,7 @@ suite("post-return async sleep scenario", () => { }, transpile: { extraArgs: { - minify: false, + // minify: false, asyncImports: [ // Host-provided async imports must be marked as such "local:local/sleep#sleep-millis", @@ -169,7 +169,6 @@ suite("post-return async sleep scenario", () => { }); const instance = res.instance; cleanup = res.cleanup; - console.log("OUTPUT DIR", res.outputDir); const result = await instance["local:local/sleep-post-return"].run(waitTimeMs); expect(result).toBeUndefined(); From 07407fe6f328c57896b1c9161f0ddc82fd4fa589 Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Mon, 9 Mar 2026 16:44:49 +0900 Subject: [PATCH 05/15] chore(bindgen): fix lint --- crates/js-component-bindgen/src/function_bindgen.rs | 6 +++--- crates/js-component-bindgen/src/transpile_bindgen.rs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/js-component-bindgen/src/function_bindgen.rs b/crates/js-component-bindgen/src/function_bindgen.rs index e8f45aba4..f99333d4b 100644 --- a/crates/js-component-bindgen/src/function_bindgen.rs +++ b/crates/js-component-bindgen/src/function_bindgen.rs @@ -1149,7 +1149,7 @@ impl Bindgen for FunctionBindgen<'_> { realloc_call = if self.is_async { format!("await {realloc}") } else { - format!("{realloc}") + realloc.to_string() }, ); @@ -1303,7 +1303,7 @@ impl Bindgen for FunctionBindgen<'_> { realloc_call = if self.is_async { format!("await {realloc}") } else { - format!("{realloc}") + realloc.to_string() }, ); @@ -1832,7 +1832,7 @@ impl Bindgen for FunctionBindgen<'_> { realloc_call = if self.is_async { format!("await {realloc}") } else { - format!("{realloc}") + realloc.to_string() }, size = size.size_wasm32() ); diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index a2fef176f..665ad9348 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -1259,7 +1259,7 @@ impl<'a> Instantiator<'a, '_> { "0".into(), "null".into(), "null".into(), - "true".into(), + "true", "false".into(), "false".into(), "false".into(), @@ -3540,7 +3540,7 @@ impl<'a> Instantiator<'a, '_> { format!( "realloc{}{}", idx.as_u32(), - is_async.then_some("Async").unwrap_or_default() + if is_async { "Async" } else { Default::default() } ) }), ) @@ -3552,7 +3552,7 @@ impl<'a> Instantiator<'a, '_> { format!( "postReturn{}{}", idx.as_u32(), - is_async.then_some("Async").unwrap_or_default() + if is_async { "Async" } else { Default::default() } ) }); From dc3fabd8df973d3167b4404233c75cd7ead3f39b Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Mon, 9 Mar 2026 16:44:54 +0900 Subject: [PATCH 06/15] chore(jco): fix lint --- .../test/p3/ported/wasmtime/component-async/post-return.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) 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 688842e5f..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 @@ -179,12 +179,7 @@ suite("post-return async sleep scenario", () => { 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", - ); - + assert.sameMembers(observedValues, expectedValues, "all expected values were not observed"); } finally { if (cleanup) { await cleanup(); From 6b4ccdddff942a872139119d24097d85d44b412c Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Mon, 9 Mar 2026 16:45:56 +0900 Subject: [PATCH 07/15] test(jco): set test timeout to 5m --- packages/jco/test/vitest.lts.ts | 2 +- packages/jco/test/vitest.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/jco/test/vitest.lts.ts b/packages/jco/test/vitest.lts.ts index 736319dbd..03e8b1643 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 * 5; // 5m 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..bde030235 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 * 5; // 5m const REPORTERS = process.env.GITHUB_ACTIONS ? ["verbose", "github-actions"] : ["verbose"]; From 022d9e583b5cdba802c61671a9b3dfc8fca3e141 Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Mon, 9 Mar 2026 16:53:00 +0900 Subject: [PATCH 08/15] fix(bindgen): cargo fmt --- crates/js-component-bindgen/src/transpile_bindgen.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index 665ad9348..0824458d1 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -3540,7 +3540,11 @@ impl<'a> Instantiator<'a, '_> { format!( "realloc{}{}", idx.as_u32(), - if is_async { "Async" } else { Default::default() } + if is_async { + "Async" + } else { + Default::default() + } ) }), ) @@ -3552,7 +3556,11 @@ impl<'a> Instantiator<'a, '_> { format!( "postReturn{}{}", idx.as_u32(), - if is_async { "Async" } else { Default::default() } + if is_async { + "Async" + } else { + Default::default() + } ) }); From 2a9fcfe68398ab2b56ed0e87759fc86b91bea0a3 Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Mon, 9 Mar 2026 16:54:36 +0900 Subject: [PATCH 09/15] test(jco): reduce test timeout to 1m --- packages/jco/test/vitest.lts.ts | 2 +- packages/jco/test/vitest.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/jco/test/vitest.lts.ts b/packages/jco/test/vitest.lts.ts index 03e8b1643..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 * 5; // 5m +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 bde030235..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 * 5; // 5m +const DEFAULT_TIMEOUT_MS = 1000 * 60 * 1; // 1m const REPORTERS = process.env.GITHUB_ACTIONS ? ["verbose", "github-actions"] : ["verbose"]; From 3c6523295d3135c9e4d974581eb39a3da1880043 Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Mon, 9 Mar 2026 21:11:42 +0900 Subject: [PATCH 10/15] refactor(bindgen): global memory->component lookup --- .../src/intrinsics/mod.rs | 120 ++++++++---------- .../src/intrinsics/p3/async_task.rs | 6 +- .../src/intrinsics/p3/host.rs | 7 +- .../src/transpile_bindgen.rs | 39 ++++-- 4 files changed, 86 insertions(+), 86 deletions(-) diff --git a/crates/js-component-bindgen/src/intrinsics/mod.rs b/crates/js-component-bindgen/src/intrinsics/mod.rs index 4d023d925..3f05578d8 100644 --- a/crates/js-component-bindgen/src/intrinsics/mod.rs +++ b/crates/js-component-bindgen/src/intrinsics/mod.rs @@ -133,14 +133,14 @@ pub enum Intrinsic { /// handle, so this helper checks for that. IsBorrowedType, - /// 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 + GlobalComponentMemoryMap, + + /// Tracking of component memories + RegisterGlobalMemoryForComponent, /// Tracking of component memories - GlobalComponentMemoriesClass, + LookupMemoriesForComponent, } impl Intrinsic { @@ -748,70 +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::GlobalComponentMemoriesClass => { - let global_component_memories_class = - Intrinsic::GlobalComponentMemoriesClass.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_memories_class} {{ - static map = new Map(); + 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 }}); + }} + "#) + ); + } - constructor() {{ throw new Error('{global_component_memories_class} should not be constructed'); }} + Intrinsic::LookupMemoriesForComponent => { + let global_component_memory_map = Intrinsic::GlobalComponentMemoryMap.name(); + let lookup_global_memories_for_component = + Intrinsic::LookupMemoriesForComponent.name(); + output.push_str(&format!( + r#" + function {lookup_global_memories_for_component}(args) {{ + const {{ componentIdx }} = args ?? {{}}; + if (args.componentIdx === undefined) {{ throw new TypeError("missing component idx"); }} - 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 }}); - }} + const metas = {global_component_memory_map}.get(componentIdx); + if (!metas) {{ return []; }} - static getMemoriesForComponentIdx(componentIdx) {{ - const metas = {global_component_memories_class}.map.get(componentIdx); - return metas.map(meta => meta.memory); - }} + if (args.memoryIdx === undefined) {{ + 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; + }} + "#) + ); } } } @@ -855,11 +841,12 @@ pub struct RenderIntrinsicsArgs<'a> { } /// Intrinsics that should be rendered as early as possible -const EARLY_INTRINSICS: [Intrinsic; 23] = [ +const EARLY_INTRINSICS: [Intrinsic; 24] = [ Intrinsic::DebugLog, Intrinsic::GlobalAsyncDeterminism, - Intrinsic::GlobalAsyncParamLowersClass, - Intrinsic::GlobalComponentMemoriesClass, + Intrinsic::GlobalComponentMemoryMap, + Intrinsic::LookupMemoriesForComponent, + Intrinsic::RegisterGlobalMemoryForComponent, Intrinsic::RepTableClass, Intrinsic::CoinFlip, Intrinsic::ScopeId, @@ -1328,8 +1315,11 @@ impl Intrinsic { // Async Intrinsic::GlobalAsyncDeterminism => "ASYNC_DETERMINISM", Intrinsic::CoinFlip => "_coinFlip", - 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", 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 8bc2286b0..492487757 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs @@ -1270,8 +1270,8 @@ impl AsyncTaskIntrinsic { let subtask_class = Self::AsyncSubtaskClass.name(); let get_or_create_async_state_fn = Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState).name(); - let global_component_memories_class = - Intrinsic::GlobalComponentMemoriesClass.name(); + let lookup_memories_for_component = + Intrinsic::LookupMemoriesForComponent.name(); output.push_str(&format!(r#" class {subtask_class} {{ @@ -1505,7 +1505,7 @@ impl AsyncTaskIntrinsic { // 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() ?? {global_component_memories_class}.getMemoriesForComponentIdx(this.#parentTask?.componentIdx())[0]; + 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 diff --git a/crates/js-component-bindgen/src/intrinsics/p3/host.rs b/crates/js-component-bindgen/src/intrinsics/p3/host.rs index 594cd8587..e537c7abe 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/host.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/host.rs @@ -270,8 +270,7 @@ impl HostIntrinsic { let async_event_code_enum = Intrinsic::AsyncEventCodeEnum.name(); let async_driver_loop_fn = Intrinsic::AsyncTask(AsyncTaskIntrinsic::DriverLoop).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 @@ -354,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]; }} diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index 0824458d1..c86179eb7 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -1377,11 +1377,11 @@ impl<'a> Instantiator<'a, '_> { // 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 global_component_memories_class = - Intrinsic::GlobalComponentMemoriesClass.name(); + let register_global_memory_for_component_fn = + Intrinsic::RegisterGlobalMemoryForComponent.name(); uwriteln!( self.src.js_init, - r#"{global_component_memories_class}.save({{ + r#"{register_global_memory_for_component_fn}({{ componentIdx: {component_instance_id}, memory: memory{memory_idx}, }});"# @@ -1454,11 +1454,11 @@ impl<'a> Instantiator<'a, '_> { // 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 global_component_memories_class = - Intrinsic::GlobalComponentMemoriesClass.name(); + let register_global_memory_for_component_fn = + Intrinsic::RegisterGlobalMemoryForComponent.name(); uwriteln!( self.src.js_init, - r#"{global_component_memories_class}.save({{ + r#"{register_global_memory_for_component_fn}({{ componentIdx: {component_instance_id}, memory: memory{memory_idx}, }});"# @@ -2570,15 +2570,18 @@ impl<'a> Instantiator<'a, '_> { let idx = r.index.as_u32(); uwriteln!(self.src.js, "let realloc{idx};"); uwriteln!(self.src.js, "let realloc{idx}Async;"); - // NOTE: we can't maintain sync and async reallocs because - // simply calling a sync host import requires waiting. - // - // We don't know if the host fn is async so we have to - // assume it is. 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, - "realloc{idx}Async = WebAssembly.promising({def});", + r#" + try {{ + realloc{idx}Async = WebAssembly.promising({def}); + }} catch(err) {{ + realloc{idx}Async = {def}; + }} + "# ); } @@ -2587,11 +2590,19 @@ impl<'a> Instantiator<'a, '_> { 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, - "postReturn{idx}Async = WebAssembly.promising({def});" + r#" + try {{ + postReturn{idx}Async = WebAssembly.promising({def}); + }} catch(err) {{ + postReturn{idx}Async = {def}; + }} + "# ); - uwriteln!(self.src.js_init, "postReturn{idx} = {def};"); } GlobalInitializer::Resource(_) => {} From c4215c90ee708cbd2c078a3552335c0399e33c70 Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Mon, 9 Mar 2026 21:14:27 +0900 Subject: [PATCH 11/15] test(jco): add note to test about rebuilds --- packages/jco/test/runtime.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 19c5f010e5770df8dc8e0992fe24cdffe42552b9 Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Mon, 9 Mar 2026 22:23:02 +0900 Subject: [PATCH 12/15] fix(bindgen): sync symmetric call usage --- .../src/function_bindgen.rs | 14 ++--------- .../src/intrinsics/p3/async_task.rs | 25 +++++++++++++++++-- .../src/intrinsics/p3/host.rs | 10 ++++---- .../src/transpile_bindgen.rs | 10 +++----- 4 files changed, 33 insertions(+), 26 deletions(-) diff --git a/crates/js-component-bindgen/src/function_bindgen.rs b/crates/js-component-bindgen/src/function_bindgen.rs index f99333d4b..86bb9372c 100644 --- a/crates/js-component-bindgen/src/function_bindgen.rs +++ b/crates/js-component-bindgen/src/function_bindgen.rs @@ -1396,8 +1396,8 @@ impl Bindgen for FunctionBindgen<'_> { // 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})"); + 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...() ') @@ -2524,16 +2524,6 @@ impl Bindgen for FunctionBindgen<'_> { 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'); }} 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 492487757..09366ae0d 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs @@ -1954,16 +1954,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) {{ @@ -1972,6 +1980,19 @@ 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', + }}); }} "#, )); diff --git a/crates/js-component-bindgen/src/intrinsics/p3/host.rs b/crates/js-component-bindgen/src/intrinsics/p3/host.rs index e537c7abe..2ea227c01 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/host.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/host.rs @@ -141,7 +141,7 @@ impl HostIntrinsic { callerInstanceIdx, calleeInstanceIdx, taskReturnTypeIdx, - isCalleeAsyncInt, + calleeIsAsyncInt, stringEncoding, resultCountOrAsync, ) {{ @@ -150,7 +150,7 @@ impl HostIntrinsic { callerInstanceIdx, calleeInstanceIdx, taskReturnTypeIdx, - isCalleeAsyncInt, + calleeIsAsyncInt, stringEncoding, resultCountOrAsync, }}); @@ -219,10 +219,10 @@ impl HostIntrinsic { throw new Error(`unrecognized string encoding enum [${{stringEncoding}}]`); }} - const isCalleeAsync = isCalleeAsyncInt !== 0; + const calleeIsAsync = calleeIsAsyncInt !== 0; const [newTask, newTaskID] = {create_new_current_task_fn}({{ componentIdx: calleeInstanceIdx, - isAsync: isCalleeAsync, + isAsync: calleeIsAsync, getCalleeParamsFn, entryFnName: 'task/' + currentCallerTask.id() + '/new-prepare-task', stringEncoding, @@ -232,7 +232,7 @@ impl HostIntrinsic { componentIdx: callerInstanceIdx, parentTask: currentCallerTask, childTask: newTask, - isAsync: isCalleeAsync, + isAsync: calleeIsAsync, callMetadata: {{ getMemoryFn, memoryIdx, diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index c86179eb7..5e97b6eeb 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -1963,20 +1963,14 @@ impl<'a> Instantiator<'a, '_> { .expect("failed to find options"); // 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 func_ty = self.types.index(*lower_ty); // Build list of lift functions for the params of the lowered import @@ -2497,7 +2491,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}; + "#, ); } From 93c04fefac9940c6f9cb4d38ccf2cc87e6a513f0 Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Tue, 10 Mar 2026 02:10:28 +0900 Subject: [PATCH 13/15] fix(bindgen): backwards compat for manually specified async --- .../src/function_bindgen.rs | 2 + .../src/intrinsics/mod.rs | 2 +- .../src/intrinsics/p3/async_task.rs | 83 +++++++++++++++++-- .../src/intrinsics/string.rs | 3 +- .../src/transpile_bindgen.rs | 29 +++++-- 5 files changed, 103 insertions(+), 16 deletions(-) diff --git a/crates/js-component-bindgen/src/function_bindgen.rs b/crates/js-component-bindgen/src/function_bindgen.rs index 86bb9372c..c84b8f651 100644 --- a/crates/js-component-bindgen/src/function_bindgen.rs +++ b/crates/js-component-bindgen/src/function_bindgen.rs @@ -359,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 @@ -408,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}', diff --git a/crates/js-component-bindgen/src/intrinsics/mod.rs b/crates/js-component-bindgen/src/intrinsics/mod.rs index 3f05578d8..a4eba7d00 100644 --- a/crates/js-component-bindgen/src/intrinsics/mod.rs +++ b/crates/js-component-bindgen/src/intrinsics/mod.rs @@ -395,7 +395,7 @@ impl Intrinsic { const {fn_name} = (...args) => {{ if (!globalThis?.process?.env?.JCO_DEBUG) {{ return; }} console.debug(...args); - }} + }}; " )); } 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 09366ae0d..4d55c2329 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs @@ -589,6 +589,7 @@ impl AsyncTaskIntrinsic { const {{ componentIdx, isAsync, + isManualAsync, entryFnName, parentSubtaskID, callbackFnName, @@ -609,6 +610,7 @@ impl AsyncTaskIntrinsic { const newTask = new {task_class}({{ componentIdx, isAsync, + isManualAsync, entryFnName, callbackFn, callbackFnName, @@ -745,6 +747,7 @@ impl AsyncTaskIntrinsic { #componentIdx; #state; #isAsync; + #isManualAsync; #entryFnName = null; #subtasks = []; @@ -801,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 {{ @@ -956,14 +960,23 @@ impl AsyncTaskIntrinsic { 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(); @@ -1194,7 +1207,8 @@ 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.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`); }} @@ -1213,11 +1227,13 @@ impl AsyncTaskIntrinsic { {clear_current_task_fn}(this.#componentIdx); }} - needsExclusiveLock() {{ return !this.#isAsync || this.hasCallback(); }} + needsExclusiveLock() {{ + return !this.#isAsync || this.hasCallback(); + }} createSubtask(args) {{ {debug_log_fn}('[{task_class}#createSubtask()] args', args); - const {{ componentIdx, childTask, callMetadata, fnName, isAsync }} = args; + const {{ componentIdx, childTask, callMetadata, fnName, isAsync, isManualAsync }} = args; const cstate = {get_or_create_async_state_fn}(this.#componentIdx); if (!cstate) {{ @@ -1235,6 +1251,7 @@ impl AsyncTaskIntrinsic { parentTask: this, callMetadata, isAsync, + isManualAsync, fnName, waitable, }}); @@ -1270,8 +1287,7 @@ impl AsyncTaskIntrinsic { let subtask_class = Self::AsyncSubtaskClass.name(); let get_or_create_async_state_fn = Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState).name(); - let lookup_memories_for_component = - Intrinsic::LookupMemoriesForComponent.name(); + let lookup_memories_for_component = Intrinsic::LookupMemoriesForComponent.name(); output.push_str(&format!(r#" class {subtask_class} {{ @@ -1322,6 +1338,7 @@ impl AsyncTaskIntrinsic { fnName; target; isAsync; + isManualAsync; constructor(args) {{ if (typeof args.componentIdx !== 'number') {{ @@ -1348,6 +1365,7 @@ impl AsyncTaskIntrinsic { 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; }} @@ -1842,6 +1860,7 @@ impl AsyncTaskIntrinsic { functionIdx, componentIdx, isAsync, + isManualAsync, paramLiftFns, resultLowerFns, funcTypeIsAsync, @@ -1879,6 +1898,7 @@ impl AsyncTaskIntrinsic { parentTask: task, fnName: importFn.fnName, isAsync, + isManualAsync, callMetadata: {{ memoryIdx, memory, @@ -1895,8 +1915,13 @@ impl AsyncTaskIntrinsic { subtask.onStart(); // If dealing with a sync lowered sync function, we can directly return results - if (!isAsync && !funcTypeIsAsync) {{ + // + // 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'); }} @@ -1906,7 +1931,10 @@ impl AsyncTaskIntrinsic { // 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) - if (!isAsync && funcTypeIsAsync) {{ + // + // 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()) {{ @@ -1936,18 +1964,47 @@ impl AsyncTaskIntrinsic { }}); }}); + + 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 queueMicrotask(async () => {{ try {{ {debug_log_fn}('[{lower_import_fn}()] calling lowered import', {{ importFn, params }}); - await importFn(...params); + const res = await importFn(...params); + + // 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; }} }}); + // 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; }} "# @@ -1993,6 +2050,14 @@ impl AsyncTaskIntrinsic { 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/string.rs b/crates/js-component-bindgen/src/intrinsics/string.rs index 1cea41a3a..92e68c89f 100644 --- a/crates/js-component-bindgen/src/intrinsics/string.rs +++ b/crates/js-component-bindgen/src/intrinsics/string.rs @@ -121,7 +121,8 @@ impl StringIntrinsic { let buf = {encoder}.encode(s); 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 5e97b6eeb..0a003487c 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -1494,7 +1494,7 @@ impl<'a> Instantiator<'a, '_> { ty, async_, } => { - let intrinsic_fn = match trampoline { + let stream_cancel_fn = match trampoline { Trampoline::StreamCancelRead { .. } => self.bindgen.intrinsic( Intrinsic::AsyncStream(AsyncStreamIntrinsic::StreamCancelRead), ), @@ -1509,7 +1509,7 @@ impl<'a> Instantiator<'a, '_> { uwriteln!( self.src.js, r#" - const trampoline{i} = new WebAssembly.Suspending({intrinsic_fn}.bind(null, {{ + const trampoline{i} = new WebAssembly.Suspending({stream_cancel_fn}.bind(null, {{ streamTableIdx: {stream_table_idx}, isAsync: {async_}, componentIdx: {component_idx}, @@ -1969,6 +1969,7 @@ impl<'a> Instantiator<'a, '_> { let component_idx = canon_opts.instance.as_u32(); let is_async = canon_opts.async_; + let cancellable = canon_opts.cancellable; let func_ty = self.types.index(*lower_ty); @@ -2031,6 +2032,7 @@ impl<'a> Instantiator<'a, '_> { 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}, @@ -2054,7 +2056,12 @@ impl<'a> Instantiator<'a, '_> { "const trampoline{i} = new WebAssembly.Suspending({call});" ); } else { - uwriteln!(self.src.js, "const trampoline{i} = {call};"); + // 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};" + ); } } @@ -2886,7 +2893,8 @@ impl<'a> Instantiator<'a, '_> { let trampoline_idx = trampoline.as_u32(); match self.bindgen.opts.import_bindings { None | Some(BindingsMode::Js) | Some(BindingsMode::Hybrid) => { - if is_async { + // 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 @@ -2928,6 +2936,14 @@ impl<'a> Instantiator<'a, '_> { "_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;" + ); + } } Some(BindingsMode::Optimized) | Some(BindingsMode::DirectOptimized) => { @@ -4050,7 +4066,10 @@ impl<'a> Instantiator<'a, '_> { uwriteln!(self.src.js, "let {local_name};"); self.bindgen .all_core_exported_funcs - .push((core_export_fn.clone(), is_async)); + // 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 } }; From 87237c92e73468ddcb40b78fdd0f9970266f300a Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Tue, 10 Mar 2026 02:10:42 +0900 Subject: [PATCH 14/15] refactor(jco): slight refactor of tests --- packages/jco/src/cmd/transpile.js | 2 +- packages/jco/test/jspi.js | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) 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/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 {} }); From 100c4d5495a91ba0d6e838c2d58eecd84972a3af Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Tue, 10 Mar 2026 02:16:14 +0900 Subject: [PATCH 15/15] test(jco): increase allowed char limit for transpiled flavorful test --- packages/jco/test/api.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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); }); });