From 0f9f909311a47bf47bedd3b65f41aea4c313da71 Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Wed, 2 Sep 2026 19:09:17 +0000 Subject: [PATCH 1/7] fix(ops): render component WAST enums as strings --- crates/xtask/src/build/wast_fixtures.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/xtask/src/build/wast_fixtures.rs b/crates/xtask/src/build/wast_fixtures.rs index 4a4ba21b0..ac651a9eb 100644 --- a/crates/xtask/src/build/wast_fixtures.rs +++ b/crates/xtask/src/build/wast_fixtures.rs @@ -473,7 +473,7 @@ fn cm_val_to_js_param(wast_val: &wast::component::WastVal<'_>) -> Result )), None => Ok(format!("{{ tag: {} }}", js_string(tag)?)), }, - wast::component::WastVal::Enum(v) => Ok(format!("{{ tag: {} }}", js_string(v)?)), + wast::component::WastVal::Enum(v) => js_string(v), wast::component::WastVal::Option(wast_val) => match wast_val { Some(v) => Ok(format!( "{{ tag: 'some', val: {} }}", @@ -944,6 +944,10 @@ mod tests { cm_val_to_js_param(&WastVal::String("quote: \" and newline:\n"))?, r#""quote: \" and newline:\n""# ); + assert_eq!( + cm_val_to_js_param(&WastVal::Enum("stream-write"))?, + r#""stream-write""# + ); assert_eq!(float_to_js(f64::NEG_INFINITY), "-Infinity"); assert_eq!(float_to_js(-0.0), "-0"); assert_eq!( From 733327079389cc2163d9393c888c302bc666a571 Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Wed, 2 Sep 2026 19:09:35 +0000 Subject: [PATCH 2/7] fix(bindgen): enforce canonical async operation state --- .../src/intrinsics/p3/async_future.rs | 11 +++-- .../src/intrinsics/p3/async_stream.rs | 45 +++++++++++-------- .../src/intrinsics/p3/mod.rs | 2 + 3 files changed, 36 insertions(+), 22 deletions(-) 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 ab046e7e6..09c59e9db 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/async_future.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/async_future.rs @@ -7,7 +7,10 @@ use crate::intrinsics::{Intrinsic, RenderIntrinsicsArgs}; use crate::source::Source; use crate::uwriteln; -use super::{CANNOT_LIFT_FUTURE_IN_WAITABLE_SET, async_task::AsyncTaskIntrinsic}; +use super::{ + CANNOT_LIFT_FUTURE_IN_WAITABLE_SET, CANNOT_START_CONCURRENT_OPERATION, + async_task::AsyncTaskIntrinsic, +}; /// This enum contains intrinsics that enable Futures #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] @@ -1562,7 +1565,7 @@ impl AsyncFutureIntrinsic { throw new {runtime_error_class}(message); }} if (!futureEnd.isIdleState()) {{ - throw new Error('future state must be idle before {future_op_fn}'); + throw new {runtime_error_class}({CANNOT_START_CONCURRENT_OPERATION:?}); }} futureEnd.{guest_op_fn}({{ @@ -1632,9 +1635,9 @@ impl AsyncFutureIntrinsic { }; output.push_str(&format!(r#" - async function {future_cancel_fn}( + function {future_cancel_fn}( ctx, - futureEndIdx, + futureEndWaitableIdx, ) {{ {debug_log_fn}('[{future_cancel_fn}()] args', {{ ctx, 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 53c5ba2be..c119a3bb4 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/async_stream.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/async_stream.rs @@ -5,7 +5,10 @@ use crate::{ source::Source, }; -use super::{CANNOT_LIFT_STREAM_IN_WAITABLE_SET, async_task::AsyncTaskIntrinsic}; +use super::{ + CANNOT_LIFT_STREAM_IN_WAITABLE_SET, CANNOT_START_CONCURRENT_OPERATION, + async_task::AsyncTaskIntrinsic, +}; /// This enum contains intrinsics that enable Stream #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)] @@ -301,7 +304,6 @@ impl AsyncStreamIntrinsic { let get_or_create_async_state_fn = render_args.require_intrinsic( Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState), ); - output.push_str(&format!( r#" function {add_stream_end_to_table_fn}(args) {{ @@ -728,6 +730,8 @@ impl AsyncStreamIntrinsic { let get_or_create_async_state_fn = render_args.require_intrinsic( Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState), ); + let runtime_error_class = + render_args.require_intrinsic(Intrinsic::WebAssemblyRuntimeError); // Internal helper fn that sets up for a `copy()` call let copy_setup_impl = format!( @@ -749,7 +753,7 @@ impl AsyncStreamIntrinsic { // 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'); + throw new {runtime_error_class}({CANNOT_START_CONCURRENT_OPERATION:?}); }} if (this.getCopyState() !== {stream_end_class}.CopyState.IDLE) {{ throw new Error(`stream copy state is not idle`); @@ -2306,7 +2310,7 @@ impl AsyncStreamIntrinsic { Intrinsic::Component(ComponentIntrinsic::GetOrCreateAsyncState), ); output.push_str(&format!(r#" - async function {stream_cancel_fn}(ctx, streamEndWaitableIdx) {{ + function {stream_cancel_fn}(ctx, streamEndWaitableIdx) {{ {debug_log_fn}('[{stream_cancel_fn}()] args', {{ ctx, streamEndWaitableIdx }}); const {{ streamTableIdx, isAsync, componentIdx }} = ctx; @@ -2321,8 +2325,22 @@ impl AsyncStreamIntrinsic { streamEnd.setCopyState({stream_end_class}.CopyState.CANCELLING_COPY); - if (!streamEnd.hasPendingEvent()) {{ + const finishCancel = () => {{ + 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; + }}; + + if (!streamEnd.hasPendingEvent()) {{ streamEnd.cancel(); if (!streamEnd.hasPendingEvent()) {{ @@ -2332,22 +2350,13 @@ impl AsyncStreamIntrinsic { 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() }}); + return task.suspendUntil({{ + readyFn: () => streamEnd.hasPendingEvent(), + }}).then(finishCancel); }} }} - 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; + return finishCancel(); }} "#)); } diff --git a/crates/js-component-bindgen/src/intrinsics/p3/mod.rs b/crates/js-component-bindgen/src/intrinsics/p3/mod.rs index 8f9e7c088..c07d3e0e6 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/mod.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/mod.rs @@ -9,3 +9,5 @@ pub(crate) const CANNOT_LIFT_FUTURE_IN_WAITABLE_SET: &str = "cannot lift future while it's in a waitable set"; pub(crate) const CANNOT_LIFT_STREAM_IN_WAITABLE_SET: &str = "cannot lift stream while it's in a waitable set"; +pub(crate) const CANNOT_START_CONCURRENT_OPERATION: &str = + "cannot have concurrent operations active on a future/stream"; From 4795ea4b4e57a4e902ad3f7b36c0316c0b890e34 Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Wed, 2 Sep 2026 19:10:10 +0000 Subject: [PATCH 3/7] fix(bindgen): preserve eager canonical async scheduling --- .../src/intrinsics/component.rs | 23 +- .../src/intrinsics/mod.rs | 147 +++++++- .../src/intrinsics/p3/async_task.rs | 340 ++++++++++++------ .../src/intrinsics/p3/waitable.rs | 113 ++++-- .../src/transpile_bindgen.rs | 144 +++++--- 5 files changed, 560 insertions(+), 207 deletions(-) diff --git a/crates/js-component-bindgen/src/intrinsics/component.rs b/crates/js-component-bindgen/src/intrinsics/component.rs index 080b04c97..956a6cd3f 100644 --- a/crates/js-component-bindgen/src/intrinsics/component.rs +++ b/crates/js-component-bindgen/src/intrinsics/component.rs @@ -565,7 +565,7 @@ impl ComponentIntrinsic { // Awaitable acquisition: takes the lock immediately when free, // otherwise queues FIFO behind the current holder and earlier // waiters. The resolved promise implies ownership. - async acquireExclusiveLock(taskID) {{ + acquireExclusiveLock(taskID) {{ if (taskID === undefined || taskID === null) {{ throw new Error('exclusive lock requires the acquiring task id'); }} @@ -586,7 +586,7 @@ impl ComponentIntrinsic { componentIdx: this.#componentIdx, queued: this.#lockWaiters.length, }}); - await new Promise((resolve) => {{ + return new Promise((resolve) => {{ this.#lockWaiters.push({{ taskID, resolve }}); }}); }} @@ -690,7 +690,7 @@ impl ComponentIntrinsic { // TODO(threads): readyFn is normally on the thread suspendTask(args) {{ - const {{ task, readyFn }} = args; + const {{ task, readyFn, cancellable, onResume }} = args; const taskID = task.id(); const componentIdx = task.componentIdx(); {debug_log_fn}('[{component_async_state_class}#suspendTask()]', {{ @@ -708,10 +708,19 @@ impl ComponentIntrinsic { throw new Error(`task [${{taskID}}] already suspended`); }} - const {{ promise, resolve, reject }} = {promise_with_resolvers_fn}(); + let promise; + let resume; + if (onResume) {{ + resume = () => onResume(!task.isCancelled()); + }} else {{ + const resolvers = {promise_with_resolvers_fn}(); + promise = resolvers.promise; + resume = () => resolvers.resolve(!task.isCancelled()); + }} this.#addSuspendedTaskMeta({{ task, taskID, + cancellable, readyFn, resume: () => {{ {debug_log_fn}('[{component_async_state_class}] resuming suspended task', {{ @@ -719,7 +728,7 @@ impl ComponentIntrinsic { componentIdx: this.#componentIdx, }}); // TODO(threads): it's thread cancellation we should be checking for below, not task - resolve(!task.isCancelled()); + resume(); }}, }}); @@ -751,6 +760,10 @@ impl ComponentIntrinsic { return meta.task.isRejected() || meta.readyFn(); }} + suspendedTaskCancellable(taskID) {{ + return !!this.#getSuspendedTaskMeta(taskID)?.cancellable; + }} + suspendedTaskMetas() {{ return this.#suspendedTasksByTaskID.values(); }} diff --git a/crates/js-component-bindgen/src/intrinsics/mod.rs b/crates/js-component-bindgen/src/intrinsics/mod.rs index 54d6f7cb2..e3c915958 100644 --- a/crates/js-component-bindgen/src/intrinsics/mod.rs +++ b/crates/js-component-bindgen/src/intrinsics/mod.rs @@ -170,6 +170,18 @@ pub enum Intrinsic { /// Wrap the JS payload of a `WebAssembly.Suspending` import so the /// importing component's current-task register survives suspension SuspendingImportWrapperFn, + + /// Build an `(i32) -> i32` Wasm trampoline that calls a plain fast path + /// before falling back to a JSPI-suspending slow path when it returns -2. + ConditionalSuspending1I32ToI32Fn, + + /// Build an `(i32, i32) -> i32` Wasm trampoline that calls a plain fast + /// path before falling back to a JSPI-suspending slow path. + ConditionalSuspending2I32ToI32Fn, + + /// Build an `(i32, i32, i32) -> ()` Wasm trampoline that calls a plain fast + /// path before falling back to a JSPI-suspending slow path. + ConditionalSuspending3I32ToVoidFn, } macro_rules! impl_from_intrinsic { @@ -1240,6 +1252,93 @@ impl Intrinsic { )); } + Self::ConditionalSuspending2I32ToI32Fn => { + let conditional_suspending_fn = + args.require_intrinsic(Self::ConditionalSuspending2I32ToI32Fn); + + output.push_str(&format!( + r#" + const {conditional_suspending_fn}Module = new WebAssembly.Module(new Uint8Array([ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, + 0x01, 0x07, 0x01, 0x60, 0x02, 0x7f, 0x7f, 0x01, 0x7f, + 0x02, 0x11, 0x02, 0x00, 0x04, 0x66, 0x61, 0x73, 0x74, + 0x00, 0x00, 0x00, 0x04, 0x73, 0x6c, 0x6f, 0x77, 0x00, + 0x00, 0x03, 0x02, 0x01, 0x00, 0x07, 0x07, 0x01, 0x03, + 0x72, 0x75, 0x6e, 0x00, 0x02, 0x0a, 0x1d, 0x01, 0x1b, + 0x01, 0x01, 0x7f, 0x20, 0x00, 0x20, 0x01, 0x10, 0x00, + 0x22, 0x02, 0x41, 0x7f, 0x47, 0x04, 0x7f, 0x20, 0x02, + 0x05, 0x20, 0x00, 0x20, 0x01, 0x10, 0x01, 0x0b, 0x0b, + ])); + + function {conditional_suspending_fn}(fast, slow) {{ + return new WebAssembly.Instance( + {conditional_suspending_fn}Module, + {{ '': {{ fast, slow: new WebAssembly.Suspending(slow) }} }}, + ).exports.run; + }} + "#, + )); + } + + Self::ConditionalSuspending1I32ToI32Fn => { + let conditional_suspending_fn = + args.require_intrinsic(Self::ConditionalSuspending1I32ToI32Fn); + + output.push_str(&format!( + r#" + const {conditional_suspending_fn}Module = new WebAssembly.Module(new Uint8Array([ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, + 0x01, 0x06, 0x01, 0x60, 0x01, 0x7f, 0x01, 0x7f, + 0x02, 0x11, 0x02, 0x00, 0x04, 0x66, 0x61, 0x73, + 0x74, 0x00, 0x00, 0x00, 0x04, 0x73, 0x6c, 0x6f, + 0x77, 0x00, 0x00, 0x03, 0x02, 0x01, 0x00, 0x07, + 0x07, 0x01, 0x03, 0x72, 0x75, 0x6e, 0x00, 0x02, + 0x0a, 0x19, 0x01, 0x17, 0x01, 0x01, 0x7f, 0x20, + 0x00, 0x10, 0x00, 0x22, 0x01, 0x41, 0x7e, 0x47, + 0x04, 0x7f, 0x20, 0x01, 0x05, 0x20, 0x00, 0x10, + 0x01, 0x0b, 0x0b, + ])); + + function {conditional_suspending_fn}(fast, slow) {{ + return new WebAssembly.Instance( + {conditional_suspending_fn}Module, + {{ '': {{ fast, slow: new WebAssembly.Suspending(slow) }} }}, + ).exports.run; + }} + "#, + )); + } + + Self::ConditionalSuspending3I32ToVoidFn => { + let conditional_suspending_fn = + args.require_intrinsic(Self::ConditionalSuspending3I32ToVoidFn); + + output.push_str(&format!( + r#" + const {conditional_suspending_fn}Module = new WebAssembly.Module(new Uint8Array([ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, + 0x01, 0x0e, 0x02, 0x60, 0x03, 0x7f, 0x7f, 0x7f, + 0x01, 0x7f, 0x60, 0x03, 0x7f, 0x7f, 0x7f, 0x00, + 0x02, 0x11, 0x02, 0x00, 0x04, 0x66, 0x61, 0x73, + 0x74, 0x00, 0x00, 0x00, 0x04, 0x73, 0x6c, 0x6f, + 0x77, 0x00, 0x00, 0x03, 0x02, 0x01, 0x01, 0x07, + 0x07, 0x01, 0x03, 0x72, 0x75, 0x6e, 0x00, 0x02, + 0x0a, 0x19, 0x01, 0x17, 0x00, 0x20, 0x00, 0x20, + 0x01, 0x20, 0x02, 0x10, 0x00, 0x45, 0x04, 0x40, + 0x20, 0x00, 0x20, 0x01, 0x20, 0x02, 0x10, 0x01, + 0x1a, 0x0b, 0x0b, + ])); + + function {conditional_suspending_fn}(fast, slow) {{ + return new WebAssembly.Instance( + {conditional_suspending_fn}Module, + {{ '': {{ fast, slow: new WebAssembly.Suspending(slow) }} }}, + ).exports.run; + }} + "#, + )); + } + // TODO(feat): customizable stream classes Intrinsic::PlatformReadableStreamClass => { let name = self.name(); @@ -1593,22 +1692,22 @@ mod tests { #[test] fn subtask_cancel_drives_one_cancellable_child_slice() { let cancel = render_intrinsic_body(Intrinsic::AsyncTask(AsyncTaskIntrinsic::SubtaskCancel)); - assert!(cancel.contains("const childProgress = childTask?.waitForProgress();")); - assert!(cancel.contains("subtask.requestCancellation();")); - assert!(cancel.contains("await childProgress;")); - assert!(cancel.contains("if (isAsync) { return 0xFFFFFFFF; }")); + assert!(cancel.contains("childState.suspendedTaskReady(childTask.id())")); + assert!(cancel.contains("childState.resumeTaskByID(childTask.id())")); + assert!(cancel.contains("function subtaskCancel")); + assert!(!cancel.contains("async function subtaskCancel")); + assert!(cancel.contains(".then(finishCancel)")); + assert!(cancel.contains("cancellationWillCompleteAsync ? 0xFFFFFFFE : 0xFFFFFFFF")); let task = render_intrinsic_body(Intrinsic::AsyncTask(AsyncTaskIntrinsic::AsyncTaskClass)); - assert!(task.contains("#progressWaiters = [];")); - assert!(task.contains("waitForProgress()")); - let exit = task - .find("exit(args)") - .expect("task class should contain an exit method"); - let exit_body = &task[exit..]; - let release = exit_body + assert!(task.contains("suspendUntilCallback(opts, onResume)")); + assert!(task.contains("this.#callbackFn._jcoMaySuspend === false")); + assert!(task.contains("if (this.isResolvedState() || this.isCancelled())")); + assert!(task.contains("cancellationRequested() { return this.cancelRequested; }")); + let release = task .find("state.exclusiveRelease(this.#id);") .expect("task exit should release component entry"); - let progress = exit_body + let progress = task .find("this.notifyProgress();") .expect("task exit should report progress"); assert!(release < progress); @@ -1617,16 +1716,34 @@ mod tests { let state = render_intrinsic_body(Intrinsic::Component( ComponentIntrinsic::ComponentAsyncStateClass, )); + assert!(state.contains("const { task, readyFn, cancellable, onResume } = args;")); + assert!(state.contains("resume = () => onResume(!task.isCancelled());")); + assert!(state.contains("suspendedTaskCancellable(taskID)")); assert!(state.contains("task.notifyProgress();")); assert!(state.contains("suspendedTaskReady(taskID)")); } #[test] fn cancellable_wait_poll_and_yield_reach_task_state() { + let waitable_set = + render_intrinsic_body(Intrinsic::Waitable(WaitableIntrinsic::WaitableSetClass)); + assert!(waitable_set.contains("tryWait(opts)")); + assert!(waitable_set.contains("waitUntil(opts)")); + assert!(!waitable_set.contains("async waitUntil(opts)")); + assert!(waitable_set.contains("if (isReady()) { return finishWait(true); }")); + let wait = render_intrinsic_body(Intrinsic::Waitable(WaitableIntrinsic::WaitableSetWait)); assert!(wait.contains("const wset = cstate.handles.get(waitableSetRep);")); assert!(!wait.contains("await cstate.handles.get(waitableSetRep);")); assert!(wait.contains("cancellable: isCancellable")); + assert!(wait.contains("function waitableSetWait")); + assert!(!wait.contains("async function waitableSetWait")); + assert!(wait.contains("syncOnly ? wset.tryWait(waitOpts) : wset.waitUntil(waitOpts)")); + assert!(wait.contains("return event.then(storeEvent);")); + + let conditional = render_intrinsic_body(Intrinsic::ConditionalSuspending2I32ToI32Fn); + assert!(conditional.contains("new WebAssembly.Suspending(slow)")); + assert!(conditional.contains("0x66, 0x61, 0x73, 0x74")); let poll = render_intrinsic_body(Intrinsic::Waitable(WaitableIntrinsic::WaitableSetPoll)); assert!(poll.contains("deliverPendingCancel({ cancellable: isCancellable })")); @@ -1751,6 +1868,9 @@ mod tests { assert!(enter.contains("isAsync: false,")); assert!(enter.contains("isAsync: !!calleeIsAsync,")); assert!(enter.contains("isManualAsync: callerTask.isManualAsync(),")); + assert!(enter.contains("syncOnly && !calleeIsAsync && cstate.isExclusivelyLocked()")); + assert!(enter.contains("return 0;")); + assert!(enter.contains("return 1;")); assert!(enter.contains("previousTaskMayBlock: CURRENT_TASK_MAY_BLOCK.value,")); assert!(enter.contains("CURRENT_TASK_MAY_BLOCK.value = newTask.mayBlock() ? 1 : 0;")); @@ -2428,6 +2548,9 @@ impl Intrinsic { Self::WithGlobalCurrentTaskMetaFnAsync => "_withGlobalCurrentTaskMetaAsync", Self::ClearGlobalCurrentTaskMetaFn => "_clearCurrentTask", Self::SuspendingImportWrapperFn => "_suspendingImport", + Self::ConditionalSuspending1I32ToI32Fn => "_conditionalSuspending1I32ToI32", + Self::ConditionalSuspending2I32ToI32Fn => "_conditionalSuspending2I32ToI32", + Self::ConditionalSuspending3I32ToVoidFn => "_conditionalSuspending3I32ToVoid", // Iteratively saved metadata Intrinsic::GlobalComponentMemoryMap => "GLOBAL_COMPONENT_MEMORY_MAP", 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 f786aebb1..30fe9de74 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs @@ -648,7 +648,7 @@ impl AsyncTaskIntrinsic { // // See: https://github.com/WebAssembly/component-model/blob/main/design/mvp/CanonicalABI.md#-canon-subtaskcancel output.push_str(&format!(" - async function {subtask_cancel_fn}(componentIdx, isAsync, subtaskRep) {{ + function {subtask_cancel_fn}(componentIdx, isAsync, subtaskRep, slowOnly = false) {{ {debug_log_fn}('[{subtask_cancel_fn}()] args', {{ componentIdx, isAsync, subtaskRep }}); const state = {get_or_create_async_state_fn}(componentIdx); @@ -661,53 +661,86 @@ impl AsyncTaskIntrinsic { if (subtask.resolveDelivered()) {{ throw new Error('`subtask.cancel` called after terminal status delivered'); }} - if (subtask.cancellationRequested()) {{ - throw new Error('cancellation has already been requested for this subtask'); - }} if (!isAsync && subtask.waitable().isInSet()) {{ throw new Error('cannot synchronously cancel a subtask that is in a waitable set'); }} + const finishCancel = () => {{ + // Consume the subtask's pending resolution event (which also marks the + // resolution as delivered), then hand the final state back to core wasm. + // Legal states here: RETURNED, CANCELLED_BEFORE_STARTED, CANCELLED_BEFORE_RETURNED. + if (subtask.hasPendingEvent()) {{ subtask.getPendingEvent(); }} + if (!subtask.resolveDelivered()) {{ subtask.deliverResolve(); }} + + return subtask.getStateNumber(); + }}; + + if (subtask.cancellationRequested()) {{ + if (!slowOnly) {{ + throw new Error('cancellation has already been requested for this subtask'); + }} + if (subtask.isResolved()) {{ return finishCancel(); }} + + const {{ taskID }} = {get_global_current_task_meta_fn}(componentIdx); + const taskMeta = {current_task_get_fn}(componentIdx, taskID); + if (!taskMeta || !taskMeta.task) {{ throw new Error('invalid/missing async task'); }} + return taskMeta.task.waitUntil({{ + cancellable: false, + readyFn: () => subtask.isResolved(), + }}).then(finishCancel); + }} + + let cancellationWillCompleteAsync = false; + if (!subtask.isResolved()) {{ - // Subscribe before requesting cancellation: the request itself - // resumes a child that is suspended at a cancellable point. - // Waiting for the next suspension/exit gives that child exactly - // one execution slice in which to acknowledge via `task.cancel`. - const childTask = subtask.getChildTask(); - const childProgress = childTask?.waitForProgress(); subtask.requestCancellation(); - if (!subtask.isResolved() && childProgress) {{ - await childProgress; - if (!subtask.isResolved()) {{ - // The resumed callee blocked again: async-lowered cancels - // report BLOCKED (resolution will arrive via a later SUBTASK event), - // while sync-lowered cancels block the current task until the - // subtask resolves. - if (isAsync) {{ return 0xFFFFFFFF; }} + if (!subtask.isResolved()) {{ + // Cancellation immediately resumes one cancellable child + // execution slice. Wait until that slice releases component + // entry by exiting or suspending again before deciding whether + // the cancel blocked. + const childTask = subtask.getChildTask(); + if (childTask) {{ + const childState = {get_or_create_async_state_fn}(childTask.componentIdx()); + if (subtask.getStateNumber() === 0 && + childTask.deliverPendingCancel({{ cancellable: true }})) {{ + childTask.cancel(); + childState.resumeTaskByID(childTask.id()); + }} else if (childState.suspendedTaskReady(childTask.id())) {{ + cancellationWillCompleteAsync = + childState.suspendedTaskCancellable(childTask.id()); + if (!childState.resumeTaskByID(childTask.id())) {{ + throw new Error('failed to resume cancellable subtask'); + }} + }} }} }} if (!subtask.isResolved()) {{ - if (isAsync) {{ return 0xFFFFFFFF; }} + // The resumed callee blocked again: async-lowered cancels + // report BLOCKED (resolution will arrive via a later SUBTASK event), + // while sync-lowered cancels block the current task until the + // subtask resolves. + if (isAsync) {{ + // -1 is the canonical BLOCKED status. -2 is an + // internal signal consumed by the conditional JSPI + // trampoline when a cancellable child was resumed but + // its suspended Wasm stack must finish in a microtask. + return cancellationWillCompleteAsync ? 0xFFFFFFFE : 0xFFFFFFFF; + }} const {{ taskID }} = {get_global_current_task_meta_fn}(componentIdx); const taskMeta = {current_task_get_fn}(componentIdx, taskID); if (!taskMeta || !taskMeta.task) {{ throw new Error('invalid/missing async task'); }} - await taskMeta.task.waitUntil({{ + return taskMeta.task.waitUntil({{ cancellable: false, readyFn: () => subtask.isResolved(), - }}); + }}).then(finishCancel); }} }} - // Consume the subtask's pending resolution event (which also marks the - // resolution as delivered), then hand the final state back to core wasm. - // Legal states here: RETURNED, CANCELLED_BEFORE_STARTED, CANCELLED_BEFORE_RETURNED. - if (subtask.hasPendingEvent()) {{ subtask.getPendingEvent(); }} - if (!subtask.resolveDelivered()) {{ subtask.deliverResolve(); }} - - return subtask.getStateNumber(); + return finishCancel(); }} ")); } @@ -1241,8 +1274,15 @@ impl AsyncTaskIntrinsic { return this.#callbackFnName; }} - async runCallbackFn(...args) {{ + runCallbackFn(...args) {{ if (!this.#callbackFn) {{ throw new Error('no callback function has been set for task'); }} + if (this.#callbackFn._jcoMaySuspend === false) {{ + return {with_global_current_task_meta_fn}({{ + taskID: this.#id, + componentIdx: this.#componentIdx, + fn: () => this.#callbackFn.apply(null, args), + }}); + }} return {with_global_current_task_meta_async_fn}({{ taskID: this.#id, componentIdx: this.#componentIdx, @@ -1405,8 +1445,8 @@ impl AsyncTaskIntrinsic { cstate.removeBackpressureWaiter(); - if (!result) {{ - this.cancel(); + if (!result || this.isCancelled()) {{ + if (!this.isResolvedState()) {{ this.cancel(); }} return false; }} }} @@ -1418,6 +1458,14 @@ impl AsyncTaskIntrinsic { await cstate.acquireExclusiveLock(this.#id); }} + // Cancellation-before-start may resolve this task while its + // queued lock acquisition is still pending. Acquiring the lock + // does not make the already-resolved task runnable again. + if (this.isResolvedState() || this.isCancelled()) {{ + cstate.exclusiveRelease(this.#id); + return false; + }} + // Cancellation can be requested while entry is waiting for // backpressure or its exclusive lock. Do not execute the guest // after acquiring a lock for a task that should no longer start. @@ -1498,6 +1546,32 @@ impl AsyncTaskIntrinsic { return completed; }} + suspendUntilCallback(opts, onResume) {{ + const {{ cancellable, readyFn }} = opts; + if (this.deliverPendingCancel({{ cancellable }})) {{ + onResume(false); + return; + }} + + const cstate = {get_or_create_async_state_fn}(this.#componentIdx); + cstate.suspendTask({{ + task: this, + cancellable, + readyFn: () => {{ + if (cancellable && this.#state === {task_class}.State.CANCEL_PENDING) {{ + return true; + }} + return readyFn(); + }}, + onResume: (keepGoing) => {{ + if (keepGoing && this.deliverPendingCancel({{ cancellable }})) {{ + keepGoing = false; + }} + onResume(keepGoing); + }}, + }}); + }} + // TODO(threads): equivalent to thread.suspend_until() async immediateSuspendUntil(opts) {{ const {{ cancellable, readyFn }} = opts; @@ -1531,6 +1605,7 @@ impl AsyncTaskIntrinsic { const cstate = {get_or_create_async_state_fn}(this.#componentIdx); const keepGoing = await cstate.suspendTask({{ task: this, + cancellable, readyFn: () => {{ // A pending cancellation request wakes cancellable waits if (cancellable && this.#state === {task_class}.State.CANCEL_PENDING) {{ @@ -1560,6 +1635,7 @@ impl AsyncTaskIntrinsic { }} isCancelled() {{ return this.cancelled }} + cancellationRequested() {{ return this.cancelRequested; }} // Request cooperative cancellation of this task, called on behalf of a // supertask performing `subtask.cancel` on the subtask this task backs. @@ -2332,9 +2408,11 @@ impl AsyncTaskIntrinsic { ); let waitable_set_class = render_args .require_intrinsic(Intrinsic::Waitable(WaitableIntrinsic::WaitableSetClass)); + let async_event_code_enum = + render_args.require_intrinsic(Intrinsic::AsyncEventCodeEnum); output.push_str(&format!(r#" - async function {driver_loop_fn}(args) {{ + function {driver_loop_fn}(args) {{ {debug_log_fn}('[{driver_loop_fn}()] args', args); const {{ componentState, @@ -2358,22 +2436,18 @@ impl AsyncTaskIntrinsic { let callbackCode; let waitableSetRep; let unpacked; - try {{ - if (!({i32_typecheck}(callbackResult))) {{ - throw new Error('invalid callback result [' + callbackResult + '], not a number'); + const unpackCallback = (value) => {{ + if (!({i32_typecheck}(value))) {{ + throw new Error('invalid callback result [' + value + '], not a number'); }} - unpacked = {unpack_callback_result_fn}(callbackResult); + unpacked = {unpack_callback_result_fn}(value); callbackCode = unpacked[0]; waitableSetRep = unpacked[1]; - }} catch(err) {{ - console.error("failed to unpack callback result", err); - throw err; - }} - - if (callbackCode < 0 || callbackCode > 3) {{ - throw new Error('invalid async return value, outside callback code range'); - }} + if (callbackCode < 0 || callbackCode > 3) {{ + throw new Error('invalid async return value, outside callback code range'); + }} + }}; const cstate = {get_or_create_async_state_fn}(componentIdx); @@ -2382,8 +2456,31 @@ impl AsyncTaskIntrinsic { let result; let asyncRes; let wset; - try {{ - while (true) {{ + + const handleError = (err) => {{ + {debug_log_fn}('[{driver_loop_fn}()] error during async driver loop', {{ + fnName, + callbackFnName, + componentIdx, + taskID: task.id(), + subtaskID: task.getParentSubtask()?.id(), + parentTaskID: task.getParentSubtask()?.getParentTask()?.id(), + event: {{ + eventCode, + index, + result, + }}, + err, + }}); + task.setErrored(err); + task.reject(err); + if (!task.isExited()) {{ + task.exit({{ skipExclusiveLockCheck: true }}); + }} + }}; + + const drive = () => {{ + try {{ if (callbackCode !== 0) {{ componentState.exclusiveRelease(task.id()); }} switch (callbackCode) {{ @@ -2404,18 +2501,23 @@ impl AsyncTaskIntrinsic { callbackFnName, taskID: task.id() }}); - asyncRes = await task.yieldUntil({{ + task.suspendUntilCallback({{ cancellable: true, readyFn: () => true, + }}, (keepGoing) => {{ + continueWithEvent(keepGoing + ? {{ + code: {async_event_code_enum}.NONE, + payload0: 0, + payload1: 0, + }} + : {{ + code: {async_event_code_enum}.TASK_CANCELLED, + payload0: 0, + payload1: 0, + }}); }}); - {debug_log_fn}('[{driver_loop_fn}()] finished yield', {{ - fnName, - componentIdx, - callbackFnName, - taskID: task.id(), - asyncRes, - }}); - break; + return; case 2: // WAIT for a given waitable set {debug_log_fn}('[{driver_loop_fn}()] waiting for event', {{ @@ -2432,34 +2534,41 @@ impl AsyncTaskIntrinsic { throw new Error(`non-waitable set returned from component state handles @ [${{waitableSetRep}}]`); }} - asyncRes = await wset.waitUntil({{ + wset.waitUntilCallback({{ readyFn: () => true, task, cancellable: true, - }}); - - {debug_log_fn}('[{driver_loop_fn}()] finished waiting for event', {{ - fnName, - componentIdx, - callbackFnName, - taskID: task.id(), - waitableSetRep, - asyncRes, - }}); - - break; + }}, continueWithEvent); + return; default: throw new Error(`Unrecognized async function result [${{ret}}]`); }} + }} catch (err) {{ + handleError(err); + }} + }}; - // Own the per-slice lock before delivering the event into - // the next callback slice (FIFO-queued when another task's - // slice is mid-flight, including across its JSPI - // suspensions. - await componentState.acquireExclusiveLock(task.id()); + const continueWithCallbackResult = (callbackRes) => {{ + try {{ + unpackCallback(callbackRes); - // If the task failed via any means, leave early and reject. + {debug_log_fn}('[{driver_loop_fn}()] callback result unpacked', {{ + fnName, + componentIdx, + callbackFnName, + callbackRes, + callbackCode, + waitableSetRep, + }}); + return drive(); + }} catch (err) {{ + handleError(err); + }} + }}; + + const runCallback = () => {{ + try {{ if (task.isRejected()) {{ {debug_log_fn}('[{driver_loop_fn}()] detected task rejection, leaving early'); componentState.exclusiveRelease(task.id()); @@ -2468,7 +2577,6 @@ impl AsyncTaskIntrinsic { }} return; }} - if (asyncRes.code === undefined) {{ throw new Error("missing event code from event"); }} if (asyncRes.payload0 === undefined) {{ throw new Error("missing payload0 from event"); }} if (asyncRes.payload1 === undefined) {{ throw new Error("missing payload1 from event"); }} @@ -2488,50 +2596,34 @@ impl AsyncTaskIntrinsic { result }}); - const callbackRes = await task.runCallbackFn( + const callbackRes = task.runCallbackFn( {to_int32_fn}(eventCode), {to_int32_fn}(index), {to_int32_fn}(result), ); + if (callbackRes && typeof callbackRes.then === 'function') {{ + return callbackRes.then(continueWithCallbackResult, handleError); + }} + return continueWithCallbackResult(callbackRes); + }} catch (err) {{ + handleError(err); + }} + }}; - unpacked = {unpack_callback_result_fn}(callbackRes); - callbackCode = unpacked[0]; - waitableSetRep = unpacked[1]; - - {debug_log_fn}('[{driver_loop_fn}()] callback result unpacked', {{ - fnName, - componentIdx, - callbackFnName, - callbackRes, - callbackCode, - waitableSetRep, - }}); + function continueWithEvent(event) {{ + asyncRes = event; + const lock = componentState.acquireExclusiveLock(task.id()); + if (lock && typeof lock.then === 'function') {{ + return lock.then(runCallback, handleError); }} + return runCallback(); + }} + + try {{ + unpackCallback(callbackResult); + return drive(); }} catch (err) {{ - {debug_log_fn}('[{driver_loop_fn}()] error during async driver loop', {{ - fnName, - callbackFnName, - componentIdx, - taskID: task.id(), - subtaskID: task.getParentSubtask()?.id(), - parentTaskID: task.getParentSubtask()?.getParentTask()?.id(), - event: {{ - eventCode, - index, - result, - }}, - err, - }}); - task.setErrored(err); - task.reject(err); - // A trapping callback has no later EXIT code to release its - // component slice or wake a supertask synchronously driving - // cancellation. Retire it here just like the call-site error - // paths do; rejection has already propagated through the - // subtask chain. - if (!task.isExited()) {{ - task.exit({{ skipExclusiveLockCheck: true }}); - }} + handleError(err); }} }} "#, @@ -2723,6 +2815,11 @@ impl AsyncTaskIntrinsic { queueMicrotask(async () => {{ try {{ + // The async host call has not started yet. If the + // enclosing guest task was cancelled in the meantime, + // leave this subtask for the guest cancellation callback + // to retire without invoking host code after cancellation. + if (task.cancellationRequested()) {{ return; }} {debug_log_fn}('[{lower_import_fn}()] calling lowered import', {{ importFn, params }}); await {with_global_current_task_meta_async_fn}({{ taskID: task.id(), @@ -3106,7 +3203,7 @@ impl AsyncTaskIntrinsic { // (ex. 'run()' that was executing in the caller when the callee is 'set_value()' in the callee) output.push_str(&format!( r#" - function {enter_symmetric_sync_guest_call_fn}(callerComponentIdx, calleeIsAsync, calleeComponentIdx) {{ + function {enter_symmetric_sync_guest_call_fn}(callerComponentIdx, calleeIsAsync, calleeComponentIdx, syncOnly = false) {{ {debug_log_fn}('[{enter_symmetric_sync_guest_call_fn}()] args', {{ callerComponentIdx, calleeIsAsync, @@ -3115,6 +3212,14 @@ impl AsyncTaskIntrinsic { const cstate = {get_or_create_async_state_fn}(calleeComponentIdx); + // The conditional JSPI trampoline probes this path before + // invoking its Suspending fallback. Avoid creating either + // task until the probe knows that entry can complete in the + // current Wasm slice. + if (syncOnly && !calleeIsAsync && cstate.isExclusivelyLocked()) {{ + return 0; + }} + const callerTaskMeta = {get_current_task_fn}(callerComponentIdx); if (!callerTaskMeta) {{ throw new Error('missing current caller task metadata'); }} const callerTask = callerTaskMeta.task; @@ -3179,14 +3284,17 @@ impl AsyncTaskIntrinsic { // promise suspends the caller's (JSPI) stack until ownership. if (!newTask.needsExclusiveLock()) {{ finishEnter(); - return; + return 1; }} if (!cstate.isExclusivelyLocked()) {{ cstate.exclusiveLock(newTask.id()); finishEnter(); - return; + return 1; }} - return cstate.acquireExclusiveLock(newTask.id()).then(finishEnter); + return cstate.acquireExclusiveLock(newTask.id()).then(() => {{ + finishEnter(); + return 1; + }}); }} "#, )); diff --git a/crates/js-component-bindgen/src/intrinsics/p3/waitable.rs b/crates/js-component-bindgen/src/intrinsics/p3/waitable.rs index a75bcd085..1aa935691 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/waitable.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/waitable.rs @@ -223,37 +223,86 @@ impl WaitableIntrinsic { throw new Error('no waitables had a pending event'); }} - async waitUntil(opts) {{ - {debug_log_fn}('[{waitable_set_class}#waitUntil()] args', {{ opts }}); + tryWait(opts) {{ + {debug_log_fn}('[{waitable_set_class}#tryWait()] args', {{ opts }}); // TODO(threads): this task should be the thread const {{ readyFn, task, cancellable }} = opts; - let event; + const isReady = () => {{ + const hasPendingEvent = this.hasPendingEvent(); + const ready = readyFn(); + return ready && hasPendingEvent; + }}; + const finishWait = (keepGoing) => {{ + const event = keepGoing + ? this.getPendingEvent() + : {{ + code: {async_event_code_enum}.TASK_CANCELLED, + payload0: 0, + payload1: 0, + }}; + return event; + }}; + + if (task.deliverPendingCancel({{ cancellable }})) {{ + return finishWait(false); + }} + if (isReady()) {{ return finishWait(true); }} + return null; + }} + waitUntil(opts) {{ + {debug_log_fn}('[{waitable_set_class}#waitUntil()] args', {{ opts }}); + const event = this.tryWait(opts); + if (event !== null) {{ return event; }} + + const {{ readyFn, task, cancellable }} = opts; + const isReady = () => readyFn() && this.hasPendingEvent(); this.incrementNumWaiting(); - const keepGoing = await task.suspendUntil({{ - readyFn: () => {{ - const hasPendingEvent = this.hasPendingEvent(); - const ready = readyFn(); - return ready && hasPendingEvent; + return task.suspendUntil({{ readyFn: isReady, cancellable }}).then( + (keepGoing) => {{ + const readyEvent = keepGoing + ? this.getPendingEvent() + : {{ + code: {async_event_code_enum}.TASK_CANCELLED, + payload0: 0, + payload1: 0, + }}; + this.decrementNumWaiting(); + return readyEvent; }}, - cancellable, - }}); + (err) => {{ + this.decrementNumWaiting(); + throw err; + }}, + ); + }} + + waitUntilCallback(opts, onEvent) {{ + {debug_log_fn}('[{waitable_set_class}#waitUntilCallback()] args', {{ opts }}); + const event = this.tryWait(opts); + if (event !== null) {{ + onEvent(event); + return; + }} - if (keepGoing) {{ - event = this.getPendingEvent(); - }} else {{ - event = {{ + const {{ readyFn, task, cancellable }} = opts; + this.incrementNumWaiting(); + task.suspendUntilCallback({{ + readyFn: () => readyFn() && this.hasPendingEvent(), + cancellable, + }}, (keepGoing) => {{ + const resumedEvent = keepGoing + ? this.getPendingEvent() + : {{ code: {async_event_code_enum}.TASK_CANCELLED, payload0: 0, payload1: 0, }}; - }} - - this.decrementNumWaiting(); - - return event; + this.decrementNumWaiting(); + onEvent(resumedEvent); + }}); }} }} @@ -437,7 +486,7 @@ impl WaitableIntrinsic { let waitable_set_class = render_args.require_intrinsic(Self::WaitableSetClass); output.push_str(&format!(r#" - async function {waitable_set_wait_fn}(ctx, waitableSetRep, resultPtr) {{ + function {waitable_set_wait_fn}(ctx, waitableSetRep, resultPtr, syncOnly = false) {{ {debug_log_fn}('[{waitable_set_wait_fn}()] args', {{ ctx, waitableSetRep, resultPtr }}); const {{ componentIdx, @@ -465,15 +514,21 @@ impl WaitableIntrinsic { throw new Error(`non-waitable set returned from component state handles @ [${{waitableSetRep}}]`); }} - const event = await wset.waitUntil({{ readyFn: () => true, task, cancellable: isCancellable }}); - return {store_event_in_component_memory_fn}({{ - memory, - ptr: resultPtr, - event, - componentIdx, - task, - memoryIdx, - }}); + const storeEvent = (event) => {store_event_in_component_memory_fn}({{ + memory, + ptr: resultPtr, + event, + componentIdx, + task, + memoryIdx, + }}); + const waitOpts = {{ readyFn: () => true, task, cancellable: isCancellable }}; + const event = syncOnly ? wset.tryWait(waitOpts) : wset.waitUntil(waitOpts); + if (event === null) {{ return -1; }} + if (event && typeof event.then === 'function') {{ + return event.then(storeEvent); + }} + return storeEvent(event); }} "#)); } diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index ad26a7d7f..8ce3d1cb9 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -1610,13 +1610,12 @@ impl<'a> Instantiator<'a, '_> { | Trampoline::FutureWrite { options, .. } | Trampoline::StreamRead { options, .. } | Trampoline::StreamWrite { options, .. } => !self.component.options[*options].async_, - Trampoline::SubtaskCancel { .. } - | Trampoline::WaitableSetWait { .. } - | Trampoline::StreamCancelRead { .. } - | Trampoline::StreamCancelWrite { .. } - | Trampoline::FutureCancelRead { .. } - | Trampoline::FutureCancelWrite { .. } - | Trampoline::ThreadYield { .. } => true, + Trampoline::SubtaskCancel { async_, .. } + | Trampoline::StreamCancelRead { async_, .. } + | Trampoline::StreamCancelWrite { async_, .. } + | Trampoline::FutureCancelRead { async_, .. } + | Trampoline::FutureCancelWrite { async_, .. } => !async_, + Trampoline::WaitableSetWait { .. } | Trampoline::ThreadYield { .. } => true, // These composition trampolines are plain functions outside JSPI; // counting them as suspending would add promising wrappers to sync output. Trampoline::SyncStartCall { .. } | Trampoline::EnterSyncCall => matches!( @@ -1808,14 +1807,32 @@ impl<'a> Instantiator<'a, '_> { .intrinsic(Intrinsic::AsyncTask(AsyncTaskIntrinsic::SubtaskCancel)); let suspending_wrap_fn = self.bindgen.intrinsic(Intrinsic::SuspendingImportWrapperFn); + let conditional_suspending_fn = self + .bindgen + .intrinsic(Intrinsic::ConditionalSuspending1I32ToI32Fn); // NOTE: core wasm passes the subtask handle as the remaining argument. - // The intrinsic is async (a sync-lowered cancel may need to block until - // the subtask resolves), so it must be JSPI-wrapped. - uwriteln!( - self.src.js, - "const trampoline{i} = new WebAssembly.Suspending({suspending_wrap_fn}({instance_idx}, {subtask_cancel_fn}.bind(null, {instance_idx}, {async_})));\n", - instance_idx = instance.as_u32(), - ); + // Async-lowered cancellation reports BLOCKED directly unless a + // cancellable child is already resuming toward eager completion. + // Only that eager path and sync-lowered cancellation may suspend. + if *async_ { + uwriteln!( + self.src.js, + r#" + const trampoline{i}Cancel = {subtask_cancel_fn}.bind(null, {instance_idx}, true); + const trampoline{i} = {conditional_suspending_fn}( + trampoline{i}Cancel, + {suspending_wrap_fn}({instance_idx}, (subtaskRep) => trampoline{i}Cancel(subtaskRep, true)), + ); + "#, + instance_idx = instance.as_u32(), + ); + } else { + uwriteln!( + self.src.js, + "const trampoline{i} = new WebAssembly.Suspending({suspending_wrap_fn}({instance_idx}, {subtask_cancel_fn}.bind(null, {instance_idx}, false)));\n", + instance_idx = instance.as_u32(), + ); + } } Trampoline::SubtaskDrop { instance } => { @@ -1876,17 +1893,24 @@ impl<'a> Instantiator<'a, '_> { .intrinsic(Intrinsic::Waitable(WaitableIntrinsic::WaitableSetWait)); let suspending_wrap_fn = self.bindgen.intrinsic(Intrinsic::SuspendingImportWrapperFn); + let conditional_suspending_fn = self + .bindgen + .intrinsic(Intrinsic::ConditionalSuspending2I32ToI32Fn); uwriteln!( self.src.js, r#" - const trampoline{i} = new WebAssembly.Suspending({suspending_wrap_fn}({instance_idx}, {waitable_set_wait_fn}.bind(null, {{ + const trampoline{i}Wait = {waitable_set_wait_fn}.bind(null, {{ componentIdx: {instance_idx}, isAsync: {async_}, isCancellable: {cancellable}, memoryIdx: {memory_idx}, getMemoryFn: () => memory{memory_idx}, - }}))); + }}); + const trampoline{i} = {conditional_suspending_fn}( + {suspending_wrap_fn}({instance_idx}, (waitableSetRep, resultPtr) => trampoline{i}Wait(waitableSetRep, resultPtr, true)), + {suspending_wrap_fn}({instance_idx}, trampoline{i}Wait), + ); "#, ); } @@ -2189,18 +2213,26 @@ impl<'a> Instantiator<'a, '_> { let stream_table_idx = ty.as_u32(); let component_idx = instance.as_u32(); - uwriteln!( - self.src.js, - r#" - const trampoline{i} = new WebAssembly.Suspending({suspending_wrap_fn}({component_idx}, {stream_cancel_fn}.bind(null, {{ + let ctx = format!( + r#"{{ streamTableIdx: {stream_table_idx}, isAsync: {async_}, componentIdx: {component_idx}, - }}))); - "#, - suspending_wrap_fn = - self.bindgen.intrinsic(Intrinsic::SuspendingImportWrapperFn), + }}"# ); + if *async_ { + uwriteln!( + self.src.js, + "const trampoline{i} = {stream_cancel_fn}.bind(null, {ctx});", + ); + } else { + let suspending_wrap_fn = + self.bindgen.intrinsic(Intrinsic::SuspendingImportWrapperFn); + uwriteln!( + self.src.js, + "const trampoline{i} = new WebAssembly.Suspending({suspending_wrap_fn}({component_idx}, {stream_cancel_fn}.bind(null, {ctx})));", + ); + } } Trampoline::StreamDropReadable { ty, instance } @@ -2443,21 +2475,26 @@ impl<'a> Instantiator<'a, '_> { let component_idx = instance.as_u32(); let future_table_idx = ty.as_u32(); - uwriteln!( - self.src.js, - r#" - const trampoline{i} = new WebAssembly.Suspending({suspending_wrap_fn}({component_idx}, {future_cancel_op_fn}.bind( - null, - {{ + let ctx = format!( + r#"{{ futureTableIdx: {future_table_idx}, componentIdx: {component_idx}, isAsync: {async_}, - }}, - ))); - "#, - suspending_wrap_fn = - self.bindgen.intrinsic(Intrinsic::SuspendingImportWrapperFn), + }}"# ); + if *async_ { + uwriteln!( + self.src.js, + "const trampoline{i} = {future_cancel_op_fn}.bind(null, {ctx});", + ); + } else { + let suspending_wrap_fn = + self.bindgen.intrinsic(Intrinsic::SuspendingImportWrapperFn); + uwriteln!( + self.src.js, + "const trampoline{i} = new WebAssembly.Suspending({suspending_wrap_fn}({component_idx}, {future_cancel_op_fn}.bind(null, {ctx})));", + ); + } } Trampoline::FutureDropReadable { instance, ty } @@ -3268,6 +3305,11 @@ impl<'a> Instantiator<'a, '_> { let enter_symmetric_sync_guest_call_fn = self.bindgen.intrinsic( Intrinsic::AsyncTask(AsyncTaskIntrinsic::EnterSymmetricSyncGuestCall), ); + let suspending_wrap_fn = + self.bindgen.intrinsic(Intrinsic::SuspendingImportWrapperFn); + let conditional_suspending_fn = self + .bindgen + .intrinsic(Intrinsic::ConditionalSuspending3I32ToVoidFn); // Under JSPI, contended entry queues for the callee's per-slice // exclusive lock by returning a promise, which requires the // trampoline to be Suspending (fused sync calls then run inside @@ -3284,7 +3326,10 @@ impl<'a> Instantiator<'a, '_> { uwriteln!( self.src.js, r#" - const trampoline{i} = new WebAssembly.Suspending({enter_symmetric_sync_guest_call_fn}); + const trampoline{i} = {conditional_suspending_fn}( + (callerComponentIdx, calleeIsAsync, calleeComponentIdx) => {enter_symmetric_sync_guest_call_fn}(callerComponentIdx, calleeIsAsync, calleeComponentIdx, true), + (callerComponentIdx, calleeIsAsync, calleeComponentIdx) => {suspending_wrap_fn}(callerComponentIdx, {enter_symmetric_sync_guest_call_fn})(callerComponentIdx, calleeIsAsync, calleeComponentIdx), + ); "#, ); } else { @@ -3319,6 +3364,7 @@ impl<'a> Instantiator<'a, '_> { // into the component after a related suspension. GlobalInitializer::ExtractCallback(ExtractCallback { index, def }) => { let callback_idx = index.as_u32(); + let may_suspend = self.core_def_may_suspend(def); let core_def = self.core_def(def); uwriteln!(self.src.js, "let callback_{callback_idx};",); @@ -3329,15 +3375,23 @@ impl<'a> Instantiator<'a, '_> { // // Here, we mark the task with an indicator that denotes whether the callback should be run this way. // - // TODO: can we be more selective here rather than wrapping every callback in WebAssembly.promising? - // every callback *could* do stream.write, but many may not. - uwriteln!( - self.src.js_init, - r#" - callback_{callback_idx} = WebAssembly.promising({core_def}); - callback_{callback_idx}.fnName = "{core_def}"; - "# - ); + if may_suspend { + uwriteln!( + self.src.js_init, + r#" + callback_{callback_idx} = WebAssembly.promising({core_def}); + callback_{callback_idx}.fnName = "{core_def}"; + "# + ); + } else { + uwriteln!( + self.src.js_init, + r#" + callback_{callback_idx} = Object.assign({core_def}, {{ _jcoMaySuspend: false }}); + callback_{callback_idx}.fnName = "{core_def}"; + "# + ); + } } GlobalInitializer::InstantiateModule(m, instance) => { From fcc7e6cb79a6499d542565fd09b7f4abe2ed9574 Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Wed, 2 Sep 2026 19:10:30 +0000 Subject: [PATCH 4/7] test(transpile): enable big-interleaving-test WAST --- .../async/big-interleaving-test.wast | 1660 +++++++++++++++++ .../test/fixtures/wast/upstream-manifest.json | 4 + .../test/p3/ported/component-model/wast.ts | 1 + 3 files changed, 1665 insertions(+) create mode 100644 packages/jco-transpile/test/fixtures/wast/component-model/async/big-interleaving-test.wast diff --git a/packages/jco-transpile/test/fixtures/wast/component-model/async/big-interleaving-test.wast b/packages/jco-transpile/test/fixtures/wast/component-model/async/big-interleaving-test.wast new file mode 100644 index 000000000..d6c18bdff --- /dev/null +++ b/packages/jco-transpile/test/fixtures/wast/component-model/async/big-interleaving-test.wast @@ -0,0 +1,1660 @@ +;; Larger, more-involved integration test for testing a bunch of different +;; concurrent interleaving of 0.3 ABI operations to catch bugs that involve multiple +;; things in flight. +;; +;; A program (list) is fed to $Driver's `run` export. run returns nothing; an +;; expect-code mismatch traps, so a passing program is just an assert_return. expect-code +;; checks the previous op's return code. This allows a variety of concurrent interleaved +;; scenarios to be checked by simply adding extra `assert_return` calls passing different +;; lists of WIT values instead of writing new WAT code. +;; +;; The $Mock component mocks imports called by the $Testee. The $Driver +;; component imports the $Mock and $Testee to drive the $Testee and control +;; what the $Mock does. +(component definition $Tester + + (component $Mock + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $MM + (import "" "mem" (memory 1)) + (import "" "task.return" (func $task.return)) + (import "" "task.cancel" (func $task.cancel)) + (import "" "backpressure.inc" (func $backpressure.inc)) + (import "" "backpressure.dec" (func $backpressure.dec)) + (import "" "context.set" (func $context.set (param i32))) + (import "" "context.get" (func $context.get (result i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.drop" (func $waitable-set.drop (param i32))) + (import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32))) + (import "" "stream.drop-readable" (func $stream.drop-readable (param i32))) + (import "" "future.read" (func $future.read (param i32 i32) (result i32))) + (import "" "future.drop-readable" (func $future.drop-readable (param i32))) + + (global $DST_BUF i32 (i32.const 256)) + + (func (export "sink") (param $insr i32) (result i32) + (local $ret i32) (local $ws i32) + (local.set $ws (call $waitable-set.new)) + (call $context.set (local.get $ws)) + (local.set $ret (call $stream.read (local.get $insr) (global.get $DST_BUF) (i32.const 4))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (call $task.return) + (call $waitable.join (local.get $insr) (local.get $ws)) + (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (local.get $ws) (i32.const 4)))) + (func (export "sink_cb") (param $event-code i32) (param $index i32) (param $payload i32) (result i32) + (if (i32.ne (local.get $event-code) (i32.const 2 (; STREAM_READ ;))) + (then unreachable)) + (call $stream.drop-readable (local.get $index)) + (call $waitable-set.drop (call $context.get)) + (i32.const 0 (; EXIT ;))) + + (func (export "sink-future") (param $infr i32) (result i32) + (local $ret i32) (local $ws i32) + (local.set $ws (call $waitable-set.new)) + (call $context.set (local.get $ws)) + (local.set $ret (call $future.read (local.get $infr) (global.get $DST_BUF))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (call $task.return) + (call $waitable.join (local.get $infr) (local.get $ws)) + (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (local.get $ws) (i32.const 4)))) + (func (export "sink-future_cb") (param $event-code i32) (param $index i32) (param $payload i32) (result i32) + (if (i32.ne (local.get $event-code) (i32.const 4 (; FUTURE_READ ;))) + (then unreachable)) + (call $future.drop-readable (local.get $index)) + (call $waitable-set.drop (call $context.get)) + (i32.const 0 (; EXIT ;))) + + (func (export "block-empty") (result i32) + (local $ws i32) + (local.set $ws (call $waitable-set.new)) + (call $context.set (local.get $ws)) + (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (local.get $ws) (i32.const 4)))) + (func (export "block-empty_cb") (param $event-code i32) (param $index i32) (param $payload i32) (result i32) + (if (i32.ne (local.get $event-code) (i32.const 6 (; TASK_CANCELLED ;))) + (then unreachable)) + (call $task.cancel) + (call $waitable-set.drop (call $context.get)) + (i32.const 0 (; EXIT ;))) + + (func (export "block-future") (param $infr i32) (result i32) + (local $ret i32) (local $ws i32) + (local.set $ws (call $waitable-set.new)) + (call $context.set (local.get $ws)) + (local.set $ret (call $future.read (local.get $infr) (global.get $DST_BUF))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (call $waitable.join (local.get $infr) (local.get $ws)) + (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (local.get $ws) (i32.const 4)))) + (func (export "block-future_cb") (param $event-code i32) (param $index i32) (param $payload i32) (result i32) + (if (i32.ne (local.get $event-code) (i32.const 4 (; FUTURE_READ ;))) + (then unreachable)) + (call $future.drop-readable (local.get $index)) + (call $waitable-set.drop (call $context.get)) + (call $task.return) + (i32.const 0 (; EXIT ;))) + + (func (export "bp-inc") (call $backpressure.inc)) + (func (export "bp-dec") (call $backpressure.dec)) + ) + (type $ST (stream u8)) + (type $FT (future u8)) + (canon task.return (core func $task.return)) + (canon task.cancel (core func $task.cancel)) + (canon backpressure.inc (core func $backpressure.inc)) + (canon backpressure.dec (core func $backpressure.dec)) + (canon context.set i32 0 (core func $context.set)) + (canon context.get i32 0 (core func $context.get)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.drop (core func $waitable-set.drop)) + (canon stream.read $ST async (memory (core memory $memory "mem")) (core func $stream.read)) + (canon stream.drop-readable $ST (core func $stream.drop-readable)) + (canon future.read $FT async (memory (core memory $memory "mem")) (core func $future.read)) + (canon future.drop-readable $FT (core func $future.drop-readable)) + (core instance $mm (instantiate $MM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "task.return" (func $task.return)) + (export "task.cancel" (func $task.cancel)) + (export "backpressure.inc" (func $backpressure.inc)) + (export "backpressure.dec" (func $backpressure.dec)) + (export "context.set" (func $context.set)) + (export "context.get" (func $context.get)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.drop" (func $waitable-set.drop)) + (export "stream.read" (func $stream.read)) + (export "stream.drop-readable" (func $stream.drop-readable)) + (export "future.read" (func $future.read)) + (export "future.drop-readable" (func $future.drop-readable)))))) + (func (export "sink") async (param "in" (stream u8)) (canon lift + (core func $mm "sink") async (memory (core memory $memory "mem")) (callback (core func $mm "sink_cb")))) + (func (export "sink-future") async (param "in" (future u8)) (canon lift + (core func $mm "sink-future") async (memory (core memory $memory "mem")) (callback (core func $mm "sink-future_cb")))) + (func (export "block-empty") async (canon lift + (core func $mm "block-empty") async (memory (core memory $memory "mem")) (callback (core func $mm "block-empty_cb")))) + (func (export "block-future") async (param "in" (future u8)) (canon lift + (core func $mm "block-future") async (memory (core memory $memory "mem")) (callback (core func $mm "block-future_cb")))) + (func (export "bp-inc") (canon lift (core func $mm "bp-inc"))) + (func (export "bp-dec") (canon lift (core func $mm "bp-dec"))) + ) + + (component $Testee + (import "sink" (func $sink async (param "in" (stream u8)))) + (import "sink-future" (func $sink-future async (param "in" (future u8)))) + (import "block-empty" (func $block-empty async)) + (import "block-future" (func $block-future async (param "in" (future u8)))) + (import "bp-inc" (func $bp-inc)) + (import "bp-dec" (func $bp-dec)) + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $TM + (import "" "mem" (memory 1)) + (import "" "sink" (func $sink (param i32) (result i32))) + (import "" "sink-future" (func $sink-future (param i32) (result i32))) + (import "" "block-empty" (func $block-empty (result i32))) + (import "" "block-future" (func $block-future (param i32) (result i32))) + (import "" "bp-inc" (func $bp-inc)) + (import "" "bp-dec" (func $bp-dec)) + (import "" "subtask.cancel" (func $subtask.cancel (param i32) (result i32))) + (import "" "subtask.drop" (func $subtask.drop (param i32))) + (import "" "stream.new" (func $stream.new (result i64))) + (import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32))) + (import "" "stream.write" (func $stream.write (param i32 i32 i32) (result i32))) + (import "" "stream.cancel-read" (func $stream.cancel-read (param i32) (result i32))) + (import "" "stream.cancel-write" (func $stream.cancel-write (param i32) (result i32))) + (import "" "stream.drop-readable" (func $stream.drop-readable (param i32))) + (import "" "stream.drop-writable" (func $stream.drop-writable (param i32))) + (import "" "future.new" (func $future.new (result i64))) + (import "" "future.read" (func $future.read (param i32 i32) (result i32))) + (import "" "future.write" (func $future.write (param i32 i32) (result i32))) + (import "" "future.cancel-read" (func $future.cancel-read (param i32) (result i32))) + (import "" "future.cancel-write" (func $future.cancel-write (param i32) (result i32))) + (import "" "future.drop-readable" (func $future.drop-readable (param i32))) + (import "" "future.drop-writable" (func $future.drop-writable (param i32))) + (import "" "task.return" (func $task.return)) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.poll" (func $waitable-set.poll (param i32 i32) (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (import "" "waitable-set.drop" (func $waitable-set.drop (param i32))) + + (global $RX_BASE i32 (i32.const 0)) + (global $TX_BASE i32 (i32.const 64)) + (global $SRC_BUF i32 (i32.const 128)) + (global $DST_BUF i32 (i32.const 256)) + (global $EVENTP i32 (i32.const 384)) + (global $SUBTASK_BASE i32 (i32.const 448)) + + (func $rx (param $slot i32) (result i32) + (i32.load (i32.add (global.get $RX_BASE) (i32.mul (local.get $slot) (i32.const 4))))) + (func $tx (param $slot i32) (result i32) + (i32.load (i32.add (global.get $TX_BASE) (i32.mul (local.get $slot) (i32.const 4))))) + (func $sub (param $slot i32) (result i32) + (i32.load (i32.add (global.get $SUBTASK_BASE) (i32.mul (local.get $slot) (i32.const 4))))) + + (func (export "call-import") (param $slot i32) (result i32) + (call $sink (call $rx (local.get $slot)))) + + (func (export "call-import-future") (param $slot i32) (result i32) + (call $sink-future (call $rx (local.get $slot)))) + + (func (export "call-block-empty") (param $sub-slot i32) (result i32) + (local $ret i32) + (local.set $ret (call $block-empty)) + (i32.store (i32.add (global.get $SUBTASK_BASE) (i32.mul (local.get $sub-slot) (i32.const 4))) + (i32.shr_u (local.get $ret) (i32.const 4))) + (i32.and (local.get $ret) (i32.const 0xf))) + + (func (export "call-block-future") (param $fut-slot i32) (param $sub-slot i32) (result i32) + (local $ret i32) + (local.set $ret (call $block-future (call $rx (local.get $fut-slot)))) + (i32.store (i32.add (global.get $SUBTASK_BASE) (i32.mul (local.get $sub-slot) (i32.const 4))) + (i32.shr_u (local.get $ret) (i32.const 4))) + (i32.and (local.get $ret) (i32.const 0xf))) + + (func (export "subtask-cancel") (param $sub-slot i32) (result i32) + (call $subtask.cancel (call $sub (local.get $sub-slot)))) + + (func (export "subtask-drop") (param $sub-slot i32) + (call $subtask.drop (call $sub (local.get $sub-slot)))) + + (func (export "mock-bp-inc") (call $bp-inc)) + (func (export "mock-bp-dec") (call $bp-dec)) + + (func (export "await-subtask") (param $sub-slot i32) (param $expected-state i32) + (local $ws i32) (local $event i32) (local $st i32) + (local.set $st (call $sub (local.get $sub-slot))) + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $st) (local.get $ws)) + (local.set $event (call $waitable-set.wait (local.get $ws) (global.get $EVENTP))) + (if (i32.ne (local.get $event) (i32.const 1 (; SUBTASK ;))) + (then unreachable)) + (if (i32.ne (i32.load (global.get $EVENTP)) (local.get $st)) + (then unreachable)) + (if (i32.ne (i32.load offset=4 (global.get $EVENTP)) (local.get $expected-state)) + (then unreachable)) + (call $waitable.join (local.get $st) (i32.const 0)) + (call $waitable-set.drop (local.get $ws)) + (call $subtask.drop (local.get $st)) + (call $task.return)) + + (func (export "stream-new") (param $slot i32) + (local $ret64 i64) + (local.set $ret64 (call $stream.new)) + (i32.store (i32.add (global.get $RX_BASE) (i32.mul (local.get $slot) (i32.const 4))) + (i32.wrap_i64 (local.get $ret64))) + (i32.store (i32.add (global.get $TX_BASE) (i32.mul (local.get $slot) (i32.const 4))) + (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32))))) + + (func (export "testee-write") (param $slot i32) (param $bytes i32) (result i32) + (call $stream.write (call $tx (local.get $slot)) (global.get $SRC_BUF) (local.get $bytes))) + + (func (export "testee-read") (param $slot i32) (param $bytes i32) (result i32) + (call $stream.read (call $rx (local.get $slot)) (global.get $DST_BUF) (local.get $bytes))) + + (func (export "testee-cancel-write") (param $slot i32) (result i32) + (call $stream.cancel-write (call $tx (local.get $slot)))) + (func (export "testee-cancel-read") (param $slot i32) (result i32) + (call $stream.cancel-read (call $rx (local.get $slot)))) + + (func (export "future-new") (param $slot i32) + (local $ret64 i64) + (local.set $ret64 (call $future.new)) + (i32.store (i32.add (global.get $RX_BASE) (i32.mul (local.get $slot) (i32.const 4))) + (i32.wrap_i64 (local.get $ret64))) + (i32.store (i32.add (global.get $TX_BASE) (i32.mul (local.get $slot) (i32.const 4))) + (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32))))) + + (func (export "future-write") (param $slot i32) (result i32) + (call $future.write (call $tx (local.get $slot)) (global.get $SRC_BUF))) + (func (export "future-read") (param $slot i32) (result i32) + (call $future.read (call $rx (local.get $slot)) (global.get $DST_BUF))) + + (func (export "future-cancel-write") (param $slot i32) (result i32) + (call $future.cancel-write (call $tx (local.get $slot)))) + (func (export "future-cancel-read") (param $slot i32) (result i32) + (call $future.cancel-read (call $rx (local.get $slot)))) + + (func (export "future-drop-readable") (param $slot i32) + (call $future.drop-readable (call $rx (local.get $slot)))) + (func (export "future-drop-writable") (param $slot i32) + (call $future.drop-writable (call $tx (local.get $slot)))) + + (func (export "drop-readable") (param $slot i32) + (call $stream.drop-readable (call $rx (local.get $slot)))) + (func (export "drop-writable") (param $slot i32) + (call $stream.drop-writable (call $tx (local.get $slot)))) + + (func (export "poll") (param $slot i32) (param $expected-event i32) (param $expected-payload i32) + (local $ws i32) (local $event i32) + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (call $tx (local.get $slot)) (local.get $ws)) + (local.set $event (call $waitable-set.poll (local.get $ws) (global.get $EVENTP))) + (if (i32.ne (local.get $event) (local.get $expected-event)) + (then unreachable)) + (if (i32.ne (i32.load offset=4 (global.get $EVENTP)) (local.get $expected-payload)) + (then unreachable)) + (call $waitable.join (call $tx (local.get $slot)) (i32.const 0)) + (call $waitable-set.drop (local.get $ws))) + + (func (export "await") (param $slot i32) (param $expected-event i32) (param $expected-payload i32) + (local $ws i32) (local $event i32) + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (call $tx (local.get $slot)) (local.get $ws)) + (local.set $event (call $waitable-set.wait (local.get $ws) (global.get $EVENTP))) + (if (i32.ne (local.get $event) (local.get $expected-event)) + (then unreachable)) + (if (i32.ne (i32.load offset=4 (global.get $EVENTP)) (local.get $expected-payload)) + (then unreachable)) + (call $waitable.join (call $tx (local.get $slot)) (i32.const 0)) + (call $waitable-set.drop (local.get $ws)) + (call $task.return)) + + (func (export "poll-readable") (param $slot i32) (param $expected-event i32) (param $expected-payload i32) + (local $ws i32) (local $event i32) + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (call $rx (local.get $slot)) (local.get $ws)) + (local.set $event (call $waitable-set.poll (local.get $ws) (global.get $EVENTP))) + (if (i32.ne (local.get $event) (local.get $expected-event)) + (then unreachable)) + (if (i32.ne (i32.load offset=4 (global.get $EVENTP)) (local.get $expected-payload)) + (then unreachable)) + (call $waitable.join (call $rx (local.get $slot)) (i32.const 0)) + (call $waitable-set.drop (local.get $ws))) + + (func (export "await-readable") (param $slot i32) (param $expected-event i32) (param $expected-payload i32) + (local $ws i32) (local $event i32) + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (call $rx (local.get $slot)) (local.get $ws)) + (local.set $event (call $waitable-set.wait (local.get $ws) (global.get $EVENTP))) + (if (i32.ne (local.get $event) (local.get $expected-event)) + (then unreachable)) + (if (i32.ne (i32.load offset=4 (global.get $EVENTP)) (local.get $expected-payload)) + (then unreachable)) + (call $waitable.join (call $rx (local.get $slot)) (i32.const 0)) + (call $waitable-set.drop (local.get $ws)) + (call $task.return)) + ) + (type $ST (stream u8)) + (type $FT (future u8)) + (canon stream.new $ST (core func $stream.new)) + (canon stream.read $ST async (memory (core memory $memory "mem")) (core func $stream.read)) + (canon stream.write $ST async (memory (core memory $memory "mem")) (core func $stream.write)) + (canon stream.cancel-read $ST async (core func $stream.cancel-read)) + (canon stream.cancel-write $ST async (core func $stream.cancel-write)) + (canon stream.drop-readable $ST (core func $stream.drop-readable)) + (canon stream.drop-writable $ST (core func $stream.drop-writable)) + (canon future.new $FT (core func $future.new)) + (canon future.read $FT async (memory (core memory $memory "mem")) (core func $future.read)) + (canon future.write $FT async (memory (core memory $memory "mem")) (core func $future.write)) + (canon future.cancel-read $FT async (core func $future.cancel-read)) + (canon future.cancel-write $FT async (core func $future.cancel-write)) + (canon future.drop-readable $FT (core func $future.drop-readable)) + (canon future.drop-writable $FT (core func $future.drop-writable)) + (canon task.return (core func $task.return)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.poll (memory (core memory $memory "mem")) (core func $waitable-set.poll)) + (canon waitable-set.wait (memory (core memory $memory "mem")) (core func $waitable-set.wait)) + (canon waitable-set.drop (core func $waitable-set.drop)) + (canon subtask.cancel async (core func $subtask.cancel)) + (canon subtask.drop (core func $subtask.drop)) + (canon lower (func $sink) async (memory (core memory $memory "mem")) (core func $sink')) + (canon lower (func $sink-future) async (memory (core memory $memory "mem")) (core func $sink-future')) + (canon lower (func $block-empty) async (memory (core memory $memory "mem")) (core func $block-empty')) + (canon lower (func $block-future) async (memory (core memory $memory "mem")) (core func $block-future')) + (canon lower (func $bp-inc) (core func $bp-inc')) + (canon lower (func $bp-dec) (core func $bp-dec')) + (core instance $tm (instantiate $TM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "sink" (func $sink')) + (export "sink-future" (func $sink-future')) + (export "block-empty" (func $block-empty')) + (export "block-future" (func $block-future')) + (export "bp-inc" (func $bp-inc')) + (export "bp-dec" (func $bp-dec')) + (export "subtask.cancel" (func $subtask.cancel)) + (export "subtask.drop" (func $subtask.drop)) + (export "stream.new" (func $stream.new)) + (export "stream.read" (func $stream.read)) + (export "stream.write" (func $stream.write)) + (export "stream.cancel-read" (func $stream.cancel-read)) + (export "stream.cancel-write" (func $stream.cancel-write)) + (export "stream.drop-readable" (func $stream.drop-readable)) + (export "stream.drop-writable" (func $stream.drop-writable)) + (export "future.new" (func $future.new)) + (export "future.read" (func $future.read)) + (export "future.write" (func $future.write)) + (export "future.cancel-read" (func $future.cancel-read)) + (export "future.cancel-write" (func $future.cancel-write)) + (export "future.drop-readable" (func $future.drop-readable)) + (export "future.drop-writable" (func $future.drop-writable)) + (export "task.return" (func $task.return)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.poll" (func $waitable-set.poll)) + (export "waitable-set.wait" (func $waitable-set.wait)) + (export "waitable-set.drop" (func $waitable-set.drop)))))) + (func (export "poll-readable") (param "slot" u8) (param "event" u8) (param "payload" u32) (canon lift (core func $tm "poll-readable"))) + (func (export "await-readable") async (param "slot" u8) (param "event" u8) (param "payload" u32) (canon lift + (core func $tm "await-readable") async (memory (core memory $memory "mem")))) + (func (export "call-import") (param "slot" u8) (result s32) (canon lift (core func $tm "call-import"))) + (func (export "call-import-future") (param "slot" u8) (result s32) (canon lift (core func $tm "call-import-future"))) + (func (export "call-block-empty") (param "sub" u8) (result s32) (canon lift (core func $tm "call-block-empty"))) + (func (export "call-block-future") (param "fut" u8) (param "sub" u8) (result s32) (canon lift (core func $tm "call-block-future"))) + (func (export "subtask-cancel") (param "sub" u8) (result s32) (canon lift (core func $tm "subtask-cancel"))) + (func (export "subtask-drop") (param "sub" u8) (canon lift (core func $tm "subtask-drop"))) + (func (export "mock-bp-inc") (canon lift (core func $tm "mock-bp-inc"))) + (func (export "mock-bp-dec") (canon lift (core func $tm "mock-bp-dec"))) + (func (export "await-subtask") async (param "sub" u8) (param "state" u8) (canon lift + (core func $tm "await-subtask") async (memory (core memory $memory "mem")))) + (func (export "stream-new") (param "slot" u8) (canon lift (core func $tm "stream-new"))) + (func (export "testee-write") (param "handle" u8) (param "bytes" u32) (result s32) (canon lift (core func $tm "testee-write"))) + (func (export "testee-read") (param "handle" u8) (param "bytes" u32) (result s32) (canon lift (core func $tm "testee-read"))) + (func (export "testee-cancel-write") (param "slot" u8) (result s32) (canon lift (core func $tm "testee-cancel-write"))) + (func (export "testee-cancel-read") (param "slot" u8) (result s32) (canon lift (core func $tm "testee-cancel-read"))) + (func (export "future-new") (param "slot" u8) (canon lift (core func $tm "future-new"))) + (func (export "future-write") (param "slot" u8) (result s32) (canon lift (core func $tm "future-write"))) + (func (export "future-read") (param "slot" u8) (result s32) (canon lift (core func $tm "future-read"))) + (func (export "future-cancel-write") (param "slot" u8) (result s32) (canon lift (core func $tm "future-cancel-write"))) + (func (export "future-cancel-read") (param "slot" u8) (result s32) (canon lift (core func $tm "future-cancel-read"))) + (func (export "future-drop-readable") (param "slot" u8) (canon lift (core func $tm "future-drop-readable"))) + (func (export "future-drop-writable") (param "slot" u8) (canon lift (core func $tm "future-drop-writable"))) + (func (export "drop-readable") (param "slot" u8) (canon lift (core func $tm "drop-readable"))) + (func (export "drop-writable") (param "slot" u8) (canon lift (core func $tm "drop-writable"))) + (func (export "poll") (param "slot" u8) (param "event" u8) (param "payload" u32) (canon lift (core func $tm "poll"))) + (func (export "await") async (param "slot" u8) (param "event" u8) (param "payload" u32) (canon lift + (core func $tm "await") async (memory (core memory $memory "mem")))) + ) + + (component $Driver + (type $event-kind (enum "none" "subtask" "stream-read" "stream-write" "future-read" "future-write")) + (export $event-kind-e "event-kind" (type $event-kind)) + (type $io-args (record (field "handle" u8) (field "bytes" u32))) + (export $io-args-e "io-args" (type $io-args)) + (type $poll-expect (record (field "slot" u8) (field "event" $event-kind-e) (field "payload" u32))) + (export $poll-expect-e "poll-expect" (type $poll-expect)) + (type $sub-args (record (field "fut" u8) (field "sub" u8))) + (export $sub-args-e "sub-args" (type $sub-args)) + (type $sub-expect (record (field "sub" u8) (field "state" u8))) + (export $sub-expect-e "sub-expect" (type $sub-expect)) + (type $command (variant + (case "stream-new" u8) + (case "future-new" u8) + (case "testee-read" $io-args-e) + (case "testee-write" $io-args-e) + (case "mock-read" $io-args-e) + (case "mock-write" $io-args-e) + (case "call-import" u8) + (case "poll" $poll-expect-e) + (case "await" $poll-expect-e) + (case "drop-readable" u8) + (case "drop-writable" u8) + (case "expect-code" s32) + (case "noop") + (case "future-read" u8) + (case "future-write" u8) + (case "future-drop-readable" u8) + (case "future-drop-writable" u8) + (case "call-import-future" u8) + (case "poll-readable" $poll-expect-e) + (case "await-readable" $poll-expect-e) + (case "testee-cancel-write" u8) + (case "testee-cancel-read" u8) + (case "future-cancel-write" u8) + (case "future-cancel-read" u8) + (case "call-block-empty" u8) + (case "call-block-future" $sub-args-e) + (case "subtask-cancel" u8) + (case "subtask-drop" u8) + (case "await-subtask" $sub-expect-e) + (case "mock-bp-inc") + (case "mock-bp-dec"))) + (export $command-e "command" (type $command)) + (import "call-import" (func $call-import (param "slot" u8) (result s32))) + (import "stream-new" (func $stream-new (param "slot" u8))) + (import "testee-write" (func $testee-write (param "handle" u8) (param "bytes" u32) (result s32))) + (import "testee-read" (func $testee-read (param "handle" u8) (param "bytes" u32) (result s32))) + (import "drop-readable" (func $drop-readable (param "slot" u8))) + (import "drop-writable" (func $drop-writable (param "slot" u8))) + (import "poll" (func $poll (param "slot" u8) (param "event" u8) (param "payload" u32))) + (import "await" (func $await async (param "slot" u8) (param "event" u8) (param "payload" u32))) + (import "future-new" (func $future-new (param "slot" u8))) + (import "future-write" (func $future-write (param "slot" u8) (result s32))) + (import "future-read" (func $future-read (param "slot" u8) (result s32))) + (import "future-drop-readable" (func $future-drop-readable (param "slot" u8))) + (import "future-drop-writable" (func $future-drop-writable (param "slot" u8))) + (import "call-import-future" (func $call-import-future (param "slot" u8) (result s32))) + (import "poll-readable" (func $poll-readable (param "slot" u8) (param "event" u8) (param "payload" u32))) + (import "await-readable" (func $await-readable async (param "slot" u8) (param "event" u8) (param "payload" u32))) + (import "testee-cancel-write" (func $testee-cancel-write (param "slot" u8) (result s32))) + (import "testee-cancel-read" (func $testee-cancel-read (param "slot" u8) (result s32))) + (import "future-cancel-write" (func $future-cancel-write (param "slot" u8) (result s32))) + (import "future-cancel-read" (func $future-cancel-read (param "slot" u8) (result s32))) + (import "call-block-empty" (func $call-block-empty (param "sub" u8) (result s32))) + (import "call-block-future" (func $call-block-future (param "fut" u8) (param "sub" u8) (result s32))) + (import "subtask-cancel" (func $subtask-cancel (param "sub" u8) (result s32))) + (import "subtask-drop" (func $subtask-drop (param "sub" u8))) + (import "await-subtask" (func $await-subtask async (param "sub" u8) (param "state" u8))) + (import "mock-bp-inc" (func $mock-bp-inc)) + (import "mock-bp-dec" (func $mock-bp-dec)) + + (core module $DM + (import "" "call-import" (func $call-import (param i32) (result i32))) + (import "" "stream-new" (func $stream-new (param i32))) + (import "" "testee-write" (func $testee-write (param i32 i32) (result i32))) + (import "" "testee-read" (func $testee-read (param i32 i32) (result i32))) + (import "" "future-new" (func $future-new (param i32))) + (import "" "future-write" (func $future-write (param i32) (result i32))) + (import "" "future-read" (func $future-read (param i32) (result i32))) + (import "" "future-drop-readable" (func $future-drop-readable (param i32))) + (import "" "future-drop-writable" (func $future-drop-writable (param i32))) + (import "" "call-import-future" (func $call-import-future (param i32) (result i32))) + (import "" "drop-readable" (func $drop-readable (param i32))) + (import "" "drop-writable" (func $drop-writable (param i32))) + (import "" "poll" (func $poll (param i32 i32 i32))) + (import "" "await" (func $await (param i32 i32 i32) (result i32))) + (import "" "poll-readable" (func $poll-readable (param i32 i32 i32))) + (import "" "await-readable" (func $await-readable (param i32 i32 i32) (result i32))) + (import "" "testee-cancel-write" (func $testee-cancel-write (param i32) (result i32))) + (import "" "testee-cancel-read" (func $testee-cancel-read (param i32) (result i32))) + (import "" "future-cancel-write" (func $future-cancel-write (param i32) (result i32))) + (import "" "future-cancel-read" (func $future-cancel-read (param i32) (result i32))) + (import "" "call-block-empty" (func $call-block-empty (param i32) (result i32))) + (import "" "call-block-future" (func $call-block-future (param i32 i32) (result i32))) + (import "" "subtask-cancel" (func $subtask-cancel (param i32) (result i32))) + (import "" "subtask-drop" (func $subtask-drop (param i32))) + (import "" "await-subtask" (func $await-subtask (param i32 i32) (result i32))) + (import "" "mock-bp-inc" (func $mock-bp-inc)) + (import "" "mock-bp-dec" (func $mock-bp-dec)) + (memory (export "mem") 1) + + (global $w (mut i32) (i32.const 1024)) + (func (export "realloc") (param $old i32) (param $os i32) (param $al i32) (param $ns i32) (result i32) + (local $r i32) + (global.set $w (i32.and (i32.add (global.get $w) (i32.const 7)) (i32.const -8))) + (local.set $r (global.get $w)) + (global.set $w (i32.add (global.get $w) (local.get $ns))) + (local.get $r)) + + (global $STREAM_NEW i32 (i32.const 0)) + (global $FUTURE_NEW i32 (i32.const 1)) + (global $TESTEE_READ i32 (i32.const 2)) + (global $TESTEE_WRITE i32 (i32.const 3)) + (global $MOCK_READ i32 (i32.const 4)) + (global $MOCK_WRITE i32 (i32.const 5)) + (global $CALL_IMPORT i32 (i32.const 6)) + (global $POLL i32 (i32.const 7)) + (global $AWAIT i32 (i32.const 8)) + (global $DROP_READABLE i32 (i32.const 9)) + (global $DROP_WRITABLE i32 (i32.const 10)) + (global $EXPECT_CODE i32 (i32.const 11)) + (global $NOOP i32 (i32.const 12)) + (global $FUTURE_READ i32 (i32.const 13)) + (global $FUTURE_WRITE i32 (i32.const 14)) + (global $FUTURE_DROP_READABLE i32 (i32.const 15)) + (global $FUTURE_DROP_WRITABLE i32 (i32.const 16)) + (global $CALL_IMPORT_FUTURE i32 (i32.const 17)) + (global $POLL_READABLE i32 (i32.const 18)) + (global $AWAIT_READABLE i32 (i32.const 19)) + (global $TESTEE_CANCEL_WRITE i32 (i32.const 20)) + (global $TESTEE_CANCEL_READ i32 (i32.const 21)) + (global $FUTURE_CANCEL_WRITE i32 (i32.const 22)) + (global $FUTURE_CANCEL_READ i32 (i32.const 23)) + (global $CALL_BLOCK_EMPTY i32 (i32.const 24)) + (global $CALL_BLOCK_FUTURE i32 (i32.const 25)) + (global $SUBTASK_CANCEL i32 (i32.const 26)) + (global $SUBTASK_DROP i32 (i32.const 27)) + (global $AWAIT_SUBTASK i32 (i32.const 28)) + (global $MOCK_BP_INC i32 (i32.const 29)) + (global $MOCK_BP_DEC i32 (i32.const 30)) + + (global $last (mut i32) (i32.const 0)) + (global $VOID_OK i32 (i32.const 1337)) + + (func (export "run") (param $ptr i32) (param $len i32) + (local $i i32) (local $insn i32) (local $op i32) (local $slot i32) (local $bytes i32) + (global.set $last (i32.const 0)) + (block $done + (loop $loop + (br_if $done (i32.ge_u (local.get $i) (local.get $len))) + (local.set $insn (i32.add (local.get $ptr) (i32.mul (local.get $i) (i32.const 12)))) + (local.set $op (i32.load8_u offset=0 (local.get $insn))) + + (if (i32.eq (local.get $op) (global.get $STREAM_NEW)) + (then + (call $stream-new (i32.load8_u offset=4 (local.get $insn))) + (global.set $last (global.get $VOID_OK)))) + + (if (i32.eq (local.get $op) (global.get $TESTEE_WRITE)) + (then + (local.set $slot (i32.load8_u offset=4 (local.get $insn))) + (local.set $bytes (i32.load offset=8 (local.get $insn))) + (global.set $last (call $testee-write (local.get $slot) (local.get $bytes))))) + + (if (i32.eq (local.get $op) (global.get $TESTEE_READ)) + (then + (local.set $slot (i32.load8_u offset=4 (local.get $insn))) + (local.set $bytes (i32.load offset=8 (local.get $insn))) + (global.set $last (call $testee-read (local.get $slot) (local.get $bytes))))) + + (if (i32.eq (local.get $op) (global.get $FUTURE_NEW)) + (then + (call $future-new (i32.load8_u offset=4 (local.get $insn))) + (global.set $last (global.get $VOID_OK)))) + + (if (i32.eq (local.get $op) (global.get $FUTURE_WRITE)) + (then (global.set $last (call $future-write (i32.load8_u offset=4 (local.get $insn)))))) + (if (i32.eq (local.get $op) (global.get $FUTURE_READ)) + (then (global.set $last (call $future-read (i32.load8_u offset=4 (local.get $insn)))))) + + (if (i32.eq (local.get $op) (global.get $FUTURE_DROP_READABLE)) + (then + (call $future-drop-readable (i32.load8_u offset=4 (local.get $insn))) + (global.set $last (global.get $VOID_OK)))) + (if (i32.eq (local.get $op) (global.get $FUTURE_DROP_WRITABLE)) + (then + (call $future-drop-writable (i32.load8_u offset=4 (local.get $insn))) + (global.set $last (global.get $VOID_OK)))) + + (if (i32.eq (local.get $op) (global.get $DROP_READABLE)) + (then + (call $drop-readable (i32.load8_u offset=4 (local.get $insn))) + (global.set $last (global.get $VOID_OK)))) + (if (i32.eq (local.get $op) (global.get $DROP_WRITABLE)) + (then + (call $drop-writable (i32.load8_u offset=4 (local.get $insn))) + (global.set $last (global.get $VOID_OK)))) + + (if (i32.eq (local.get $op) (global.get $CALL_IMPORT)) + (then (global.set $last (call $call-import (i32.load8_u offset=4 (local.get $insn)))))) + + (if (i32.eq (local.get $op) (global.get $CALL_IMPORT_FUTURE)) + (then (global.set $last (call $call-import-future (i32.load8_u offset=4 (local.get $insn)))))) + + (if (i32.eq (local.get $op) (global.get $POLL)) + (then + (call $poll + (i32.load8_u offset=4 (local.get $insn)) + (i32.load8_u offset=5 (local.get $insn)) + (i32.load offset=8 (local.get $insn))) + (global.set $last (global.get $VOID_OK)))) + + (if (i32.eq (local.get $op) (global.get $AWAIT)) + (then + (global.set $last (call $await + (i32.load8_u offset=4 (local.get $insn)) + (i32.load8_u offset=5 (local.get $insn)) + (i32.load offset=8 (local.get $insn)))))) + + (if (i32.eq (local.get $op) (global.get $POLL_READABLE)) + (then + (call $poll-readable + (i32.load8_u offset=4 (local.get $insn)) + (i32.load8_u offset=5 (local.get $insn)) + (i32.load offset=8 (local.get $insn))) + (global.set $last (global.get $VOID_OK)))) + + (if (i32.eq (local.get $op) (global.get $AWAIT_READABLE)) + (then + (global.set $last (call $await-readable + (i32.load8_u offset=4 (local.get $insn)) + (i32.load8_u offset=5 (local.get $insn)) + (i32.load offset=8 (local.get $insn)))))) + + (if (i32.eq (local.get $op) (global.get $TESTEE_CANCEL_WRITE)) + (then (global.set $last (call $testee-cancel-write (i32.load8_u offset=4 (local.get $insn)))))) + (if (i32.eq (local.get $op) (global.get $TESTEE_CANCEL_READ)) + (then (global.set $last (call $testee-cancel-read (i32.load8_u offset=4 (local.get $insn)))))) + (if (i32.eq (local.get $op) (global.get $FUTURE_CANCEL_WRITE)) + (then (global.set $last (call $future-cancel-write (i32.load8_u offset=4 (local.get $insn)))))) + (if (i32.eq (local.get $op) (global.get $FUTURE_CANCEL_READ)) + (then (global.set $last (call $future-cancel-read (i32.load8_u offset=4 (local.get $insn)))))) + + (if (i32.eq (local.get $op) (global.get $CALL_BLOCK_EMPTY)) + (then (global.set $last (call $call-block-empty (i32.load8_u offset=4 (local.get $insn)))))) + (if (i32.eq (local.get $op) (global.get $CALL_BLOCK_FUTURE)) + (then (global.set $last (call $call-block-future + (i32.load8_u offset=4 (local.get $insn)) + (i32.load8_u offset=5 (local.get $insn)))))) + (if (i32.eq (local.get $op) (global.get $SUBTASK_CANCEL)) + (then (global.set $last (call $subtask-cancel (i32.load8_u offset=4 (local.get $insn)))))) + (if (i32.eq (local.get $op) (global.get $SUBTASK_DROP)) + (then + (call $subtask-drop (i32.load8_u offset=4 (local.get $insn))) + (global.set $last (global.get $VOID_OK)))) + (if (i32.eq (local.get $op) (global.get $AWAIT_SUBTASK)) + (then + (global.set $last (call $await-subtask + (i32.load8_u offset=4 (local.get $insn)) + (i32.load8_u offset=5 (local.get $insn)))))) + + (if (i32.eq (local.get $op) (global.get $MOCK_BP_INC)) + (then + (call $mock-bp-inc) + (global.set $last (global.get $VOID_OK)))) + (if (i32.eq (local.get $op) (global.get $MOCK_BP_DEC)) + (then + (call $mock-bp-dec) + (global.set $last (global.get $VOID_OK)))) + + (if (i32.eq (local.get $op) (global.get $EXPECT_CODE)) + (then + (if (i32.ne (global.get $last) (i32.load offset=4 (local.get $insn))) + (then unreachable)))) + + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br $loop) + ) + ) + ) + ) + + (canon lower (func $call-import) (core func $call-import')) + (canon lower (func $call-import-future) (core func $call-import-future')) + (canon lower (func $stream-new) (core func $stream-new')) + (canon lower (func $testee-write) (core func $testee-write')) + (canon lower (func $testee-read) (core func $testee-read')) + (canon lower (func $future-new) (core func $future-new')) + (canon lower (func $future-write) (core func $future-write')) + (canon lower (func $future-read) (core func $future-read')) + (canon lower (func $future-drop-readable) (core func $future-drop-readable')) + (canon lower (func $future-drop-writable) (core func $future-drop-writable')) + (canon lower (func $drop-readable) (core func $drop-readable')) + (canon lower (func $drop-writable) (core func $drop-writable')) + (canon lower (func $poll) (core func $poll')) + (canon lower (func $await) async (core func $await')) + (canon lower (func $poll-readable) (core func $poll-readable')) + (canon lower (func $await-readable) async (core func $await-readable')) + (canon lower (func $testee-cancel-write) (core func $testee-cancel-write')) + (canon lower (func $testee-cancel-read) (core func $testee-cancel-read')) + (canon lower (func $future-cancel-write) (core func $future-cancel-write')) + (canon lower (func $future-cancel-read) (core func $future-cancel-read')) + (canon lower (func $call-block-empty) (core func $call-block-empty')) + (canon lower (func $call-block-future) (core func $call-block-future')) + (canon lower (func $subtask-cancel) (core func $subtask-cancel')) + (canon lower (func $subtask-drop) (core func $subtask-drop')) + (canon lower (func $await-subtask) async (core func $await-subtask')) + (canon lower (func $mock-bp-inc) (core func $mock-bp-inc')) + (canon lower (func $mock-bp-dec) (core func $mock-bp-dec')) + (core instance $dm (instantiate $DM (with "" (instance + (export "call-import" (func $call-import')) + (export "call-import-future" (func $call-import-future')) + (export "stream-new" (func $stream-new')) + (export "testee-write" (func $testee-write')) + (export "testee-read" (func $testee-read')) + (export "future-new" (func $future-new')) + (export "future-write" (func $future-write')) + (export "future-read" (func $future-read')) + (export "future-drop-readable" (func $future-drop-readable')) + (export "future-drop-writable" (func $future-drop-writable')) + (export "drop-readable" (func $drop-readable')) + (export "drop-writable" (func $drop-writable')) + (export "poll" (func $poll')) + (export "await" (func $await')) + (export "poll-readable" (func $poll-readable')) + (export "await-readable" (func $await-readable')) + (export "testee-cancel-write" (func $testee-cancel-write')) + (export "testee-cancel-read" (func $testee-cancel-read')) + (export "future-cancel-write" (func $future-cancel-write')) + (export "future-cancel-read" (func $future-cancel-read')) + (export "call-block-empty" (func $call-block-empty')) + (export "call-block-future" (func $call-block-future')) + (export "subtask-cancel" (func $subtask-cancel')) + (export "subtask-drop" (func $subtask-drop')) + (export "await-subtask" (func $await-subtask')) + (export "mock-bp-inc" (func $mock-bp-inc')) + (export "mock-bp-dec" (func $mock-bp-dec')))))) + (func (export "run") (param "prog" (list $command-e)) + (canon lift (core func $dm "run") (memory (core memory $dm "mem")) (realloc (core func $dm "realloc"))))) + + (instance $mock (instantiate $Mock)) + (instance $testee (instantiate $Testee + (with "sink" (func $mock "sink")) + (with "sink-future" (func $mock "sink-future")) + (with "block-empty" (func $mock "block-empty")) + (with "block-future" (func $mock "block-future")) + (with "bp-inc" (func $mock "bp-inc")) + (with "bp-dec" (func $mock "bp-dec")))) + (instance $driver (instantiate $Driver + (with "call-import" (func $testee "call-import")) + (with "call-import-future" (func $testee "call-import-future")) + (with "stream-new" (func $testee "stream-new")) + (with "testee-write" (func $testee "testee-write")) + (with "testee-read" (func $testee "testee-read")) + (with "future-new" (func $testee "future-new")) + (with "future-write" (func $testee "future-write")) + (with "future-read" (func $testee "future-read")) + (with "future-drop-readable" (func $testee "future-drop-readable")) + (with "future-drop-writable" (func $testee "future-drop-writable")) + (with "drop-readable" (func $testee "drop-readable")) + (with "drop-writable" (func $testee "drop-writable")) + (with "poll" (func $testee "poll")) + (with "await" (func $testee "await")) + (with "poll-readable" (func $testee "poll-readable")) + (with "await-readable" (func $testee "await-readable")) + (with "testee-cancel-write" (func $testee "testee-cancel-write")) + (with "testee-cancel-read" (func $testee "testee-cancel-read")) + (with "future-cancel-write" (func $testee "future-cancel-write")) + (with "future-cancel-read" (func $testee "future-cancel-read")) + (with "call-block-empty" (func $testee "call-block-empty")) + (with "call-block-future" (func $testee "call-block-future")) + (with "subtask-cancel" (func $testee "subtask-cancel")) + (with "subtask-drop" (func $testee "subtask-drop")) + (with "await-subtask" (func $testee "await-subtask")) + (with "mock-bp-inc" (func $testee "mock-bp-inc")) + (with "mock-bp-dec" (func $testee "mock-bp-dec")))) + (instance $types + (export "event-kind" (type $driver "event-kind")) + (export "io-args" (type $driver "io-args")) + (export "poll-expect" (type $driver "poll-expect")) + (export "sub-args" (type $driver "sub-args")) + (export "sub-expect" (type $driver "sub-expect")) + (export "command" (type $driver "command"))) + (export "types" (instance $types)) + (alias export $driver "run" (func $run)) + (export "run" (func $run)) +) + +(component instance $i $Tester) + +(assert_return (invoke "run" (list.const))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "expect-code" (s32.const 1337)) + (variant.const "drop-writable" (u8.const 0)) + (variant.const "drop-readable" (u8.const 0)) + (variant.const "expect-code" (s32.const 1337))))) + +(assert_trap + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "expect-code" (s32.const 99)))) + "unreachable") +(component instance $i $Tester) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-read" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "poll" (record.const (field "slot" u8.const 0) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "drop-writable" (u8.const 0)) + (variant.const "drop-readable" (u8.const 0))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 1)) + (variant.const "drop-writable" (u8.const 1)) + (variant.const "drop-readable" (u8.const 1))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 2)) + (variant.const "call-import" (u8.const 2)) + (variant.const "expect-code" (s32.const 2)) + (variant.const "testee-write" (record.const (field "handle" u8.const 2) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "drop-writable" (u8.const 2))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 3)) + (variant.const "testee-write" (record.const (field "handle" u8.const 3) (field "bytes" u32.const 4))) + (variant.const "testee-read" (record.const (field "handle" u8.const 3) (field "bytes" u32.const 4))) + (variant.const "await" (record.const (field "slot" u8.const 3) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "expect-code" (s32.const 2)) + (variant.const "drop-writable" (u8.const 3)) + (variant.const "drop-readable" (u8.const 3))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "future-new" (u8.const 4)) + (variant.const "future-write" (u8.const 4)) + (variant.const "expect-code" (s32.const -1)) + (variant.const "future-read" (u8.const 4)) + (variant.const "expect-code" (s32.const 0)) + (variant.const "poll" (record.const (field "slot" u8.const 4) (field "event" enum.const "future-write") (field "payload" u32.const 0))) + (variant.const "future-drop-writable" (u8.const 4)) + (variant.const "future-drop-readable" (u8.const 4))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "future-new" (u8.const 5)) + (variant.const "call-import-future" (u8.const 5)) + (variant.const "expect-code" (s32.const 2)) + (variant.const "future-write" (u8.const 5)) + (variant.const "expect-code" (s32.const 0)) + (variant.const "future-drop-writable" (u8.const 5))))) + +(assert_trap + (invoke "run" + (list.const + (variant.const "future-new" (u8.const 11)) + (variant.const "future-drop-writable" (u8.const 11)))) + "cannot drop future write end without first writing a value") +(component instance $i $Tester) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 6)) + (variant.const "stream-new" (u8.const 7)) + (variant.const "testee-write" (record.const (field "handle" u8.const 6) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 7) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-read" (record.const (field "handle" u8.const 7) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-read" (record.const (field "handle" u8.const 6) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "poll" (record.const (field "slot" u8.const 6) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 7) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "drop-writable" (u8.const 6)) + (variant.const "drop-readable" (u8.const 6)) + (variant.const "drop-writable" (u8.const 7)) + (variant.const "drop-readable" (u8.const 7))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 8)) + (variant.const "testee-write" (record.const (field "handle" u8.const 8) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "poll" (record.const (field "slot" u8.const 8) (field "event" enum.const "none") (field "payload" u32.const 0))) + (variant.const "testee-read" (record.const (field "handle" u8.const 8) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "await" (record.const (field "slot" u8.const 8) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "expect-code" (s32.const 2))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 9)) + (variant.const "stream-new" (u8.const 10)) + (variant.const "call-import" (u8.const 9)) + (variant.const "expect-code" (s32.const 2)) + (variant.const "testee-write" (record.const (field "handle" u8.const 10) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 9) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-read" (record.const (field "handle" u8.const 10) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "drop-writable" (u8.const 9)) + (variant.const "poll" (record.const (field "slot" u8.const 10) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "drop-writable" (u8.const 10)) + (variant.const "drop-readable" (u8.const 10))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "stream-new" (u8.const 1)) + (variant.const "stream-new" (u8.const 2)) + (variant.const "stream-new" (u8.const 3)) + (variant.const "stream-new" (u8.const 4)) + (variant.const "stream-new" (u8.const 5)) + (variant.const "stream-new" (u8.const 6)) + (variant.const "stream-new" (u8.const 7)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 1) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 2) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 3) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 4) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 5) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 6) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 7) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-read" (record.const (field "handle" u8.const 3) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-read" (record.const (field "handle" u8.const 7) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-read" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-read" (record.const (field "handle" u8.const 5) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-read" (record.const (field "handle" u8.const 1) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-read" (record.const (field "handle" u8.const 6) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-read" (record.const (field "handle" u8.const 2) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-read" (record.const (field "handle" u8.const 4) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "poll" (record.const (field "slot" u8.const 0) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 1) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 2) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 3) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 4) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 5) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 6) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 7) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "drop-writable" (u8.const 0)) (variant.const "drop-readable" (u8.const 0)) + (variant.const "drop-writable" (u8.const 1)) (variant.const "drop-readable" (u8.const 1)) + (variant.const "drop-writable" (u8.const 2)) (variant.const "drop-readable" (u8.const 2)) + (variant.const "drop-writable" (u8.const 3)) (variant.const "drop-readable" (u8.const 3)) + (variant.const "drop-writable" (u8.const 4)) (variant.const "drop-readable" (u8.const 4)) + (variant.const "drop-writable" (u8.const 5)) (variant.const "drop-readable" (u8.const 5)) + (variant.const "drop-writable" (u8.const 6)) (variant.const "drop-readable" (u8.const 6)) + (variant.const "drop-writable" (u8.const 7)) (variant.const "drop-readable" (u8.const 7))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "stream-new" (u8.const 1)) + (variant.const "stream-new" (u8.const 2)) + (variant.const "stream-new" (u8.const 3)) + (variant.const "stream-new" (u8.const 4)) + (variant.const "call-import" (u8.const 0)) + (variant.const "expect-code" (s32.const 2)) + (variant.const "call-import" (u8.const 1)) + (variant.const "expect-code" (s32.const 2)) + (variant.const "call-import" (u8.const 2)) + (variant.const "expect-code" (s32.const 2)) + (variant.const "call-import" (u8.const 3)) + (variant.const "expect-code" (s32.const 2)) + (variant.const "call-import" (u8.const 4)) + (variant.const "expect-code" (s32.const 2)) + (variant.const "testee-write" (record.const (field "handle" u8.const 2) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-write" (record.const (field "handle" u8.const 4) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-write" (record.const (field "handle" u8.const 3) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-write" (record.const (field "handle" u8.const 1) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "drop-writable" (u8.const 0)) + (variant.const "drop-writable" (u8.const 1)) + (variant.const "drop-writable" (u8.const 2)) + (variant.const "drop-writable" (u8.const 3)) + (variant.const "drop-writable" (u8.const 4))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "future-new" (u8.const 3)) + (variant.const "stream-new" (u8.const 1)) + (variant.const "future-new" (u8.const 4)) + (variant.const "stream-new" (u8.const 2)) + (variant.const "future-new" (u8.const 5)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "future-write" (u8.const 3)) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 1) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "future-write" (u8.const 4)) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 2) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "future-write" (u8.const 5)) + (variant.const "expect-code" (s32.const -1)) + (variant.const "future-read" (u8.const 4)) + (variant.const "expect-code" (s32.const 0)) + (variant.const "testee-read" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "future-read" (u8.const 3)) + (variant.const "expect-code" (s32.const 0)) + (variant.const "testee-read" (record.const (field "handle" u8.const 2) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "future-read" (u8.const 5)) + (variant.const "expect-code" (s32.const 0)) + (variant.const "testee-read" (record.const (field "handle" u8.const 1) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "poll" (record.const (field "slot" u8.const 0) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 3) (field "event" enum.const "future-write") (field "payload" u32.const 0))) + (variant.const "poll" (record.const (field "slot" u8.const 1) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 4) (field "event" enum.const "future-write") (field "payload" u32.const 0))) + (variant.const "poll" (record.const (field "slot" u8.const 2) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 5) (field "event" enum.const "future-write") (field "payload" u32.const 0))) + (variant.const "drop-writable" (u8.const 0)) (variant.const "drop-readable" (u8.const 0)) + (variant.const "drop-writable" (u8.const 1)) (variant.const "drop-readable" (u8.const 1)) + (variant.const "drop-writable" (u8.const 2)) (variant.const "drop-readable" (u8.const 2)) + (variant.const "future-drop-writable" (u8.const 3)) (variant.const "future-drop-readable" (u8.const 3)) + (variant.const "future-drop-writable" (u8.const 4)) (variant.const "future-drop-readable" (u8.const 4)) + (variant.const "future-drop-writable" (u8.const 5)) (variant.const "future-drop-readable" (u8.const 5))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "stream-new" (u8.const 1)) + (variant.const "future-new" (u8.const 2)) + (variant.const "future-new" (u8.const 3)) + (variant.const "call-import" (u8.const 0)) + (variant.const "expect-code" (s32.const 2)) + (variant.const "call-import-future" (u8.const 2)) + (variant.const "expect-code" (s32.const 2)) + (variant.const "call-import" (u8.const 1)) + (variant.const "expect-code" (s32.const 2)) + (variant.const "call-import-future" (u8.const 3)) + (variant.const "expect-code" (s32.const 2)) + (variant.const "future-write" (u8.const 2)) + (variant.const "expect-code" (s32.const 0)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "future-write" (u8.const 3)) + (variant.const "expect-code" (s32.const 0)) + (variant.const "testee-write" (record.const (field "handle" u8.const 1) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "drop-writable" (u8.const 0)) + (variant.const "drop-writable" (u8.const 1)) + (variant.const "future-drop-writable" (u8.const 2)) + (variant.const "future-drop-writable" (u8.const 3))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "stream-new" (u8.const 1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 1) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-read" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-read" (record.const (field "handle" u8.const 1) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "poll" (record.const (field "slot" u8.const 0) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 1) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "testee-write" (record.const (field "handle" u8.const 1) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-read" (record.const (field "handle" u8.const 1) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-read" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "poll" (record.const (field "slot" u8.const 1) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 0) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "drop-writable" (u8.const 0)) (variant.const "drop-readable" (u8.const 0)) + (variant.const "drop-writable" (u8.const 1)) (variant.const "drop-readable" (u8.const 1))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "stream-new" (u8.const 1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 1) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-read" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-read" (record.const (field "handle" u8.const 1) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "poll" (record.const (field "slot" u8.const 0) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 1) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "drop-writable" (u8.const 0)) (variant.const "drop-readable" (u8.const 0)) + (variant.const "drop-writable" (u8.const 1)) (variant.const "drop-readable" (u8.const 1)) + (variant.const "stream-new" (u8.const 0)) + (variant.const "stream-new" (u8.const 1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 1) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-read" (record.const (field "handle" u8.const 1) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-read" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "poll" (record.const (field "slot" u8.const 1) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 0) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "drop-writable" (u8.const 0)) (variant.const "drop-readable" (u8.const 0)) + (variant.const "drop-writable" (u8.const 1)) (variant.const "drop-readable" (u8.const 1)) + (variant.const "stream-new" (u8.const 0)) + (variant.const "stream-new" (u8.const 1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 1) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-read" (record.const (field "handle" u8.const 1) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-read" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "poll" (record.const (field "slot" u8.const 0) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 1) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "drop-writable" (u8.const 0)) (variant.const "drop-readable" (u8.const 0)) + (variant.const "drop-writable" (u8.const 1)) (variant.const "drop-readable" (u8.const 1))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "stream-new" (u8.const 1)) + (variant.const "stream-new" (u8.const 2)) + (variant.const "stream-new" (u8.const 3)) + (variant.const "stream-new" (u8.const 4)) + (variant.const "stream-new" (u8.const 5)) + (variant.const "stream-new" (u8.const 6)) + (variant.const "call-import" (u8.const 0)) + (variant.const "expect-code" (s32.const 2)) + (variant.const "call-import" (u8.const 1)) + (variant.const "expect-code" (s32.const 2)) + (variant.const "call-import" (u8.const 2)) + (variant.const "expect-code" (s32.const 2)) + (variant.const "testee-write" (record.const (field "handle" u8.const 3) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 4) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 1) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-write" (record.const (field "handle" u8.const 5) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 6) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-read" (record.const (field "handle" u8.const 5) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-read" (record.const (field "handle" u8.const 3) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-read" (record.const (field "handle" u8.const 6) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-write" (record.const (field "handle" u8.const 2) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-read" (record.const (field "handle" u8.const 4) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "poll" (record.const (field "slot" u8.const 3) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 4) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 5) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 6) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "drop-writable" (u8.const 3)) (variant.const "drop-readable" (u8.const 3)) + (variant.const "drop-writable" (u8.const 4)) (variant.const "drop-readable" (u8.const 4)) + (variant.const "drop-writable" (u8.const 5)) (variant.const "drop-readable" (u8.const 5)) + (variant.const "drop-writable" (u8.const 6)) (variant.const "drop-readable" (u8.const 6)) + (variant.const "drop-writable" (u8.const 0)) + (variant.const "drop-writable" (u8.const 1)) + (variant.const "drop-writable" (u8.const 2))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "stream-new" (u8.const 1)) + (variant.const "stream-new" (u8.const 2)) + (variant.const "stream-new" (u8.const 3)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 1) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 2) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 3) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-read" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-read" (record.const (field "handle" u8.const 1) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-read" (record.const (field "handle" u8.const 2) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-read" (record.const (field "handle" u8.const 3) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "await" (record.const (field "slot" u8.const 0) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "expect-code" (s32.const 2)) + (variant.const "await" (record.const (field "slot" u8.const 1) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "expect-code" (s32.const 2)) + (variant.const "await" (record.const (field "slot" u8.const 2) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "expect-code" (s32.const 2)) + (variant.const "await" (record.const (field "slot" u8.const 3) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "expect-code" (s32.const 2))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "stream-new" (u8.const 1)) + (variant.const "future-new" (u8.const 2)) + (variant.const "future-new" (u8.const 3)) + (variant.const "stream-new" (u8.const 4)) + (variant.const "stream-new" (u8.const 5)) + (variant.const "future-new" (u8.const 6)) + (variant.const "future-new" (u8.const 7)) + (variant.const "call-import" (u8.const 0)) + (variant.const "expect-code" (s32.const 2)) + (variant.const "call-import-future" (u8.const 2)) + (variant.const "expect-code" (s32.const 2)) + (variant.const "call-import" (u8.const 1)) + (variant.const "expect-code" (s32.const 2)) + (variant.const "call-import-future" (u8.const 3)) + (variant.const "expect-code" (s32.const 2)) + (variant.const "testee-write" (record.const (field "handle" u8.const 4) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "future-write" (u8.const 6)) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 5) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "future-write" (u8.const 7)) + (variant.const "expect-code" (s32.const -1)) + (variant.const "future-write" (u8.const 2)) + (variant.const "expect-code" (s32.const 0)) + (variant.const "testee-read" (record.const (field "handle" u8.const 4) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "future-read" (u8.const 6)) + (variant.const "expect-code" (s32.const 0)) + (variant.const "future-write" (u8.const 3)) + (variant.const "expect-code" (s32.const 0)) + (variant.const "testee-read" (record.const (field "handle" u8.const 5) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-write" (record.const (field "handle" u8.const 1) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "future-read" (u8.const 7)) + (variant.const "expect-code" (s32.const 0)) + (variant.const "poll" (record.const (field "slot" u8.const 4) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 5) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 6) (field "event" enum.const "future-write") (field "payload" u32.const 0))) + (variant.const "poll" (record.const (field "slot" u8.const 7) (field "event" enum.const "future-write") (field "payload" u32.const 0))) + (variant.const "drop-writable" (u8.const 4)) (variant.const "drop-readable" (u8.const 4)) + (variant.const "drop-writable" (u8.const 5)) (variant.const "drop-readable" (u8.const 5)) + (variant.const "future-drop-writable" (u8.const 6)) (variant.const "future-drop-readable" (u8.const 6)) + (variant.const "future-drop-writable" (u8.const 7)) (variant.const "future-drop-readable" (u8.const 7)) + (variant.const "drop-writable" (u8.const 0)) + (variant.const "drop-writable" (u8.const 1)) + (variant.const "future-drop-writable" (u8.const 2)) + (variant.const "future-drop-writable" (u8.const 3))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "stream-new" (u8.const 1)) + (variant.const "stream-new" (u8.const 2)) + (variant.const "stream-new" (u8.const 3)) + (variant.const "stream-new" (u8.const 4)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 1) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 2) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 3) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 4) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "poll" (record.const (field "slot" u8.const 0) (field "event" enum.const "none") (field "payload" u32.const 0))) + (variant.const "poll" (record.const (field "slot" u8.const 1) (field "event" enum.const "none") (field "payload" u32.const 0))) + (variant.const "poll" (record.const (field "slot" u8.const 2) (field "event" enum.const "none") (field "payload" u32.const 0))) + (variant.const "poll" (record.const (field "slot" u8.const 3) (field "event" enum.const "none") (field "payload" u32.const 0))) + (variant.const "poll" (record.const (field "slot" u8.const 4) (field "event" enum.const "none") (field "payload" u32.const 0))) + (variant.const "testee-read" (record.const (field "handle" u8.const 2) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "poll" (record.const (field "slot" u8.const 2) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 0) (field "event" enum.const "none") (field "payload" u32.const 0))) + (variant.const "testee-read" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "poll" (record.const (field "slot" u8.const 0) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 4) (field "event" enum.const "none") (field "payload" u32.const 0))) + (variant.const "testee-read" (record.const (field "handle" u8.const 4) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "poll" (record.const (field "slot" u8.const 4) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 1) (field "event" enum.const "none") (field "payload" u32.const 0))) + (variant.const "testee-read" (record.const (field "handle" u8.const 1) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "poll" (record.const (field "slot" u8.const 1) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 3) (field "event" enum.const "none") (field "payload" u32.const 0))) + (variant.const "testee-read" (record.const (field "handle" u8.const 3) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "poll" (record.const (field "slot" u8.const 3) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "drop-writable" (u8.const 0)) (variant.const "drop-readable" (u8.const 0)) + (variant.const "drop-writable" (u8.const 1)) (variant.const "drop-readable" (u8.const 1)) + (variant.const "drop-writable" (u8.const 2)) (variant.const "drop-readable" (u8.const 2)) + (variant.const "drop-writable" (u8.const 3)) (variant.const "drop-readable" (u8.const 3)) + (variant.const "drop-writable" (u8.const 4)) (variant.const "drop-readable" (u8.const 4))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 8))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-read" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-read" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "poll" (record.const (field "slot" u8.const 0) (field "event" enum.const "stream-write") (field "payload" u32.const 0x80))) + (variant.const "drop-writable" (u8.const 0)) + (variant.const "drop-readable" (u8.const 0))))) + +(component instance $i $Tester) +(assert_trap + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))))) + "cannot have concurrent operations active on a future/stream") +(component instance $i $Tester) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "call-import" (u8.const 0)) + (variant.const "expect-code" (s32.const 2)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 12))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "drop-writable" (u8.const 0))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "drop-readable" (u8.const 0)) + (variant.const "poll" (record.const (field "slot" u8.const 0) (field "event" enum.const "stream-write") (field "payload" u32.const 0x01))) + (variant.const "drop-writable" (u8.const 0))))) + +(component instance $i $Tester) +(assert_trap + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "drop-writable" (u8.const 0)))) + "cannot drop busy stream") +(component instance $i $Tester) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "drop-writable" (u8.const 0)) + (variant.const "testee-read" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x01)) + (variant.const "drop-readable" (u8.const 0))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "testee-read" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 8))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "poll-readable" (record.const (field "slot" u8.const 0) (field "event" enum.const "stream-read") (field "payload" u32.const 0x40))) + (variant.const "drop-writable" (u8.const 0)) + (variant.const "drop-readable" (u8.const 0))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "testee-read" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 8))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "await-readable" (record.const (field "slot" u8.const 0) (field "event" enum.const "stream-read") (field "payload" u32.const 0x40))) + (variant.const "drop-writable" (u8.const 0)) + (variant.const "drop-readable" (u8.const 0))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "testee-read" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "drop-writable" (u8.const 0)) + (variant.const "poll-readable" (record.const (field "slot" u8.const 0) (field "event" enum.const "stream-read") (field "payload" u32.const 0x01))) + (variant.const "drop-readable" (u8.const 0))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 0))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-read" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 0))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "poll" (record.const (field "slot" u8.const 0) (field "event" enum.const "stream-write") (field "payload" u32.const 0x00))) + (variant.const "drop-writable" (u8.const 0)) + (variant.const "poll-readable" (record.const (field "slot" u8.const 0) (field "event" enum.const "stream-read") (field "payload" u32.const 0x01))) + (variant.const "drop-readable" (u8.const 0))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 8))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-read" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "poll" (record.const (field "slot" u8.const 0) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "testee-read" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-cancel-read" (u8.const 0)) + (variant.const "expect-code" (s32.const 0x02)) + (variant.const "drop-writable" (u8.const 0)) + (variant.const "drop-readable" (u8.const 0))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 8))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-read" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-cancel-write" (u8.const 0)) + (variant.const "expect-code" (s32.const 0x42)) + (variant.const "drop-writable" (u8.const 0)) + (variant.const "drop-readable" (u8.const 0))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-cancel-write" (u8.const 0)) + (variant.const "expect-code" (s32.const 0x02)) + (variant.const "drop-writable" (u8.const 0)) + (variant.const "drop-readable" (u8.const 0))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "testee-read" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-cancel-read" (u8.const 0)) + (variant.const "expect-code" (s32.const 0x02)) + (variant.const "drop-writable" (u8.const 0)) + (variant.const "drop-readable" (u8.const 0))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "testee-read" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 8))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "testee-cancel-read" (u8.const 0)) + (variant.const "expect-code" (s32.const 0x42)) + (variant.const "drop-writable" (u8.const 0)) + (variant.const "drop-readable" (u8.const 0))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "future-new" (u8.const 0)) + (variant.const "future-write" (u8.const 0)) + (variant.const "expect-code" (s32.const -1)) + (variant.const "future-cancel-write" (u8.const 0)) + (variant.const "expect-code" (s32.const 0x02)) + (variant.const "future-drop-readable" (u8.const 0)) + (variant.const "future-new" (u8.const 1)) + (variant.const "future-read" (u8.const 1)) + (variant.const "expect-code" (s32.const -1)) + (variant.const "future-cancel-read" (u8.const 1)) + (variant.const "expect-code" (s32.const 0x02)) + (variant.const "future-drop-readable" (u8.const 1))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "call-block-empty" (u8.const 0)) + (variant.const "expect-code" (s32.const 1)) + (variant.const "subtask-cancel" (u8.const 0)) + (variant.const "expect-code" (s32.const 4)) + (variant.const "subtask-drop" (u8.const 0))))) + +(component instance $i $Tester) +(assert_trap + (invoke "run" + (list.const + (variant.const "call-block-empty" (u8.const 0)) + (variant.const "expect-code" (s32.const 1)) + (variant.const "subtask-drop" (u8.const 0)))) + "cannot drop a subtask which has not yet resolved") +(component instance $i $Tester) + +(assert_return + (invoke "run" + (list.const + (variant.const "future-new" (u8.const 0)) + (variant.const "call-block-future" (record.const (field "fut" u8.const 0) (field "sub" u8.const 0))) + (variant.const "expect-code" (s32.const 1)) + (variant.const "future-write" (u8.const 0)) + (variant.const "expect-code" (s32.const 0)) + (variant.const "await-subtask" (record.const (field "sub" u8.const 0) (field "state" u8.const 2))) + (variant.const "future-drop-writable" (u8.const 0))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "call-block-empty" (u8.const 0)) + (variant.const "expect-code" (s32.const 1)) + (variant.const "call-block-empty" (u8.const 1)) + (variant.const "expect-code" (s32.const 1)) + (variant.const "call-block-empty" (u8.const 2)) + (variant.const "expect-code" (s32.const 1)) + (variant.const "subtask-cancel" (u8.const 2)) + (variant.const "expect-code" (s32.const 4)) + (variant.const "subtask-cancel" (u8.const 0)) + (variant.const "expect-code" (s32.const 4)) + (variant.const "subtask-cancel" (u8.const 1)) + (variant.const "expect-code" (s32.const 4)) + (variant.const "subtask-drop" (u8.const 0)) + (variant.const "subtask-drop" (u8.const 1)) + (variant.const "subtask-drop" (u8.const 2))))) + +(assert_return + (invoke "run" + (list.const + (variant.const "mock-bp-inc") + (variant.const "call-block-empty" (u8.const 0)) + (variant.const "expect-code" (s32.const 0)) + (variant.const "subtask-cancel" (u8.const 0)) + (variant.const "expect-code" (s32.const 3)) + (variant.const "subtask-drop" (u8.const 0)) + (variant.const "mock-bp-dec")))) + +(assert_return + (invoke "run" + (list.const + (variant.const "mock-bp-inc") + (variant.const "mock-bp-inc") + (variant.const "call-block-empty" (u8.const 0)) + (variant.const "expect-code" (s32.const 0)) + (variant.const "subtask-cancel" (u8.const 0)) + (variant.const "expect-code" (s32.const 3)) + (variant.const "subtask-drop" (u8.const 0)) + (variant.const "mock-bp-dec") + (variant.const "call-block-empty" (u8.const 1)) + (variant.const "expect-code" (s32.const 0)) + (variant.const "subtask-cancel" (u8.const 1)) + (variant.const "expect-code" (s32.const 3)) + (variant.const "subtask-drop" (u8.const 1)) + (variant.const "mock-bp-dec")))) diff --git a/packages/jco-transpile/test/fixtures/wast/upstream-manifest.json b/packages/jco-transpile/test/fixtures/wast/upstream-manifest.json index 17c9b9fb2..fd904ac7f 100644 --- a/packages/jco-transpile/test/fixtures/wast/upstream-manifest.json +++ b/packages/jco-transpile/test/fixtures/wast/upstream-manifest.json @@ -7,6 +7,10 @@ "name": "async-calls-sync.wast", "revision": "7c676115e93cd7d54c1732d95c54c6a3de7c5ae0" }, + { + "name": "big-interleaving-test.wast", + "revision": "7c676115e93cd7d54c1732d95c54c6a3de7c5ae0" + }, { "name": "builtin-trap-poisons-instance.wast", "revision": "7c676115e93cd7d54c1732d95c54c6a3de7c5ae0" diff --git a/packages/jco-transpile/test/p3/ported/component-model/wast.ts b/packages/jco-transpile/test/p3/ported/component-model/wast.ts index b8ae27674..a10bc1d09 100644 --- a/packages/jco-transpile/test/p3/ported/component-model/wast.ts +++ b/packages/jco-transpile/test/p3/ported/component-model/wast.ts @@ -56,6 +56,7 @@ const WAST_TESTS: readonly WastTest[] = [ { relPath: 'async/trap-if-done.wast' }, { relPath: 'async/cross-abi-calls.wast' }, { relPath: 'async/cross-task-future.wast' }, + { relPath: 'async/big-interleaving-test.wast' }, { relPath: 'async/trap-on-reenter.wast' }, { relPath: 'async/validate-no-stream-char.wast' }, { relPath: 'async/validate-no-async-abi-for-sync-type.wast' }, From 455df0e0ce398896c92d410e7f4ad3158d110d90 Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Fri, 4 Sep 2026 16:41:38 +0000 Subject: [PATCH 5/7] fix(bindgen): reconcile eager async scheduling --- .../src/function_bindgen.rs | 10 +++++-- .../src/intrinsics/mod.rs | 9 ++++--- .../src/intrinsics/p3/async_future.rs | 26 ++++++++++++------- .../src/transpile_bindgen.rs | 2 +- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/crates/js-component-bindgen/src/function_bindgen.rs b/crates/js-component-bindgen/src/function_bindgen.rs index c273c9138..f320ac0d5 100644 --- a/crates/js-component-bindgen/src/function_bindgen.rs +++ b/crates/js-component-bindgen/src/function_bindgen.rs @@ -3228,7 +3228,10 @@ impl Bindgen for FunctionBindgen<'_> { let result_var = format!("futureResult{tmp}"); // Optionally preform the lift for the future in question - match (self.is_async, self.for_import.unwrap_or_default()) { + match ( + self.canonical_abi_async, + self.for_import.unwrap_or_default(), + ) { // It is possible for lifting to be called both at the *start* and *end* of // a given function depending on how it called: // @@ -3542,7 +3545,10 @@ impl Bindgen for FunctionBindgen<'_> { let result_var = format!("streamResult{tmp}"); // Optionally preform the lift for the stream in question - match (self.is_async, self.for_import.unwrap_or_default()) { + match ( + self.canonical_abi_async, + self.for_import.unwrap_or_default(), + ) { // It is possible for lifting to be called both at the *start* and *end* of // a given function depending on how it called: // diff --git a/crates/js-component-bindgen/src/intrinsics/mod.rs b/crates/js-component-bindgen/src/intrinsics/mod.rs index e3c915958..536bebf1b 100644 --- a/crates/js-component-bindgen/src/intrinsics/mod.rs +++ b/crates/js-component-bindgen/src/intrinsics/mod.rs @@ -1201,7 +1201,7 @@ impl Intrinsic { output.push_str(&format!( r#" - function {suspending_import_wrapper_fn}(componentIdx, fn) {{ + function {suspending_import_wrapper_fn}(componentIdx, fn, syncOnly = false) {{ return function (...args) {{ {check_may_leave_fn}(componentIdx); const saved = {global_current_task_meta_obj}[componentIdx] ?? null; @@ -1216,7 +1216,7 @@ impl Intrinsic { throw new {runtime_error_class}('cannot block a synchronous task before returning'); }} - if (!mayBlock) {{ + if (syncOnly || !mayBlock) {{ let result; try {{ result = fn.apply(null, args); @@ -1562,11 +1562,12 @@ mod tests { let source = render_intrinsic_body(Intrinsic::SuspendingImportWrapperFn); assert!(source.contains("return function (...args) {")); + assert!(source.contains("syncOnly = false")); assert!( source.contains("? (savedTask?.mayBlock() ?? (CURRENT_TASK_MAY_BLOCK.value !== 0))") ); assert!(source.contains(": false;")); - assert!(source.contains("if (!mayBlock) {")); + assert!(source.contains("if (syncOnly || !mayBlock) {")); assert!(!source.contains("return async function (...args) {")); assert!(source.contains("if (!saved && !mayBlock) {")); @@ -1574,7 +1575,7 @@ mod tests { assert!(source.contains("result = fn.apply(null, args);")); assert!(source.contains("typeof result.then === 'function'")); assert!(source.contains("Promise.resolve(result).catch(() => {});")); - assert!(source.contains("if (!mayBlock) {")); + assert!(source.contains("if (syncOnly || !mayBlock) {")); assert!(source.contains( "new WebAssemblyRuntimeError('cannot block a synchronous task before returning')" )); 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 09c59e9db..c087260b9 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/async_future.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/async_future.rs @@ -1641,15 +1641,15 @@ impl AsyncFutureIntrinsic { ) {{ {debug_log_fn}('[{future_cancel_fn}()] args', {{ ctx, - futureEndIdx, + futureEndWaitableIdx, }}); const {{ componentIdx, futureTableIdx, isAsync }} = ctx; const cstate = {get_or_create_async_state_fn}(componentIdx); if (!cstate.mayLeave) {{ throw new Error('component instance is not marked as may leave'); }} - const futureEnd = {get_future_end_fn}({{ tableIdx: futureTableIdx, futureEndWaitableIdx: futureEndIdx }}); - if (!futureEnd) {{ throw new Error(`missing future end with idx [${{futureEndIdx}}]`); }} + const futureEnd = {get_future_end_fn}({{ tableIdx: futureTableIdx, futureEndWaitableIdx }}); + if (!futureEnd) {{ throw new Error(`missing future end with idx [${{futureEndWaitableIdx}}]`); }} if (!(futureEnd instanceof {future_end_class})) {{ throw new Error('invalid future end, expected value of type [{future_end_class}]'); }} @@ -1658,6 +1658,15 @@ impl AsyncFutureIntrinsic { futureEnd.setCopyState({future_end_class}.CopyState.CANCELLING_COPY); + const finishCancel = () => {{ + const {{ code, payload0: index, payload1: payload }} = futureEnd.getPendingEvent(); + if (futureEnd.isCopying()) {{ throw new Error('future end is still in copying state'); }} + if (code !== {event_code}) {{ throw new Error('unexpected event code [' + code + '], expected [' + {event_code} + ']'); }} + if (index !== futureEndWaitableIdx) {{ throw new Error('index does not match future end'); }} + + return payload; + }}; + if (!futureEnd.hasPendingEvent()) {{ futureEnd.cancel(); @@ -1666,16 +1675,13 @@ impl AsyncFutureIntrinsic { const taskMeta = {current_task_get_fn}(componentIdx); if (!taskMeta?.task) {{ throw new Error('missing current task while cancelling future'); }} - await taskMeta.task.suspendUntil({{ readyFn: () => futureEnd.hasPendingEvent() }}); + return taskMeta.task + .suspendUntil({{ readyFn: () => futureEnd.hasPendingEvent() }}) + .then(finishCancel); }} }} - const {{ code, payload0: index, payload1: payload }} = futureEnd.getPendingEvent(); - if (futureEnd.isCopying()) {{ throw new Error('future end is still in copying state'); }} - if (code !== {event_code}) {{ throw new Error('unexpected event code [' + code + '], expected [' + {event_code} + ']'); }} - if (index !== futureEndIdx) {{ throw new Error('index does not match future end'); }} - - return payload; + return finishCancel(); }} "#)); } diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index 8ce3d1cb9..6e5c3ed73 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -1908,7 +1908,7 @@ impl<'a> Instantiator<'a, '_> { getMemoryFn: () => memory{memory_idx}, }}); const trampoline{i} = {conditional_suspending_fn}( - {suspending_wrap_fn}({instance_idx}, (waitableSetRep, resultPtr) => trampoline{i}Wait(waitableSetRep, resultPtr, true)), + {suspending_wrap_fn}({instance_idx}, (waitableSetRep, resultPtr) => trampoline{i}Wait(waitableSetRep, resultPtr, true), true), {suspending_wrap_fn}({instance_idx}, trampoline{i}Wait), ); "#, From 77ec3e034988b9d4e6c170852d7ab6b1a9f58b02 Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Sat, 5 Sep 2026 14:21:08 +0000 Subject: [PATCH 6/7] fix(bindgen): resolve async transpilation regressions Restore task context after asynchronous argument allocation and derive result storage from the canonical lower. Publish completed exports once the guest slice yields or exits so HTTP response bodies and detached tasks can make progress. Preserve undefined stream items, align option assertions with the existing nullable representation, and verify sync-lowered host calls return their scalar result. --- .../src/function_bindgen.rs | 11 +++++-- .../src/intrinsics/p3/async_stream.rs | 8 ++--- .../src/intrinsics/p3/async_task.rs | 31 ++++++------------- .../src/transpile_bindgen.rs | 8 ++--- packages/jco-transpile/test/common.ts | 2 +- .../test/p3/async-result-scalars.ts | 8 ++--- .../jco-transpile/test/p3/future-lifts.ts | 12 ++----- .../jco-transpile/test/p3/future-lowers.ts | 11 ++----- .../jco-transpile/test/p3/helpers/http.js | 4 +-- .../jco-transpile/test/p3/stream-lifts.ts | 12 ++----- .../jco-transpile/test/p3/stream-lowers.ts | 11 ++----- .../test/p3/sync-lowered-async-import.ts | 7 ++--- 12 files changed, 46 insertions(+), 79 deletions(-) diff --git a/crates/js-component-bindgen/src/function_bindgen.rs b/crates/js-component-bindgen/src/function_bindgen.rs index f320ac0d5..5ea25aac0 100644 --- a/crates/js-component-bindgen/src/function_bindgen.rs +++ b/crates/js-component-bindgen/src/function_bindgen.rs @@ -698,7 +698,7 @@ impl FunctionBindgen<'_> { r#" if ({memory_idx_expr} !== null) {{ task.setReturnMemoryIdx({memory_idx_expr}); - task.setReturnMemory({get_memory_fn_expr}()); + task.setReturnMemory(({get_memory_fn_expr})()); }} "# ); @@ -1967,12 +1967,19 @@ impl Bindgen for FunctionBindgen<'_> { } } + // Argument lowering can await realloc and allow another task + // to run. Reinstall this task immediately before entering Wasm. + let call_wrapper = self.intrinsic(Intrinsic::WithGlobalCurrentTaskMetaFn); uwriteln!( self.src, r#" {vars_init} try {{ - {assignment_lhs} {call_prefix}{callee_invoke}; + {assignment_lhs} {call_prefix}{call_wrapper}({{ + taskID: task.id(), + componentIdx: task.componentIdx(), + fn: () => {callee_invoke}, + }}); }} catch (err) {{ {call_err_cleanup} }} 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 c119a3bb4..1637be3ac 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/async_stream.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/async_stream.rs @@ -1514,10 +1514,11 @@ impl AsyncStreamIntrinsic { const {{ typedArray }} = this.#elemMeta; const value = typedArray === undefined ? count === 1 ? values[0] : values : new typedArray(values); this.#result = null; - resolve(value); + // An option's none value is undefined, but still an item. + resolve({{ value, done: false }}); }} else {{ this.#result = null; - resolve(undefined); + resolve({{ value: undefined, done: true }}); }} }} catch (err) {{ @@ -1525,10 +1526,9 @@ impl AsyncStreamIntrinsic { reject(err); }} - const res = await promise; + const result = await promise; const rejectedLength = this.#rejectedLength; this.#rejectedLength = null; - const result = {{ value: res, done: res === undefined }}; if (rejectedLength !== null) {{ result.rejectedLength = rejectedLength; }} 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 30fe9de74..7fa819caf 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs @@ -995,6 +995,7 @@ impl AsyncTaskIntrinsic { #completionPromise = null; #completionValue; #completionReady = false; + #settleCompletionPromise; #rejected = false; #exitPromise = null; @@ -1075,18 +1076,6 @@ impl AsyncTaskIntrinsic { completionPromise.catch(() => {{}}); let completionSettled = false; - const completionContainsAsyncValue = (value, seen = new Set()) => {{ - if (value === null || value === undefined) {{ return false; }} - const ty = typeof value; - if (ty !== 'object' && ty !== 'function') {{ return false; }} - if (typeof value.then === 'function') {{ return true; }} - if (typeof value[Symbol.asyncIterator] === 'function') {{ return true; }} - if (seen.has(value)) {{ return false; }} - seen.add(value); - return Object.values(value).some((item) => - completionContainsAsyncValue(item, seen) - ); - }}; const settleCompletionPromise = () => {{ if (completionSettled || !this.#completionReady) {{ return; }} completionSettled = true; @@ -1104,21 +1093,16 @@ impl AsyncTaskIntrinsic { }} }}; + this.#settleCompletionPromise = settleCompletionPromise; this.#onResolveHandlers.push((results) => {{ if (this.#parentSubtask !== null) {{ return; }} if (!this.#isAsync && !this.#isManualAsync) {{ return; }} - // `task.return` makes the result available to a supertask, - // but the host-facing promise cannot settle until the task - // exits: guest cleanup and spawned work may still trap. this.#completionValue = results; this.#completionReady = true; - // Streams and futures must be exposed at task.return so the - // host can drive the operations the task itself may be - // waiting on. Plain results wait for EXIT, preserving any - // cleanup trap. - if (completionContainsAsyncValue(results)) {{ - settleCompletionPromise(); - }} + // Publish after the current guest slice returns, so a trap + // in that slice can still reject the call. Do not wait for + // task exit: detached work may require further host calls + // or consumption of a returned resource's stream. }}); const {{ @@ -1194,6 +1178,7 @@ impl AsyncTaskIntrinsic { }} completionPromise() {{ return this.#completionPromise; }} + settleCompletion() {{ this.#settleCompletionPromise(); }} exitPromise() {{ return this.#exitPromise; }} waitForProgress() {{ @@ -2517,6 +2502,7 @@ impl AsyncTaskIntrinsic { payload1: 0, }}); }}); + task.settleCompletion(); return; case 2: // WAIT for a given waitable set @@ -2539,6 +2525,7 @@ impl AsyncTaskIntrinsic { task, cancellable: true, }}, continueWithEvent); + task.settleCompletion(); return; default: diff --git a/crates/js-component-bindgen/src/transpile_bindgen.rs b/crates/js-component-bindgen/src/transpile_bindgen.rs index 6e5c3ed73..c102f1891 100644 --- a/crates/js-component-bindgen/src/transpile_bindgen.rs +++ b/crates/js-component-bindgen/src/transpile_bindgen.rs @@ -2858,11 +2858,9 @@ impl<'a> Instantiator<'a, '_> { // Build the lower import call that will wrap the actual trampoline let func_ty_async = func_ty.async_; - let max_direct_results = if is_async || func_ty_async { - 0 - } else { - MAX_FLAT_RESULTS - }; + // Result storage follows the canonical lower, independently of + // whether the component function's WIT type is async. + let max_direct_results = if is_async { 0 } else { MAX_FLAT_RESULTS }; let has_result_pointer = result_flat_count .map(|count| count > max_direct_results) .unwrap_or(true); diff --git a/packages/jco-transpile/test/common.ts b/packages/jco-transpile/test/common.ts index a4f939194..ec63ffda0 100644 --- a/packages/jco-transpile/test/common.ts +++ b/packages/jco-transpile/test/common.ts @@ -123,7 +123,7 @@ export async function checkFutureValues(args) { for (const [idx, v] of vals.entries()) { future = await func(v); res = await future; - await eq(res, expectedValues[idx] ?? v, `${typeName} future read is incorrect`); + await eq(res, idx in expectedValues ? expectedValues[idx] : v, `${typeName} future read is incorrect`); } } diff --git a/packages/jco-transpile/test/p3/async-result-scalars.ts b/packages/jco-transpile/test/p3/async-result-scalars.ts index 8f988859f..ce9bfd822 100644 --- a/packages/jco-transpile/test/p3/async-result-scalars.ts +++ b/packages/jco-transpile/test/p3/async-result-scalars.ts @@ -74,18 +74,16 @@ suite('async export scalar results (direct-param task.return)', () => { assert.strictEqual(await api().getF32Only(1.5), 1.5); }); - // NOTE: non-nullable option lifts are currently represented as - // tagged objects rather than smoothed to the payload value test('option', async () => { - assert.deepEqual(await api().getOptionU64(42n), { tag: 'some', val: 42n }); + assert.strictEqual(await api().getOptionU64(42n), 42n); }); test('option', async () => { - assert.deepEqual(await api().getOptionF64(2.5), { tag: 'some', val: 2.5 }); + assert.strictEqual(await api().getOptionF64(2.5), 2.5); }); test('option', async () => { - assert.deepEqual(await api().getOptionF32(2.5), { tag: 'some', val: 2.5 }); + assert.strictEqual(await api().getOptionF32(2.5), 2.5); }); test('result ok (joined slots inside a record payload)', async () => { diff --git a/packages/jco-transpile/test/p3/future-lifts.ts b/packages/jco-transpile/test/p3/future-lifts.ts index 112e0587b..0e0a0cfcd 100644 --- a/packages/jco-transpile/test/p3/future-lifts.ts +++ b/packages/jco-transpile/test/p3/future-lifts.ts @@ -236,10 +236,8 @@ suite('future lifts', () => { func: instance['jco:test-components/get-future-async'].getFutureVariant, typeName: 'variant', expectedValues: [ - // TODO: wit type representation smoothing mismatch, - // non-nullable option values are *not* wrapped as objects - { tag: 'maybe-u32', val: { tag: 'some', val: 123 } }, - { tag: 'maybe-u32', val: { tag: 'none' } }, + { tag: 'maybe-u32', val: 123 }, + { tag: 'maybe-u32', val: undefined }, ], assertEqFn: assert.deepEqual, }); @@ -321,11 +319,7 @@ suite('future lifts', () => { vals, func: instance['jco:test-components/get-future-async'].getFutureOptionString, typeName: 'option', - expectedValues: [ - // TODO: wit type representation smoothing mismatch - { tag: 'some', val: 'present string' }, - { tag: 'none' }, - ], + expectedValues: ['present string', undefined], assertEqFn: assert.deepEqual, }); }); diff --git a/packages/jco-transpile/test/p3/future-lowers.ts b/packages/jco-transpile/test/p3/future-lowers.ts index 130fc431d..8d024ace2 100644 --- a/packages/jco-transpile/test/p3/future-lowers.ts +++ b/packages/jco-transpile/test/p3/future-lowers.ts @@ -299,9 +299,8 @@ suite('future lowers', () => { { tag: 'num', val: 1 }, ]; const expected = [ - // TODO: wit type representation smoothing mismatch - { tag: 'maybe-u32', val: { tag: 'some', val: 123 } }, - { tag: 'maybe-u32', val: { tag: 'none' } }, + { tag: 'maybe-u32', val: 123 }, + { tag: 'maybe-u32', val: undefined }, { tag: 'str', val: 'string-value' }, { tag: 'num', val: 1 }, ]; @@ -387,11 +386,7 @@ suite('future lowers', () => { ), ); } - assert.deepEqual(returnedVals, [ - // TODO: wit type representation smoothing mismatch - { tag: 'some', val: 'present string' }, - { tag: 'none' }, - ]); + assert.deepEqual(returnedVals, ['present string', undefined]); }); // TODO(FIX): it's returning the actual nope value if it's an error?? diff --git a/packages/jco-transpile/test/p3/helpers/http.js b/packages/jco-transpile/test/p3/helpers/http.js index 519dfa1f2..df7f07464 100644 --- a/packages/jco-transpile/test/p3/helpers/http.js +++ b/packages/jco-transpile/test/p3/helpers/http.js @@ -126,7 +126,7 @@ export async function runHandlerFixture({ esModule, outbound = {}, expect = {} } await bodyTx.write(Buffer.from(req.body)); } await bodyTx.close(); - const tval = req.trailers ? { tag: 'some', val: buildFields(req.trailers) } : { tag: 'none' }; + const tval = req.trailers ? buildFields(req.trailers) : undefined; await trailersTx.write({ tag: 'ok', val: tval }); })(); @@ -152,7 +152,7 @@ export async function runHandlerFixture({ esModule, outbound = {}, expect = {} } assert.strictEqual(trailerResult?.tag, 'ok', `trailers errored: ${JSON.stringify(trailerResult)}`); const maybeTrailer = trailerResult.val; - const actualTrailers = maybeTrailer?.tag === 'some' ? fieldsToObject(maybeTrailer.val) : {}; + const actualTrailers = maybeTrailer ? fieldsToObject(maybeTrailer) : {}; for (const [name, value] of Object.entries(exp.trailers ?? {})) { assert.strictEqual(actualTrailers[name], value, `response trailer ${name}`); diff --git a/packages/jco-transpile/test/p3/stream-lifts.ts b/packages/jco-transpile/test/p3/stream-lifts.ts index 438cf5419..53a44359c 100644 --- a/packages/jco-transpile/test/p3/stream-lifts.ts +++ b/packages/jco-transpile/test/p3/stream-lifts.ts @@ -305,10 +305,8 @@ suite('stream lifts', () => { await checkStreamValues({ stream, expectedValues: [ - // TODO: wit type representation smoothing mismatch, - // non-nullable option values are *not* wrapped as objects - { tag: 'maybe-u32', val: { tag: 'some', val: 123 } }, - { tag: 'maybe-u32', val: { tag: 'none' } }, + { tag: 'maybe-u32', val: 123 }, + { tag: 'maybe-u32', val: undefined }, ], partial: true, typeName: 'variant', @@ -428,11 +426,7 @@ suite('stream lifts', () => { stream, typeName: 'option', assertEqFn: assert.deepEqual, - expectedValues: [ - // TODO: wit type representation smoothing mismatch - { tag: 'some', val: 'present string' }, - { tag: 'none' }, - ], + expectedValues: ['present string', undefined], }); }); diff --git a/packages/jco-transpile/test/p3/stream-lowers.ts b/packages/jco-transpile/test/p3/stream-lowers.ts index 1708c8014..1dcfc15e1 100644 --- a/packages/jco-transpile/test/p3/stream-lowers.ts +++ b/packages/jco-transpile/test/p3/stream-lowers.ts @@ -344,9 +344,8 @@ suite('stream lowers', () => { createReadableStreamFromValues(vals), ); assert.deepEqual(returnedVals, [ - // TODO: wit type representation smoothing mismatch - { tag: 'maybe-u32', val: { tag: 'some', val: 123 } }, - { tag: 'maybe-u32', val: { tag: 'none' } }, + { tag: 'maybe-u32', val: 123 }, + { tag: 'maybe-u32', val: undefined }, { tag: 'str', val: 'string-value' }, { tag: 'num', val: 1 }, ]); @@ -496,11 +495,7 @@ suite('stream lowers', () => { const returnedVals = await instance['jco:test-components/stream-lower-async'].readStreamValuesOptionString( createReadableStreamFromValues(vals), ); - assert.deepEqual(returnedVals, [ - // TODO: wit type representation smoothing mismatch - { tag: 'some', val: 'present string' }, - { tag: 'none' }, - ]); + assert.deepEqual(returnedVals, ['present string', undefined]); }); test.concurrent('result', async () => { diff --git a/packages/jco-transpile/test/p3/sync-lowered-async-import.ts b/packages/jco-transpile/test/p3/sync-lowered-async-import.ts index ccda49c08..37f2ff3f7 100644 --- a/packages/jco-transpile/test/p3/sync-lowered-async-import.ts +++ b/packages/jco-transpile/test/p3/sync-lowered-async-import.ts @@ -10,7 +10,7 @@ const HOST_INTERFACE = 'jco:test-components/sync-lowered-async-import-host'; const RUNNER_INTERFACE = 'jco:test-components/sync-lowered-async-import-runner'; suite('sync-lowered async host import', () => { - test('invokes the host', async () => { + test('invokes the host and returns its scalar result', async () => { let hostCallCount = 0; let markHostCalled!: (value: number) => void; const hostCalled = new Promise((resolve) => { @@ -36,15 +36,14 @@ suite('sync-lowered async host import', () => { try { const run = instance[RUNNER_INTERFACE].run; assert.instanceOf(run, AsyncFunction); - // This test isolates issue #1898 bug 2. Awaiting the export would - // also exercise bug 4's independently broken completion promise. - void run(); + const result = run(); const hostValue = await Promise.race([ hostCalled, new Promise((_, reject) => setTimeout(() => reject(new Error('export call timed out')), 5_000)), ]); assert.strictEqual(hostValue, 41); assert.strictEqual(hostCallCount, 1); + assert.strictEqual(await result, 42); } finally { await cleanup(); } From be83fb1b0ae0f2283b5deb7d74cfca931505bfa6 Mon Sep 17 00:00:00 2001 From: Victor Adossi Date: Sat, 5 Sep 2026 14:49:37 +0000 Subject: [PATCH 7/7] fix(bindgen): drive cancellation cleanup through JSPI suspension --- .../src/intrinsics/component.rs | 4 +++ .../src/intrinsics/mod.rs | 14 ++++++++ .../src/intrinsics/p3/async_task.rs | 32 +++++++++++++++++-- .../ported/wasmtime/component-async/cancel.ts | 5 +++ 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/crates/js-component-bindgen/src/intrinsics/component.rs b/crates/js-component-bindgen/src/intrinsics/component.rs index 956a6cd3f..5b2aaefb8 100644 --- a/crates/js-component-bindgen/src/intrinsics/component.rs +++ b/crates/js-component-bindgen/src/intrinsics/component.rs @@ -764,6 +764,10 @@ impl ComponentIntrinsic { return !!this.#getSuspendedTaskMeta(taskID)?.cancellable; }} + isTaskSuspended(taskID) {{ + return this.#suspendedTasksByTaskID.has(taskID); + }} + suspendedTaskMetas() {{ return this.#suspendedTasksByTaskID.values(); }} diff --git a/crates/js-component-bindgen/src/intrinsics/mod.rs b/crates/js-component-bindgen/src/intrinsics/mod.rs index 536bebf1b..51c593f77 100644 --- a/crates/js-component-bindgen/src/intrinsics/mod.rs +++ b/crates/js-component-bindgen/src/intrinsics/mod.rs @@ -1699,6 +1699,12 @@ mod tests { assert!(!cancel.contains("async function subtaskCancel")); assert!(cancel.contains(".then(finishCancel)")); assert!(cancel.contains("cancellationWillCompleteAsync ? 0xFFFFFFFE : 0xFFFFFFFF")); + assert!(cancel.contains("childState.exclusivelyLockedBy(childTask.id())")); + assert!(cancel.contains("!childState.isTaskSuspended(childTask.id())")); + assert!(cancel.contains("return progress.then(() =>")); + let subscribe = cancel.find("childTask?.waitForProgress()").unwrap(); + let request = cancel.find("subtask.requestCancellation();").unwrap(); + assert!(subscribe < request); let task = render_intrinsic_body(Intrinsic::AsyncTask(AsyncTaskIntrinsic::AsyncTaskClass)); assert!(task.contains("suspendUntilCallback(opts, onResume)")); @@ -1724,6 +1730,14 @@ mod tests { assert!(state.contains("suspendedTaskReady(taskID)")); } + #[test] + fn cancellation_cleanup_can_start_host_imports() { + let lower = render_intrinsic_body(Intrinsic::AsyncTask(AsyncTaskIntrinsic::LowerImport)); + assert!(lower.contains("subtask.cancellationRequested()")); + assert!(lower.contains("task.taskState() === AsyncTask.State.CANCEL_PENDING")); + assert!(!lower.contains("if (task.cancellationRequested())")); + } + #[test] fn cancellable_wait_poll_and_yield_reach_task_state() { let waitable_set = 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 7fa819caf..cdd5649b1 100644 --- a/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs +++ b/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs @@ -681,6 +681,15 @@ impl AsyncTaskIntrinsic { }} if (subtask.isResolved()) {{ return finishCancel(); }} + if (subtask.cancelProgress) {{ + const progress = subtask.cancelProgress; + subtask.cancelProgress = null; + return progress.then(() => {{ + if (subtask.isResolved()) {{ return finishCancel(); }} + return 0xFFFFFFFF; + }}); + }} + const {{ taskID }} = {get_global_current_task_meta_fn}(componentIdx); const taskMeta = {current_task_get_fn}(componentIdx, taskID); if (!taskMeta || !taskMeta.task) {{ throw new Error('invalid/missing async task'); }} @@ -693,6 +702,10 @@ impl AsyncTaskIntrinsic { let cancellationWillCompleteAsync = false; if (!subtask.isResolved()) {{ + const childTask = subtask.getChildTask(); + // Subscribe before resuming: a synchronous callback may + // reach its next suspension during this call. + const childProgress = isAsync ? childTask?.waitForProgress() : null; subtask.requestCancellation(); if (!subtask.isResolved()) {{ @@ -700,7 +713,6 @@ impl AsyncTaskIntrinsic { // execution slice. Wait until that slice releases component // entry by exiting or suspending again before deciding whether // the cancel blocked. - const childTask = subtask.getChildTask(); if (childTask) {{ const childState = {get_or_create_async_state_fn}(childTask.componentIdx()); if (subtask.getStateNumber() === 0 && @@ -713,6 +725,14 @@ impl AsyncTaskIntrinsic { if (!childState.resumeTaskByID(childTask.id())) {{ throw new Error('failed to resume cancellable subtask'); }} + }} else if (childTask.hasCallback() && + childState.exclusivelyLockedBy(childTask.id()) && + !childState.isTaskSuspended(childTask.id())) {{ + // JSPI can still be returning the initial guest + // slice, before its callback wait is registered. + // An existing non-cancellable wait must instead + // report BLOCKED without waiting for progress. + cancellationWillCompleteAsync = true; }} }} }} @@ -723,6 +743,7 @@ impl AsyncTaskIntrinsic { // while sync-lowered cancels block the current task until the // subtask resolves. if (isAsync) {{ + if (cancellationWillCompleteAsync) {{ subtask.cancelProgress = childProgress; }} // -1 is the canonical BLOCKED status. -2 is an // internal signal consumed by the conditional JSPI // trampoline when a cancellable child was resumed but @@ -1989,6 +2010,8 @@ impl AsyncTaskIntrinsic { target; isAsync; isManualAsync; + // One execution slice awaited by the conditional cancel trampoline. + cancelProgress = null; constructor(args) {{ if (typeof args.componentIdx !== 'number') {{ @@ -2635,6 +2658,7 @@ impl AsyncTaskIntrinsic { // Self::LowerImport => { let debug_log_fn = render_args.require_intrinsic(Intrinsic::DebugLog); + let task_class = render_args.require_intrinsic(Self::AsyncTaskClass); let lower_import_fn = render_args.require_intrinsic(Self::LowerImport); let current_task_get_fn = render_args.require_intrinsic(Self::GetCurrentTask); let get_or_create_async_state_fn = render_args.require_intrinsic( @@ -2803,10 +2827,12 @@ impl AsyncTaskIntrinsic { queueMicrotask(async () => {{ try {{ // The async host call has not started yet. If the - // enclosing guest task was cancelled in the meantime, + // enclosing guest task has pending cancellation, // leave this subtask for the guest cancellation callback // to retire without invoking host code after cancellation. - if (task.cancellationRequested()) {{ return; }} + // Imports issued by the cancellation callback itself + // must still run so the guest can finish its cleanup. + if (subtask.cancellationRequested() || task.taskState() === {task_class}.State.CANCEL_PENDING) {{ return; }} {debug_log_fn}('[{lower_import_fn}()] calling lowered import', {{ importFn, params }}); await {with_global_current_task_meta_async_fn}({{ taskID: task.id(), diff --git a/packages/jco-transpile/test/p3/ported/wasmtime/component-async/cancel.ts b/packages/jco-transpile/test/p3/ported/wasmtime/component-async/cancel.ts index c46b20aaa..0eb1acbd1 100644 --- a/packages/jco-transpile/test/p3/ported/wasmtime/component-async/cancel.ts +++ b/packages/jco-transpile/test/p3/ported/wasmtime/component-async/cancel.ts @@ -33,11 +33,13 @@ suite('cancel scenario', () => { let cleanup; try { const longYield = new Promise(() => {}); + let cleanupYieldCalls = 0; const cancelYieldTimes = async (count) => { if (count > 100n) { await longYield; return; } + cleanupYieldCalls++; await new Promise((resolve) => setTimeout(resolve, 0)); }; const res = await buildAndTranspile({ @@ -51,6 +53,9 @@ suite('cancel scenario', () => { cleanup = res.cleanup; await res.instance['local:local/cancel'].run({ tag: mode }, 100n); + // Cancellation callbacks must be allowed to issue host calls while + // unwinding, including the fixture's asynchronous cleanup delay. + assert.ok(cleanupYieldCalls > 0, 'cancellation cleanup should call the host'); } finally { if (cleanup) { await cleanup();