diff --git a/crates/js-component-bindgen/src/function_bindgen.rs b/crates/js-component-bindgen/src/function_bindgen.rs index c273c9138..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} }} @@ -3228,7 +3235,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 +3552,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/component.rs b/crates/js-component-bindgen/src/intrinsics/component.rs index 080b04c97..5b2aaefb8 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,14 @@ impl ComponentIntrinsic { return meta.task.isRejected() || meta.readyFn(); }} + suspendedTaskCancellable(taskID) {{ + 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 54d6f7cb2..51c593f77 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 { @@ -1189,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; @@ -1204,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); @@ -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(); @@ -1463,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) {")); @@ -1475,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')" )); @@ -1593,22 +1693,28 @@ 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")); + 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("#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 +1723,42 @@ 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 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 = + 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 +1883,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 +2563,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_future.rs b/crates/js-component-bindgen/src/intrinsics/p3/async_future.rs index ab046e7e6..c087260b9 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,21 +1635,21 @@ 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, - 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}]'); }} @@ -1655,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(); @@ -1663,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/intrinsics/p3/async_stream.rs b/crates/js-component-bindgen/src/intrinsics/p3/async_stream.rs index 53c5ba2be..1637be3ac 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`); @@ -1510,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) {{ @@ -1521,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; }} @@ -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/async_task.rs b/crates/js-component-bindgen/src/intrinsics/p3/async_task.rs index f786aebb1..cdd5649b1 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,107 @@ 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(); }} + + 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'); }} + 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(); + // 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() && 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. + 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'); + }} + }} 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; + }} }} }} 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) {{ + 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 + // 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(); }} ")); } @@ -962,6 +1016,7 @@ impl AsyncTaskIntrinsic { #completionPromise = null; #completionValue; #completionReady = false; + #settleCompletionPromise; #rejected = false; #exitPromise = null; @@ -1042,18 +1097,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; @@ -1071,21 +1114,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 {{ @@ -1161,6 +1199,7 @@ impl AsyncTaskIntrinsic { }} completionPromise() {{ return this.#completionPromise; }} + settleCompletion() {{ this.#settleCompletionPromise(); }} exitPromise() {{ return this.#exitPromise; }} waitForProgress() {{ @@ -1241,8 +1280,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 +1451,8 @@ impl AsyncTaskIntrinsic { cstate.removeBackpressureWaiter(); - if (!result) {{ - this.cancel(); + if (!result || this.isCancelled()) {{ + if (!this.isResolvedState()) {{ this.cancel(); }} return false; }} }} @@ -1418,6 +1464,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 +1552,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 +1611,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 +1641,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. @@ -1928,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') {{ @@ -2332,9 +2416,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 +2444,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 +2464,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 +2509,24 @@ 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; + task.settleCompletion(); + return; case 2: // WAIT for a given waitable set {debug_log_fn}('[{driver_loop_fn}()] waiting for event', {{ @@ -2432,34 +2543,42 @@ 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); + task.settleCompletion(); + 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); + + {debug_log_fn}('[{driver_loop_fn}()] callback result unpacked', {{ + fnName, + componentIdx, + callbackFnName, + callbackRes, + callbackCode, + waitableSetRep, + }}); + return drive(); + }} catch (err) {{ + handleError(err); + }} + }}; - // If the task failed via any means, leave early and reject. + const runCallback = () => {{ + try {{ if (task.isRejected()) {{ {debug_log_fn}('[{driver_loop_fn}()] detected task rejection, leaving early'); componentState.exclusiveRelease(task.id()); @@ -2468,7 +2587,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 +2606,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); }} }} "#, @@ -2556,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( @@ -2723,6 +2826,13 @@ impl AsyncTaskIntrinsic { queueMicrotask(async () => {{ try {{ + // The async host call has not started yet. If the + // enclosing guest task has pending cancellation, + // leave this subtask for the guest cancellation callback + // to retire without invoking host code after cancellation. + // 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(), @@ -3106,7 +3216,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 +3225,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 +3297,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/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"; 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..c102f1891 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), 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 } @@ -2821,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); @@ -3268,6 +3303,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 +3324,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 +3362,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 +3373,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) => { 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!( 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/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/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/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' }, 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(); 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(); }