From 154003e80c52e523a941ac79e44db6e087de453e Mon Sep 17 00:00:00 2001 From: Saicharan Ramineni <84414237+GodlyDonuts@users.noreply.github.com> Date: Wed, 24 Jun 2026 11:32:17 -0400 Subject: [PATCH 01/23] async: remove inter-task wakeup stream from its waitable set before cancelling (#1638) `cancel_inter_task_stream_read` issued a synchronous `stream.cancel-read` on the inter-task wakeup stream while that stream was still a member of the task's waitable set, only calling `remove_waitable` afterwards. Per the Component Model (component-model#647), a synchronous `{stream,future}.cancel-{read,write}` traps if the waitable is still in a waitable set, for the same reason synchronous reads/writes do. Runtimes that enforce this trap (e.g. recent Wasmtime) therefore fault here during ordinary async operation. Reorder so the stream leaves the waitable set before the synchronous cancel, matching the unregister-then-cancel ordering already used by the general `WaitableOperation::cancel` path. The cancel result is discarded as before, so behavior is otherwise unchanged. --- crates/guest-rust/src/rt/async_support/inter_task_wakeup.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/guest-rust/src/rt/async_support/inter_task_wakeup.rs b/crates/guest-rust/src/rt/async_support/inter_task_wakeup.rs index d36d34bd6..31a84ea15 100644 --- a/crates/guest-rust/src/rt/async_support/inter_task_wakeup.rs +++ b/crates/guest-rust/src/rt/async_support/inter_task_wakeup.rs @@ -57,13 +57,17 @@ impl FutureState<'_> { } self.inter_task_wakeup.stream_reading = false; let handle = self.inter_task_wakeup.stream.as_mut().unwrap().handle(); + // Remove the stream from our waitable set before cancelling. A + // synchronous `stream.cancel-read` traps if the stream is still a member + // of a waitable set, so this must happen first. This matches the + // unregister-then-cancel ordering in `WaitableOperation::cancel`. + self.remove_waitable(handle); // Note that the return code here is discarded. No matter what the read // is cancelled, and whether we actually read something or whether we // cancelled doesn't matter. unsafe { UnitStreamOps.cancel_read(handle); } - self.remove_waitable(handle); } } From 4b1ba9a0019b0e8ae662cc8c034310042d211f32 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 29 Jun 2026 09:06:46 -0500 Subject: [PATCH 02/23] Evolve the wasip3 async C ABI for tasks (#1637) * Evolve the wasip3 async C ABI for tasks This commit is an evolution of the C ABI used to managed task-related infrastructure in WASIp3 with the goal of solving #1618. The basic problem of #1618 is that waitables in Rust aren't guaranteed to be polled within the context of the original task. For example by mixing an `async` Rust export and `block_on` it's possible to "cross the wires" and poll in one context while dropping/completing in another context. This can lead to buggy situations where a waitable is left in a set, not added to an appropriate set, or generally mis-managed. The solution here is to enhance the current C ABI of task management with clone/drop operations. Notably this enables waitables to retain a strong reference to the task state as opposed to always consulting what the current task in. This fixes a few situations such as: * When dropping a half-finished waitable it no longer needs to be dropped in the context of the original task. Dropping will unregister the waitable from a task that it was originally registered with. * When a waitable is moved from one task to another it needs to implicitly de-register with the previous task, and this was not previously done. Now with a retained strong reference it's able to clear out previous state upon re-registering with a new task. This change requires some finesse as this needs to be ABI-stable to work with previous versions of the `wit-bindgen` crate. The runtime support additionally can't assume that the new ABI bits are available and instead needs to handle the previous ABI as well. Not too too bad, in the end, though. This additionally did some refactoring of the state associated with async tasks to juggle things around and better represent the raw pointers/`Arc`/etc from before. Closes #1618 * Document the C ABI * Fix disabled compile * Just one version marker --- crates/guest-rust/src/rt/async_support.rs | 271 +++++++++++------- .../guest-rust/src/rt/async_support/cabi.rs | 69 +++++ .../src/rt/async_support/inter_task_wakeup.rs | 12 +- .../inter_task_wakeup_disabled.rs | 4 +- .../src/rt/async_support/waitable.rs | 167 ++++++++--- .../async/rust-async-and-block-on/runner.rs | 49 ++++ .../async/rust-async-and-block-on/test.rs | 13 + .../async/rust-async-and-block-on/test.wit | 15 + 8 files changed, 453 insertions(+), 147 deletions(-) create mode 100644 tests/runtime-async/async/rust-async-and-block-on/runner.rs create mode 100644 tests/runtime-async/async/rust-async-and-block-on/test.rs create mode 100644 tests/runtime-async/async/rust-async-and-block-on/test.wit diff --git a/crates/guest-rust/src/rt/async_support.rs b/crates/guest-rust/src/rt/async_support.rs index 2a5218ed1..680e5f9ab 100644 --- a/crates/guest-rust/src/rt/async_support.rs +++ b/crates/guest-rust/src/rt/async_support.rs @@ -1,12 +1,13 @@ #![deny(missing_docs)] +use self::try_lock::TryLock; use alloc::boxed::Box; use alloc::collections::BTreeMap; use alloc::sync::Arc; use alloc::task::Wake; use core::ffi::c_void; use core::future::Future; -use core::mem; +use core::mem::{self, ManuallyDrop}; use core::pin::Pin; use core::ptr; use core::sync::atomic::{AtomicU32, Ordering}; @@ -104,15 +105,28 @@ use spawn_disabled as spawn; /// Represents a task created by either a call to an async-lifted export or a /// future run using `block_on` or `start_task`. -struct FutureState<'a> { +struct TaskState<'a> { /// Remaining work to do (if any) before this task can be considered "done". /// /// Note that we won't tell the host the task is done until this is drained /// and `waitables` is empty. tasks: spawn::Tasks<'a>, - /// The waitable set containing waitables created by this task, if any. - waitable_set: Option, + /// Dual-mode rust-level Waker and C ABI level "task" for wasip3 + /// integration. + shared: Arc, + + /// Clone of `shared` field, but represented as `std::task::Waker`. + waker: Waker, + + /// State related to supporting inter-task wakeup scenarios. + inter_task_wakeup: inter_task_wakeup::State, +} + +struct SharedTaskState { + /// One of `SLEEP_STATE_*` indicating the current status. + sleep_state: AtomicU32, + inter_task_stream: inter_task_wakeup::WakerState, /// State of all waitables in `waitable_set`, and the ptr/callback they're /// associated with. @@ -123,56 +137,44 @@ struct FutureState<'a> { // the `wasi_snapshot_preview1` adapter when targeting `wasm32-wasip2` and // later, and that's expensive enough that we'd prefer to avoid it for apps // which otherwise make no use of the adapter. - waitables: BTreeMap, - - /// Raw structure used to pass to `cabi::wasip3_task_set` - wasip3_task: cabi::wasip3_task, - - /// Rust-level state for the waker, notably a bool as to whether this has - /// been woken. - waker: Arc, + // + // Also note that the `TryLock` here should never be contended, but it's + // used for interior mutability. + waitables: TryLock>, - /// Clone of `waker` field, but represented as `std::task::Waker`. - waker_clone: Waker, + /// The waitable set containing waitables created by this task, if any. + // + // Note the `TryLock` is the same as `waitables` above, it's serving the + // purpose of interior mutability. + waitable_set: TryLock>, +} - /// State related to supporting inter-task wakeup scenarios. - inter_task_wakeup: inter_task_wakeup::State, +/// An entry of `SharedTaskState::waitables` which is added through the C ABI. +struct CabiWaitable { + callback: unsafe extern "C" fn(*mut c_void, u32), + callback_ptr: *mut c_void, } -impl FutureState<'_> { - fn new(future: BoxFuture<'_>) -> FutureState<'_> { - let waker = Arc::new(FutureWaker::default()); - FutureState { - waker_clone: waker.clone().into(), - waker, +unsafe impl Send for CabiWaitable {} + +impl TaskState<'_> { + fn new(future: BoxFuture<'_>) -> TaskState<'_> { + let shared = Arc::new(SharedTaskState { + sleep_state: AtomicU32::new(0), + inter_task_stream: Default::default(), + waitables: Default::default(), + waitable_set: Default::default(), + }); + TaskState { + waker: shared.clone().into(), + shared, tasks: spawn::Tasks::new(future), - waitable_set: None, - waitables: BTreeMap::new(), - wasip3_task: cabi::wasip3_task { - // This pointer is filled in before calling `wasip3_task_set`. - ptr: ptr::null_mut(), - version: cabi::WASIP3_TASK_V1, - waitable_register, - waitable_unregister, - }, inter_task_wakeup: Default::default(), } } - fn get_or_create_waitable_set(&mut self) -> &WaitableSet { - self.waitable_set.get_or_insert_with(WaitableSet::new) - } - - fn add_waitable(&mut self, waitable: u32) { - self.get_or_create_waitable_set().join(waitable) - } - - fn remove_waitable(&mut self, waitable: u32) { - WaitableSet::remove_waitable_from_all_sets(waitable) - } - fn remaining_work(&self) -> bool { - !self.waitables.is_empty() + !self.shared.waitables.try_lock().unwrap().is_empty() } /// Handles the `event{0,1,2}` event codes and returns a corresponding @@ -190,7 +192,7 @@ impl FutureState<'_> { // Cancellation is mapped to destruction in Rust, so return a // code/bool indicating we're done. The caller will then - // appropriately deallocate this `FutureState` which will + // appropriately deallocate this `TaskState` which will // transitively run all destructors. return CallbackCode::Exit; } @@ -200,7 +202,7 @@ impl FutureState<'_> { self.with_p3_task_set(|me| { // Transition our sleep state to ensure that the inter-task stream // isn't used since there's no need to use that here. - me.waker + me.shared .sleep_state .store(SLEEP_STATE_WOKEN, Ordering::Relaxed); @@ -221,13 +223,13 @@ impl FutureState<'_> { me.cancel_inter_task_stream_read(); loop { - let mut context = Context::from_waker(&me.waker_clone); + let mut context = Context::from_waker(&me.waker); // On each turn of this loop reset the state to "polling" // which clears out any pending wakeup if one was sent. This // in theory helps minimize wakeups from previous iterations // happening in this iteration. - me.waker + me.shared .sleep_state .store(SLEEP_STATE_POLLING, Ordering::Relaxed); @@ -242,7 +244,8 @@ impl FutureState<'_> { Poll::Ready(()) => { assert!(me.tasks.is_empty()); if me.remaining_work() { - let waitable = me.waitable_set.as_ref().unwrap().as_raw(); + let set = me.shared.waitable_set.try_lock().unwrap(); + let waitable = set.as_ref().unwrap().as_raw(); break CallbackCode::Wait(waitable); } else { break CallbackCode::Exit; @@ -255,10 +258,12 @@ impl FutureState<'_> { // something. Poll::Pending => { assert!(!me.tasks.is_empty()); - if me.waker.sleep_state.load(Ordering::Relaxed) == SLEEP_STATE_WOKEN { + if me.shared.sleep_state.load(Ordering::Relaxed) == SLEEP_STATE_WOKEN { if me.remaining_work() { - let (event0, event1, event2) = - me.waitable_set.as_ref().unwrap().poll(); + let (event0, event1, event2) = { + let set = me.shared.waitable_set.try_lock().unwrap(); + set.as_ref().unwrap().poll() + }; if event0 != EVENT_NONE { me.deliver_waitable_event(event1, event2); continue; @@ -270,11 +275,12 @@ impl FutureState<'_> { // Transition our state to "sleeping" so wakeup // notifications know that they need to signal the // inter-task stream. - me.waker + me.shared .sleep_state .store(SLEEP_STATE_SLEEPING, Ordering::Relaxed); me.read_inter_task_stream(); - let waitable = me.waitable_set.as_ref().unwrap().as_raw(); + let set = me.shared.waitable_set.try_lock().unwrap(); + let waitable = set.as_ref().unwrap().as_raw(); break CallbackCode::Wait(waitable); } } @@ -286,7 +292,7 @@ impl FutureState<'_> { /// waitable should be present because it's part of the waitable set which /// is kept in-sync with our map. fn deliver_waitable_event(&mut self, waitable: u32, code: u32) { - self.remove_waitable(waitable); + WaitableSet::remove_waitable_from_all_sets(waitable); if self .inter_task_wakeup @@ -295,17 +301,19 @@ impl FutureState<'_> { return; } - let (ptr, callback) = self.waitables.remove(&waitable).unwrap(); + let c = { + let mut waitables = self.shared.waitables.try_lock().unwrap(); + waitables.remove(&waitable).unwrap() + }; unsafe { - callback(ptr, code); + (c.callback)(c.callback_ptr, code); } } fn with_p3_task_set(&mut self, f: impl FnOnce(&mut Self) -> R) -> R { - // Finish our `wasip3_task` by initializing its self-referential pointer, - // and then register it for the duration of this function with - // `wasip3_task_set`. The previous value of `wasip3_task_set` will get - // restored when this function returns. + // Initialize a temporary `wasip3_task` structure on the stack and + // inform `wasip3_task_set` that we're now within that task. Note the + // RAII guard to reset the task back to its previous contents. struct ResetTask(*mut cabi::wasip3_task); impl Drop for ResetTask { fn drop(&mut self) { @@ -314,16 +322,32 @@ impl FutureState<'_> { } } } - let self_raw = self as *mut FutureState<'_>; - self.wasip3_task.ptr = self_raw.cast(); - let prev = unsafe { cabi::wasip3_task_set(&mut self.wasip3_task) }; + // The `ptr` field of `wasip3_task` is to `SharedTaskState` which is + // what's cloned/handed out/etc. + let shared_raw: *const SharedTaskState = &*self.shared; + let mut wasip3_task = cabi::wasip3_task_v2 { + v1: cabi::wasip3_task { + ptr: shared_raw.cast_mut().cast(), + version: cabi::WASIP3_TASK_V2, + waitable_register: SharedTaskState::CABI_VTABLE.waitable_register, + waitable_unregister: SharedTaskState::CABI_VTABLE.waitable_unregister, + }, + vtable: &SharedTaskState::CABI_VTABLE, + }; + + // Explicitly take a mutable borrow on the entire `wasip3_task` + // structure, and then cast its raw pointer to the "smaller" historical + // version, ensuring the final pointer has provenace over the entire + // structure. + let wasip3_task: *mut cabi::wasip3_task_v2 = &mut wasip3_task; + let prev = unsafe { cabi::wasip3_task_set(wasip3_task.cast::()) }; let _reset = ResetTask(prev); f(self) } } -impl Drop for FutureState<'_> { +impl Drop for TaskState<'_> { fn drop(&mut self) { // If there's an active read of the inter-task stream, go ahead and // cancel it, since we're about to drop the stream anyway. @@ -342,33 +366,79 @@ impl Drop for FutureState<'_> { } } -unsafe extern "C" fn waitable_register( - ptr: *mut c_void, - waitable: u32, - callback: unsafe extern "C" fn(*mut c_void, u32), - callback_ptr: *mut c_void, -) -> *mut c_void { - let ptr = ptr.cast::>(); - assert!(!ptr.is_null()); - unsafe { - (*ptr).add_waitable(waitable); - match (*ptr).waitables.insert(waitable, (callback_ptr, callback)) { - Some((prev, _)) => prev, +impl SharedTaskState { + const CABI_VTABLE: cabi::wasip3_task_vtable = cabi::wasip3_task_vtable { + waitable_register: Self::cabi_waitable_register, + waitable_unregister: Self::cabi_waitable_unregister, + drop: Self::cabi_drop, + clone: Self::cabi_clone, + }; + + /// Adds the `waitable` provided to this task's waitable set. + fn add_waitable(&self, waitable: u32) { + let mut set = self.waitable_set.try_lock().unwrap(); + set.get_or_insert_with(WaitableSet::new).join(waitable); + } + + /// Implementation of the CABI `waitable_register` function. + fn waitable_register( + &self, + waitable: u32, + callback: unsafe extern "C" fn(*mut c_void, u32), + callback_ptr: *mut c_void, + ) -> *mut c_void { + self.add_waitable(waitable); + let mut waitables = self.waitables.try_lock().unwrap(); + let c = CabiWaitable { + callback, + callback_ptr, + }; + match waitables.insert(waitable, c) { + Some(prev) => prev.callback_ptr, None => ptr::null_mut(), } } -} -unsafe extern "C" fn waitable_unregister(ptr: *mut c_void, waitable: u32) -> *mut c_void { - let ptr = ptr.cast::>(); - assert!(!ptr.is_null()); - unsafe { - (*ptr).remove_waitable(waitable); - match (*ptr).waitables.remove(&waitable) { - Some((prev, _)) => prev, + /// Implementation of the CABI `waitable_unregister` function. + fn waitable_unregister(&self, waitable: u32) -> *mut c_void { + WaitableSet::remove_waitable_from_all_sets(waitable); + let mut waitables = self.waitables.try_lock().unwrap(); + match waitables.remove(&waitable) { + Some(prev) => prev.callback_ptr, None => ptr::null_mut(), } } + + /// Helper to go from a raw `c_void` FFI pointer to a typed + /// self-representation. + unsafe fn cabi_to_self(ptr: *mut c_void) -> ManuallyDrop> { + unsafe { ManuallyDrop::new(Arc::from_raw(ptr.cast::())) } + } + + unsafe extern "C" fn cabi_waitable_register( + ptr: *mut c_void, + waitable: u32, + callback: unsafe extern "C" fn(*mut c_void, u32), + callback_ptr: *mut c_void, + ) -> *mut c_void { + let me = unsafe { Self::cabi_to_self(ptr) }; + me.waitable_register(waitable, callback, callback_ptr) + } + + unsafe extern "C" fn cabi_waitable_unregister(ptr: *mut c_void, waitable: u32) -> *mut c_void { + let me = unsafe { Self::cabi_to_self(ptr) }; + me.waitable_unregister(waitable) + } + + unsafe extern "C" fn cabi_clone(ptr: *mut c_void) -> *mut c_void { + let me = unsafe { Self::cabi_to_self(ptr) }; + Arc::into_raw(Arc::clone(&me)).cast_mut().cast() + } + + unsafe extern "C" fn cabi_drop(ptr: *mut c_void) { + let mut me = unsafe { Self::cabi_to_self(ptr) }; + unsafe { ManuallyDrop::drop(&mut me) } + } } /// Status for "this task is actively being polled" @@ -380,14 +450,7 @@ const SLEEP_STATE_WOKEN: u32 = 1; /// Wakeups on this status signal the inter-task stream. const SLEEP_STATE_SLEEPING: u32 = 2; -#[derive(Default)] -struct FutureWaker { - /// One of `SLEEP_STATE_*` indicating the current status. - sleep_state: AtomicU32, - inter_task_stream: inter_task_wakeup::WakerState, -} - -impl Wake for FutureWaker { +impl Wake for SharedTaskState { fn wake(self: Arc) { Self::wake_by_ref(&self) } @@ -483,11 +546,11 @@ impl ReturnCode { /// immediately with its result. #[doc(hidden)] pub fn start_task(task: impl Future + 'static) -> i32 { - // Allocate a new `FutureState` which will track all state necessary for + // Allocate a new `TaskState` which will track all state necessary for // our exported task. - let state = Box::into_raw(Box::new(FutureState::new(Box::pin(task)))); + let state = Box::into_raw(Box::new(TaskState::new(Box::pin(task)))); - // Store our `FutureState` into our context-local-storage slot and then + // Store our `TaskState` into our context-local-storage slot and then // pretend we got EVENT_NONE to kick off everything. // // SAFETY: we should own `context.set` as we're the root level exported @@ -505,13 +568,13 @@ pub fn start_task(task: impl Future + 'static) -> i32 { /// /// # Unsafety /// -/// This function assumes that `context_get()` returns a `FutureState`. +/// This function assumes that `context_get()` returns a `TaskState`. #[doc(hidden)] pub unsafe fn callback(event0: u32, event1: u32, event2: u32) -> u32 { // Acquire our context-local state, assert it's not-null, and then reset // the state to null while we're running to help prevent any unintended // usage. - let state = context_get().cast::>(); + let state = context_get().cast::>(); assert!(!state.is_null()); unsafe { context_set(ptr::null_mut()); @@ -540,7 +603,7 @@ pub unsafe fn callback(event0: u32, event1: u32, event2: u32) -> u32 { // TODO: refactor so `'static` bounds aren't necessary pub fn block_on(future: impl Future) -> T { let mut result = None; - let mut state = FutureState::new(Box::pin(async { + let mut state = TaskState::new(Box::pin(async { result = Some(future.await); })); let mut event = (EVENT_NONE, 0, 0); @@ -550,8 +613,14 @@ pub fn block_on(future: impl Future) -> T { drop(state); break result.unwrap(); } - CallbackCode::Yield => event = state.waitable_set.as_ref().unwrap().poll(), - CallbackCode::Wait(_) => event = state.waitable_set.as_ref().unwrap().wait(), + CallbackCode::Yield => { + let set = state.shared.waitable_set.try_lock().unwrap(); + event = set.as_ref().unwrap().poll() + } + CallbackCode::Wait(_) => { + let set = state.shared.waitable_set.try_lock().unwrap(); + event = set.as_ref().unwrap().wait() + } } } } diff --git a/crates/guest-rust/src/rt/async_support/cabi.rs b/crates/guest-rust/src/rt/async_support/cabi.rs index d696d1e55..b03f5da92 100644 --- a/crates/guest-rust/src/rt/async_support/cabi.rs +++ b/crates/guest-rust/src/rt/async_support/cabi.rs @@ -43,6 +43,29 @@ //! //! Additionally for now this file is serving as documentation of this //! interface. +//! +//! # Revisions +//! +//! This interface is intended to be evolvable over time if needed. Notably the +//! original task structure, `wasip3_task`, has a `version` field where certain +//! version levels imply the existence of certain fields. The historical +//! revisions are: +//! +//! ### V1 +//! +//! This was the original version. This is the original specification of +//! `wasip3_task_set` and `wasip3_task`. +//! +//! ### V2 +//! +//! This was added 2026-06-17 in response to #1618. This added +//! `wasip3_task_v2` and `wasip3_task_vtable`. This version enables cloning a +//! task to create a strong reference to it independent of the stack lifetime +//! that `wasip3_task_set` is required to uphold. This necessitated introducing +//! `clone` and `drop` callbacks to manage the lifetime of this reference. +//! While doing this everything was moved into a vtable structure instead of +//! inline in `wasip3_task` to make it easier to add more function pointers +//! in the future if necessary. use core::ffi::c_void; @@ -66,6 +89,7 @@ extern_wasm! { /// The first version of `wasip3_task` which implies the existence of the /// fields `ptr`, `waitable_register`, and `waitable_unregister`. pub const WASIP3_TASK_V1: u32 = 1; +pub const WASIP3_TASK_V2: u32 = 2; /// Indirect "vtable" used to connect imported functions and exported tasks. /// Executors (e.g. exported functions) define and manage this while imports @@ -80,6 +104,37 @@ pub struct wasip3_task { /// below as the first argument. pub ptr: *mut c_void, + /// See `wasip3_task_vtable::waitable_register`. + pub waitable_register: unsafe extern "C" fn( + ptr: *mut c_void, + waitable: u32, + callback: unsafe extern "C" fn(callback_ptr: *mut c_void, code: u32), + callback_ptr: *mut c_void, + ) -> *mut c_void, + + /// See `wasip3_task_vtable::waitable_unregister`. + pub waitable_unregister: unsafe extern "C" fn(ptr: *mut c_void, waitable: u32) -> *mut c_void, +} + +unsafe impl Send for wasip3_task {} +unsafe impl Sync for wasip3_task {} + +/// Representation when `wasip3_task::version` is `WASIP3_TASK_V2`. +#[repr(C)] +pub struct wasip3_task_v2 { + /// The original task structure. + pub v1: wasip3_task, + + /// An always-valid pointer to a list of function pointers, described + /// below. + pub vtable: &'static wasip3_task_vtable, +} + +/// Function pointer operations that can operate on `wasip3_task::ptr`. +/// +/// This was introduced in the "v2" ABI and is a member of `wasip3_task_v2`. +#[repr(C)] +pub struct wasip3_task_vtable { /// Register a new `waitable` for this exported task. /// /// This exported task will add `waitable` to its `waitable-set`. When it @@ -104,4 +159,18 @@ pub struct wasip3_task { /// Returns the `callback_ptr` passed to `waitable_register` if present, or /// `NULL` if it's not present. pub waitable_unregister: unsafe extern "C" fn(ptr: *mut c_void, waitable: u32) -> *mut c_void, + + /// Clones this task's pointer to create a separately owned pointer which + /// can be persisted outside the stack frame that this is being used + /// within. + /// + /// Cloned values must be dropped/deallocated with `drop` below. + pub clone: unsafe extern "C" fn(ptr: *mut c_void) -> *mut c_void, + + /// Drops and deallocates the provided pointer previously created by a + /// call to the `clone` callback above. + /// + /// This must not be called on the `ptr` value within `wasip3_task::ptr` as + /// that's not managed with this lifetime. + pub drop: unsafe extern "C" fn(ptr: *mut c_void), } diff --git a/crates/guest-rust/src/rt/async_support/inter_task_wakeup.rs b/crates/guest-rust/src/rt/async_support/inter_task_wakeup.rs index 31a84ea15..96ee57308 100644 --- a/crates/guest-rust/src/rt/async_support/inter_task_wakeup.rs +++ b/crates/guest-rust/src/rt/async_support/inter_task_wakeup.rs @@ -1,6 +1,6 @@ -use super::FutureState; +use super::TaskState; use crate::rt::async_support::try_lock::TryLock; -use crate::rt::async_support::{BLOCKED, COMPLETED}; +use crate::rt::async_support::{BLOCKED, COMPLETED, WaitableSet}; use crate::{RawStreamReader, RawStreamWriter, StreamOps, UnitStreamOps}; use core::ptr; @@ -18,7 +18,7 @@ pub struct State { stream_reading: bool, } -impl FutureState<'_> { +impl TaskState<'_> { pub(super) fn read_inter_task_stream(&mut self) { // Lazily allocate the inter-task stream now that we're actually going // to sleep. We don't know where the wakeup notification will come from @@ -27,7 +27,7 @@ impl FutureState<'_> { assert!(!self.inter_task_wakeup.stream_reading); let (writer, reader) = UnitStreamOps::new(); self.inter_task_wakeup.stream = Some(reader); - let mut waker_stream = self.waker.inter_task_stream.lock.try_lock().unwrap(); + let mut waker_stream = self.shared.inter_task_stream.lock.try_lock().unwrap(); assert!(waker_stream.is_none()); *waker_stream = Some(writer); } @@ -44,7 +44,7 @@ impl FutureState<'_> { let rc = unsafe { UnitStreamOps.start_read(handle, ptr::null_mut(), 1) }; assert_eq!(rc, BLOCKED); self.inter_task_wakeup.stream_reading = true; - self.add_waitable(handle); + self.shared.add_waitable(handle); } } @@ -61,7 +61,7 @@ impl FutureState<'_> { // synchronous `stream.cancel-read` traps if the stream is still a member // of a waitable set, so this must happen first. This matches the // unregister-then-cancel ordering in `WaitableOperation::cancel`. - self.remove_waitable(handle); + WaitableSet::remove_waitable_from_all_sets(handle); // Note that the return code here is discarded. No matter what the read // is cancelled, and whether we actually read something or whether we // cancelled doesn't matter. diff --git a/crates/guest-rust/src/rt/async_support/inter_task_wakeup_disabled.rs b/crates/guest-rust/src/rt/async_support/inter_task_wakeup_disabled.rs index 3f7462cfb..6cd1fd140 100644 --- a/crates/guest-rust/src/rt/async_support/inter_task_wakeup_disabled.rs +++ b/crates/guest-rust/src/rt/async_support/inter_task_wakeup_disabled.rs @@ -1,9 +1,9 @@ -use super::FutureState; +use super::TaskState; #[derive(Default)] pub struct State; -impl FutureState<'_> { +impl TaskState<'_> { pub(super) fn read_inter_task_stream(&mut self) { assert!( self.remaining_work(), diff --git a/crates/guest-rust/src/rt/async_support/waitable.rs b/crates/guest-rust/src/rt/async_support/waitable.rs index 4e45f911c..d0b8c97f0 100644 --- a/crates/guest-rust/src/rt/async_support/waitable.rs +++ b/crates/guest-rust/src/rt/async_support/waitable.rs @@ -19,11 +19,59 @@ use core::task::{Context, Poll, Waker}; pub struct WaitableOperation { op: S, state: WaitableOperationState, + task: Option, /// Storage for the final result of this asynchronous operation, if it's /// completed asynchronously. completion_status: CompletionStatus, } +struct CabiTask { + ptr: *mut c_void, + registered: Option, + vtable: &'static cabi::wasip3_task_vtable, +} + +impl CabiTask { + /// Creates a new task from the raw C ABI representation provided. + /// + /// # Safety + /// + /// The `task` provided must be valid and adhere to C ABI conventions. + unsafe fn new(task: *mut cabi::wasip3_task_v2) -> CabiTask { + // SAFETY: the validity of `task` is up to the caller. + unsafe { + CabiTask { + ptr: ((*task).vtable.clone)((*task).v1.ptr), + registered: None, + vtable: (*task).vtable, + } + } + } + + fn unregister(&mut self, waitable: u32) -> *mut c_void { + self.registered = None; + // SAFETY: this was created from a valid task, so this should be safe + // to invoke. + unsafe { (self.vtable.waitable_unregister)(self.ptr, waitable) } + } +} + +unsafe impl Send for CabiTask {} +unsafe impl Sync for CabiTask {} + +impl Drop for CabiTask { + fn drop(&mut self) { + if let Some(waitable) = self.registered { + self.unregister(waitable); + } + // SAFETY: this was created from a valid atask, so this should be safe + // to invoke. + unsafe { + (self.vtable.drop)(self.ptr); + } + } +} + /// Structure used to store the `u32` return code from the canonical ABI about /// an asynchronous operation. /// @@ -140,6 +188,7 @@ where WaitableOperation { op, state: WaitableOperationState::Start(state), + task: None, completion_status: CompletionStatus { code: None, waker: None, @@ -153,6 +202,7 @@ where ) -> ( &mut S, &mut WaitableOperationState, + &mut Option, Pin<&mut CompletionStatus>, ) { // SAFETY: this is the one method used to project from `Pin<&mut Self>` @@ -165,6 +215,7 @@ where ( &mut me.op, &mut me.state, + &mut me.task, Pin::new_unchecked(&mut me.completion_status), ) } @@ -175,7 +226,7 @@ where /// * Fill in `completion_status` with the result of a completion event. /// * Call `cx.waker().wake()`. pub fn register_waker(self: Pin<&mut Self>, waitable: u32, cx: &mut Context) { - let (_, _, mut completion_status) = self.pin_project(); + let (_, _, last_task, mut completion_status) = self.pin_project(); debug_assert!(completion_status.as_mut().code_mut().is_none()); *completion_status.as_mut().waker_mut() = Some(cx.waker().clone()); @@ -193,13 +244,33 @@ where assert!(!task.is_null()); assert!((*task).version >= cabi::WASIP3_TASK_V1); let ptr: *mut CompletionStatus = completion_status.get_unchecked_mut(); + + // For the v2+ ABI clone the task structure to store internally + // within this waitable operation, if we're not already storing + // this task. This ensures that `unregister_waker` below works + // correctly in cross-task situations. + // + // Note that this must happen before the `waitable_register` below + // to ensure we're fully removed from the previous task, if + // applicable, before registering with another task. + if (*task).version >= cabi::WASIP3_TASK_V2 { + let task = task.cast::(); + let last_task = match last_task { + Some(prev) if prev.ptr == (*task).v1.ptr => prev, + _ => last_task.insert(CabiTask::new(task)), + }; + last_task.registered = Some(waitable); + } + let prev = ((*task).waitable_register)((*task).ptr, waitable, cabi_wake, ptr.cast()); + // We might be inserting a waker for the first time or overwriting // the previous waker. Only assert the expected value here if the // previous value was non-null. if !prev.is_null() { assert_eq!(ptr, prev.cast()); } + cabi::wasip3_task_set(task); } @@ -216,42 +287,59 @@ where /// This relinquishes control of the original `completion_status` pointer /// passed to `register_waker` after this call has completed. pub fn unregister_waker(self: Pin<&mut Self>, waitable: u32) { - // SAFETY: the contract of `wasip3_task_set` is that the returned - // pointer is valid for the lifetime of our entire task, so it's valid - // for this stack frame. Additionally we assert it's non-null to - // double-check it's initialized and additionally check the version for - // the fields that we access. - // - // Otherwise the `waitable_unregister` callback should be safe because: - // - // * We're fulfilling the contract where the first argument must be - // `(*task).ptr` - // * We own the `waitable` that we're passing in, so we're fulfilling - // the contract that arbitrary waitables for other units of work - // aren't being manipulated. - unsafe { - let task = cabi::wasip3_task_set(ptr::null_mut()); - assert!(!task.is_null()); - assert!((*task).version >= cabi::WASIP3_TASK_V1); - let prev = ((*task).waitable_unregister)((*task).ptr, waitable); - - // Note that `_prev` here is not guaranteed to be either `NULL` or - // not. A racy completion notification may have come in and - // removed our waitable from the map even though we're in the - // `InProgress` state, meaning it may not be present. + let (_, _, task, completion) = self.pin_project(); + + let prev = match task { + // Note that in this case we leave `task` as-is to avoid re-cloning + // it in the future if we're re-registered with it, so this only + // unregisters. + Some(prev) => prev.unregister(waitable), + + // If we don't have a previous task listed then that means that we + // are registered with a "v1" task that can't be cloned out. Assume + // blindly that we're still under the same task and unregister from + // it. // - // The main thing is that after this method is called the - // internal `completion_status` is guaranteed to no longer be in - // `task`. + // SAFETY: the contract of `wasip3_task_set` is that the returned + // pointer is valid for the lifetime of our entire task, so it's + // valid for this stack frame. Additionally we assert it's non-null + // to double-check it's initialized and additionally check the + // version for the fields that we access. // - // Note, though, that if present this must be our `CompletionStatus` - // pointer. - if !prev.is_null() { - let ptr: *mut CompletionStatus = self.pin_project().2.get_unchecked_mut(); - assert_eq!(ptr, prev.cast()); - } + // Otherwise the `waitable_unregister` callback should be safe + // because: + // + // * We're fulfilling the contract where the first argument must be + // `(*task).ptr` + // * We own the `waitable` that we're passing in, so we're + // fulfilling the contract that arbitrary waitables for other + // units of work aren't being manipulated. + None => unsafe { + let task = cabi::wasip3_task_set(ptr::null_mut()); + assert!(!task.is_null()); + assert!((*task).version >= cabi::WASIP3_TASK_V1); + let prev = ((*task).waitable_unregister)((*task).ptr, waitable); + cabi::wasip3_task_set(task); + prev + }, + }; - cabi::wasip3_task_set(task); + // Note that `prev` here may be null or may be valid. A racy completion + // notification may have come in and removed our waitable from the map + // even though we're in the `InProgress` state, meaning it may not be + // present. + // + // The main thing is that after this method is called the + // internal `completion_status` is guaranteed to no longer be in + // `task`. + // + // Note, though, that if present this must be our `CompletionStatus` + // pointer. If it's not then something's been corrupted and this is + // intended to catch that early. + if !prev.is_null() { + // SAFETY: only used for a comparison, not mutated. + let ptr: *mut CompletionStatus = unsafe { completion.get_unchecked_mut() }; + assert_eq!(ptr, prev.cast()); } } @@ -261,7 +349,7 @@ where pub fn poll_complete(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll { use WaitableOperationState::*; - let (op, state, completion_status) = self.as_mut().pin_project(); + let (op, state, _task, completion_status) = self.as_mut().pin_project(); // First up, determine the completion status, if any, that's available. let optional_code = match state { @@ -305,7 +393,7 @@ where ) -> Poll { use WaitableOperationState::*; - let (op, state, _completion_status) = self.as_mut().pin_project(); + let (op, state, task, _completion_status) = self.as_mut().pin_project(); // If a status code is provided, then extract the in-progress state and // see what it thinks about this code. If we're done, yay! If not then @@ -315,6 +403,9 @@ where // If no status code is available then that means we were polled before // the status came back, so just re-register the waker. if let Some(code) = optional_code { + if let Some(task) = task { + task.registered = None; + } let InProgress(in_progress) = mem::replace(state, Done) else { unreachable!() }; @@ -354,7 +445,7 @@ where pub fn cancel(mut self: Pin<&mut Self>) -> S::Cancel { use WaitableOperationState::*; - let (op, state, mut completion_status) = self.as_mut().pin_project(); + let (op, state, _task, mut completion_status) = self.as_mut().pin_project(); let in_progress = match state { // This operation was never actually started, so there's no need to // cancel anything, just pull out the value and return it. @@ -432,7 +523,7 @@ where // this to be sound. Rust doesn't currently have linear types or async // destructors for example to ensure otherwise that if this were to // proceed asynchronously that we could rely on it being invoked. - let (op, InProgress(in_progress), _) = self.as_mut().pin_project() else { + let (op, InProgress(in_progress), _, _) = self.as_mut().pin_project() else { unreachable!() }; let code = op.in_progress_cancel(in_progress); diff --git a/tests/runtime-async/async/rust-async-and-block-on/runner.rs b/tests/runtime-async/async/rust-async-and-block-on/runner.rs new file mode 100644 index 000000000..e9347009a --- /dev/null +++ b/tests/runtime-async/async/rust-async-and-block-on/runner.rs @@ -0,0 +1,49 @@ +//@ wasmtime-flags = '-Wcomponent-model-async' + +include!(env!("BINDINGS")); + +use std::future::Future; +use std::pin::pin; +use std::task::{Context, Poll, Waker}; +use wit_bindgen::block_on; + +struct Component; + +export!(Component); + +impl Guest for Component { + async fn run() { + let (writer, reader) = wit_stream::new::(); + let reader = a::b::i::launder(reader); + let noop_cx = &mut Context::from_waker(Waker::noop()); + + let mut w1 = pin!(async { + let mut w = writer; + let _ = w.write(vec![1u8]).await; + w + }); + + // Step 1 — register &w1.completion_status in export_task.waitables[H]. + assert!(matches!(w1.as_mut().poll(noop_cx), Poll::Pending)); + + // Step 2 — block_on completes w1; export_task.waitables[H] goes stale. + // _reader must stay alive so the step-3 write blocks (Dropped skips + // register_waker and hides the bug). + let (writer, _reader) = block_on(async move { + let mut reader = reader; + let (w, _) = futures::join!(w1, reader.read(Vec::with_capacity(1))); + (w, reader) + }); + + // Step 3 — register &w2.completion_status; gets freed + // &w1.completion_status back as prev → assert_eq!(ptr, prev.cast()) + // panics at waitable.rs:201. + let mut w2 = pin!(async move { + let mut w = writer; + let pad = [0u64; 16]; + let _ = w.write(vec![2u8]).await; + let _ = pad; // explicit use after await keeps pad in the state machine + }); + let _ = w2.as_mut().poll(noop_cx); // panics + } +} diff --git a/tests/runtime-async/async/rust-async-and-block-on/test.rs b/tests/runtime-async/async/rust-async-and-block-on/test.rs new file mode 100644 index 000000000..2011d1ae0 --- /dev/null +++ b/tests/runtime-async/async/rust-async-and-block-on/test.rs @@ -0,0 +1,13 @@ +include!(env!("BINDINGS")); + +use wit_bindgen::StreamReader; + +struct Component; + +export!(Component); + +impl crate::exports::a::b::i::Guest for Component { + fn launder(x: StreamReader) -> StreamReader { + x + } +} diff --git a/tests/runtime-async/async/rust-async-and-block-on/test.wit b/tests/runtime-async/async/rust-async-and-block-on/test.wit new file mode 100644 index 000000000..3e66f4289 --- /dev/null +++ b/tests/runtime-async/async/rust-async-and-block-on/test.wit @@ -0,0 +1,15 @@ +package a:b; + +interface i { + launder: func(x: stream) -> stream; +} + +world test { + export i; +} + +world runner { + import i; + + export run: async func(); +} From f1e55577645c5e0caad85e9e31ff069272359171 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Mon, 29 Jun 2026 11:29:41 -0400 Subject: [PATCH 03/23] feat(cpp): add map type support (#1590) * feat(cpp): add map type support * fix(cpp): add operator< to wit::string for std::map key support and fix rustfmt wit::string lacked comparison operators, causing compilation failures when used as a std::map key. Also fixes rustfmt formatting issues. * fix(cpp): const_cast map keys during lowering for ownership transfer std::map keys are const, but the ABI lowering needs to call leak() on string keys to transfer ownership to the flat buffer. Use const_cast since the map is consumed during the lowering operation. * fix(cpp): copy map keys by value instead of const_cast during lowering const_cast fails when the map key type differs between contexts (e.g. std::string_view in imports vs wit::string in exports). Copying by value works universally and is safe since the map is consumed. * fix(cpp): remove map runtime test until wasi-sdk supports map types wasm-component-ld bundled with wasi-sdk 30 doesn't support the map type encoding (0x63) in the component model binary format. The C++ map codegen is still validated by the codegen test. The runtime test can be re-added when wasi-sdk ships a compatible wasm-component-ld. * refactor(cpp): use wit::map instead of std::map for map types std::map requires per-entry rb-tree allocation during lift; wit::map mirrors the canonical ABI layout (flat pair array) so lift is a single allocation, matching the existing wit::vector / wit::string pattern. Signed-off-by: Yordis Prieto * refactor(cpp): drop redundant size_t cast in MapLower wit::map::size() and std::span::size() already return size_t, so the C-style cast was a no-op. Signed-off-by: Yordis Prieto * refactor(cpp): skip guest-dealloc loop when body is empty Avoids an unused `base` local and the `(void) base;` suppression when the element has no nested heap-owned fields (e.g. list, map). Signed-off-by: Yordis Prieto * refactor(cpp): use static_cast for MapLower realloc result The cast converts void* (from cabi_realloc) to uint8_t*, which is a well-defined static_cast and doesn't need a C-style cast. Signed-off-by: Yordis Prieto * fix(cpp): zero length when moving wit::map Leaves the moved-from map in a fully consistent state so any later inspection of size()/empty() reflects the empty invariant, not just the destructor's short-circuit on a null data_ pointer. Signed-off-by: Yordis Prieto * style(cpp): break long single-line methods in wit::map Signed-off-by: Yordis Prieto * refactor(cpp): drop wit::map operator[] Index-based subscript on a map is a footgun: integral keys would silently compile while doing positional access. Iteration via range-for and pointer access via data() are sufficient for codegen and don't carry the same expectation mismatch. Signed-off-by: Yordis Prieto * refactor(cpp): drop std::span accessors from wit::map A span of pairs is a vector-shaped view that doesn't fit a map abstraction; codegen sites that need a flat pair span build it explicitly from data() and size() at the call site. Signed-off-by: Yordis Prieto * feat(cpp): introduce wit::map_view for borrowed map arguments Borrowed map arguments were surfacing as std::span const>, which carries vector-shaped affordances (positional indexing, span conversions) on what is conceptually a map. wit::map_view is a borrow- only counterpart to wit::map that mimics map semantics rather than vector semantics. Signed-off-by: Yordis Prieto * refactor(cpp): rename wit::map to wit::unordered_map Component-model map carries no ordering or hashing guarantee, and the type's API surface deliberately mirrors std::unordered_map (size, empty, begin/end) plus bindings-construction primitives, so the name should reflect the unordered semantics rather than std::map's ordered ones. Companion borrow type renamed to wit::unordered_map_view. Signed-off-by: Yordis Prieto * fix(cpp): align map iteration variable with renamed _base Adjacent block bodies (string/list/option dealloc) now reference _base after main's rename, so MapLower / MapLift / GuestDeallocateMap must declare the per-entry pointer under the same name to compile. Adds the matching (void) _base; suppression so loops still compile when the inner body doesn't reference it. Signed-off-by: Yordis Prieto * refactor(cpp): remove unordered_map_view and update map handling The `unordered_map_view` class has been removed to streamline the API, as it was a borrow-only handle that mimicked map semantics. The code now directly utilizes `std::span` for borrowed map arguments, ensuring a clearer distinction between map and vector semantics. Additionally, the handling of dependencies has been updated to include `` where necessary. Signed-off-by: Yordis Prieto --------- Signed-off-by: Yordis Prieto --- crates/cpp/helper-types/wit.h | 74 ++++++++++++++ crates/cpp/src/lib.rs | 184 ++++++++++++++++++++++++++++++---- crates/test/src/cpp.rs | 2 +- 3 files changed, 239 insertions(+), 21 deletions(-) diff --git a/crates/cpp/helper-types/wit.h b/crates/cpp/helper-types/wit.h index 8a79db32c..e643a5262 100644 --- a/crates/cpp/helper-types/wit.h +++ b/crates/cpp/helper-types/wit.h @@ -13,6 +13,7 @@ #include // free #include #include +#include // pair namespace wit { /// @brief Helper class to map between IDs and resources @@ -170,6 +171,79 @@ template class vector { } }; +/// @brief A map stored as a contiguous array of key-value pairs in linear +/// memory, freed unconditionally using free. +/// +/// Mirrors the canonical ABI representation of `map` (`list>`) +/// to enable lift without per-entry tree allocation. The container has no +/// ordering or hashing guarantees and exposes only the subset of the +/// `std::unordered_map` API that's meaningful over a flat pair buffer plus +/// the bindings-construction primitives (`allocate`, `initialize`, `leak`, +/// `drop_raw`, `data`). +template class unordered_map { +public: + using entry_type = std::pair; + +private: + entry_type *data_; + size_t length; + + static entry_type* empty_ptr() { return (entry_type*)alignof(entry_type); } + +public: + unordered_map(unordered_map const &) = delete; + unordered_map(unordered_map &&b) : data_(b.data_), length(b.length) { + b.data_ = nullptr; + b.length = 0; + } + unordered_map &operator=(unordered_map const &) = delete; + unordered_map &operator=(unordered_map &&b) { + if (data_ && length > 0) { + for (unsigned i = 0; i < length; ++i) { data_[i].~entry_type(); } + free(data_); + } + data_ = b.data_; + length = b.length; + b.data_ = nullptr; + b.length = 0; + return *this; + } + unordered_map(entry_type *d, size_t l) : data_(d), length(l) {} + unordered_map() : data_(empty_ptr()), length() {} + entry_type const *data() const { return data_; } + entry_type *data() { return data_; } + size_t size() const { return length; } + bool empty() const { return !length; } + ~unordered_map() { + if (data_ && length > 0) { + for (unsigned i = 0; i < length; ++i) { data_[i].~entry_type(); } + free((void*)data_); + } + } + // WARNING: unordered_map contains uninitialized entries; caller must + // construct them via `initialize` before the map is observed or destroyed. + static unordered_map allocate(size_t len) { + if (!len) return unordered_map(empty_ptr(), 0); + return unordered_map( + (entry_type*)malloc(sizeof(entry_type) * len), len); + } + void initialize(size_t n, entry_type&& entry) { + new ((void*)(data_ + n)) entry_type(std::move(entry)); + } + entry_type *leak() { + entry_type *result = data_; + data_ = nullptr; + return result; + } + static void drop_raw(void *ptr) { + if (ptr != empty_ptr()) free(ptr); + } + entry_type *begin() { return data_; } + entry_type *end() { return data_ + length; } + entry_type const *begin() const { return data_; } + entry_type const *end() const { return data_ + length; } +}; + /// @brief A Resource defined within the guest (guest side) /// /// It registers with the host and should remain in a static location. diff --git a/crates/cpp/src/lib.rs b/crates/cpp/src/lib.rs index 468aa2b17..e672feb10 100644 --- a/crates/cpp/src/lib.rs +++ b/crates/cpp/src/lib.rs @@ -425,6 +425,9 @@ impl Cpp { if self.dependencies.needs_tuple { self.include(""); } + if self.dependencies.needs_span { + self.include(""); + } if self.dependencies.needs_wit { self.include("\"wit.h\""); } @@ -906,7 +909,7 @@ impl CppInterfaceGenerator<'_> { TypeDefKind::Stream(_) => todo!("generate for stream"), TypeDefKind::Handle(_) => todo!("generate for handle"), TypeDefKind::FixedLengthList(_, _) => todo!(), - TypeDefKind::Map(_, _) => todo!(), + TypeDefKind::Map(k, v) => self.type_map(id, name, k, v, &ty.docs), TypeDefKind::Unknown => unreachable!(), } } @@ -1733,7 +1736,30 @@ impl CppInterfaceGenerator<'_> { self.type_name(ty, from_namespace, flavor) ) } - TypeDefKind::Map(_, _) => todo!(), + TypeDefKind::Map(key, value) => { + let borrowed = match flavor { + Flavor::BorrowedArgument => true, + Flavor::Argument(var) => { + matches!(var, AbiVariant::GuestImport) + || self.r#gen.opts.api_style == APIStyle::Symmetric + } + _ => false, + }; + let element_flavor = if borrowed { + Flavor::BorrowedArgument + } else { + Flavor::InStruct + }; + let k = self.type_name(key, from_namespace, element_flavor); + let v = self.type_name(value, from_namespace, element_flavor); + if borrowed { + self.r#gen.dependencies.needs_span = true; + format!("std::span const>") + } else { + self.r#gen.dependencies.needs_wit = true; + format!("wit::unordered_map<{k}, {v}>") + } + } TypeDefKind::Unknown => todo!(), }, Type::ErrorContext => todo!(), @@ -2258,7 +2284,7 @@ impl<'a> wit_bindgen_core::InterfaceGenerator<'a> for CppInterfaceGenerator<'a> _value: &wit_bindgen_core::wit_parser::Type, _docs: &wit_bindgen_core::wit_parser::Docs, ) { - todo!("map types are not yet supported in the C++ backend") + // nothing to do here } fn type_builtin( @@ -3483,17 +3509,19 @@ impl<'a, 'b> Bindgen for FunctionBindgen<'a, 'b> { let len = self.tempname("_len", tmp); uwriteln!(self.src, "uint8_t* {ptr} = {};", operands[0]); uwriteln!(self.src, "size_t {len} = {};", operands[1]); - let i = self.tempname("i", tmp); - uwriteln!(self.src, "for (size_t {i} = 0; {i} < {len}; {i}++) {{"); - let size = self.r#gen.sizes.size(element); - uwriteln!( - self.src, - "uint8_t* _base = {ptr} + {i} * {size};", - size = size.format(POINTER_SIZE_EXPRESSION) - ); - uwriteln!(self.src, "(void) _base;"); - uwrite!(self.src, "{body}"); - uwriteln!(self.src, "}}"); + if !body.trim().is_empty() { + let i = self.tempname("i", tmp); + uwriteln!(self.src, "for (size_t {i} = 0; {i} < {len}; {i}++) {{"); + let size = self.r#gen.sizes.size(element); + uwriteln!( + self.src, + "uint8_t* _base = {ptr} + {i} * {size};", + size = size.format(POINTER_SIZE_EXPRESSION) + ); + uwriteln!(self.src, "(void) _base;"); + uwrite!(self.src, "{body}"); + uwriteln!(self.src, "}}"); + } uwriteln!(self.src, "if ({len} > 0) {{"); uwriteln!(self.src, "free((void*) ({ptr}));"); uwriteln!(self.src, "}}"); @@ -3541,12 +3569,128 @@ impl<'a, 'b> Bindgen for FunctionBindgen<'a, 'b> { } abi::Instruction::AsyncTaskReturn { .. } => todo!(), abi::Instruction::DropHandle { .. } => todo!(), - abi::Instruction::MapLower { .. } - | abi::Instruction::MapLift { .. } - | abi::Instruction::IterMapKey { .. } - | abi::Instruction::IterMapValue { .. } - | abi::Instruction::GuestDeallocateMap { .. } => { - todo!("map types are not yet supported in this backend") + abi::Instruction::MapLower { + key, + value, + realloc, + } => { + let tmp = self.tmp(); + let body = self.blocks.pop().unwrap(); + let val = format!("map{tmp}"); + let ptr = format!("ptr{tmp}"); + let len = format!("len{tmp}"); + let entry = self.r#gen.sizes.record([*key, *value]); + let size = entry.size.format(POINTER_SIZE_EXPRESSION); + let align = entry.align.format(POINTER_SIZE_EXPRESSION); + // The canonical ABI entry layout can differ from the C++ entry + // layout (see wit-bindgen#1592), so always allocate a fresh ABI + // buffer rather than reusing the source map's storage. + self.push_str(&format!("auto&& {val} = {};\n", operands[0])); + self.push_str(&format!("auto {len} = {val}.size();\n")); + uwriteln!( + self.src, + "auto {ptr} = static_cast<{ptr_type}>({len} > 0 ? cabi_realloc(nullptr, 0, {align}, {len} * {size}) : nullptr);", + ptr_type = self.r#gen.r#gen.opts.ptr_type() + ); + uwriteln!(self.src, "for (size_t i = 0; i < {len}; ++i) {{"); + uwriteln!(self.src, "auto _base = {ptr} + i * {size};"); + uwriteln!(self.src, "(void) _base;"); + uwriteln!(self.src, "auto&& iter_entry = {val}.data()[i];"); + uwriteln!(self.src, "auto&& iter_map_key = iter_entry.first;"); + uwriteln!(self.src, "auto&& iter_map_value = iter_entry.second;"); + uwrite!(self.src, "{}", body.0); + uwriteln!(self.src, "}}"); + if realloc.is_some() { + uwriteln!(self.src, "{}.leak();", operands[0]); + } + results.push(ptr); + results.push(len); + } + abi::Instruction::MapLift { key, value, .. } => { + let body = self.blocks.pop().unwrap(); + let tmp = self.tmp(); + let entry = self.r#gen.sizes.record([*key, *value]); + let size = entry.size.format(POINTER_SIZE_EXPRESSION); + let flavor = if self.r#gen.r#gen.opts.api_style == APIStyle::Symmetric + && matches!(self.variant, AbiVariant::GuestExport) + { + Flavor::BorrowedArgument + } else { + Flavor::InStruct + }; + let key_type = self.r#gen.type_name(key, &self.namespace, flavor); + let value_type = self.r#gen.type_name(value, &self.namespace, flavor); + let len = format!("len{tmp}"); + let base = format!("base{tmp}"); + let result = format!("result{tmp}"); + uwriteln!(self.src, "auto {base} = {};", operands[0]); + uwriteln!(self.src, "auto {len} = {};", operands[1]); + uwriteln!( + self.src, + "auto {result} = wit::unordered_map<{key_type}, {value_type}>::allocate({len});" + ); + if self.r#gen.r#gen.opts.api_style == APIStyle::Symmetric + && matches!(self.variant, AbiVariant::GuestExport) + { + assert!(self.needs_dealloc); + uwriteln!(self.src, "if ({len}>0) _deallocate.push_back({base});"); + } + uwriteln!(self.src, "for (unsigned i=0; i<{len}; ++i) {{"); + uwriteln!(self.src, "auto _base = {base} + i * {size};"); + uwriteln!(self.src, "(void) _base;"); + uwrite!(self.src, "{}", body.0); + let body_key = &body.1[0]; + let body_value = &body.1[1]; + uwriteln!( + self.src, + "{result}.initialize(i, std::make_pair({}, {}));", + move_if_necessary(body_key), + move_if_necessary(body_value) + ); + uwriteln!(self.src, "}}"); + + if self.r#gen.r#gen.opts.api_style == APIStyle::Symmetric + && matches!(self.variant, AbiVariant::GuestExport) + { + self.r#gen.r#gen.dependencies.needs_wit = true; + self.r#gen.r#gen.dependencies.needs_span = true; + results.push(format!( + "std::span const>({result}.data(), {result}.size())" + )); + self.leak_on_insertion.replace(format!( + "if ({len}>0) _deallocate.push_back((void*){result}.leak());\n" + )); + } else { + results.push(move_if_necessary(&result)); + } + } + abi::Instruction::IterMapKey { .. } => { + results.push("iter_map_key".to_string()); + } + abi::Instruction::IterMapValue { .. } => { + results.push("iter_map_value".to_string()); + } + abi::Instruction::GuestDeallocateMap { key, value } => { + let (body, results) = self.blocks.pop().unwrap(); + assert!(results.is_empty()); + let tmp = self.tmp(); + let ptr = self.tempname("_ptr", tmp); + let len = self.tempname("_len", tmp); + uwriteln!(self.src, "uint8_t* {ptr} = {};", operands[0]); + uwriteln!(self.src, "size_t {len} = {};", operands[1]); + if !body.trim().is_empty() { + let i = self.tempname("i", tmp); + uwriteln!(self.src, "for (size_t {i} = 0; {i} < {len}; {i}++) {{"); + let entry = self.r#gen.sizes.record([*key, *value]); + let size = entry.size.format(POINTER_SIZE_EXPRESSION); + uwriteln!(self.src, "uint8_t* _base = {ptr} + {i} * {size};"); + uwriteln!(self.src, "(void) _base;"); + uwrite!(self.src, "{body}"); + uwriteln!(self.src, "}}"); + } + uwriteln!(self.src, "if ({len} > 0) {{"); + uwriteln!(self.src, "free((void*) ({ptr}));"); + uwriteln!(self.src, "}}"); } } } diff --git a/crates/test/src/cpp.rs b/crates/test/src/cpp.rs index 4946a1fef..bf49ad3a8 100644 --- a/crates/test/src/cpp.rs +++ b/crates/test/src/cpp.rs @@ -50,7 +50,7 @@ impl LanguageMethods for Cpp { return false; } return match name { - "issue1514-6.wit" | "named-fixed-length-list.wit" | "map.wit" => true, + "issue1514-6.wit" | "named-fixed-length-list.wit" => true, _ => false, } || config.async_; } From 1ccc7c72cf6824b1b3541eef175d5280e12a9037 Mon Sep 17 00:00:00 2001 From: EvianZhang Date: Mon, 6 Jul 2026 23:26:00 +0800 Subject: [PATCH 04/23] Fix exception safety violation in AbiBuffer::advance (#1649) * Fix exception safety violation in AbiBuffer::advance * Add more explanation --- crates/guest-rust/src/rt/async_support/abi_buffer.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/guest-rust/src/rt/async_support/abi_buffer.rs b/crates/guest-rust/src/rt/async_support/abi_buffer.rs index 9b0841999..fe97b687e 100644 --- a/crates/guest-rust/src/rt/async_support/abi_buffer.rs +++ b/crates/guest-rust/src/rt/async_support/abi_buffer.rs @@ -133,6 +133,12 @@ impl AbiBuffer { let (mut ptr, len) = self.abi_ptr_and_len(); assert!(amt <= len); for _ in 0..amt { + // Update self.cursor incrementally for exception safety. + // When `self.ops.dealloc_lists` panics (which is a user-provided + // callback), we can make sure any item before (including the + // panic one) will not be dealloced again, and the remaining items + // can still get advanced properly. + self.cursor += 1; // SAFETY: we're managing the pointer passed to `dealloc_lists` and // it was initialized with a `lower`, and then the pointer // arithmetic should all be in-bounds. @@ -141,7 +147,6 @@ impl AbiBuffer { ptr = ptr.add(self.ops.elem_layout().size()); } } - self.cursor += amt; } fn take_vec(&mut self) -> Vec { From 4642b6bcc4283361b5d85eb00b00addf618458a9 Mon Sep 17 00:00:00 2001 From: Andrew Steurer <94206073+asteurer@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:10:52 -0500 Subject: [PATCH 05/23] chore: bump go-pkg version (#1645) Signed-off-by: Andrew Steurer <94206073+asteurer@users.noreply.github.com> --- crates/go/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/go/src/lib.rs b/crates/go/src/lib.rs index 4caf53a04..d72b2ae9d 100644 --- a/crates/go/src/lib.rs +++ b/crates/go/src/lib.rs @@ -45,7 +45,7 @@ fn remote_pkg(name: &str) -> String { } /// The version of github.com/bytecodealliance/go-pkg that's being used -const REMOTE_PKG_VERSION: &str = "v0.2.1"; +const REMOTE_PKG_VERSION: &str = "v0.2.2"; /// If a user specifies the `pkg_name` flag, the required version for the /// shared remote package isn't recorded. This enables downstream users to retrieve the version programmatically. From 893636588c2b106ec7136bef6c19f984f49dc696 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Mon, 6 Jul 2026 17:26:28 -0500 Subject: [PATCH 06/23] Propagate errors in `preprocess` instead of unwrapping (#1648) Closes #1647 --- crates/c/src/lib.rs | 3 ++- crates/core/src/lib.rs | 5 +++-- crates/cpp/src/lib.rs | 3 ++- crates/csharp/src/world_generator.rs | 3 ++- crates/go/src/lib.rs | 3 ++- crates/markdown/src/lib.rs | 3 ++- crates/moonbit/src/lib.rs | 3 ++- crates/rust/src/lib.rs | 7 ++++--- 8 files changed, 19 insertions(+), 11 deletions(-) diff --git a/crates/c/src/lib.rs b/crates/c/src/lib.rs index d28908057..970d34aa2 100644 --- a/crates/c/src/lib.rs +++ b/crates/c/src/lib.rs @@ -173,7 +173,7 @@ enum Scalar { } impl WorldGenerator for C { - fn preprocess(&mut self, resolve: &Resolve, world: WorldId) { + fn preprocess(&mut self, resolve: &Resolve, world: WorldId) -> Result<()> { self.world = self .opts .rename_world @@ -204,6 +204,7 @@ impl WorldGenerator for C { } } } + Ok(()) } fn import_interface( diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index b85754160..ee5a63b30 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -28,7 +28,7 @@ pub trait WorldGenerator { resolve.generate_nominal_type_ids(id); } let world = &resolve.worlds[id]; - self.preprocess(resolve, id); + self.preprocess(resolve, id)?; fn unwrap_name(key: &WorldKey) -> &str { match key { @@ -95,8 +95,9 @@ pub trait WorldGenerator { let _ = (resolve, world, files); } - fn preprocess(&mut self, resolve: &Resolve, world: WorldId) { + fn preprocess(&mut self, resolve: &Resolve, world: WorldId) -> Result<()> { let _ = (resolve, world); + Ok(()) } fn import_interface( diff --git a/crates/cpp/src/lib.rs b/crates/cpp/src/lib.rs index e672feb10..52e13ccab 100644 --- a/crates/cpp/src/lib.rs +++ b/crates/cpp/src/lib.rs @@ -487,7 +487,7 @@ struct FileContext { } impl WorldGenerator for Cpp { - fn preprocess(&mut self, resolve: &Resolve, world: WorldId) { + fn preprocess(&mut self, resolve: &Resolve, world: WorldId) -> anyhow::Result<()> { let name = &resolve.worlds[world].name; self.world = name.to_string(); self.types.analyze(resolve); @@ -511,6 +511,7 @@ impl WorldGenerator for Cpp { "#, self.world.to_snake_case(), ); + Ok(()) } fn import_interface( diff --git a/crates/csharp/src/world_generator.rs b/crates/csharp/src/world_generator.rs index fb7d2450b..dcfb6b9bc 100644 --- a/crates/csharp/src/world_generator.rs +++ b/crates/csharp/src/world_generator.rs @@ -144,7 +144,7 @@ impl CSharp { } impl WorldGenerator for CSharp { - fn preprocess(&mut self, resolve: &Resolve, world: WorldId) { + fn preprocess(&mut self, resolve: &Resolve, world: WorldId) -> anyhow::Result<()> { let name = &resolve.worlds[world].name; self.types.analyze(resolve); self.types.collect_equal_types(resolve, world, &|a| { @@ -171,6 +171,7 @@ impl WorldGenerator for CSharp { }); self.name = name.to_string(); self.sizes.fill(resolve); + Ok(()) } fn import_interface( diff --git a/crates/go/src/lib.rs b/crates/go/src/lib.rs index d72b2ae9d..a6a99d4ba 100644 --- a/crates/go/src/lib.rs +++ b/crates/go/src/lib.rs @@ -724,10 +724,11 @@ impl WorldGenerator for Go { false } - fn preprocess(&mut self, resolve: &Resolve, world: WorldId) { + fn preprocess(&mut self, resolve: &Resolve, world: WorldId) -> Result<()> { _ = world; self.sizes.fill(resolve); self.imports.insert(remote_pkg("runtime")); + Ok(()) } fn import_interface( diff --git a/crates/markdown/src/lib.rs b/crates/markdown/src/lib.rs index a1db52cf2..706023c95 100644 --- a/crates/markdown/src/lib.rs +++ b/crates/markdown/src/lib.rs @@ -37,7 +37,7 @@ impl Opts { } impl WorldGenerator for Markdown { - fn preprocess(&mut self, resolve: &Resolve, world: WorldId) { + fn preprocess(&mut self, resolve: &Resolve, world: WorldId) -> Result<()> { self.sizes.fill(resolve); let world = &resolve.worlds[world]; @@ -109,6 +109,7 @@ impl WorldGenerator for Markdown { } } r#gen.push_str("\n"); + Ok(()) } fn import_interface( diff --git a/crates/moonbit/src/lib.rs b/crates/moonbit/src/lib.rs index 4aa29852f..7939d6146 100644 --- a/crates/moonbit/src/lib.rs +++ b/crates/moonbit/src/lib.rs @@ -244,7 +244,7 @@ impl MoonBit { /// - Inline FFI helpers and builtins are collected and written once into the /// final export FFI module. Async helpers are emitted when required. impl WorldGenerator for MoonBit { - fn preprocess(&mut self, resolve: &Resolve, world: WorldId) { + fn preprocess(&mut self, resolve: &Resolve, world: WorldId) -> Result<()> { self.pkg_resolver.resolve = resolve.clone(); self.project_name = self .opts @@ -256,6 +256,7 @@ impl WorldGenerator for MoonBit { })) .unwrap_or("generated".into()); self.sizes.fill(resolve); + Ok(()) } fn import_interface( diff --git a/crates/rust/src/lib.rs b/crates/rust/src/lib.rs index 7241f2d09..e7f4a36ed 100644 --- a/crates/rust/src/lib.rs +++ b/crates/rust/src/lib.rs @@ -1069,7 +1069,7 @@ macro_rules! __export_{world_name}_impl {{ } impl WorldGenerator for RustWasm { - fn preprocess(&mut self, resolve: &Resolve, world: WorldId) { + fn preprocess(&mut self, resolve: &Resolve, world: WorldId) -> Result<()> { wit_bindgen_core::generated_preamble(&mut self.src_preamble, env!("CARGO_PKG_VERSION")); // Render some generator options to assist with debugging and/or to help @@ -1239,14 +1239,15 @@ impl WorldGenerator for RustWasm { self.with.generate_by_default = self.opts.generate_all; for (key, item) in world.imports.iter() { if let WorldItem::Interface { id, .. } = item { - self.name_interface(resolve, *id, &key, false).unwrap(); + self.name_interface(resolve, *id, &key, false)?; } } for (key, item) in world.exports.iter() { if let WorldItem::Interface { id, .. } = item { - self.name_interface(resolve, *id, &key, true).unwrap(); + self.name_interface(resolve, *id, &key, true)?; } } + Ok(()) } fn import_interface( From 2943424b605bec47acf2e229f28e28fce1e8cdea Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Tue, 7 Jul 2026 11:32:16 -0500 Subject: [PATCH 07/23] Move all async tests to general folder (#1651) * Move all async tests to general folder WASIp3 and component-model-async have shipped and are stable, so these are no longer expected to fail. * Try to fix CI failure * Try to fix Go in CI * Fix codegen test expectation * Another round of attempting ignores * Disable broken tests I'm tired of trying to work around them, just disable them. * Tweak CI configuration --- .github/actions/install-wasi-sdk/action.yml | 4 +- .github/workflows/main.yml | 37 +++---- Cargo.lock | 4 +- crates/test/Cargo.toml | 2 +- crates/test/src/c.rs | 1 + crates/test/src/cpp.rs | 1 + crates/test/src/csharp.rs | 1 + crates/test/src/custom.rs | 1 + crates/test/src/go.rs | 98 +++++++++++++++++-- crates/test/src/lib.rs | 36 ++++++- crates/test/src/moonbit.rs | 1 + crates/test/src/rust.rs | 1 + crates/test/src/wat.rs | 1 + .../async => runtime}/cancel-import/runner.c | 0 .../async => runtime}/cancel-import/runner.rs | 0 .../async => runtime}/cancel-import/test.c | 0 .../async => runtime}/cancel-import/test.rs | 0 .../async => runtime}/cancel-import/test.wit | 1 + .../future-cancel-read/disabled}/runner.cs | 0 .../future-cancel-read/disabled}/test.cs | 0 .../future-cancel-read/runner.c | 0 .../future-cancel-read/runner.rs | 0 .../future-cancel-read/test.c | 0 .../future-cancel-read/test.mbt | 0 .../future-cancel-read/test.rs | 0 .../future-cancel-read/test.wit | 1 + .../future-cancel-write-then-read/runner.rs | 0 .../future-cancel-write-then-read/test.rs | 0 .../future-cancel-write-then-read/test.wit | 1 + .../future-cancel-write/runner.c | 0 .../future-cancel-write/runner.rs | 0 .../future-cancel-write/test.c | 0 .../future-cancel-write/test.mbt | 0 .../future-cancel-write/test.rs | 0 .../future-cancel-write/test.wit | 1 + .../future-close-after-coming-back/runner.rs | 0 .../future-close-after-coming-back/test.mbt | 0 .../future-close-after-coming-back/test.rs | 0 .../future-close-after-coming-back}/test.wit | 1 + .../future-close-then-receive-read/runner.rs | 0 .../future-close-then-receive-read/test.rs | 0 .../future-close-then-receive-read/test.wit | 1 + .../future-closes-with-error/runner.rs | 0 .../future-closes-with-error/test.rs | 0 .../future-closes-with-error/test.wit | 1 + .../runner.rs | 0 .../future-write-then-read-comes-back/test.rs | 0 .../test.wit | 1 + .../future-write-then-read-remote/runner.rs | 0 .../future-write-then-read-remote/runner2.rs | 0 .../future-write-then-read-remote/test.rs | 0 .../future-write-then-read-remote/test.wit | 1 + .../incomplete-writes/leaf.go | 0 .../incomplete-writes/runner.go | 0 .../incomplete-writes/test.go | 0 .../incomplete-writes/test.wit | 1 + .../async => runtime}/pending-import/runner.c | 0 .../pending-import/runner.cs | 0 .../pending-import/runner.rs | 0 .../async => runtime}/pending-import/test.c | 0 .../async => runtime}/pending-import/test.cs | 0 .../async => runtime}/pending-import/test.rs | 0 .../async => runtime}/pending-import/test.wit | 1 + .../ping-pong/disabled}/runner.cs | 0 .../ping-pong/disabled}/test.cs | 0 .../async => runtime}/ping-pong/runner.c | 0 .../async => runtime}/ping-pong/runner.go | 0 .../async => runtime}/ping-pong/runner.rs | 0 .../async => runtime}/ping-pong/test.c | 0 .../async => runtime}/ping-pong/test.go | 0 .../async => runtime}/ping-pong/test.rs | 0 .../async => runtime}/ping-pong/test.wit | 1 + .../async => runtime}/return-string/runner.go | 0 .../async => runtime}/return-string/test.go | 0 .../async => runtime}/return-string/test.wit | 1 + .../rust-async-and-block-on/runner.rs | 0 .../rust-async-and-block-on/test.rs | 0 .../rust-async-and-block-on/test.wit | 1 + .../rust-cross-task-wakeup/runner.rs | 0 .../rust-cross-task-wakeup/test.rs | 0 .../rust-cross-task-wakeup/test.wit | 1 + .../rust-lowered-send/runner.rs | 0 .../rust-lowered-send/test.rs | 0 .../rust-lowered-send/test.wit | 1 + .../simple-call-import/runner.c | 0 .../simple-call-import/runner.go | 0 .../simple-call-import/runner.rs | 0 .../simple-call-import/test.c | 0 .../simple-call-import/test.go | 0 .../simple-call-import/test.rs | 0 .../simple-call-import}/test.wit | 1 + .../simple-future/runner-nostd.rs | 0 .../async => runtime}/simple-future/runner.c | 0 .../async => runtime}/simple-future/runner.cs | 0 .../async => runtime}/simple-future/runner.go | 0 .../async => runtime}/simple-future/runner.rs | 0 .../async => runtime}/simple-future/test.c | 0 .../async => runtime}/simple-future/test.cs | 0 .../async => runtime}/simple-future/test.go | 0 .../async => runtime}/simple-future/test.mbt | 0 .../async => runtime}/simple-future/test.rs | 0 .../async => runtime}/simple-future/test.wit | 1 + .../simple-import-params-results/runner.c | 0 .../simple-import-params-results/runner.cs | 0 .../simple-import-params-results/runner.go | 0 .../simple-import-params-results/runner.rs | 0 .../simple-import-params-results/test.c | 0 .../simple-import-params-results/test.cs | 0 .../simple-import-params-results/test.go | 0 .../simple-import-params-results/test.mbt | 0 .../simple-import-params-results/test.rs | 0 .../simple-import-params-results/test.wit | 1 + .../simple-pending-import/runner.c | 0 .../simple-pending-import/runner.go | 0 .../simple-pending-import/runner.rs | 0 .../simple-pending-import/test.c | 0 .../simple-pending-import/test.go | 0 .../simple-pending-import/test.rs | 0 .../simple-pending-import/test.wit | 1 + .../simple-stream-payload/runner.c | 0 .../simple-stream-payload/runner.cs | 0 .../simple-stream-payload/runner.go | 0 .../simple-stream-payload/runner.rs | 0 .../simple-stream-payload/test.c | 0 .../simple-stream-payload/test.cs | 0 .../simple-stream-payload/test.go | 0 .../simple-stream-payload/test.mbt | 0 .../simple-stream-payload/test.rs | 0 .../simple-stream-payload/test.wit | 1 + .../simple-stream/runner-nostd.rs | 0 .../async => runtime}/simple-stream/runner.c | 0 .../async => runtime}/simple-stream/runner.go | 0 .../async => runtime}/simple-stream/runner.rs | 0 .../async => runtime}/simple-stream/test.c | 0 .../async => runtime}/simple-stream/test.go | 0 .../async => runtime}/simple-stream/test.mbt | 0 .../async => runtime}/simple-stream/test.wit | 1 + .../simple-yield/runner-nostd.rs | 0 .../async => runtime}/simple-yield/runner.c | 0 .../async => runtime}/simple-yield/runner.rs | 0 .../async => runtime}/simple-yield/test.c | 0 .../async => runtime}/simple-yield/test.rs | 0 .../simple-yield}/test.wit | 1 + .../stream-to-futures-stream/runner.rs | 0 .../stream-to-futures-stream/test.rs | 0 .../stream-to-futures-stream/test.wit | 1 + .../runtime/strings/{ => disabled}/runner.go | 0 tests/runtime/strings/{ => disabled}/test.go | 0 .../yield-loop-receives-events/leaf.rs | 0 .../yield-loop-receives-events/middle.rs | 0 .../yield-loop-receives-events/runner.rs | 0 .../yield-loop-receives-events/test.wit | 1 + .../threading-builtins/runner.c | 0 .../threading-builtins/test.c | 0 .../threading-builtins}/test.wit | 0 155 files changed, 175 insertions(+), 38 deletions(-) rename tests/{runtime-async/async => runtime}/cancel-import/runner.c (100%) rename tests/{runtime-async/async => runtime}/cancel-import/runner.rs (100%) rename tests/{runtime-async/async => runtime}/cancel-import/test.c (100%) rename tests/{runtime-async/async => runtime}/cancel-import/test.rs (100%) rename tests/{runtime-async/async => runtime}/cancel-import/test.wit (92%) rename tests/{runtime-async/async/future-cancel-read => runtime/future-cancel-read/disabled}/runner.cs (100%) rename tests/{runtime-async/async/future-cancel-read => runtime/future-cancel-read/disabled}/test.cs (100%) rename tests/{runtime-async/async => runtime}/future-cancel-read/runner.c (100%) rename tests/{runtime-async/async => runtime}/future-cancel-read/runner.rs (100%) rename tests/{runtime-async/async => runtime}/future-cancel-read/test.c (100%) rename tests/{runtime-async/async => runtime}/future-cancel-read/test.mbt (100%) rename tests/{runtime-async/async => runtime}/future-cancel-read/test.rs (100%) rename tests/{runtime-async/async => runtime}/future-cancel-read/test.wit (94%) rename tests/{runtime-async/async => runtime}/future-cancel-write-then-read/runner.rs (100%) rename tests/{runtime-async/async => runtime}/future-cancel-write-then-read/test.rs (100%) rename tests/{runtime-async/async => runtime}/future-cancel-write-then-read/test.wit (91%) rename tests/{runtime-async/async => runtime}/future-cancel-write/runner.c (100%) rename tests/{runtime-async/async => runtime}/future-cancel-write/runner.rs (100%) rename tests/{runtime-async/async => runtime}/future-cancel-write/test.c (100%) rename tests/{runtime-async/async => runtime}/future-cancel-write/test.mbt (100%) rename tests/{runtime-async/async => runtime}/future-cancel-write/test.rs (100%) rename tests/{runtime-async/async => runtime}/future-cancel-write/test.wit (92%) rename tests/{runtime-async/async => runtime}/future-close-after-coming-back/runner.rs (100%) rename tests/{runtime-async/async => runtime}/future-close-after-coming-back/test.mbt (100%) rename tests/{runtime-async/async => runtime}/future-close-after-coming-back/test.rs (100%) rename tests/{runtime-async/async/future-write-then-read-comes-back => runtime/future-close-after-coming-back}/test.wit (91%) rename tests/{runtime-async/async => runtime}/future-close-then-receive-read/runner.rs (100%) rename tests/{runtime-async/async => runtime}/future-close-then-receive-read/test.rs (100%) rename tests/{runtime-async/async => runtime}/future-close-then-receive-read/test.wit (91%) rename tests/{runtime-async/async => runtime}/future-closes-with-error/runner.rs (100%) rename tests/{runtime-async/async => runtime}/future-closes-with-error/test.rs (100%) rename tests/{runtime-async/async => runtime}/future-closes-with-error/test.wit (90%) rename tests/{runtime-async/async => runtime}/future-write-then-read-comes-back/runner.rs (100%) rename tests/{runtime-async/async => runtime}/future-write-then-read-comes-back/test.rs (100%) rename tests/{runtime-async/async/future-close-after-coming-back => runtime/future-write-then-read-comes-back}/test.wit (91%) rename tests/{runtime-async/async => runtime}/future-write-then-read-remote/runner.rs (100%) rename tests/{runtime-async/async => runtime}/future-write-then-read-remote/runner2.rs (100%) rename tests/{runtime-async/async => runtime}/future-write-then-read-remote/test.rs (100%) rename tests/{runtime-async/async => runtime}/future-write-then-read-remote/test.wit (90%) rename tests/{runtime-async/async => runtime}/incomplete-writes/leaf.go (100%) rename tests/{runtime-async/async => runtime}/incomplete-writes/runner.go (100%) rename tests/{runtime-async/async => runtime}/incomplete-writes/test.go (100%) rename tests/{runtime-async/async => runtime}/incomplete-writes/test.wit (98%) rename tests/{runtime-async/async => runtime}/pending-import/runner.c (100%) rename tests/{runtime-async/async => runtime}/pending-import/runner.cs (100%) rename tests/{runtime-async/async => runtime}/pending-import/runner.rs (100%) rename tests/{runtime-async/async => runtime}/pending-import/test.c (100%) rename tests/{runtime-async/async => runtime}/pending-import/test.cs (100%) rename tests/{runtime-async/async => runtime}/pending-import/test.rs (100%) rename tests/{runtime-async/async => runtime}/pending-import/test.wit (90%) rename tests/{runtime-async/async/ping-pong => runtime/ping-pong/disabled}/runner.cs (100%) rename tests/{runtime-async/async/ping-pong => runtime/ping-pong/disabled}/test.cs (100%) rename tests/{runtime-async/async => runtime}/ping-pong/runner.c (100%) rename tests/{runtime-async/async => runtime}/ping-pong/runner.go (100%) rename tests/{runtime-async/async => runtime}/ping-pong/runner.rs (100%) rename tests/{runtime-async/async => runtime}/ping-pong/test.c (100%) rename tests/{runtime-async/async => runtime}/ping-pong/test.go (100%) rename tests/{runtime-async/async => runtime}/ping-pong/test.rs (100%) rename tests/{runtime-async/async => runtime}/ping-pong/test.wit (93%) rename tests/{runtime-async/async => runtime}/return-string/runner.go (100%) rename tests/{runtime-async/async => runtime}/return-string/test.go (100%) rename tests/{runtime-async/async => runtime}/return-string/test.wit (90%) rename tests/{runtime-async/async => runtime}/rust-async-and-block-on/runner.rs (100%) rename tests/{runtime-async/async => runtime}/rust-async-and-block-on/test.rs (100%) rename tests/{runtime-async/async => runtime}/rust-async-and-block-on/test.wit (90%) rename tests/{runtime-async/async => runtime}/rust-cross-task-wakeup/runner.rs (100%) rename tests/{runtime-async/async => runtime}/rust-cross-task-wakeup/test.rs (100%) rename tests/{runtime-async/async => runtime}/rust-cross-task-wakeup/test.wit (91%) rename tests/{runtime-async/async => runtime}/rust-lowered-send/runner.rs (100%) rename tests/{runtime-async/async => runtime}/rust-lowered-send/test.rs (100%) rename tests/{runtime-async/async => runtime}/rust-lowered-send/test.wit (90%) rename tests/{runtime-async/async => runtime}/simple-call-import/runner.c (100%) rename tests/{runtime-async/async => runtime}/simple-call-import/runner.go (100%) rename tests/{runtime-async/async => runtime}/simple-call-import/runner.rs (100%) rename tests/{runtime-async/async => runtime}/simple-call-import/test.c (100%) rename tests/{runtime-async/async => runtime}/simple-call-import/test.go (100%) rename tests/{runtime-async/async => runtime}/simple-call-import/test.rs (100%) rename tests/{runtime-async/async/simple-yield => runtime/simple-call-import}/test.wit (88%) rename tests/{runtime-async/async => runtime}/simple-future/runner-nostd.rs (100%) rename tests/{runtime-async/async => runtime}/simple-future/runner.c (100%) rename tests/{runtime-async/async => runtime}/simple-future/runner.cs (100%) rename tests/{runtime-async/async => runtime}/simple-future/runner.go (100%) rename tests/{runtime-async/async => runtime}/simple-future/runner.rs (100%) rename tests/{runtime-async/async => runtime}/simple-future/test.c (100%) rename tests/{runtime-async/async => runtime}/simple-future/test.cs (100%) rename tests/{runtime-async/async => runtime}/simple-future/test.go (100%) rename tests/{runtime-async/async => runtime}/simple-future/test.mbt (100%) rename tests/{runtime-async/async => runtime}/simple-future/test.rs (100%) rename tests/{runtime-async/async => runtime}/simple-future/test.wit (92%) rename tests/{runtime-async/async => runtime}/simple-import-params-results/runner.c (100%) rename tests/{runtime-async/async => runtime}/simple-import-params-results/runner.cs (100%) rename tests/{runtime-async/async => runtime}/simple-import-params-results/runner.go (100%) rename tests/{runtime-async/async => runtime}/simple-import-params-results/runner.rs (100%) rename tests/{runtime-async/async => runtime}/simple-import-params-results/test.c (100%) rename tests/{runtime-async/async => runtime}/simple-import-params-results/test.cs (100%) rename tests/{runtime-async/async => runtime}/simple-import-params-results/test.go (100%) rename tests/{runtime-async/async => runtime}/simple-import-params-results/test.mbt (100%) rename tests/{runtime-async/async => runtime}/simple-import-params-results/test.rs (100%) rename tests/{runtime-async/async => runtime}/simple-import-params-results/test.wit (95%) rename tests/{runtime-async/async => runtime}/simple-pending-import/runner.c (100%) rename tests/{runtime-async/async => runtime}/simple-pending-import/runner.go (100%) rename tests/{runtime-async/async => runtime}/simple-pending-import/runner.rs (100%) rename tests/{runtime-async/async => runtime}/simple-pending-import/test.c (100%) rename tests/{runtime-async/async => runtime}/simple-pending-import/test.go (100%) rename tests/{runtime-async/async => runtime}/simple-pending-import/test.rs (100%) rename tests/{runtime-async/async => runtime}/simple-pending-import/test.wit (88%) rename tests/{runtime-async/async => runtime}/simple-stream-payload/runner.c (100%) rename tests/{runtime-async/async => runtime}/simple-stream-payload/runner.cs (100%) rename tests/{runtime-async/async => runtime}/simple-stream-payload/runner.go (100%) rename tests/{runtime-async/async => runtime}/simple-stream-payload/runner.rs (100%) rename tests/{runtime-async/async => runtime}/simple-stream-payload/test.c (100%) rename tests/{runtime-async/async => runtime}/simple-stream-payload/test.cs (100%) rename tests/{runtime-async/async => runtime}/simple-stream-payload/test.go (100%) rename tests/{runtime-async/async => runtime}/simple-stream-payload/test.mbt (100%) rename tests/{runtime-async/async => runtime}/simple-stream-payload/test.rs (100%) rename tests/{runtime-async/async => runtime}/simple-stream-payload/test.wit (90%) rename tests/{runtime-async/async => runtime}/simple-stream/runner-nostd.rs (100%) rename tests/{runtime-async/async => runtime}/simple-stream/runner.c (100%) rename tests/{runtime-async/async => runtime}/simple-stream/runner.go (100%) rename tests/{runtime-async/async => runtime}/simple-stream/runner.rs (100%) rename tests/{runtime-async/async => runtime}/simple-stream/test.c (100%) rename tests/{runtime-async/async => runtime}/simple-stream/test.go (100%) rename tests/{runtime-async/async => runtime}/simple-stream/test.mbt (100%) rename tests/{runtime-async/async => runtime}/simple-stream/test.wit (90%) rename tests/{runtime-async/async => runtime}/simple-yield/runner-nostd.rs (100%) rename tests/{runtime-async/async => runtime}/simple-yield/runner.c (100%) rename tests/{runtime-async/async => runtime}/simple-yield/runner.rs (100%) rename tests/{runtime-async/async => runtime}/simple-yield/test.c (100%) rename tests/{runtime-async/async => runtime}/simple-yield/test.rs (100%) rename tests/{runtime-async/async/threading-builtins => runtime/simple-yield}/test.wit (88%) rename tests/{runtime-async/async => runtime}/stream-to-futures-stream/runner.rs (100%) rename tests/{runtime-async/async => runtime}/stream-to-futures-stream/test.rs (100%) rename tests/{runtime-async/async => runtime}/stream-to-futures-stream/test.wit (90%) rename tests/runtime/strings/{ => disabled}/runner.go (100%) rename tests/runtime/strings/{ => disabled}/test.go (100%) rename tests/{runtime-async/async => runtime}/yield-loop-receives-events/leaf.rs (100%) rename tests/{runtime-async/async => runtime}/yield-loop-receives-events/middle.rs (100%) rename tests/{runtime-async/async => runtime}/yield-loop-receives-events/runner.rs (100%) rename tests/{runtime-async/async => runtime}/yield-loop-receives-events/test.wit (94%) rename tests/{runtime-async/async => threading}/threading-builtins/runner.c (100%) rename tests/{runtime-async/async => threading}/threading-builtins/test.c (100%) rename tests/{runtime-async/async/simple-call-import => threading/threading-builtins}/test.wit (100%) diff --git a/.github/actions/install-wasi-sdk/action.yml b/.github/actions/install-wasi-sdk/action.yml index 169f68c93..5b0798a59 100644 --- a/.github/actions/install-wasi-sdk/action.yml +++ b/.github/actions/install-wasi-sdk/action.yml @@ -34,8 +34,8 @@ runs: - name: Setup `wasm-tools` uses: bytecodealliance/actions/wasm-tools/setup@v1 with: - version: "1.247.0" + version: "1.252.0" - name: Setup `wasmtime` uses: bytecodealliance/actions/wasmtime/setup@v1 with: - version: "44.0.0" + version: "46.0.1" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c22eee340..3a86dcb49 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -104,11 +104,22 @@ jobs: dotnet-version: '9.x' if: matrix.lang == 'csharp' + # As of this writing async tests require [a patched build of + # Go](https://github.com/dicej/go/releases/tag/go1.25.5-wasi-on-idle). + # Install this on Linux to get coverage, but don't install it on + # macOS/Windows to also get coverage for test-without-a-patched-toolchain. + - name: Install Patched Go + run: | + curl -OL https://github.com/dicej/go/releases/download/go1.25.5-wasi-on-idle/go-linux-amd64-bootstrap.tbz + tar xf go-linux-amd64-bootstrap.tbz + echo "$(pwd)/go-linux-amd64-bootstrap/bin" >> $GITHUB_PATH + if: matrix.lang == 'go' && matrix.os == 'ubuntu-latest' + - name: Setup Go uses: actions/setup-go@v5 with: go-version: 1.25.4 - if: matrix.lang == 'go' + if: matrix.lang == 'go' && matrix.os != 'ubuntu-latest' # Hacky work-around for https://github.com/dotnet/runtime/issues/80619 - run: dotnet new console -o /tmp/foo @@ -130,17 +141,17 @@ jobs: --artifacts target/artifacts \ --rust-wit-bindgen-path ./crates/guest-rust - # Run all runtime tests for this language, and also enable Rust in case this - # language only implements either the runner or test component + # Run all runtime tests for this language, and also enable Rust & C in case + # this language only implements either the runner or test component - run: | - cargo run test --languages rust,${{ matrix.lang }} tests/runtime \ + cargo run test --languages rust,c,${{ matrix.lang }} tests/runtime \ --artifacts target/artifacts \ --rust-wit-bindgen-path ./crates/guest-rust - # While async is off-by-default and toolchains are percolating this is a + # While threading is off-by-default and toolchains are percolating this is a # separate job to get configured slightly differently. - async: - name: Test Async (allowed to fail) + threading: + name: Test WASIp3 Threading (allowed to fail) runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -151,18 +162,9 @@ jobs: run: rustup update stable --no-self-update && rustup default stable - run: rustup target add wasm32-wasip2 - # As of this writing, we need [a patched build of - # Go](https://github.com/dicej/go/releases/tag/go1.25.5-wasi-on-idle) to - # support async. - - name: Install Patched Go - run: | - curl -OL https://github.com/dicej/go/releases/download/go1.25.5-wasi-on-idle/go-linux-amd64-bootstrap.tbz - tar xf go-linux-amd64-bootstrap.tbz - echo "$(pwd)/go-linux-amd64-bootstrap/bin" >> $GITHUB_PATH - - uses: ./.github/actions/install-wasi-sdk - run: | - cargo run test --languages rust,c,go tests/runtime-async \ + cargo run test --languages rust,c,go tests/threading \ --artifacts target/artifacts \ --rust-wit-bindgen-path ./crates/guest-rust @@ -287,7 +289,6 @@ jobs: - verify-publish - check - msrv - # - async if: always() steps: diff --git a/Cargo.lock b/Cargo.lock index 2e6530677..63a642d15 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1215,9 +1215,9 @@ dependencies = [ [[package]] name = "wasi-preview1-component-adapter-provider" -version = "45.0.1" +version = "46.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02ed52111afc004605074d1a3fad77ba3ac85c0c0b8ad028919275a46fec8cfd" +checksum = "1b6c48003fe59c201c97a7786ff55feabe6b6f83b598aa9ff5bcc4f94d940bf3" [[package]] name = "wasm-compose" diff --git a/crates/test/Cargo.toml b/crates/test/Cargo.toml index 4100f71a5..14c80e0d9 100644 --- a/crates/test/Cargo.toml +++ b/crates/test/Cargo.toml @@ -26,7 +26,7 @@ log = "0.4.26" regex = "1.11.1" serde = { workspace = true } toml = "1.1.2" -wasi-preview1-component-adapter-provider = "45.0.1" +wasi-preview1-component-adapter-provider = "46.0.1" wac-parser = "0.10.0" wac-types = "0.10.0" wac-graph = "0.10.0" diff --git a/crates/test/src/c.rs b/crates/test/src/c.rs index a34f2fc55..a04dbf967 100644 --- a/crates/test/src/c.rs +++ b/crates/test/src/c.rs @@ -52,6 +52,7 @@ impl LanguageMethods for C { fn should_fail_verify( &self, + _runner: &Runner, name: &str, config: &crate::config::WitConfig, _args: &[String], diff --git a/crates/test/src/cpp.rs b/crates/test/src/cpp.rs index bf49ad3a8..d33cefa11 100644 --- a/crates/test/src/cpp.rs +++ b/crates/test/src/cpp.rs @@ -41,6 +41,7 @@ impl LanguageMethods for Cpp { fn should_fail_verify( &self, + _runner: &Runner, name: &str, config: &crate::config::WitConfig, _args: &[String], diff --git a/crates/test/src/csharp.rs b/crates/test/src/csharp.rs index 8e11ca7f7..6327ca029 100644 --- a/crates/test/src/csharp.rs +++ b/crates/test/src/csharp.rs @@ -36,6 +36,7 @@ impl LanguageMethods for Csharp { fn should_fail_verify( &self, + _runner: &Runner, name: &str, _config: &crate::config::WitConfig, _args: &[String], diff --git a/crates/test/src/custom.rs b/crates/test/src/custom.rs index 402fe65f4..8a56032d9 100644 --- a/crates/test/src/custom.rs +++ b/crates/test/src/custom.rs @@ -101,6 +101,7 @@ impl LanguageMethods for Language { fn should_fail_verify( &self, + _runner: &Runner, _name: &str, _config: &crate::config::WitConfig, _args: &[String], diff --git a/crates/test/src/go.rs b/crates/test/src/go.rs index afe651951..7655b26df 100644 --- a/crates/test/src/go.rs +++ b/crates/test/src/go.rs @@ -7,6 +7,22 @@ use std::process::Command; pub struct Go; +/// Go-specific state, stored in `Runner`, detected during `prepare`. +pub struct State { + /// Whether the Go toolchain in use supports the `runtime.wasiOnIdle` hook + /// required for component model async support. + async_supported: bool, +} + +impl Runner { + fn go_async_supported(&self) -> bool { + self.go_state + .as_ref() + .map(|s| s.async_supported) + .unwrap_or(false) + } +} + impl LanguageMethods for Go { fn display(&self) -> &str { "go" @@ -18,19 +34,43 @@ impl LanguageMethods for Go { fn should_fail_verify( &self, + runner: &Runner, name: &str, config: &crate::config::WitConfig, _args: &[String], ) -> bool { - // TODO: We _do_ support async, but only with a build of Go that has - // [this - // patch](https://github.com/dicej/go/commit/40fc123d5bce6448fc4e4601fd33bad4250b36a5). - // Once we upstream something equivalent, we can remove the ` || name == - // "async-trait-function.wit"` here. - config.error_context - || name == "async-trait-function.wit" - || name == "named-fixed-length-list.wit" - || name == "issue-1598.wit" + if config.error_context { + return true; + } + if name == "named-fixed-length-list.wit" { + return true; + } + if !runner.go_async_supported() { + return name == "async-trait-function.wit" || name == "issue-1598.wit"; + } + + false + } + + fn should_fail_compile( + &self, + runner: &Runner, + path: &Path, + config: &crate::config::WitConfig, + ) -> bool { + // This test, even though it's part of async, compiles on any + // toolchain. + if path.ends_with("incomplete-writes/leaf.go") { + return false; + } + + // Bindings for async tests rely on `runtime.wasiOnIdle` (see `prepare` + // below) and fail to link without a toolchain that provides it. + if !runner.go_async_supported() { + return config.async_; + } + + false } fn default_bindgen_args_for_codegen(&self) -> &[&str] { @@ -56,7 +96,45 @@ impl LanguageMethods for Go { .arg("build") .arg("-buildmode=c-shared") .arg("-ldflags=-checklinkname=0"), - ) + )?; + + // Component model async support requires a `runtime.wasiOnIdle` hook + // which, as of the time of this writing, is only available in a + // patched build of Go. Detect whether the toolchain in use has + // this hook by building a program that links against it, and if not + // then async tests are expected to fail to build. + println!("Testing if `go` supports `runtime.wasiOnIdle`..."); + let probe_dir = dir.join("wasi-on-idle-probe"); + super::write_if_different( + &probe_dir.join("main.go"), + r#"package main + +import _ "unsafe" + +//go:linkname wasiOnIdle runtime.wasiOnIdle +func wasiOnIdle(callback func() bool) +func init() { defer wasiOnIdle(func() bool { return false }) } +func main() {} +"#, + )?; + super::write_if_different(&probe_dir.join("go.mod"), "module probe\n\ngo 1.25")?; + let async_supported = runner + .run_command( + Command::new("go") + .current_dir(&probe_dir) + .env("GOOS", "wasip1") + .env("GOARCH", "wasm") + .arg("build") + .arg("-o") + .arg("probe.wasm") + .arg("-buildmode=c-shared") + .arg("-ldflags=-checklinkname=0"), + ) + .is_ok(); + println!("`runtime.wasiOnIdle` supported: {async_supported}"); + runner.go_state = Some(State { async_supported }); + + Ok(()) } fn compile(&self, runner: &Runner, compile: &Compile<'_>) -> Result<()> { diff --git a/crates/test/src/lib.rs b/crates/test/src/lib.rs index 615fb9ce0..7a54f4612 100644 --- a/crates/test/src/lib.rs +++ b/crates/test/src/lib.rs @@ -137,6 +137,7 @@ impl Opts { Runner { opts: self.clone(), rust_state: None, + go_state: None, wit_bindgen: wit_bindgen.to_path_buf(), test_runner: runner::TestRunner::new(&self.runner)?, } @@ -254,6 +255,7 @@ struct Verify<'a> { struct Runner { opts: Opts, rust_state: Option, + go_state: Option, wit_bindgen: PathBuf, test_runner: runner::TestRunner, } @@ -578,7 +580,7 @@ impl Runner { let me = self.clone(); let should_fail = language .obj() - .should_fail_verify(&args_kind, &config, &args); + .should_fail_verify(self, &args_kind, &config, &args); let name = format!("{language} {args_kind} {test:?}"); Trial::test(&name, move || { @@ -703,17 +705,23 @@ impl Runner { let compilations = compilations.clone(); let test = test.clone(); let component = component.clone(); + let should_fail = component.language.obj().should_fail_compile( + self, + &component.path, + &component.bindgen.wit_config, + ); Trial::test(&component.path.display().to_string(), move || { let result = me.compile_component(&test, &component).with_context(|| { format!("failed to compile component {:?}", component.path) }); match result { - Ok(path) => { + Ok(path) if !should_fail => { compilations.lock().unwrap().push((test, component, path)); Ok(()) } - Err(e) => me.render_error( - StepResult::new(Err(e)) + other => me.render_error( + StepResult::new(other.map(|_| ())) + .should_fail(should_fail) .metadata("component", &component.name) .metadata("path", component.path.display()), ), @@ -1280,7 +1288,25 @@ trait LanguageMethods { /// Returns whether this language is supposed to fail this codegen tests /// given the `config` and `args` for the test. - fn should_fail_verify(&self, name: &str, config: &config::WitConfig, args: &[String]) -> bool; + fn should_fail_verify( + &self, + runner: &Runner, + name: &str, + config: &config::WitConfig, + args: &[String], + ) -> bool; + + /// Returns whether this language is expected to fail to compile the + /// runtime test component described by `config`. + fn should_fail_compile( + &self, + runner: &Runner, + path: &Path, + config: &config::WitConfig, + ) -> bool { + let _ = (runner, path, config); + false + } /// Performs a "check" or a verify that the generated bindings described by /// `Verify` are indeed valid. diff --git a/crates/test/src/moonbit.rs b/crates/test/src/moonbit.rs index a837ae93e..8811314f5 100644 --- a/crates/test/src/moonbit.rs +++ b/crates/test/src/moonbit.rs @@ -120,6 +120,7 @@ impl LanguageMethods for MoonBit { fn should_fail_verify( &self, + _runner: &Runner, name: &str, config: &crate::config::WitConfig, _args: &[String], diff --git a/crates/test/src/rust.rs b/crates/test/src/rust.rs index fe492d60e..53484c29c 100644 --- a/crates/test/src/rust.rs +++ b/crates/test/src/rust.rs @@ -56,6 +56,7 @@ impl LanguageMethods for Rust { fn should_fail_verify( &self, + _runner: &Runner, name: &str, _config: &crate::config::WitConfig, _args: &[String], diff --git a/crates/test/src/wat.rs b/crates/test/src/wat.rs index 1cd59cdce..8278ade8e 100644 --- a/crates/test/src/wat.rs +++ b/crates/test/src/wat.rs @@ -14,6 +14,7 @@ impl LanguageMethods for Wat { fn should_fail_verify( &self, + _runner: &Runner, _name: &str, _config: &crate::config::WitConfig, _args: &[String], diff --git a/tests/runtime-async/async/cancel-import/runner.c b/tests/runtime/cancel-import/runner.c similarity index 100% rename from tests/runtime-async/async/cancel-import/runner.c rename to tests/runtime/cancel-import/runner.c diff --git a/tests/runtime-async/async/cancel-import/runner.rs b/tests/runtime/cancel-import/runner.rs similarity index 100% rename from tests/runtime-async/async/cancel-import/runner.rs rename to tests/runtime/cancel-import/runner.rs diff --git a/tests/runtime-async/async/cancel-import/test.c b/tests/runtime/cancel-import/test.c similarity index 100% rename from tests/runtime-async/async/cancel-import/test.c rename to tests/runtime/cancel-import/test.c diff --git a/tests/runtime-async/async/cancel-import/test.rs b/tests/runtime/cancel-import/test.rs similarity index 100% rename from tests/runtime-async/async/cancel-import/test.rs rename to tests/runtime/cancel-import/test.rs diff --git a/tests/runtime-async/async/cancel-import/test.wit b/tests/runtime/cancel-import/test.wit similarity index 92% rename from tests/runtime-async/async/cancel-import/test.wit rename to tests/runtime/cancel-import/test.wit index 78e57ec13..66a8c365e 100644 --- a/tests/runtime-async/async/cancel-import/test.wit +++ b/tests/runtime/cancel-import/test.wit @@ -1,3 +1,4 @@ +//@ async = true package my:test; interface i { diff --git a/tests/runtime-async/async/future-cancel-read/runner.cs b/tests/runtime/future-cancel-read/disabled/runner.cs similarity index 100% rename from tests/runtime-async/async/future-cancel-read/runner.cs rename to tests/runtime/future-cancel-read/disabled/runner.cs diff --git a/tests/runtime-async/async/future-cancel-read/test.cs b/tests/runtime/future-cancel-read/disabled/test.cs similarity index 100% rename from tests/runtime-async/async/future-cancel-read/test.cs rename to tests/runtime/future-cancel-read/disabled/test.cs diff --git a/tests/runtime-async/async/future-cancel-read/runner.c b/tests/runtime/future-cancel-read/runner.c similarity index 100% rename from tests/runtime-async/async/future-cancel-read/runner.c rename to tests/runtime/future-cancel-read/runner.c diff --git a/tests/runtime-async/async/future-cancel-read/runner.rs b/tests/runtime/future-cancel-read/runner.rs similarity index 100% rename from tests/runtime-async/async/future-cancel-read/runner.rs rename to tests/runtime/future-cancel-read/runner.rs diff --git a/tests/runtime-async/async/future-cancel-read/test.c b/tests/runtime/future-cancel-read/test.c similarity index 100% rename from tests/runtime-async/async/future-cancel-read/test.c rename to tests/runtime/future-cancel-read/test.c diff --git a/tests/runtime-async/async/future-cancel-read/test.mbt b/tests/runtime/future-cancel-read/test.mbt similarity index 100% rename from tests/runtime-async/async/future-cancel-read/test.mbt rename to tests/runtime/future-cancel-read/test.mbt diff --git a/tests/runtime-async/async/future-cancel-read/test.rs b/tests/runtime/future-cancel-read/test.rs similarity index 100% rename from tests/runtime-async/async/future-cancel-read/test.rs rename to tests/runtime/future-cancel-read/test.rs diff --git a/tests/runtime-async/async/future-cancel-read/test.wit b/tests/runtime/future-cancel-read/test.wit similarity index 94% rename from tests/runtime-async/async/future-cancel-read/test.wit rename to tests/runtime/future-cancel-read/test.wit index 52bbcd51c..df8e3e30c 100644 --- a/tests/runtime-async/async/future-cancel-read/test.wit +++ b/tests/runtime/future-cancel-read/test.wit @@ -1,3 +1,4 @@ +//@ async = true package my:test; interface i { diff --git a/tests/runtime-async/async/future-cancel-write-then-read/runner.rs b/tests/runtime/future-cancel-write-then-read/runner.rs similarity index 100% rename from tests/runtime-async/async/future-cancel-write-then-read/runner.rs rename to tests/runtime/future-cancel-write-then-read/runner.rs diff --git a/tests/runtime-async/async/future-cancel-write-then-read/test.rs b/tests/runtime/future-cancel-write-then-read/test.rs similarity index 100% rename from tests/runtime-async/async/future-cancel-write-then-read/test.rs rename to tests/runtime/future-cancel-write-then-read/test.rs diff --git a/tests/runtime-async/async/future-cancel-write-then-read/test.wit b/tests/runtime/future-cancel-write-then-read/test.wit similarity index 91% rename from tests/runtime-async/async/future-cancel-write-then-read/test.wit rename to tests/runtime/future-cancel-write-then-read/test.wit index 1995d936a..78780f2db 100644 --- a/tests/runtime-async/async/future-cancel-write-then-read/test.wit +++ b/tests/runtime/future-cancel-write-then-read/test.wit @@ -1,3 +1,4 @@ +//@ async = true package a:b; interface the-test { diff --git a/tests/runtime-async/async/future-cancel-write/runner.c b/tests/runtime/future-cancel-write/runner.c similarity index 100% rename from tests/runtime-async/async/future-cancel-write/runner.c rename to tests/runtime/future-cancel-write/runner.c diff --git a/tests/runtime-async/async/future-cancel-write/runner.rs b/tests/runtime/future-cancel-write/runner.rs similarity index 100% rename from tests/runtime-async/async/future-cancel-write/runner.rs rename to tests/runtime/future-cancel-write/runner.rs diff --git a/tests/runtime-async/async/future-cancel-write/test.c b/tests/runtime/future-cancel-write/test.c similarity index 100% rename from tests/runtime-async/async/future-cancel-write/test.c rename to tests/runtime/future-cancel-write/test.c diff --git a/tests/runtime-async/async/future-cancel-write/test.mbt b/tests/runtime/future-cancel-write/test.mbt similarity index 100% rename from tests/runtime-async/async/future-cancel-write/test.mbt rename to tests/runtime/future-cancel-write/test.mbt diff --git a/tests/runtime-async/async/future-cancel-write/test.rs b/tests/runtime/future-cancel-write/test.rs similarity index 100% rename from tests/runtime-async/async/future-cancel-write/test.rs rename to tests/runtime/future-cancel-write/test.rs diff --git a/tests/runtime-async/async/future-cancel-write/test.wit b/tests/runtime/future-cancel-write/test.wit similarity index 92% rename from tests/runtime-async/async/future-cancel-write/test.wit rename to tests/runtime/future-cancel-write/test.wit index 3b4dc207a..4d71ca6b9 100644 --- a/tests/runtime-async/async/future-cancel-write/test.wit +++ b/tests/runtime/future-cancel-write/test.wit @@ -1,3 +1,4 @@ +//@ async = true package my:test; interface i { diff --git a/tests/runtime-async/async/future-close-after-coming-back/runner.rs b/tests/runtime/future-close-after-coming-back/runner.rs similarity index 100% rename from tests/runtime-async/async/future-close-after-coming-back/runner.rs rename to tests/runtime/future-close-after-coming-back/runner.rs diff --git a/tests/runtime-async/async/future-close-after-coming-back/test.mbt b/tests/runtime/future-close-after-coming-back/test.mbt similarity index 100% rename from tests/runtime-async/async/future-close-after-coming-back/test.mbt rename to tests/runtime/future-close-after-coming-back/test.mbt diff --git a/tests/runtime-async/async/future-close-after-coming-back/test.rs b/tests/runtime/future-close-after-coming-back/test.rs similarity index 100% rename from tests/runtime-async/async/future-close-after-coming-back/test.rs rename to tests/runtime/future-close-after-coming-back/test.rs diff --git a/tests/runtime-async/async/future-write-then-read-comes-back/test.wit b/tests/runtime/future-close-after-coming-back/test.wit similarity index 91% rename from tests/runtime-async/async/future-write-then-read-comes-back/test.wit rename to tests/runtime/future-close-after-coming-back/test.wit index 12083a297..fb7865ef6 100644 --- a/tests/runtime-async/async/future-write-then-read-comes-back/test.wit +++ b/tests/runtime/future-close-after-coming-back/test.wit @@ -1,3 +1,4 @@ +//@ async = true package a:b; interface the-test { diff --git a/tests/runtime-async/async/future-close-then-receive-read/runner.rs b/tests/runtime/future-close-then-receive-read/runner.rs similarity index 100% rename from tests/runtime-async/async/future-close-then-receive-read/runner.rs rename to tests/runtime/future-close-then-receive-read/runner.rs diff --git a/tests/runtime-async/async/future-close-then-receive-read/test.rs b/tests/runtime/future-close-then-receive-read/test.rs similarity index 100% rename from tests/runtime-async/async/future-close-then-receive-read/test.rs rename to tests/runtime/future-close-then-receive-read/test.rs diff --git a/tests/runtime-async/async/future-close-then-receive-read/test.wit b/tests/runtime/future-close-then-receive-read/test.wit similarity index 91% rename from tests/runtime-async/async/future-close-then-receive-read/test.wit rename to tests/runtime/future-close-then-receive-read/test.wit index cc7f391e1..ec970d290 100644 --- a/tests/runtime-async/async/future-close-then-receive-read/test.wit +++ b/tests/runtime/future-close-then-receive-read/test.wit @@ -1,3 +1,4 @@ +//@ async = true package a:b; interface the-test { diff --git a/tests/runtime-async/async/future-closes-with-error/runner.rs b/tests/runtime/future-closes-with-error/runner.rs similarity index 100% rename from tests/runtime-async/async/future-closes-with-error/runner.rs rename to tests/runtime/future-closes-with-error/runner.rs diff --git a/tests/runtime-async/async/future-closes-with-error/test.rs b/tests/runtime/future-closes-with-error/test.rs similarity index 100% rename from tests/runtime-async/async/future-closes-with-error/test.rs rename to tests/runtime/future-closes-with-error/test.rs diff --git a/tests/runtime-async/async/future-closes-with-error/test.wit b/tests/runtime/future-closes-with-error/test.wit similarity index 90% rename from tests/runtime-async/async/future-closes-with-error/test.wit rename to tests/runtime/future-closes-with-error/test.wit index 58211d988..634fcae53 100644 --- a/tests/runtime-async/async/future-closes-with-error/test.wit +++ b/tests/runtime/future-closes-with-error/test.wit @@ -1,3 +1,4 @@ +//@ async = true package a:b; interface the-test { diff --git a/tests/runtime-async/async/future-write-then-read-comes-back/runner.rs b/tests/runtime/future-write-then-read-comes-back/runner.rs similarity index 100% rename from tests/runtime-async/async/future-write-then-read-comes-back/runner.rs rename to tests/runtime/future-write-then-read-comes-back/runner.rs diff --git a/tests/runtime-async/async/future-write-then-read-comes-back/test.rs b/tests/runtime/future-write-then-read-comes-back/test.rs similarity index 100% rename from tests/runtime-async/async/future-write-then-read-comes-back/test.rs rename to tests/runtime/future-write-then-read-comes-back/test.rs diff --git a/tests/runtime-async/async/future-close-after-coming-back/test.wit b/tests/runtime/future-write-then-read-comes-back/test.wit similarity index 91% rename from tests/runtime-async/async/future-close-after-coming-back/test.wit rename to tests/runtime/future-write-then-read-comes-back/test.wit index 12083a297..fb7865ef6 100644 --- a/tests/runtime-async/async/future-close-after-coming-back/test.wit +++ b/tests/runtime/future-write-then-read-comes-back/test.wit @@ -1,3 +1,4 @@ +//@ async = true package a:b; interface the-test { diff --git a/tests/runtime-async/async/future-write-then-read-remote/runner.rs b/tests/runtime/future-write-then-read-remote/runner.rs similarity index 100% rename from tests/runtime-async/async/future-write-then-read-remote/runner.rs rename to tests/runtime/future-write-then-read-remote/runner.rs diff --git a/tests/runtime-async/async/future-write-then-read-remote/runner2.rs b/tests/runtime/future-write-then-read-remote/runner2.rs similarity index 100% rename from tests/runtime-async/async/future-write-then-read-remote/runner2.rs rename to tests/runtime/future-write-then-read-remote/runner2.rs diff --git a/tests/runtime-async/async/future-write-then-read-remote/test.rs b/tests/runtime/future-write-then-read-remote/test.rs similarity index 100% rename from tests/runtime-async/async/future-write-then-read-remote/test.rs rename to tests/runtime/future-write-then-read-remote/test.rs diff --git a/tests/runtime-async/async/future-write-then-read-remote/test.wit b/tests/runtime/future-write-then-read-remote/test.wit similarity index 90% rename from tests/runtime-async/async/future-write-then-read-remote/test.wit rename to tests/runtime/future-write-then-read-remote/test.wit index 58211d988..634fcae53 100644 --- a/tests/runtime-async/async/future-write-then-read-remote/test.wit +++ b/tests/runtime/future-write-then-read-remote/test.wit @@ -1,3 +1,4 @@ +//@ async = true package a:b; interface the-test { diff --git a/tests/runtime-async/async/incomplete-writes/leaf.go b/tests/runtime/incomplete-writes/leaf.go similarity index 100% rename from tests/runtime-async/async/incomplete-writes/leaf.go rename to tests/runtime/incomplete-writes/leaf.go diff --git a/tests/runtime-async/async/incomplete-writes/runner.go b/tests/runtime/incomplete-writes/runner.go similarity index 100% rename from tests/runtime-async/async/incomplete-writes/runner.go rename to tests/runtime/incomplete-writes/runner.go diff --git a/tests/runtime-async/async/incomplete-writes/test.go b/tests/runtime/incomplete-writes/test.go similarity index 100% rename from tests/runtime-async/async/incomplete-writes/test.go rename to tests/runtime/incomplete-writes/test.go diff --git a/tests/runtime-async/async/incomplete-writes/test.wit b/tests/runtime/incomplete-writes/test.wit similarity index 98% rename from tests/runtime-async/async/incomplete-writes/test.wit rename to tests/runtime/incomplete-writes/test.wit index 6e3604133..6498e3a90 100644 --- a/tests/runtime-async/async/incomplete-writes/test.wit +++ b/tests/runtime/incomplete-writes/test.wit @@ -1,3 +1,4 @@ +//@ async = true //@ dependencies = ['test', 'leaf'] package my:test; diff --git a/tests/runtime-async/async/pending-import/runner.c b/tests/runtime/pending-import/runner.c similarity index 100% rename from tests/runtime-async/async/pending-import/runner.c rename to tests/runtime/pending-import/runner.c diff --git a/tests/runtime-async/async/pending-import/runner.cs b/tests/runtime/pending-import/runner.cs similarity index 100% rename from tests/runtime-async/async/pending-import/runner.cs rename to tests/runtime/pending-import/runner.cs diff --git a/tests/runtime-async/async/pending-import/runner.rs b/tests/runtime/pending-import/runner.rs similarity index 100% rename from tests/runtime-async/async/pending-import/runner.rs rename to tests/runtime/pending-import/runner.rs diff --git a/tests/runtime-async/async/pending-import/test.c b/tests/runtime/pending-import/test.c similarity index 100% rename from tests/runtime-async/async/pending-import/test.c rename to tests/runtime/pending-import/test.c diff --git a/tests/runtime-async/async/pending-import/test.cs b/tests/runtime/pending-import/test.cs similarity index 100% rename from tests/runtime-async/async/pending-import/test.cs rename to tests/runtime/pending-import/test.cs diff --git a/tests/runtime-async/async/pending-import/test.rs b/tests/runtime/pending-import/test.rs similarity index 100% rename from tests/runtime-async/async/pending-import/test.rs rename to tests/runtime/pending-import/test.rs diff --git a/tests/runtime-async/async/pending-import/test.wit b/tests/runtime/pending-import/test.wit similarity index 90% rename from tests/runtime-async/async/pending-import/test.wit rename to tests/runtime/pending-import/test.wit index a5ab30cda..0a586a2d3 100644 --- a/tests/runtime-async/async/pending-import/test.wit +++ b/tests/runtime/pending-import/test.wit @@ -1,3 +1,4 @@ +//@ async = true package my:test; interface i { diff --git a/tests/runtime-async/async/ping-pong/runner.cs b/tests/runtime/ping-pong/disabled/runner.cs similarity index 100% rename from tests/runtime-async/async/ping-pong/runner.cs rename to tests/runtime/ping-pong/disabled/runner.cs diff --git a/tests/runtime-async/async/ping-pong/test.cs b/tests/runtime/ping-pong/disabled/test.cs similarity index 100% rename from tests/runtime-async/async/ping-pong/test.cs rename to tests/runtime/ping-pong/disabled/test.cs diff --git a/tests/runtime-async/async/ping-pong/runner.c b/tests/runtime/ping-pong/runner.c similarity index 100% rename from tests/runtime-async/async/ping-pong/runner.c rename to tests/runtime/ping-pong/runner.c diff --git a/tests/runtime-async/async/ping-pong/runner.go b/tests/runtime/ping-pong/runner.go similarity index 100% rename from tests/runtime-async/async/ping-pong/runner.go rename to tests/runtime/ping-pong/runner.go diff --git a/tests/runtime-async/async/ping-pong/runner.rs b/tests/runtime/ping-pong/runner.rs similarity index 100% rename from tests/runtime-async/async/ping-pong/runner.rs rename to tests/runtime/ping-pong/runner.rs diff --git a/tests/runtime-async/async/ping-pong/test.c b/tests/runtime/ping-pong/test.c similarity index 100% rename from tests/runtime-async/async/ping-pong/test.c rename to tests/runtime/ping-pong/test.c diff --git a/tests/runtime-async/async/ping-pong/test.go b/tests/runtime/ping-pong/test.go similarity index 100% rename from tests/runtime-async/async/ping-pong/test.go rename to tests/runtime/ping-pong/test.go diff --git a/tests/runtime-async/async/ping-pong/test.rs b/tests/runtime/ping-pong/test.rs similarity index 100% rename from tests/runtime-async/async/ping-pong/test.rs rename to tests/runtime/ping-pong/test.rs diff --git a/tests/runtime-async/async/ping-pong/test.wit b/tests/runtime/ping-pong/test.wit similarity index 93% rename from tests/runtime-async/async/ping-pong/test.wit rename to tests/runtime/ping-pong/test.wit index cdfa4d43d..41468b2c8 100644 --- a/tests/runtime-async/async/ping-pong/test.wit +++ b/tests/runtime/ping-pong/test.wit @@ -1,3 +1,4 @@ +//@ async = true package my:test; interface i { diff --git a/tests/runtime-async/async/return-string/runner.go b/tests/runtime/return-string/runner.go similarity index 100% rename from tests/runtime-async/async/return-string/runner.go rename to tests/runtime/return-string/runner.go diff --git a/tests/runtime-async/async/return-string/test.go b/tests/runtime/return-string/test.go similarity index 100% rename from tests/runtime-async/async/return-string/test.go rename to tests/runtime/return-string/test.go diff --git a/tests/runtime-async/async/return-string/test.wit b/tests/runtime/return-string/test.wit similarity index 90% rename from tests/runtime-async/async/return-string/test.wit rename to tests/runtime/return-string/test.wit index 943d8cea7..c8b23cdaf 100644 --- a/tests/runtime-async/async/return-string/test.wit +++ b/tests/runtime/return-string/test.wit @@ -1,3 +1,4 @@ +//@ async = true package my:test; interface i { diff --git a/tests/runtime-async/async/rust-async-and-block-on/runner.rs b/tests/runtime/rust-async-and-block-on/runner.rs similarity index 100% rename from tests/runtime-async/async/rust-async-and-block-on/runner.rs rename to tests/runtime/rust-async-and-block-on/runner.rs diff --git a/tests/runtime-async/async/rust-async-and-block-on/test.rs b/tests/runtime/rust-async-and-block-on/test.rs similarity index 100% rename from tests/runtime-async/async/rust-async-and-block-on/test.rs rename to tests/runtime/rust-async-and-block-on/test.rs diff --git a/tests/runtime-async/async/rust-async-and-block-on/test.wit b/tests/runtime/rust-async-and-block-on/test.wit similarity index 90% rename from tests/runtime-async/async/rust-async-and-block-on/test.wit rename to tests/runtime/rust-async-and-block-on/test.wit index 3e66f4289..94ebaa738 100644 --- a/tests/runtime-async/async/rust-async-and-block-on/test.wit +++ b/tests/runtime/rust-async-and-block-on/test.wit @@ -1,3 +1,4 @@ +//@ async = true package a:b; interface i { diff --git a/tests/runtime-async/async/rust-cross-task-wakeup/runner.rs b/tests/runtime/rust-cross-task-wakeup/runner.rs similarity index 100% rename from tests/runtime-async/async/rust-cross-task-wakeup/runner.rs rename to tests/runtime/rust-cross-task-wakeup/runner.rs diff --git a/tests/runtime-async/async/rust-cross-task-wakeup/test.rs b/tests/runtime/rust-cross-task-wakeup/test.rs similarity index 100% rename from tests/runtime-async/async/rust-cross-task-wakeup/test.rs rename to tests/runtime/rust-cross-task-wakeup/test.rs diff --git a/tests/runtime-async/async/rust-cross-task-wakeup/test.wit b/tests/runtime/rust-cross-task-wakeup/test.wit similarity index 91% rename from tests/runtime-async/async/rust-cross-task-wakeup/test.wit rename to tests/runtime/rust-cross-task-wakeup/test.wit index ae07023b3..35e8f2dfe 100644 --- a/tests/runtime-async/async/rust-cross-task-wakeup/test.wit +++ b/tests/runtime/rust-cross-task-wakeup/test.wit @@ -1,3 +1,4 @@ +//@ async = true package my:test; interface i { diff --git a/tests/runtime-async/async/rust-lowered-send/runner.rs b/tests/runtime/rust-lowered-send/runner.rs similarity index 100% rename from tests/runtime-async/async/rust-lowered-send/runner.rs rename to tests/runtime/rust-lowered-send/runner.rs diff --git a/tests/runtime-async/async/rust-lowered-send/test.rs b/tests/runtime/rust-lowered-send/test.rs similarity index 100% rename from tests/runtime-async/async/rust-lowered-send/test.rs rename to tests/runtime/rust-lowered-send/test.rs diff --git a/tests/runtime-async/async/rust-lowered-send/test.wit b/tests/runtime/rust-lowered-send/test.wit similarity index 90% rename from tests/runtime-async/async/rust-lowered-send/test.wit rename to tests/runtime/rust-lowered-send/test.wit index 6ad54cc50..692778822 100644 --- a/tests/runtime-async/async/rust-lowered-send/test.wit +++ b/tests/runtime/rust-lowered-send/test.wit @@ -1,3 +1,4 @@ +//@ async = true package a:b; interface i { diff --git a/tests/runtime-async/async/simple-call-import/runner.c b/tests/runtime/simple-call-import/runner.c similarity index 100% rename from tests/runtime-async/async/simple-call-import/runner.c rename to tests/runtime/simple-call-import/runner.c diff --git a/tests/runtime-async/async/simple-call-import/runner.go b/tests/runtime/simple-call-import/runner.go similarity index 100% rename from tests/runtime-async/async/simple-call-import/runner.go rename to tests/runtime/simple-call-import/runner.go diff --git a/tests/runtime-async/async/simple-call-import/runner.rs b/tests/runtime/simple-call-import/runner.rs similarity index 100% rename from tests/runtime-async/async/simple-call-import/runner.rs rename to tests/runtime/simple-call-import/runner.rs diff --git a/tests/runtime-async/async/simple-call-import/test.c b/tests/runtime/simple-call-import/test.c similarity index 100% rename from tests/runtime-async/async/simple-call-import/test.c rename to tests/runtime/simple-call-import/test.c diff --git a/tests/runtime-async/async/simple-call-import/test.go b/tests/runtime/simple-call-import/test.go similarity index 100% rename from tests/runtime-async/async/simple-call-import/test.go rename to tests/runtime/simple-call-import/test.go diff --git a/tests/runtime-async/async/simple-call-import/test.rs b/tests/runtime/simple-call-import/test.rs similarity index 100% rename from tests/runtime-async/async/simple-call-import/test.rs rename to tests/runtime/simple-call-import/test.rs diff --git a/tests/runtime-async/async/simple-yield/test.wit b/tests/runtime/simple-call-import/test.wit similarity index 88% rename from tests/runtime-async/async/simple-yield/test.wit rename to tests/runtime/simple-call-import/test.wit index 378d915eb..7635d9993 100644 --- a/tests/runtime-async/async/simple-yield/test.wit +++ b/tests/runtime/simple-call-import/test.wit @@ -1,3 +1,4 @@ +//@ async = true package a:b; interface i { diff --git a/tests/runtime-async/async/simple-future/runner-nostd.rs b/tests/runtime/simple-future/runner-nostd.rs similarity index 100% rename from tests/runtime-async/async/simple-future/runner-nostd.rs rename to tests/runtime/simple-future/runner-nostd.rs diff --git a/tests/runtime-async/async/simple-future/runner.c b/tests/runtime/simple-future/runner.c similarity index 100% rename from tests/runtime-async/async/simple-future/runner.c rename to tests/runtime/simple-future/runner.c diff --git a/tests/runtime-async/async/simple-future/runner.cs b/tests/runtime/simple-future/runner.cs similarity index 100% rename from tests/runtime-async/async/simple-future/runner.cs rename to tests/runtime/simple-future/runner.cs diff --git a/tests/runtime-async/async/simple-future/runner.go b/tests/runtime/simple-future/runner.go similarity index 100% rename from tests/runtime-async/async/simple-future/runner.go rename to tests/runtime/simple-future/runner.go diff --git a/tests/runtime-async/async/simple-future/runner.rs b/tests/runtime/simple-future/runner.rs similarity index 100% rename from tests/runtime-async/async/simple-future/runner.rs rename to tests/runtime/simple-future/runner.rs diff --git a/tests/runtime-async/async/simple-future/test.c b/tests/runtime/simple-future/test.c similarity index 100% rename from tests/runtime-async/async/simple-future/test.c rename to tests/runtime/simple-future/test.c diff --git a/tests/runtime-async/async/simple-future/test.cs b/tests/runtime/simple-future/test.cs similarity index 100% rename from tests/runtime-async/async/simple-future/test.cs rename to tests/runtime/simple-future/test.cs diff --git a/tests/runtime-async/async/simple-future/test.go b/tests/runtime/simple-future/test.go similarity index 100% rename from tests/runtime-async/async/simple-future/test.go rename to tests/runtime/simple-future/test.go diff --git a/tests/runtime-async/async/simple-future/test.mbt b/tests/runtime/simple-future/test.mbt similarity index 100% rename from tests/runtime-async/async/simple-future/test.mbt rename to tests/runtime/simple-future/test.mbt diff --git a/tests/runtime-async/async/simple-future/test.rs b/tests/runtime/simple-future/test.rs similarity index 100% rename from tests/runtime-async/async/simple-future/test.rs rename to tests/runtime/simple-future/test.rs diff --git a/tests/runtime-async/async/simple-future/test.wit b/tests/runtime/simple-future/test.wit similarity index 92% rename from tests/runtime-async/async/simple-future/test.wit rename to tests/runtime/simple-future/test.wit index 6a1a7d1fa..45d6517b9 100644 --- a/tests/runtime-async/async/simple-future/test.wit +++ b/tests/runtime/simple-future/test.wit @@ -1,3 +1,4 @@ +//@ async = true package my:test; interface i { diff --git a/tests/runtime-async/async/simple-import-params-results/runner.c b/tests/runtime/simple-import-params-results/runner.c similarity index 100% rename from tests/runtime-async/async/simple-import-params-results/runner.c rename to tests/runtime/simple-import-params-results/runner.c diff --git a/tests/runtime-async/async/simple-import-params-results/runner.cs b/tests/runtime/simple-import-params-results/runner.cs similarity index 100% rename from tests/runtime-async/async/simple-import-params-results/runner.cs rename to tests/runtime/simple-import-params-results/runner.cs diff --git a/tests/runtime-async/async/simple-import-params-results/runner.go b/tests/runtime/simple-import-params-results/runner.go similarity index 100% rename from tests/runtime-async/async/simple-import-params-results/runner.go rename to tests/runtime/simple-import-params-results/runner.go diff --git a/tests/runtime-async/async/simple-import-params-results/runner.rs b/tests/runtime/simple-import-params-results/runner.rs similarity index 100% rename from tests/runtime-async/async/simple-import-params-results/runner.rs rename to tests/runtime/simple-import-params-results/runner.rs diff --git a/tests/runtime-async/async/simple-import-params-results/test.c b/tests/runtime/simple-import-params-results/test.c similarity index 100% rename from tests/runtime-async/async/simple-import-params-results/test.c rename to tests/runtime/simple-import-params-results/test.c diff --git a/tests/runtime-async/async/simple-import-params-results/test.cs b/tests/runtime/simple-import-params-results/test.cs similarity index 100% rename from tests/runtime-async/async/simple-import-params-results/test.cs rename to tests/runtime/simple-import-params-results/test.cs diff --git a/tests/runtime-async/async/simple-import-params-results/test.go b/tests/runtime/simple-import-params-results/test.go similarity index 100% rename from tests/runtime-async/async/simple-import-params-results/test.go rename to tests/runtime/simple-import-params-results/test.go diff --git a/tests/runtime-async/async/simple-import-params-results/test.mbt b/tests/runtime/simple-import-params-results/test.mbt similarity index 100% rename from tests/runtime-async/async/simple-import-params-results/test.mbt rename to tests/runtime/simple-import-params-results/test.mbt diff --git a/tests/runtime-async/async/simple-import-params-results/test.rs b/tests/runtime/simple-import-params-results/test.rs similarity index 100% rename from tests/runtime-async/async/simple-import-params-results/test.rs rename to tests/runtime/simple-import-params-results/test.rs diff --git a/tests/runtime-async/async/simple-import-params-results/test.wit b/tests/runtime/simple-import-params-results/test.wit similarity index 95% rename from tests/runtime-async/async/simple-import-params-results/test.wit rename to tests/runtime/simple-import-params-results/test.wit index 567040647..828a1e14b 100644 --- a/tests/runtime-async/async/simple-import-params-results/test.wit +++ b/tests/runtime/simple-import-params-results/test.wit @@ -1,3 +1,4 @@ +//@ async = true package a:b; interface i { diff --git a/tests/runtime-async/async/simple-pending-import/runner.c b/tests/runtime/simple-pending-import/runner.c similarity index 100% rename from tests/runtime-async/async/simple-pending-import/runner.c rename to tests/runtime/simple-pending-import/runner.c diff --git a/tests/runtime-async/async/simple-pending-import/runner.go b/tests/runtime/simple-pending-import/runner.go similarity index 100% rename from tests/runtime-async/async/simple-pending-import/runner.go rename to tests/runtime/simple-pending-import/runner.go diff --git a/tests/runtime-async/async/simple-pending-import/runner.rs b/tests/runtime/simple-pending-import/runner.rs similarity index 100% rename from tests/runtime-async/async/simple-pending-import/runner.rs rename to tests/runtime/simple-pending-import/runner.rs diff --git a/tests/runtime-async/async/simple-pending-import/test.c b/tests/runtime/simple-pending-import/test.c similarity index 100% rename from tests/runtime-async/async/simple-pending-import/test.c rename to tests/runtime/simple-pending-import/test.c diff --git a/tests/runtime-async/async/simple-pending-import/test.go b/tests/runtime/simple-pending-import/test.go similarity index 100% rename from tests/runtime-async/async/simple-pending-import/test.go rename to tests/runtime/simple-pending-import/test.go diff --git a/tests/runtime-async/async/simple-pending-import/test.rs b/tests/runtime/simple-pending-import/test.rs similarity index 100% rename from tests/runtime-async/async/simple-pending-import/test.rs rename to tests/runtime/simple-pending-import/test.rs diff --git a/tests/runtime-async/async/simple-pending-import/test.wit b/tests/runtime/simple-pending-import/test.wit similarity index 88% rename from tests/runtime-async/async/simple-pending-import/test.wit rename to tests/runtime/simple-pending-import/test.wit index 378d915eb..7635d9993 100644 --- a/tests/runtime-async/async/simple-pending-import/test.wit +++ b/tests/runtime/simple-pending-import/test.wit @@ -1,3 +1,4 @@ +//@ async = true package a:b; interface i { diff --git a/tests/runtime-async/async/simple-stream-payload/runner.c b/tests/runtime/simple-stream-payload/runner.c similarity index 100% rename from tests/runtime-async/async/simple-stream-payload/runner.c rename to tests/runtime/simple-stream-payload/runner.c diff --git a/tests/runtime-async/async/simple-stream-payload/runner.cs b/tests/runtime/simple-stream-payload/runner.cs similarity index 100% rename from tests/runtime-async/async/simple-stream-payload/runner.cs rename to tests/runtime/simple-stream-payload/runner.cs diff --git a/tests/runtime-async/async/simple-stream-payload/runner.go b/tests/runtime/simple-stream-payload/runner.go similarity index 100% rename from tests/runtime-async/async/simple-stream-payload/runner.go rename to tests/runtime/simple-stream-payload/runner.go diff --git a/tests/runtime-async/async/simple-stream-payload/runner.rs b/tests/runtime/simple-stream-payload/runner.rs similarity index 100% rename from tests/runtime-async/async/simple-stream-payload/runner.rs rename to tests/runtime/simple-stream-payload/runner.rs diff --git a/tests/runtime-async/async/simple-stream-payload/test.c b/tests/runtime/simple-stream-payload/test.c similarity index 100% rename from tests/runtime-async/async/simple-stream-payload/test.c rename to tests/runtime/simple-stream-payload/test.c diff --git a/tests/runtime-async/async/simple-stream-payload/test.cs b/tests/runtime/simple-stream-payload/test.cs similarity index 100% rename from tests/runtime-async/async/simple-stream-payload/test.cs rename to tests/runtime/simple-stream-payload/test.cs diff --git a/tests/runtime-async/async/simple-stream-payload/test.go b/tests/runtime/simple-stream-payload/test.go similarity index 100% rename from tests/runtime-async/async/simple-stream-payload/test.go rename to tests/runtime/simple-stream-payload/test.go diff --git a/tests/runtime-async/async/simple-stream-payload/test.mbt b/tests/runtime/simple-stream-payload/test.mbt similarity index 100% rename from tests/runtime-async/async/simple-stream-payload/test.mbt rename to tests/runtime/simple-stream-payload/test.mbt diff --git a/tests/runtime-async/async/simple-stream-payload/test.rs b/tests/runtime/simple-stream-payload/test.rs similarity index 100% rename from tests/runtime-async/async/simple-stream-payload/test.rs rename to tests/runtime/simple-stream-payload/test.rs diff --git a/tests/runtime-async/async/simple-stream-payload/test.wit b/tests/runtime/simple-stream-payload/test.wit similarity index 90% rename from tests/runtime-async/async/simple-stream-payload/test.wit rename to tests/runtime/simple-stream-payload/test.wit index b776823fd..aa882a185 100644 --- a/tests/runtime-async/async/simple-stream-payload/test.wit +++ b/tests/runtime/simple-stream-payload/test.wit @@ -1,3 +1,4 @@ +//@ async = true package my:test; interface i { diff --git a/tests/runtime-async/async/simple-stream/runner-nostd.rs b/tests/runtime/simple-stream/runner-nostd.rs similarity index 100% rename from tests/runtime-async/async/simple-stream/runner-nostd.rs rename to tests/runtime/simple-stream/runner-nostd.rs diff --git a/tests/runtime-async/async/simple-stream/runner.c b/tests/runtime/simple-stream/runner.c similarity index 100% rename from tests/runtime-async/async/simple-stream/runner.c rename to tests/runtime/simple-stream/runner.c diff --git a/tests/runtime-async/async/simple-stream/runner.go b/tests/runtime/simple-stream/runner.go similarity index 100% rename from tests/runtime-async/async/simple-stream/runner.go rename to tests/runtime/simple-stream/runner.go diff --git a/tests/runtime-async/async/simple-stream/runner.rs b/tests/runtime/simple-stream/runner.rs similarity index 100% rename from tests/runtime-async/async/simple-stream/runner.rs rename to tests/runtime/simple-stream/runner.rs diff --git a/tests/runtime-async/async/simple-stream/test.c b/tests/runtime/simple-stream/test.c similarity index 100% rename from tests/runtime-async/async/simple-stream/test.c rename to tests/runtime/simple-stream/test.c diff --git a/tests/runtime-async/async/simple-stream/test.go b/tests/runtime/simple-stream/test.go similarity index 100% rename from tests/runtime-async/async/simple-stream/test.go rename to tests/runtime/simple-stream/test.go diff --git a/tests/runtime-async/async/simple-stream/test.mbt b/tests/runtime/simple-stream/test.mbt similarity index 100% rename from tests/runtime-async/async/simple-stream/test.mbt rename to tests/runtime/simple-stream/test.mbt diff --git a/tests/runtime-async/async/simple-stream/test.wit b/tests/runtime/simple-stream/test.wit similarity index 90% rename from tests/runtime-async/async/simple-stream/test.wit rename to tests/runtime/simple-stream/test.wit index bdf2bc03b..4281d2826 100644 --- a/tests/runtime-async/async/simple-stream/test.wit +++ b/tests/runtime/simple-stream/test.wit @@ -1,3 +1,4 @@ +//@ async = true package my:test; interface i { diff --git a/tests/runtime-async/async/simple-yield/runner-nostd.rs b/tests/runtime/simple-yield/runner-nostd.rs similarity index 100% rename from tests/runtime-async/async/simple-yield/runner-nostd.rs rename to tests/runtime/simple-yield/runner-nostd.rs diff --git a/tests/runtime-async/async/simple-yield/runner.c b/tests/runtime/simple-yield/runner.c similarity index 100% rename from tests/runtime-async/async/simple-yield/runner.c rename to tests/runtime/simple-yield/runner.c diff --git a/tests/runtime-async/async/simple-yield/runner.rs b/tests/runtime/simple-yield/runner.rs similarity index 100% rename from tests/runtime-async/async/simple-yield/runner.rs rename to tests/runtime/simple-yield/runner.rs diff --git a/tests/runtime-async/async/simple-yield/test.c b/tests/runtime/simple-yield/test.c similarity index 100% rename from tests/runtime-async/async/simple-yield/test.c rename to tests/runtime/simple-yield/test.c diff --git a/tests/runtime-async/async/simple-yield/test.rs b/tests/runtime/simple-yield/test.rs similarity index 100% rename from tests/runtime-async/async/simple-yield/test.rs rename to tests/runtime/simple-yield/test.rs diff --git a/tests/runtime-async/async/threading-builtins/test.wit b/tests/runtime/simple-yield/test.wit similarity index 88% rename from tests/runtime-async/async/threading-builtins/test.wit rename to tests/runtime/simple-yield/test.wit index 378d915eb..7635d9993 100644 --- a/tests/runtime-async/async/threading-builtins/test.wit +++ b/tests/runtime/simple-yield/test.wit @@ -1,3 +1,4 @@ +//@ async = true package a:b; interface i { diff --git a/tests/runtime-async/async/stream-to-futures-stream/runner.rs b/tests/runtime/stream-to-futures-stream/runner.rs similarity index 100% rename from tests/runtime-async/async/stream-to-futures-stream/runner.rs rename to tests/runtime/stream-to-futures-stream/runner.rs diff --git a/tests/runtime-async/async/stream-to-futures-stream/test.rs b/tests/runtime/stream-to-futures-stream/test.rs similarity index 100% rename from tests/runtime-async/async/stream-to-futures-stream/test.rs rename to tests/runtime/stream-to-futures-stream/test.rs diff --git a/tests/runtime-async/async/stream-to-futures-stream/test.wit b/tests/runtime/stream-to-futures-stream/test.wit similarity index 90% rename from tests/runtime-async/async/stream-to-futures-stream/test.wit rename to tests/runtime/stream-to-futures-stream/test.wit index b776823fd..aa882a185 100644 --- a/tests/runtime-async/async/stream-to-futures-stream/test.wit +++ b/tests/runtime/stream-to-futures-stream/test.wit @@ -1,3 +1,4 @@ +//@ async = true package my:test; interface i { diff --git a/tests/runtime/strings/runner.go b/tests/runtime/strings/disabled/runner.go similarity index 100% rename from tests/runtime/strings/runner.go rename to tests/runtime/strings/disabled/runner.go diff --git a/tests/runtime/strings/test.go b/tests/runtime/strings/disabled/test.go similarity index 100% rename from tests/runtime/strings/test.go rename to tests/runtime/strings/disabled/test.go diff --git a/tests/runtime-async/async/yield-loop-receives-events/leaf.rs b/tests/runtime/yield-loop-receives-events/leaf.rs similarity index 100% rename from tests/runtime-async/async/yield-loop-receives-events/leaf.rs rename to tests/runtime/yield-loop-receives-events/leaf.rs diff --git a/tests/runtime-async/async/yield-loop-receives-events/middle.rs b/tests/runtime/yield-loop-receives-events/middle.rs similarity index 100% rename from tests/runtime-async/async/yield-loop-receives-events/middle.rs rename to tests/runtime/yield-loop-receives-events/middle.rs diff --git a/tests/runtime-async/async/yield-loop-receives-events/runner.rs b/tests/runtime/yield-loop-receives-events/runner.rs similarity index 100% rename from tests/runtime-async/async/yield-loop-receives-events/runner.rs rename to tests/runtime/yield-loop-receives-events/runner.rs diff --git a/tests/runtime-async/async/yield-loop-receives-events/test.wit b/tests/runtime/yield-loop-receives-events/test.wit similarity index 94% rename from tests/runtime-async/async/yield-loop-receives-events/test.wit rename to tests/runtime/yield-loop-receives-events/test.wit index 343a5294d..6136b613b 100644 --- a/tests/runtime-async/async/yield-loop-receives-events/test.wit +++ b/tests/runtime/yield-loop-receives-events/test.wit @@ -1,3 +1,4 @@ +//@ async = true //@ dependencies = ['middle', 'leaf'] package test:common; diff --git a/tests/runtime-async/async/threading-builtins/runner.c b/tests/threading/threading-builtins/runner.c similarity index 100% rename from tests/runtime-async/async/threading-builtins/runner.c rename to tests/threading/threading-builtins/runner.c diff --git a/tests/runtime-async/async/threading-builtins/test.c b/tests/threading/threading-builtins/test.c similarity index 100% rename from tests/runtime-async/async/threading-builtins/test.c rename to tests/threading/threading-builtins/test.c diff --git a/tests/runtime-async/async/simple-call-import/test.wit b/tests/threading/threading-builtins/test.wit similarity index 100% rename from tests/runtime-async/async/simple-call-import/test.wit rename to tests/threading/threading-builtins/test.wit From 6f156096b24bd75d511afd46a03ba42107389cb1 Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Tue, 7 Jul 2026 15:43:30 -0500 Subject: [PATCH 08/23] Update wasm-tools dependencies (#1654) * Update wasm-tools dependencies Additionally update C intrinsics for threading as they've been renamed slightly. * Remove unused import --- Cargo.lock | 76 ++++++------ Cargo.toml | 14 +-- crates/c/src/lib.rs | 137 +++++----------------- crates/cpp/src/lib.rs | 5 + crates/guest-rust/macro/src/lib.rs | 4 +- tests/threading/threading-builtins/test.c | 40 ++++--- 6 files changed, 105 insertions(+), 171 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 63a642d15..885e23beb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1024,7 +1024,7 @@ name = "test-helpers" version = "0.0.0" dependencies = [ "codegen-macro", - "wasm-encoder 0.252.0", + "wasm-encoder 0.253.0", "wit-bindgen-core", "wit-component", "wit-parser", @@ -1221,9 +1221,9 @@ checksum = "1b6c48003fe59c201c97a7786ff55feabe6b6f83b598aa9ff5bcc4f94d940bf3" [[package]] name = "wasm-compose" -version = "0.252.0" +version = "0.253.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59b710751a35d54732a63851cdfacbfb2266b7160ce174faf269d3f4e84e31b" +checksum = "a00572a13a43b4754066ddeb46aec65091cd4cdb5b632768295a4236574da28a" dependencies = [ "anyhow", "heck", @@ -1234,8 +1234,8 @@ dependencies = [ "serde_derive", "serde_yaml2", "smallvec", - "wasm-encoder 0.252.0", - "wasmparser 0.252.0", + "wasm-encoder 0.253.0", + "wasmparser 0.253.0", "wat", ] @@ -1251,12 +1251,12 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.252.0" +version = "0.253.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8185ae345fa5687c054626ff9a50e7089797a343d9904d1dc9820eb4c4d3196f" +checksum = "59972d6cd272259de647b7c1f1912e45e289c75ffd4be04e10695507cd7e1b59" dependencies = [ "leb128fmt", - "wasmparser 0.252.0", + "wasmparser 0.253.0", ] [[package]] @@ -1280,14 +1280,14 @@ dependencies = [ [[package]] name = "wasm-metadata" -version = "0.252.0" +version = "0.253.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b7e08e02a3cd55bf778009d4cd6faae50da011f293644daf78a531a32d6d142" +checksum = "b3f45816ef616806f48498bcd831377de578c4fa51db0c83ab8ceb78cc13523b" dependencies = [ "anyhow", "indexmap", - "wasm-encoder 0.252.0", - "wasmparser 0.252.0", + "wasm-encoder 0.253.0", + "wasmparser 0.253.0", ] [[package]] @@ -1305,9 +1305,9 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.252.0" +version = "0.253.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3eb099dcadcde5be9eef55e3a337128efd4e44b4c93122487e4d2e4e1c6627c" +checksum = "19db11f87d2486580e1e8b6f494c54df7e0566b87d0b599db843c24019667339" dependencies = [ "bitflags", "hashbrown 0.17.1", @@ -1318,22 +1318,22 @@ dependencies = [ [[package]] name = "wast" -version = "252.0.0" +version = "253.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942a3449d6a593fccc111a6241c8df52bda168af30e40bf9580d4394d7374c65" +checksum = "d3264542f8965c5d84fb1085d924bfba9a6314bb228eff13a2de14d7627664d0" dependencies = [ "bumpalo", "leb128fmt", "memchr", "unicode-width 0.2.2", - "wasm-encoder 0.252.0", + "wasm-encoder 0.253.0", ] [[package]] name = "wat" -version = "1.252.0" +version = "1.253.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c72a4ba7088f7bac94cf516e49882bdf97068904a563768cf249efc839ec42cb" +checksum = "4bfc5ce906144200c972ec617470aa35bd847472e170b26dde3e80541c674055" dependencies = [ "wast", ] @@ -1378,8 +1378,8 @@ dependencies = [ "clap", "heck", "indexmap", - "wasm-encoder 0.252.0", - "wasm-metadata 0.252.0", + "wasm-encoder 0.253.0", + "wasm-metadata 0.253.0", "wit-bindgen-core", "wit-component", ] @@ -1391,7 +1391,7 @@ dependencies = [ "anyhow", "clap", "env_logger", - "wasm-encoder 0.252.0", + "wasm-encoder 0.253.0", "wit-bindgen-c", "wit-bindgen-core", "wit-bindgen-cpp", @@ -1424,8 +1424,8 @@ dependencies = [ "heck", "indexmap", "test-helpers", - "wasm-encoder 0.252.0", - "wasm-metadata 0.252.0", + "wasm-encoder 0.253.0", + "wasm-metadata 0.253.0", "wit-bindgen-c", "wit-bindgen-core", "wit-component", @@ -1441,7 +1441,7 @@ dependencies = [ "heck", "indexmap", "regex", - "wasm-metadata 0.252.0", + "wasm-metadata 0.253.0", "wit-bindgen-core", "wit-component", "wit-parser", @@ -1454,8 +1454,8 @@ dependencies = [ "anyhow", "clap", "heck", - "wasm-encoder 0.252.0", - "wasm-metadata 0.252.0", + "wasm-encoder 0.253.0", + "wasm-metadata 0.253.0", "wit-bindgen-core", "wit-component", ] @@ -1496,7 +1496,7 @@ dependencies = [ "serde_json", "syn", "test-helpers", - "wasm-metadata 0.252.0", + "wasm-metadata 0.253.0", "wit-bindgen", "wit-bindgen-core", "wit-component", @@ -1534,8 +1534,8 @@ dependencies = [ "wac-types", "wasi-preview1-component-adapter-provider", "wasm-compose", - "wasm-encoder 0.252.0", - "wasmparser 0.252.0", + "wasm-encoder 0.253.0", + "wasmparser 0.253.0", "wat", "wit-bindgen-csharp", "wit-component", @@ -1544,9 +1544,9 @@ dependencies = [ [[package]] name = "wit-component" -version = "0.252.0" +version = "0.253.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76db0662b590f45d33d0e363fa13539a5a1eecd35d5a12fe208c335461c1053d" +checksum = "dbbd2500ac3488489ee8c6e59b79d7e47e6da5bfb019efd35d5dca57b78af624" dependencies = [ "anyhow", "bitflags", @@ -1555,18 +1555,18 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "wasm-encoder 0.252.0", - "wasm-metadata 0.252.0", - "wasmparser 0.252.0", + "wasm-encoder 0.253.0", + "wasm-metadata 0.253.0", + "wasmparser 0.253.0", "wat", "wit-parser", ] [[package]] name = "wit-parser" -version = "0.252.0" +version = "0.253.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4266bea110371c620ccf3201c5023676046bc4556e5c7cfb5d500bda5ebc162d" +checksum = "4d997b8e5920fcbeec742b58e583325d6419a6aca617ae8075c406a61c65ba8a" dependencies = [ "anyhow", "hashbrown 0.17.1", @@ -1578,7 +1578,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-ident", - "wasmparser 0.252.0", + "wasmparser 0.253.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5bf23e8ae..98e1378a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,13 +47,13 @@ syn = { version = "2.0.89", features = ["printing"] } futures = "0.3.31" macro-string = "0.2.0" -wat = "1.252.0" -wasmparser = "0.252.0" -wasm-encoder = "0.252.0" -wasm-metadata = { version = "0.252.0", default-features = false } -wit-parser = "0.252.0" -wit-component = "0.252.0" -wasm-compose = "0.252.0" +wat = "1.253.0" +wasmparser = "0.253.0" +wasm-encoder = "0.253.0" +wasm-metadata = { version = "0.253.0", default-features = false } +wit-parser = "0.253.0" +wit-component = "0.253.0" +wasm-compose = "0.253.0" wit-bindgen-core = { path = 'crates/core', version = '0.58.0' } wit-bindgen-c = { path = 'crates/c', version = '0.58.0' } diff --git a/crates/c/src/lib.rs b/crates/c/src/lib.rs index 970d34aa2..91d30b14e 100644 --- a/crates/c/src/lib.rs +++ b/crates/c/src/lib.rs @@ -728,123 +728,48 @@ impl C { let snake = self.world.to_snake_case(); uwriteln!( self.src.h_async, - " -void* {snake}_context_get_1(void); -void {snake}_context_set_1(void* value); -uint32_t {snake}_thread_yield_cancellable(void); -uint32_t {snake}_thread_index(void); -uint32_t {snake}_thread_new_indirect(void (*start_function)(void*), void* arg); -void {snake}_thread_suspend_to(uint32_t thread); -uint32_t {snake}_thread_suspend_to_cancellable(uint32_t thread); -void {snake}_thread_suspend_to_suspended(uint32_t thread); -uint32_t {snake}_thread_suspend_to_suspended_cancellable(uint32_t thread); -void {snake}_thread_unsuspend(uint32_t thread); -void {snake}_thread_yield_to_suspended(uint32_t thread); -uint32_t {snake}_thread_yield_to_suspended_cancellable(uint32_t thread); -void {snake}_thread_suspend(void); -uint32_t {snake}_thread_suspend_cancellable(void); - " - ); - uwriteln!( - self.src.c_async, r#" __attribute__((__import_module__("$root"), __import_name__("[context-get-1]"))) -extern void* __context_get_1(void); - -void* {snake}_context_get_1(void) {{ - return __context_get_1(); -}} +extern void* {snake}_context_get_1(void); __attribute__((__import_module__("$root"), __import_name__("[context-set-1]"))) -extern void __context_set_1(void*); - -void {snake}_context_set_1(void* value) {{ - __context_set_1(value); -}} - -__attribute__((__import_module__("$root"), __import_name__("[cancellable][thread-yield]"))) -extern uint32_t __thread_yield_cancellable(void); - -uint32_t {snake}_thread_yield_cancellable(void) {{ - return __thread_yield_cancellable(); -}} +extern void {snake}_context_set_1(void* value); __attribute__((__import_module__("$root"), __import_name__("[thread-index]"))) -extern uint32_t __thread_index(void); - -uint32_t {snake}_thread_index(void) {{ - return __thread_index(); -}} +extern uint32_t {snake}_thread_index(void); __attribute__((__import_module__("$root"), __import_name__("[thread-new-indirect-v0]"))) -extern uint32_t __thread_new_indirect(uint32_t, void*); - -uint32_t {snake}_thread_new_indirect(void (*start_function)(void*), void* arg) {{ - return __thread_new_indirect((uint32_t)(uintptr_t)start_function, arg -); -}} - -__attribute__((__import_module__("$root"), __import_name__("[thread-suspend-to-suspended]"))) -extern uint32_t __thread_suspend_to_suspended(uint32_t); - -void {snake}_thread_suspend_to_suspended(uint32_t thread) {{ - __thread_suspend_to_suspended(thread); -}} - -__attribute__((__import_module__("$root"), __import_name__("[cancellable][thread-suspend-to-suspended]"))) -extern uint32_t __thread_suspend_to_suspended_cancellable(uint32_t); - -uint32_t {snake}_thread_suspend_to_suspended_cancellable(uint32_t thread) {{ - return __thread_suspend_to_suspended_cancellable(thread); -}} - -__attribute__((__import_module__("$root"), __import_name__("[thread-suspend-to]"))) -extern uint32_t __thread_suspend_to(uint32_t); - -void {snake}_thread_suspend_to(uint32_t thread) {{ - __thread_suspend_to(thread); -}} - -__attribute__((__import_module__("$root"), __import_name__("[cancellable][thread-suspend-to]"))) -extern uint32_t __thread_suspend_to_cancellable(uint32_t); - -uint32_t {snake}_thread_suspend_to_cancellable(uint32_t thread) {{ - return __thread_suspend_to_cancellable(thread); -}} - -__attribute__((__import_module__("$root"), __import_name__("[thread-unsuspend]"))) -extern void __thread_unsuspend(uint32_t); - -void {snake}_thread_unsuspend(uint32_t thread) {{ - __thread_unsuspend(thread); -}} - -__attribute__((__import_module__("$root"), __import_name__("[thread-yield-to-suspended]"))) -extern uint32_t __thread_yield_to_suspended(uint32_t); - -void {snake}_thread_yield_to_suspended(uint32_t thread) {{ - __thread_yield_to_suspended(thread); -}} - -__attribute__((__import_module__("$root"), __import_name__("[cancellable][thread-yield-to-suspended]"))) -extern uint32_t __thread_yield_to_suspended_cancellable(uint32_t); - -uint32_t {snake}_thread_yield_to_suspended_cancellable(uint32_t thread) {{ - return __thread_yield_to_suspended_cancellable(thread); -}} +extern uint32_t {snake}_thread_new_indirect(void (*start_function)(void*), void* arg); +__attribute__((__import_module__("$root"), __import_name__("[thread-resume-later]"))) +extern void {snake}_thread_resume_later(uint32_t thread); __attribute__((__import_module__("$root"), __import_name__("[thread-suspend]"))) -extern uint32_t __thread_suspend(void); - -void {snake}_thread_suspend(void) {{ - __thread_suspend(); -}} - +extern uint32_t {snake}_thread_suspend(void); __attribute__((__import_module__("$root"), __import_name__("[cancellable][thread-suspend]"))) -extern uint32_t __thread_suspend_cancellable(void); -uint32_t {snake}_thread_suspend_cancellable(void) {{ - return __thread_suspend_cancellable(); -}} +extern uint32_t {snake}_thread_suspend_cancellable(void); + +__attribute__((__import_module__("$root"), __import_name__("[cancellable][thread-yield]"))) +extern uint32_t {snake}_thread_yield_cancellable(void); + +__attribute__((__import_module__("$root"), __import_name__("[thread-suspend-then-resume]"))) +extern uint32_t {snake}_thread_suspend_then_resume(uint32_t thread); +__attribute__((__import_module__("$root"), __import_name__("[cancellable][thread-suspend-then-resume]"))) +extern uint32_t {snake}_thread_suspend_then_resume_cancellable(uint32_t thread); + +__attribute__((__import_module__("$root"), __import_name__("[thread-yield-then-resume]"))) +extern uint32_t {snake}_thread_yield_then_resume(uint32_t thread); +__attribute__((__import_module__("$root"), __import_name__("[cancellable][thread-yield-then-resume]"))) +extern uint32_t {snake}_thread_yield_then_resume_cancellable(uint32_t thread); + +__attribute__((__import_module__("$root"), __import_name__("[thread-suspend-then-promote]"))) +extern uint32_t {snake}_thread_suspend_then_promote(uint32_t thread); +__attribute__((__import_module__("$root"), __import_name__("[cancellable][thread-suspend-then-promote]"))) +extern uint32_t {snake}_thread_suspend_then_promote_cancellable(uint32_t thread); + +__attribute__((__import_module__("$root"), __import_name__("[thread-yield-then-promote]"))) +extern uint32_t {snake}_thread_yield_then_promote(uint32_t thread); +__attribute__((__import_module__("$root"), __import_name__("[cancellable][thread-yield-then-promote]"))) +extern uint32_t {snake}_thread_yield_then_promote_cancellable(uint32_t thread); "# ); } diff --git a/crates/cpp/src/lib.rs b/crates/cpp/src/lib.rs index 52e13ccab..8cd392616 100644 --- a/crates/cpp/src/lib.rs +++ b/crates/cpp/src/lib.rs @@ -1976,6 +1976,7 @@ impl<'a> wit_bindgen_core::InterfaceGenerator<'a> for CppInterfaceGenerator<'a> docs: Docs::default(), stability: Stability::Unknown, span: Default::default(), + external_id: None, }; self.generate_function(&func, &TypeOwner::Interface(intf), variant); } @@ -2010,6 +2011,7 @@ impl<'a> wit_bindgen_core::InterfaceGenerator<'a> for CppInterfaceGenerator<'a> docs: Docs::default(), stability: Stability::Unknown, span: Default::default(), + external_id: None, }; self.generate_function(&func2, &TypeOwner::Interface(intf), variant); } @@ -2044,6 +2046,7 @@ impl<'a> wit_bindgen_core::InterfaceGenerator<'a> for CppInterfaceGenerator<'a> docs: Docs::default(), stability: Stability::Unknown, span: Default::default(), + external_id: None, }; self.generate_function(&func, &TypeOwner::Interface(intf), variant); @@ -2059,6 +2062,7 @@ impl<'a> wit_bindgen_core::InterfaceGenerator<'a> for CppInterfaceGenerator<'a> docs: Docs::default(), stability: Stability::Unknown, span: Default::default(), + external_id: None, }; self.generate_function(&func1, &TypeOwner::Interface(intf), variant); @@ -2074,6 +2078,7 @@ impl<'a> wit_bindgen_core::InterfaceGenerator<'a> for CppInterfaceGenerator<'a> docs: Docs::default(), stability: Stability::Unknown, span: Default::default(), + external_id: None, }; self.generate_function(&func2, &TypeOwner::Interface(intf), variant); } diff --git a/crates/guest-rust/macro/src/lib.rs b/crates/guest-rust/macro/src/lib.rs index acf97ad53..d0393a834 100644 --- a/crates/guest-rust/macro/src/lib.rs +++ b/crates/guest-rust/macro/src/lib.rs @@ -8,7 +8,7 @@ use syn::punctuated::Punctuated; use syn::{Token, braced, token}; use wit_bindgen_core::AsyncFilterSet; use wit_bindgen_core::WorldGenerator; -use wit_bindgen_core::wit_parser::{PackageId, Resolve, UnresolvedPackageGroup, WorldId}; +use wit_bindgen_core::wit_parser::{PackageId, Resolve, WorldId}; use wit_bindgen_rust::{Opts, Ownership, WithOption}; #[proc_macro] @@ -238,7 +238,7 @@ fn parse_source( } } pkgs.truncate(0); - pkgs.push(resolve.push_group(UnresolvedPackageGroup::parse("macro-input", s)?)?); + pkgs.push(resolve.push_str("macro-input", s)?); } Some(Source::Paths(p)) => parse(p)?, None => parse(&[default])?, diff --git a/tests/threading/threading-builtins/test.c b/tests/threading/threading-builtins/test.c index 7a23d610a..b9baa2092 100644 --- a/tests/threading/threading-builtins/test.c +++ b/tests/threading/threading-builtins/test.c @@ -19,13 +19,15 @@ void thread_start(void *arg) { test_thread_yield_cancellable(); test_thread_suspend(); test_thread_suspend_cancellable(); - test_thread_yield_to_suspended(main_tid); - test_thread_yield_to_suspended_cancellable(main_tid); - test_thread_suspend_to_suspended(main_tid); - test_thread_suspend_to_suspended_cancellable(main_tid); - test_thread_suspend_to(main_tid); - test_thread_suspend_to_cancellable(main_tid); - test_thread_unsuspend(main_tid); + test_thread_yield_then_resume(main_tid); + test_thread_yield_then_resume_cancellable(main_tid); + test_thread_yield_then_promote(main_tid); + test_thread_yield_then_promote_cancellable(main_tid); + test_thread_suspend_then_resume(main_tid); + test_thread_suspend_then_resume_cancellable(main_tid); + test_thread_suspend_then_promote(main_tid); + test_thread_suspend_then_promote_cancellable(main_tid); + test_thread_resume_later(main_tid); } test_subtask_status_t exports_test_f_callback(test_event_t *event) { @@ -36,17 +38,19 @@ test_subtask_status_t exports_test_f_callback(test_event_t *event) { spawned_tid = test_thread_new_indirect(thread_start, &main_tid); // Now drive the other thread to completion by switching/yielding to it - test_thread_yield_to_suspended(spawned_tid); // other yields - test_thread_yield(); // other yields - test_thread_yield(); // other suspends - test_thread_yield_to_suspended(spawned_tid); // other suspends - test_thread_suspend_to_suspended(spawned_tid); // other yields to me - test_thread_suspend(); // other yields to me - test_thread_suspend(); // other suspends to me - test_thread_suspend_to_suspended(spawned_tid); // other suspends to me - test_thread_suspend_to_suspended(spawned_tid); // other suspends to me - test_thread_suspend_to_suspended(spawned_tid); // other suspends to me - test_thread_suspend_to_suspended( + test_thread_yield_then_resume(spawned_tid); // other yields + test_thread_yield(); // other yields + test_thread_yield(); // other suspends + test_thread_yield_then_resume(spawned_tid); // other suspends + test_thread_suspend_then_resume(spawned_tid); // other yields to me + test_thread_suspend(); // other yields to me + test_thread_suspend(); // other yields to me + test_thread_suspend(); // other yields to me + test_thread_suspend(); // other suspends to me + test_thread_suspend_then_resume(spawned_tid); // other suspends to me + test_thread_suspend_then_resume(spawned_tid); // other suspends to me + test_thread_suspend_then_resume(spawned_tid); // other suspends to me + test_thread_suspend_then_resume( spawned_tid); // other unsuspends me and terminates exports_test_f_return(); return TEST_CALLBACK_CODE_EXIT; From 93a5f4e3a91c3d4bf73b48e9f09ca090335194cc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 7 Jul 2026 16:06:48 -0500 Subject: [PATCH 09/23] Release wit-bindgen 0.59.0 (#1655) [automatically-tag-and-release-this-commit] Co-authored-by: Auto Release Process --- Cargo.lock | 24 +++++++++--------- Cargo.toml | 22 ++++++++-------- crates/cpp/Cargo.toml | 2 +- crates/guest-rust/Cargo.toml | 2 +- .../guest-rust/src/rt/libwit_bindgen_cabi.a | Bin 858 -> 858 bytes .../src/rt/wit_bindgen_cabi_realloc.c | 4 +-- .../src/rt/wit_bindgen_cabi_realloc.o | Bin 251 -> 251 bytes .../src/rt/wit_bindgen_cabi_realloc.rs | 2 +- 8 files changed, 28 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 885e23beb..231411c6b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1361,7 +1361,7 @@ checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" [[package]] name = "wit-bindgen" -version = "0.58.0" +version = "0.59.0" dependencies = [ "bitflags", "futures", @@ -1372,7 +1372,7 @@ dependencies = [ [[package]] name = "wit-bindgen-c" -version = "0.58.0" +version = "0.59.0" dependencies = [ "anyhow", "clap", @@ -1386,7 +1386,7 @@ dependencies = [ [[package]] name = "wit-bindgen-cli" -version = "0.58.0" +version = "0.59.0" dependencies = [ "anyhow", "clap", @@ -1406,7 +1406,7 @@ dependencies = [ [[package]] name = "wit-bindgen-core" -version = "0.58.0" +version = "0.59.0" dependencies = [ "anyhow", "clap", @@ -1417,7 +1417,7 @@ dependencies = [ [[package]] name = "wit-bindgen-cpp" -version = "0.58.0" +version = "0.59.0" dependencies = [ "anyhow", "clap", @@ -1434,7 +1434,7 @@ dependencies = [ [[package]] name = "wit-bindgen-csharp" -version = "0.58.0" +version = "0.59.0" dependencies = [ "anyhow", "clap", @@ -1449,7 +1449,7 @@ dependencies = [ [[package]] name = "wit-bindgen-go" -version = "0.58.0" +version = "0.59.0" dependencies = [ "anyhow", "clap", @@ -1462,7 +1462,7 @@ dependencies = [ [[package]] name = "wit-bindgen-markdown" -version = "0.58.0" +version = "0.59.0" dependencies = [ "anyhow", "clap", @@ -1473,7 +1473,7 @@ dependencies = [ [[package]] name = "wit-bindgen-moonbit" -version = "0.58.0" +version = "0.59.0" dependencies = [ "anyhow", "clap", @@ -1483,7 +1483,7 @@ dependencies = [ [[package]] name = "wit-bindgen-rust" -version = "0.58.0" +version = "0.59.0" dependencies = [ "anyhow", "bytes", @@ -1504,7 +1504,7 @@ dependencies = [ [[package]] name = "wit-bindgen-rust-macro" -version = "0.58.0" +version = "0.59.0" dependencies = [ "anyhow", "macro-string", @@ -1518,7 +1518,7 @@ dependencies = [ [[package]] name = "wit-bindgen-test" -version = "0.58.0" +version = "0.59.0" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index 98e1378a2..d7fabf202 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,7 @@ resolver = "2" [workspace.package] edition = "2024" -version = "0.58.0" +version = "0.59.0" license = "Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT" repository = "https://github.com/bytecodealliance/wit-bindgen" rust-version = "1.88.0" @@ -55,16 +55,16 @@ wit-parser = "0.253.0" wit-component = "0.253.0" wasm-compose = "0.253.0" -wit-bindgen-core = { path = 'crates/core', version = '0.58.0' } -wit-bindgen-c = { path = 'crates/c', version = '0.58.0' } -wit-bindgen-cpp = { path = 'crates/cpp', version = '0.58.0' } -wit-bindgen-rust = { path = "crates/rust", version = "0.58.0" } -wit-bindgen-csharp = { path = 'crates/csharp', version = '0.58.0' } -wit-bindgen-markdown = { path = 'crates/markdown', version = '0.58.0' } -wit-bindgen-moonbit = { path = 'crates/moonbit', version = '0.58.0' } -wit-bindgen-go = { path = 'crates/go', version = '0.58.0' } -wit-bindgen = { path = 'crates/guest-rust', version = '0.58.0', default-features = false } -wit-bindgen-test = { path = 'crates/test', version = '0.58.0' } +wit-bindgen-core = { path = 'crates/core', version = '0.59.0' } +wit-bindgen-c = { path = 'crates/c', version = '0.59.0' } +wit-bindgen-cpp = { path = 'crates/cpp', version = '0.59.0' } +wit-bindgen-rust = { path = "crates/rust", version = "0.59.0" } +wit-bindgen-csharp = { path = 'crates/csharp', version = '0.59.0' } +wit-bindgen-markdown = { path = 'crates/markdown', version = '0.59.0' } +wit-bindgen-moonbit = { path = 'crates/moonbit', version = '0.59.0' } +wit-bindgen-go = { path = 'crates/go', version = '0.59.0' } +wit-bindgen = { path = 'crates/guest-rust', version = '0.59.0', default-features = false } +wit-bindgen-test = { path = 'crates/test', version = '0.59.0' } [workspace.lints.clippy] # The default set of lints in Clippy is viewed as "too noisy" right now so diff --git a/crates/cpp/Cargo.toml b/crates/cpp/Cargo.toml index f03cfe57a..e7a8e098a 100644 --- a/crates/cpp/Cargo.toml +++ b/crates/cpp/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "wit-bindgen-cpp" authors = ["Christof Petig "] -version = "0.58.0" +version = "0.59.0" edition.workspace = true rust-version.workspace = true repository = 'https://github.com/cpetig/wit-bindgen' diff --git a/crates/guest-rust/Cargo.toml b/crates/guest-rust/Cargo.toml index efccac493..ef0ec9220 100644 --- a/crates/guest-rust/Cargo.toml +++ b/crates/guest-rust/Cargo.toml @@ -25,7 +25,7 @@ workspace = true all-features = true [dependencies] -wit-bindgen-rust-macro = { path = "./macro", optional = true, default-features = false, version = "0.58.0" } +wit-bindgen-rust-macro = { path = "./macro", optional = true, default-features = false, version = "0.59.0" } bitflags = { workspace = true, optional = true } futures = { version = "0.3.30", optional = true, default-features = false, features = ["alloc"] } diff --git a/crates/guest-rust/src/rt/libwit_bindgen_cabi.a b/crates/guest-rust/src/rt/libwit_bindgen_cabi.a index 1864d5525a1edb6c049ec589f356157dcdad76fc..6a023dd49bc01e212bc922e5e49d70dbc43dace4 100644 GIT binary patch delta 14 Vcmcb`c8hI84kM%G=3K_Ni~uR*1t$Oi delta 14 Vcmcb`c8hI84kM$*=3K_Ni~uR#1ttIh diff --git a/crates/guest-rust/src/rt/wit_bindgen_cabi_realloc.c b/crates/guest-rust/src/rt/wit_bindgen_cabi_realloc.c index e958945c8..5dd719ed3 100644 --- a/crates/guest-rust/src/rt/wit_bindgen_cabi_realloc.c +++ b/crates/guest-rust/src/rt/wit_bindgen_cabi_realloc.c @@ -2,9 +2,9 @@ #include -extern void *cabi_realloc_wit_bindgen_0_58_0(void *ptr, size_t old_size, size_t align, size_t new_size); +extern void *cabi_realloc_wit_bindgen_0_59_0(void *ptr, size_t old_size, size_t align, size_t new_size); __attribute__((__weak__, __export_name__("cabi_realloc"))) void *cabi_realloc(void *ptr, size_t old_size, size_t align, size_t new_size) { - return cabi_realloc_wit_bindgen_0_58_0(ptr, old_size, align, new_size); + return cabi_realloc_wit_bindgen_0_59_0(ptr, old_size, align, new_size); } diff --git a/crates/guest-rust/src/rt/wit_bindgen_cabi_realloc.o b/crates/guest-rust/src/rt/wit_bindgen_cabi_realloc.o index cd87964d408840d4d451e33bd53ec338f00e6ac1..8c230259420896a77676376c10725b73c92e0f27 100644 GIT binary patch delta 11 Scmey(_?vNpAEV_&|1|&|V+5T5 delta 11 Scmey(_?vNpAEU)Y|1|&|Uj&>0 diff --git a/crates/guest-rust/src/rt/wit_bindgen_cabi_realloc.rs b/crates/guest-rust/src/rt/wit_bindgen_cabi_realloc.rs index 65431605b..1c0a8894d 100644 --- a/crates/guest-rust/src/rt/wit_bindgen_cabi_realloc.rs +++ b/crates/guest-rust/src/rt/wit_bindgen_cabi_realloc.rs @@ -1,7 +1,7 @@ // This file is generated by ./ci/rebuild-libwit-bindgen-cabi.sh #[unsafe(no_mangle)] -pub unsafe extern "C" fn cabi_realloc_wit_bindgen_0_58_0( +pub unsafe extern "C" fn cabi_realloc_wit_bindgen_0_59_0( old_ptr: *mut u8, old_len: usize, align: usize, From 50b799afcb886551565cb29c190371026aa55a30 Mon Sep 17 00:00:00 2001 From: Jeremy Fleitz Date: Mon, 20 Jul 2026 15:52:04 -0400 Subject: [PATCH 10/23] Add support for named implements in Go (#1657) * Add support for named implements in Go Signed-off-by: Jeremy Fleitz * clippy: use package.clear() Signed-off-by: Jeremy Fleitz * support implements for cpp Signed-off-by: Jeremy Fleitz * linter fix Signed-off-by: Jeremy Fleitz * update to go_package_name Signed-off-by: Jeremy Fleitz * linter update Signed-off-by: Jeremy Fleitz --------- Signed-off-by: Jeremy Fleitz --- crates/cpp/src/lib.rs | 71 ++++++++++++++++++------------ crates/go/src/lib.rs | 58 ++++++++++++------------ crates/guest-rust/macro/src/lib.rs | 2 +- tests/codegen/issue1642.wit | 28 ++++++++++++ 4 files changed, 103 insertions(+), 56 deletions(-) create mode 100644 tests/codegen/issue1642.wit diff --git a/crates/cpp/src/lib.rs b/crates/cpp/src/lib.rs index 8cd392616..2d7b30099 100644 --- a/crates/cpp/src/lib.rs +++ b/crates/cpp/src/lib.rs @@ -103,6 +103,7 @@ struct Cpp { world: String, world_id: Option, imported_interfaces: HashSet, + instance_names: HashMap, user_class_files: HashMap, defined_types: HashSet<(Vec, String)>, types: Types, @@ -283,6 +284,17 @@ impl Cpp { Cpp::default() } + fn update_instance_name(&mut self, resolve: &Resolve, name: &WorldKey, id: InterfaceId) { + match name { + WorldKey::Name(instance) if resolve.interfaces[id].name.is_some() => { + self.instance_names.insert(id, instance.clone()); + } + _ => { + self.instance_names.remove(&id); + } + } + } + pub fn is_first_definition(&mut self, ns: &Vec, name: &str) -> bool { let owned = (ns.to_owned(), name.to_owned()); if !self.defined_types.contains(&owned) { @@ -522,6 +534,7 @@ impl WorldGenerator for Cpp { _files: &mut Files, ) -> anyhow::Result<()> { self.imported_interfaces.insert(id); + self.update_instance_name(resolve, name, id); let full_name = resolve.name_world_key(name); match self.opts.with.iter().find(|e| e.0 == full_name) { @@ -538,8 +551,7 @@ impl WorldGenerator for Cpp { let binding = Some(name); let mut r#gen = self.interface(resolve, binding, true, Some(wasm_import_module)); r#gen.interface = Some(id); - let namespace = - namespace(resolve, &TypeOwner::Interface(id), false, &r#gen.r#gen.opts); + let namespace = namespace(resolve, &TypeOwner::Interface(id), false, &*r#gen.r#gen); let docs = resolve.interfaces[id].docs.contents.as_deref(); r#gen .r#gen @@ -590,11 +602,12 @@ impl WorldGenerator for Cpp { .src .push_str(&format!("// export_interface {name:?}\n")); self.imported_interfaces.remove(&id); + self.update_instance_name(resolve, name, id); let wasm_import_module = resolve.name_world_key(name); let binding = Some(name); let mut r#gen = self.interface(resolve, binding, false, Some(wasm_import_module)); r#gen.interface = Some(id); - let namespace = namespace(resolve, &TypeOwner::Interface(id), true, &r#gen.r#gen.opts); + let namespace = namespace(resolve, &TypeOwner::Interface(id), true, &*r#gen.r#gen); let docs = resolve.interfaces[id].docs.contents.as_deref(); r#gen .r#gen @@ -624,7 +637,7 @@ impl WorldGenerator for Cpp { let wasm_import_module = resolve.name_world_key(&name); let binding = Some(name); let mut r#gen = self.interface(resolve, binding.as_ref(), true, Some(wasm_import_module)); - let namespace = namespace(resolve, &TypeOwner::World(world), false, &r#gen.r#gen.opts); + let namespace = namespace(resolve, &TypeOwner::World(world), false, &*r#gen.r#gen); for (_name, func) in funcs.iter() { if matches!(func.kind, FunctionKind::Freestanding) { @@ -644,7 +657,7 @@ impl WorldGenerator for Cpp { let name = WorldKey::Name(resolve.worlds[world].name.clone()); let binding = Some(name); let mut r#gen = self.interface(resolve, binding.as_ref(), false, None); - let namespace = namespace(resolve, &TypeOwner::World(world), true, &r#gen.r#gen.opts); + let namespace = namespace(resolve, &TypeOwner::World(world), true, &*r#gen.r#gen); for (_name, func) in funcs.iter() { if matches!(func.kind, FunctionKind::Freestanding) { @@ -779,9 +792,9 @@ impl WorldGenerator for Cpp { } // determine namespace (for the lifted C++ function) -fn namespace(resolve: &Resolve, owner: &TypeOwner, guest_export: bool, opts: &Opts) -> Vec { +fn namespace(resolve: &Resolve, owner: &TypeOwner, guest_export: bool, r#gen: &Cpp) -> Vec { let mut result = Vec::default(); - if let Some(prefix) = &opts.internal_prefix { + if let Some(prefix) = &r#gen.opts.internal_prefix { result.push(prefix.clone()); } if guest_export { @@ -790,14 +803,18 @@ fn namespace(resolve: &Resolve, owner: &TypeOwner, guest_export: bool, opts: &Op match owner { TypeOwner::World(w) => result.push(to_c_ident(&resolve.worlds[*w].name)), TypeOwner::Interface(i) => { - let iface = &resolve.interfaces[*i]; - let pkg_id = iface.package.unwrap(); - let pkg = &resolve.packages[pkg_id]; - result.push(to_c_ident(&pkg.name.namespace)); - // Use name_package_module to get version-specific package names - result.push(to_c_ident(&name_package_module(resolve, pkg_id))); - if let Some(name) = &iface.name { - result.push(to_c_ident(name)); + if let Some(instance) = r#gen.instance_names.get(i) { + result.push(to_c_ident(instance)); + } else { + let iface = &resolve.interfaces[*i]; + let pkg_id = iface.package.unwrap(); + let pkg = &resolve.packages[pkg_id]; + result.push(to_c_ident(&pkg.name.namespace)); + // Use name_package_module to get version-specific package names + result.push(to_c_ident(&name_package_module(resolve, pkg_id))); + if let Some(name) = &iface.name { + result.push(to_c_ident(name)); + } } } TypeOwner::None => (), @@ -941,7 +958,7 @@ impl CppInterfaceGenerator<'_> { .map(TypeOwner::Interface) .unwrap_or(TypeOwner::World(self.r#gen.world_id.unwrap())), )); - let mut namespace = namespace(self.resolve, &owner, guest_export, &self.r#gen.opts); + let mut namespace = namespace(self.resolve, &owner, guest_export, &*self.r#gen); let is_drop = is_special_method(func); let func_name_h = if !matches!(&func.kind, FunctionKind::Freestanding) { namespace.push(object.clone()); @@ -1294,7 +1311,7 @@ impl CppInterfaceGenerator<'_> { cifg.resolve, &owner.owner, matches!(variant, AbiVariant::GuestExport), - &cifg.r#gen.opts, + &*cifg.r#gen, ); namespace.push(owner.name.as_ref().unwrap().to_upper_camel_case()); namespace @@ -1403,7 +1420,7 @@ impl CppInterfaceGenerator<'_> { self.resolve, owner, matches!(variant, AbiVariant::GuestExport), - &self.r#gen.opts, + &*self.r#gen, ) } else { let owner = &self.resolve.types[match &func.kind { @@ -1420,7 +1437,7 @@ impl CppInterfaceGenerator<'_> { self.resolve, &owner.owner, matches!(variant, AbiVariant::GuestExport), - &self.r#gen.opts, + &*self.r#gen, ); namespace.push(owner.name.as_ref().unwrap().to_upper_camel_case()); namespace @@ -1534,7 +1551,7 @@ impl CppInterfaceGenerator<'_> { guest_export: bool, ) -> String { let ty = &self.resolve.types[id]; - let namespc = namespace(self.resolve, &ty.owner, guest_export, &self.r#gen.opts); + let namespc = namespace(self.resolve, &ty.owner, guest_export, &*self.r#gen); let mut relative = SourceWithState { namespace: Vec::from(from_namespace), ..Default::default() @@ -1882,7 +1899,7 @@ impl<'a> wit_bindgen_core::InterfaceGenerator<'a> for CppInterfaceGenerator<'a> ) { let ty = &self.resolve.types[id]; let guest_export = self.is_exported_type(ty); - let namespc = namespace(self.resolve, &ty.owner, guest_export, &self.r#gen.opts); + let namespc = namespace(self.resolve, &ty.owner, guest_export, &*self.r#gen); if self.r#gen.is_first_definition(&namespc, name) { self.r#gen.h_src.change_namespace(&namespc); @@ -1914,7 +1931,7 @@ impl<'a> wit_bindgen_core::InterfaceGenerator<'a> for CppInterfaceGenerator<'a> let store = self.r#gen.start_new_file(Some(definition)); let mut world_name = to_c_ident(&self.r#gen.world); world_name.push_str("::"); - let namespc = namespace(self.resolve, &type_.owner, !guest_import, &self.r#gen.opts); + let namespc = namespace(self.resolve, &type_.owner, !guest_import, &*self.r#gen); let pascal = name.to_upper_camel_case(); let mut user_filename = namespc.clone(); user_filename.push(pascal.clone()); @@ -2087,7 +2104,7 @@ impl<'a> wit_bindgen_core::InterfaceGenerator<'a> for CppInterfaceGenerator<'a> } else if matches!(type_.owner, TypeOwner::World(_)) { // Handle world-level resources - treat as imported resources let guest_export = false; // World-level resources are treated as imports - let namespc = namespace(self.resolve, &type_.owner, guest_export, &self.r#gen.opts); + let namespc = namespace(self.resolve, &type_.owner, guest_export, &*self.r#gen); self.r#gen.h_src.change_namespace(&namespc); let pascal = name.to_upper_camel_case(); @@ -2123,7 +2140,7 @@ impl<'a> wit_bindgen_core::InterfaceGenerator<'a> for CppInterfaceGenerator<'a> ) { let ty = &self.resolve.types[id]; let guest_export = self.is_exported_type(ty); - let namespc = namespace(self.resolve, &ty.owner, guest_export, &self.r#gen.opts); + let namespc = namespace(self.resolve, &ty.owner, guest_export, &*self.r#gen); if self.r#gen.is_first_definition(&namespc, name) { self.r#gen.h_src.change_namespace(&namespc); Self::docs(&mut self.r#gen.h_src.src, docs); @@ -2164,7 +2181,7 @@ impl<'a> wit_bindgen_core::InterfaceGenerator<'a> for CppInterfaceGenerator<'a> ) { let ty = &self.resolve.types[id]; let guest_export = self.is_exported_type(ty); - let namespc = namespace(self.resolve, &ty.owner, guest_export, &self.r#gen.opts); + let namespc = namespace(self.resolve, &ty.owner, guest_export, &*self.r#gen); if self.r#gen.is_first_definition(&namespc, name) { self.r#gen.h_src.change_namespace(&namespc); Self::docs(&mut self.r#gen.h_src.src, docs); @@ -2225,7 +2242,7 @@ impl<'a> wit_bindgen_core::InterfaceGenerator<'a> for CppInterfaceGenerator<'a> ) { let ty = &self.resolve.types[id]; let guest_export = self.is_exported_type(ty); - let namespc = namespace(self.resolve, &ty.owner, guest_export, &self.r#gen.opts); + let namespc = namespace(self.resolve, &ty.owner, guest_export, &*self.r#gen); if self.r#gen.is_first_definition(&namespc, name) { self.r#gen.h_src.change_namespace(&namespc); let pascal = name.to_pascal_case(); @@ -2253,7 +2270,7 @@ impl<'a> wit_bindgen_core::InterfaceGenerator<'a> for CppInterfaceGenerator<'a> ) { let ty = &self.resolve.types[id]; let guest_export = self.is_exported_type(ty); - let namespc = namespace(self.resolve, &ty.owner, guest_export, &self.r#gen.opts); + let namespc = namespace(self.resolve, &ty.owner, guest_export, &*self.r#gen); self.r#gen.h_src.change_namespace(&namespc); let pascal = name.to_pascal_case(); Self::docs(&mut self.r#gen.h_src.src, docs); diff --git a/crates/go/src/lib.rs b/crates/go/src/lib.rs index a6a99d4ba..faa5e5b8d 100644 --- a/crates/go/src/lib.rs +++ b/crates/go/src/lib.rs @@ -221,7 +221,8 @@ struct Go { interface_names: HashMap, interfaces: BTreeMap, export_interfaces: BTreeMap, - types: HashSet, + // Add String to the types to allow for implements of the same type in different interfaces to be generated. + types: HashSet<(String, TypeId)>, resources: HashMap, futures_and_streams: HashMap<(TypeId, bool), Option>, } @@ -251,7 +252,7 @@ impl Go { if local == owner && (exported ^ in_import) { String::new() } else { - let package = self.interface_name(resolve, owner); + let package = self.go_package_name(resolve, owner); let package = if exported { format!("export_{package}") } else { @@ -637,7 +638,7 @@ func Lift{upper_kind}{camel}(handle int32) *witTypes.{upper_kind}Reader[{payload } else { format!( "{}_", - self.interface_name( + self.go_package_name( resolve, Some( &self @@ -738,15 +739,17 @@ impl WorldGenerator for Go { id: InterfaceId, _files: &mut Files, ) -> Result<()> { - if let WorldKey::Name(_) = name { - self.interface_names.insert(id, name.clone()); - } + self.interface_names.insert(id, name.clone()); + let go_package_name = self.go_package_name(resolve, Some(name)); let mut data = { let mut generator = InterfaceGenerator::new(self, resolve, Some((id, name)), true); for (name, ty) in resolve.interfaces[id].types.iter() { - if !generator.generator.types.contains(ty) { - generator.generator.types.insert(*ty); + if generator + .generator + .types + .insert((go_package_name.clone(), *ty)) + { generator.define_type(name, *ty); } } @@ -757,7 +760,7 @@ impl WorldGenerator for Go { data.extend(self.import(resolve, func, Some(name))); } self.interfaces - .entry(self.interface_name(resolve, Some(name))) + .entry(go_package_name) .or_default() .extend(data); @@ -776,7 +779,7 @@ impl WorldGenerator for Go { data.extend(self.import(resolve, func, None)); } self.interfaces - .entry(self.interface_name(resolve, None)) + .entry(self.go_package_name(resolve, None)) .or_default() .extend(data); } @@ -788,30 +791,32 @@ impl WorldGenerator for Go { id: InterfaceId, _files: &mut Files, ) -> Result<()> { - if let WorldKey::Name(_) = name { - self.interface_names.insert(id, name.clone()); - } + self.interface_names.insert(id, name.clone()); + let go_package_name = self.go_package_name(resolve, Some(name)); for (type_name, ty) in &resolve.interfaces[id].types { let exported = matches!(resolve.types[*ty].kind, TypeDefKind::Resource) || self.has_exported_resource(resolve, Type::Id(*ty)); let mut generator = InterfaceGenerator::new(self, resolve, Some((id, name)), false); - if exported || !generator.generator.types.contains(ty) { - generator.generator.types.insert(*ty); + if generator + .generator + .types + .insert((go_package_name.clone(), *ty)) + || exported + { generator.define_type(type_name, *ty); } let data = generator.into(); - let name = self.interface_name(resolve, Some(name)); if exported { &mut self.export_interfaces } else { &mut self.interfaces } - .entry(name) + .entry(go_package_name.clone()) .or_default() .extend(data); } @@ -845,18 +850,15 @@ impl WorldGenerator for Go { types: &[(&str, TypeId)], _files: &mut Files, ) { + let package = self.go_package_name(resolve, None); let mut generator = InterfaceGenerator::new(self, resolve, None, true); for (name, ty) in types { - if !generator.generator.types.contains(ty) { - generator.generator.types.insert(*ty); + if generator.generator.types.insert((package.clone(), *ty)) { generator.define_type(name, *ty); } } let data = generator.into(); - self.interfaces - .entry(self.interface_name(resolve, None)) - .or_default() - .extend(data); + self.interfaces.entry(package).or_default().extend(data); } fn finish(&mut self, resolve: &Resolve, id: WorldId, files: &mut Files) -> Result<()> { @@ -1377,7 +1379,7 @@ func wasm_export_post_return_{name}(result {results}) {{ let results = self.func_results(resolve, func, interface, false, &mut imports); self.export_interfaces - .entry(self.interface_name(resolve, interface)) + .entry(self.go_package_name(resolve, interface)) .or_default() .extend(InterfaceData { code: format!( @@ -1496,7 +1498,7 @@ func wasm_export_{name}({params}) {results} {{ &func.name, ); - let name = self.interface_name(resolve, interface); + let name = self.go_package_name(resolve, interface); if in_import || !exported { &mut self.interfaces } else { @@ -1522,7 +1524,7 @@ func wasm_export_{name}({params}) {results} {{ }) } - fn interface_name(&self, resolve: &Resolve, interface: Option<&WorldKey>) -> String { + fn go_package_name(&self, resolve: &Resolve, interface: Option<&WorldKey>) -> String { match interface { Some(WorldKey::Name(name)) => name.to_snake_case(), Some(WorldKey::Interface(id)) => { @@ -1557,7 +1559,7 @@ func wasm_export_{name}({params}) {results} {{ interface: Option<&WorldKey>, func: &Function, ) -> String { - let prefix = self.interface_name(resolve, interface); + let prefix = self.go_package_name(resolve, interface); let name = func.name.to_snake_case().replace('.', "_"); format!("{prefix}_{name}") @@ -1853,7 +1855,7 @@ for index := 0; index < int({length}); index++ {{ let name = func.item_name().to_upper_camel_case(); let package = format!( "export_{}", - self.generator.interface_name(resolve, self.interface) + self.generator.go_package_name(resolve, self.interface) ); let call = match &func.kind { diff --git a/crates/guest-rust/macro/src/lib.rs b/crates/guest-rust/macro/src/lib.rs index d0393a834..78a1197ad 100644 --- a/crates/guest-rust/macro/src/lib.rs +++ b/crates/guest-rust/macro/src/lib.rs @@ -237,7 +237,7 @@ fn parse_source( } } } - pkgs.truncate(0); + pkgs.clear(); pkgs.push(resolve.push_str("macro-input", s)?); } Some(Source::Paths(p)) => parse(p)?, diff --git a/tests/codegen/issue1642.wit b/tests/codegen/issue1642.wit new file mode 100644 index 000000000..1e74494ed --- /dev/null +++ b/tests/codegen/issue1642.wit @@ -0,0 +1,28 @@ +package foo:foo; + +interface store { + variant error { + no-such-store, + access-denied, + other(string), + } + + record key-response { + keys: list, + cursor: option, + } + + resource bucket { + open: static func(identifier: string) -> result; + get: func(key: string) -> result>, error>; + list-keys: func(cursor: option) -> result; + } +} + +world the-world { + // Import the same type under separate names to test implements. + import primary: store; + import secondary: store; + // Ensure non-named imports still works. + import store; +} From 0dd13858baf0ff55ac081e3908205c52890f0592 Mon Sep 17 00:00:00 2001 From: zihang Date: Mon, 13 Jul 2026 16:11:15 +0800 Subject: [PATCH 11/23] fix(moonbit): update flavorful buffer type --- tests/runtime/flavorful/test.mbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/runtime/flavorful/test.mbt b/tests/runtime/flavorful/test.mbt index f4b230ef4..be0294ab1 100644 --- a/tests/runtime/flavorful/test.mbt +++ b/tests/runtime/flavorful/test.mbt @@ -65,7 +65,7 @@ pub fn errno_result() -> Result[Unit, MyErrno] { } } -pub fn write_utf8_char(buf : @buffer.T, value : Char) -> Unit { +pub fn write_utf8_char(buf : @buffer.Buffer, value : Char) -> Unit { let code = value.to_uint() match code { _..<0x80 => { From ed2a77afb8d7060ae1f587d330e37b5057e7a214 Mon Sep 17 00:00:00 2001 From: zihang Date: Tue, 14 Jul 2026 11:54:43 +0800 Subject: [PATCH 12/23] feat(moonbit): preserve kebab-case package names --- crates/moonbit/src/pkg.rs | 59 +++++++++++++++---- tests/runtime/demo/test.mbt | 2 +- tests/runtime/flavorful/test.mbt | 2 +- tests/runtime/future-cancel-read/test.mbt | 3 +- tests/runtime/future-cancel-write/test.mbt | 2 +- .../future-close-after-coming-back/test.mbt | 3 +- tests/runtime/list-in-variant/runner.mbt | 20 +++---- tests/runtime/lists/test.mbt | 2 +- tests/runtime/many-arguments/test.mbt | 2 +- tests/runtime/map/test.mbt | 2 +- tests/runtime/numbers/test.mbt | 2 +- tests/runtime/options/test.mbt | 2 +- tests/runtime/records/test.mbt | 2 +- tests/runtime/results/leaf.mbt | 2 +- tests/runtime/simple-future/test.mbt | 2 +- tests/runtime/simple-stream-payload/test.mbt | 2 +- tests/runtime/simple-stream/test.mbt | 3 +- tests/runtime/strings/test.mbt | 2 +- tests/runtime/variants/test.mbt | 2 +- 19 files changed, 74 insertions(+), 42 deletions(-) diff --git a/crates/moonbit/src/pkg.rs b/crates/moonbit/src/pkg.rs index e603ff7b6..ccae5ac18 100644 --- a/crates/moonbit/src/pkg.rs +++ b/crates/moonbit/src/pkg.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use heck::{ToLowerCamelCase, ToSnakeCase, ToUpperCamelCase}; +use heck::{ToSnakeCase, ToUpperCamelCase}; use wit_bindgen_core::{ Ns, dealias, wit_parser::{ @@ -39,9 +39,7 @@ impl PkgResolver { if let Some(alias) = imports.packages.get(name) { format!("@{alias}.") } else { - let alias = imports - .ns - .tmp(&name.split(".").last().unwrap().to_lower_camel_case()); + let alias = imports.ns.tmp(name.split(".").last().unwrap()); imports .packages .entry(name.to_string()) @@ -352,7 +350,7 @@ impl PkgResolver { } pub(crate) fn world_name(resolve: &Resolve, world: WorldId) -> String { - format!("world.{}", resolve.worlds[world].name.to_lower_camel_case()) + format!("world.{}", resolve.worlds[world].name) } pub(crate) fn interface_name(resolve: &Resolve, name: &WorldKey) -> String { @@ -367,17 +365,12 @@ impl PkgResolver { let name = match name { WorldKey::Name(name) => name, WorldKey::Interface(id) => resolve.interfaces[*id].name.as_ref().unwrap(), - } - .to_lower_camel_case(); + }; format!( "interface.{}{name}", if let Some(name) = &pkg { - format!( - "{}.{}.", - name.namespace.to_moonbit_ident(), - name.name.to_moonbit_ident() - ) + format!("{}.{}.", name.namespace, name.name) } else { String::new() } @@ -385,6 +378,48 @@ impl PkgResolver { } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn package_names_and_aliases_preserve_kebab_case() { + let mut resolve = Resolve::default(); + let package = resolve + .push_str( + "test.wit", + r#" + package my:test; + + interface leaf-interface {} + + world http-proxy { + import leaf-interface; + } + "#, + ) + .unwrap(); + let world = resolve + .select_world(&[package], Some("http-proxy")) + .unwrap(); + let (key, _) = resolve.worlds[world].imports.iter().next().unwrap(); + + let interface = PkgResolver::interface_name(&resolve, key); + assert_eq!(interface, "interface.my.test.leaf-interface"); + assert_eq!(PkgResolver::world_name(&resolve, world), "world.http-proxy"); + + let mut packages = PkgResolver::default(); + assert_eq!( + packages.qualify_package("world.http-proxy", &interface), + "@leaf-interface." + ); + assert_eq!( + packages.package_import["world.http-proxy"].packages[&interface], + "leaf-interface" + ); + } +} + pub(crate) trait ToMoonBitIdent: ToOwned { fn to_moonbit_ident(&self) -> Self::Owned; } diff --git a/tests/runtime/demo/test.mbt b/tests/runtime/demo/test.mbt index 3b7ba8f8c..90c565866 100644 --- a/tests/runtime/demo/test.mbt +++ b/tests/runtime/demo/test.mbt @@ -1,5 +1,5 @@ //@ [lang] -//@ path = 'gen/interface/a/b/theTest/stub.mbt' +//@ path = 'gen/interface/a/b/the-test/stub.mbt' ///| pub fn x() -> Unit { diff --git a/tests/runtime/flavorful/test.mbt b/tests/runtime/flavorful/test.mbt index be0294ab1..ba178359a 100644 --- a/tests/runtime/flavorful/test.mbt +++ b/tests/runtime/flavorful/test.mbt @@ -1,5 +1,5 @@ //@ [lang] -//@ path = 'gen/interface/test_/flavorful/toTest/stub.mbt' +//@ path = 'gen/interface/test/flavorful/to-test/stub.mbt' ///| pub fn f_list_in_record1(_a : ListInRecord1) -> Unit { diff --git a/tests/runtime/future-cancel-read/test.mbt b/tests/runtime/future-cancel-read/test.mbt index bdb83de7b..5354c920e 100644 --- a/tests/runtime/future-cancel-read/test.mbt +++ b/tests/runtime/future-cancel-read/test.mbt @@ -1,5 +1,5 @@ //@ [lang] -//@ path = 'gen/interface/my/test_/i/stub.mbt' +//@ path = 'gen/interface/my/test/i/stub.mbt' ///| pub async fn cancel_before_read( @@ -35,4 +35,3 @@ pub async fn start_read_then_cancel( fn() { signal.read() catch { _ => raise @ffi.Cancelled::Cancelled } }, ) } - diff --git a/tests/runtime/future-cancel-write/test.mbt b/tests/runtime/future-cancel-write/test.mbt index 2b1f21e8d..ac2349645 100644 --- a/tests/runtime/future-cancel-write/test.mbt +++ b/tests/runtime/future-cancel-write/test.mbt @@ -1,5 +1,5 @@ //@ [lang] -//@ path = 'gen/interface/my/test_/i/stub.mbt' +//@ path = 'gen/interface/my/test/i/stub.mbt' ///| pub fn take_then_drop(x : @ffi.FutureReader[String]) -> Unit { diff --git a/tests/runtime/future-close-after-coming-back/test.mbt b/tests/runtime/future-close-after-coming-back/test.mbt index 9f6250656..a11ed06d6 100644 --- a/tests/runtime/future-close-after-coming-back/test.mbt +++ b/tests/runtime/future-close-after-coming-back/test.mbt @@ -1,7 +1,6 @@ //@ [lang] -//@ path = 'gen/interface/a/b/theTest/stub.mbt' +//@ path = 'gen/interface/a/b/the-test/stub.mbt' pub fn f(_param : @ffi.FutureReader[Unit]) -> @ffi.FutureReader[Unit] { _param } - diff --git a/tests/runtime/list-in-variant/runner.mbt b/tests/runtime/list-in-variant/runner.mbt index 00a92dc48..24228b707 100644 --- a/tests/runtime/list-in-variant/runner.mbt +++ b/tests/runtime/list-in-variant/runner.mbt @@ -1,36 +1,36 @@ //@ [lang] //@ path = 'gen/world/runner/stub.mbt' -//@ pkg_config = """{ "import": ["test/list-in-variant/interface/test_/list_in_variant/toTest"] }""" +//@ pkg_config = """{ "import": ["test/list-in-variant/interface/test/list-in-variant/to-test"] }""" ///| pub fn run() -> Unit { // list-in-option - let r1 = @toTest.list_in_option(Some(["hello", "world"])) + let r1 = @to-test.list_in_option(Some(["hello", "world"])) guard r1 == "hello,world" - let r2 = @toTest.list_in_option(None) + let r2 = @to-test.list_in_option(None) guard r2 == "none" // list-in-variant - let r3 = @toTest.list_in_variant(@toTest.PayloadOrEmpty::WithData(["foo", "bar", "baz"])) + let r3 = @to-test.list_in_variant(@to-test.PayloadOrEmpty::WithData(["foo", "bar", "baz"])) guard r3 == "foo,bar,baz" - let r4 = @toTest.list_in_variant(@toTest.PayloadOrEmpty::Empty) + let r4 = @to-test.list_in_variant(@to-test.PayloadOrEmpty::Empty) guard r4 == "empty" // list-in-result - let r5 = @toTest.list_in_result(Ok(["a", "b", "c"])) + let r5 = @to-test.list_in_result(Ok(["a", "b", "c"])) guard r5 == "a,b,c" - let r6 = @toTest.list_in_result(Err("oops")) + let r6 = @to-test.list_in_result(Err("oops")) guard r6 == "err:oops" // list-in-option-with-return (Bug 1 + Bug 2) - let s1 = @toTest.list_in_option_with_return(Some(["hello", "world"])) + let s1 = @to-test.list_in_option_with_return(Some(["hello", "world"])) guard s1.count == 2U guard s1.label == "hello,world" - let s2 = @toTest.list_in_option_with_return(None) + let s2 = @to-test.list_in_option_with_return(None) guard s2.count == 0U guard s2.label == "none" // top-level-list (contrast) - let r7 = @toTest.top_level_list(["x", "y", "z"]) + let r7 = @to-test.top_level_list(["x", "y", "z"]) guard r7 == "x,y,z" } diff --git a/tests/runtime/lists/test.mbt b/tests/runtime/lists/test.mbt index 6aa24a3fb..a4c19f926 100644 --- a/tests/runtime/lists/test.mbt +++ b/tests/runtime/lists/test.mbt @@ -1,5 +1,5 @@ //@ [lang] -//@ path = 'gen/interface/test_/lists/toTest/stub.mbt' +//@ path = 'gen/interface/test/lists/to-test/stub.mbt' ///| pub fn empty_list_param(a : FixedArray[Byte]) -> Unit { diff --git a/tests/runtime/many-arguments/test.mbt b/tests/runtime/many-arguments/test.mbt index ce19472f8..ff67014f4 100644 --- a/tests/runtime/many-arguments/test.mbt +++ b/tests/runtime/many-arguments/test.mbt @@ -1,5 +1,5 @@ //@ [lang] -//@ path = 'gen/interface/test_/many_arguments/toTest/stub.mbt' +//@ path = 'gen/interface/test/many-arguments/to-test/stub.mbt' ///| pub fn many_arguments( _a1 : UInt64, diff --git a/tests/runtime/map/test.mbt b/tests/runtime/map/test.mbt index 6c24ca5c8..4113d78af 100644 --- a/tests/runtime/map/test.mbt +++ b/tests/runtime/map/test.mbt @@ -1,5 +1,5 @@ //@ [lang] -//@ path = 'gen/interface/test_/maps/toTest/stub.mbt' +//@ path = 'gen/interface/test/maps/to-test/stub.mbt' ///| pub fn named_roundtrip(a : Map[UInt, String]) -> Map[String, UInt] { diff --git a/tests/runtime/numbers/test.mbt b/tests/runtime/numbers/test.mbt index 2f2e94d6a..bd1679c7b 100644 --- a/tests/runtime/numbers/test.mbt +++ b/tests/runtime/numbers/test.mbt @@ -1,5 +1,5 @@ //@ [lang] -//@ path = 'gen/interface/test_/numbers/numbers/stub.mbt' +//@ path = 'gen/interface/test/numbers/numbers/stub.mbt' ///| pub fn roundtrip_u8(a : Byte) -> Byte { diff --git a/tests/runtime/options/test.mbt b/tests/runtime/options/test.mbt index c2bbca782..b3c974e85 100644 --- a/tests/runtime/options/test.mbt +++ b/tests/runtime/options/test.mbt @@ -1,5 +1,5 @@ //@ [lang] -//@ path = 'gen/interface/test_/options/toTest/stub.mbt' +//@ path = 'gen/interface/test/options/to-test/stub.mbt' ///| pub fn option_none_param(a : String?) -> Unit { diff --git a/tests/runtime/records/test.mbt b/tests/runtime/records/test.mbt index eabb0a1e1..e9e423569 100644 --- a/tests/runtime/records/test.mbt +++ b/tests/runtime/records/test.mbt @@ -1,5 +1,5 @@ //@ [lang] -//@ path = 'gen/interface/test_/records/toTest/stub.mbt' +//@ path = 'gen/interface/test/records/to-test/stub.mbt' ///| pub fn multiple_results() -> (Byte, UInt) { diff --git a/tests/runtime/results/leaf.mbt b/tests/runtime/results/leaf.mbt index 40fc74fd1..860bba28f 100644 --- a/tests/runtime/results/leaf.mbt +++ b/tests/runtime/results/leaf.mbt @@ -1,5 +1,5 @@ //@ [lang] -//@ path = 'gen/interface/test_/results/test/stub.mbt' +//@ path = 'gen/interface/test/results/test/stub.mbt' ///| pub fn string_error(_a : Float) -> Result[Float, String] { diff --git a/tests/runtime/simple-future/test.mbt b/tests/runtime/simple-future/test.mbt index f4a0e49f0..d4fc4b7a0 100644 --- a/tests/runtime/simple-future/test.mbt +++ b/tests/runtime/simple-future/test.mbt @@ -1,5 +1,5 @@ //@ [lang] -//@ path = 'gen/interface/my/test_/i/stub.mbt' +//@ path = 'gen/interface/my/test/i/stub.mbt' ///| pub async fn read_future( diff --git a/tests/runtime/simple-stream-payload/test.mbt b/tests/runtime/simple-stream-payload/test.mbt index 31eb79be8..ef38dd6a6 100644 --- a/tests/runtime/simple-stream-payload/test.mbt +++ b/tests/runtime/simple-stream-payload/test.mbt @@ -1,5 +1,5 @@ //@ [lang] -//@ path = 'gen/interface/my/test_/i/stub.mbt' +//@ path = 'gen/interface/my/test/i/stub.mbt' pub async fn read_stream(x : @ffi.StreamReader[Byte]) -> Unit raise { let task = @ffi.current_task() diff --git a/tests/runtime/simple-stream/test.mbt b/tests/runtime/simple-stream/test.mbt index c227a69c4..c2f38c7a6 100644 --- a/tests/runtime/simple-stream/test.mbt +++ b/tests/runtime/simple-stream/test.mbt @@ -1,5 +1,5 @@ //@ [lang] -//@ path = 'gen/interface/my/test_/i/stub.mbt' +//@ path = 'gen/interface/my/test/i/stub.mbt' pub async fn read_stream(x : @ffi.StreamReader[Unit]) -> Unit noraise { let task = @ffi.current_task() @@ -12,4 +12,3 @@ pub async fn read_stream(x : @ffi.StreamReader[Unit]) -> Unit noraise { let _ = x.read(buffer, 2) catch { _ => raise @ffi.Cancelled::Cancelled } }) } - diff --git a/tests/runtime/strings/test.mbt b/tests/runtime/strings/test.mbt index 66095b056..e5956de65 100644 --- a/tests/runtime/strings/test.mbt +++ b/tests/runtime/strings/test.mbt @@ -1,5 +1,5 @@ //@ [lang] -//@ path = 'gen/interface/test_/strings/toTest/stub.mbt' +//@ path = 'gen/interface/test/strings/to-test/stub.mbt' ///| pub fn take_basic(s : String) -> Unit { diff --git a/tests/runtime/variants/test.mbt b/tests/runtime/variants/test.mbt index 88ab721a4..ecb4a9b98 100644 --- a/tests/runtime/variants/test.mbt +++ b/tests/runtime/variants/test.mbt @@ -1,5 +1,5 @@ //@ [lang] -//@ path = 'gen/interface/test_/variants/toTest/stub.mbt' +//@ path = 'gen/interface/test/variants/to-test/stub.mbt' ///| pub fn roundtrip_option(a : Float?) -> Byte? { From 3869c6fb2bddca139bdcb652e80fa5aa3ca9033e Mon Sep 17 00:00:00 2001 From: zihang Date: Fri, 17 Jul 2026 16:03:50 +0800 Subject: [PATCH 13/23] feat(moonbit): implement component async bindings --- crates/moonbit/CONTEXT.md | 34 + .../adr/0001-async-ffi-boundary-conversion.md | 141 + .../docs/adr/0002-local-future-promise.md | 43 + crates/moonbit/docs/async-design.md | 367 +++ crates/moonbit/docs/async-glossary.md | 488 +++ crates/moonbit/src/async/async_abi.mbt | 69 +- crates/moonbit/src/async/async_primitive.mbt | 6 +- crates/moonbit/src/async/cond_var.mbt | 72 + crates/moonbit/src/async/coroutine.mbt | 85 +- crates/moonbit/src/async/ev.mbt | 707 +++- crates/moonbit/src/async/moon.pkg.json | 10 +- crates/moonbit/src/async/mutex.mbt | 44 + crates/moonbit/src/async/promise.mbt | 200 ++ crates/moonbit/src/async/scheduler.mbt | 62 +- crates/moonbit/src/async/semaphore.mbt | 94 + crates/moonbit/src/async/task.mbt | 14 +- crates/moonbit/src/async/task_group.mbt | 114 +- crates/moonbit/src/async/trait.mbt | 914 +++++- crates/moonbit/src/async_support.rs | 2855 ++++++++++++++--- crates/moonbit/src/ffi/async_primitive.mbt | 2 +- crates/moonbit/src/ffi/future.mbt | 540 ---- crates/moonbit/src/lib.rs | 1602 ++++++--- crates/moonbit/src/pkg.rs | 10 +- crates/test/src/moonbit.rs | 8 +- tests/runtime/cancel-import/test.mbt | 19 + tests/runtime/future-cancel-read/test.mbt | 40 +- tests/runtime/future-cancel-write/test.mbt | 14 +- .../future-close-after-coming-back/test.mbt | 2 +- .../moonbit/async-import-cancel/gate.mbt | 28 + .../moonbit/async-import-cancel/runner.mbt | 313 ++ .../moonbit/async-import-cancel/test.rs | 181 ++ .../moonbit/async-import-cancel/test.wit | 66 + .../concurrent-export-isolation/runner.mbt | 133 + .../concurrent-export-isolation/test.mbt | 127 + .../concurrent-export-isolation/test.wit | 25 + .../deps/wasi-cli-0.3.0/package.wit | 27 + .../deps/wasi-clocks-0.3.0/package.wit | 42 + .../deps/wasi-http-0.3.0/package.wit | 509 +++ .../deps/wasi-random-0.3.0/package.wit | 17 + .../http-background-hook/runner-invalid.mbt | 51 + .../moonbit/http-background-hook/runner.mbt | 49 + .../moonbit/http-background-hook/test.mbt | 82 + .../moonbit/http-background-hook/test.wit | 12 + .../moonbit/http-background-hook/wkg.lock | 12 + .../http-body-trailers/deps/clocks.wit | 7 + .../moonbit/http-body-trailers/deps/http.wit | 460 +++ .../moonbit/http-body-trailers/runner.mbt | 47 + .../moonbit/http-body-trailers/test.mbt | 66 + .../moonbit/http-body-trailers/test.wit | 14 + .../moonbit/local-async-primitives/runner.mbt | 9 + .../moonbit/local-async-primitives/test.mbt | 311 ++ .../moonbit/local-async-primitives/test.wit | 15 + .../moonbit/nested-future-stream/runner.rs | 144 + .../moonbit/nested-future-stream/test.mbt | 149 + .../moonbit/nested-future-stream/test.wit | 30 + .../moonbit/resource-payloads/leaf.mbt | 213 ++ .../runtime/moonbit/resource-payloads/leaf.rs | 144 + .../moonbit/resource-payloads/runner.mbt | 321 ++ .../moonbit/resource-payloads/test.mbt | 714 +++++ .../moonbit/resource-payloads/test.wit | 75 + .../stream-write-cancel/holder-service.rs | 90 + .../moonbit/stream-write-cancel/runner.rs | 43 + .../moonbit/stream-write-cancel/test.mbt | 96 + .../moonbit/stream-write-cancel/test.wit | 44 + .../moonbit/wasi-cli-p3-stdout/deps/cli.wit | 26 + .../moonbit/wasi-cli-p3-stdout/runner.mbt | 9 + .../moonbit/wasi-cli-p3-stdout/test.mbt | 28 + .../moonbit/wasi-cli-p3-stdout/test.wit | 17 + tests/runtime/simple-future/test.mbt | 19 +- .../simple-import-params-results/test.mbt | 24 +- tests/runtime/simple-stream-payload/test.mbt | 36 +- tests/runtime/simple-stream/test.mbt | 17 +- 72 files changed, 11688 insertions(+), 1710 deletions(-) create mode 100644 crates/moonbit/CONTEXT.md create mode 100644 crates/moonbit/docs/adr/0001-async-ffi-boundary-conversion.md create mode 100644 crates/moonbit/docs/adr/0002-local-future-promise.md create mode 100644 crates/moonbit/docs/async-design.md create mode 100644 crates/moonbit/docs/async-glossary.md create mode 100644 crates/moonbit/src/async/cond_var.mbt create mode 100644 crates/moonbit/src/async/mutex.mbt create mode 100644 crates/moonbit/src/async/promise.mbt create mode 100644 crates/moonbit/src/async/semaphore.mbt delete mode 100644 crates/moonbit/src/ffi/future.mbt create mode 100644 tests/runtime/cancel-import/test.mbt create mode 100644 tests/runtime/moonbit/async-import-cancel/gate.mbt create mode 100644 tests/runtime/moonbit/async-import-cancel/runner.mbt create mode 100644 tests/runtime/moonbit/async-import-cancel/test.rs create mode 100644 tests/runtime/moonbit/async-import-cancel/test.wit create mode 100644 tests/runtime/moonbit/concurrent-export-isolation/runner.mbt create mode 100644 tests/runtime/moonbit/concurrent-export-isolation/test.mbt create mode 100644 tests/runtime/moonbit/concurrent-export-isolation/test.wit create mode 100644 tests/runtime/moonbit/http-background-hook/deps/wasi-cli-0.3.0/package.wit create mode 100644 tests/runtime/moonbit/http-background-hook/deps/wasi-clocks-0.3.0/package.wit create mode 100644 tests/runtime/moonbit/http-background-hook/deps/wasi-http-0.3.0/package.wit create mode 100644 tests/runtime/moonbit/http-background-hook/deps/wasi-random-0.3.0/package.wit create mode 100644 tests/runtime/moonbit/http-background-hook/runner-invalid.mbt create mode 100644 tests/runtime/moonbit/http-background-hook/runner.mbt create mode 100644 tests/runtime/moonbit/http-background-hook/test.mbt create mode 100644 tests/runtime/moonbit/http-background-hook/test.wit create mode 100644 tests/runtime/moonbit/http-background-hook/wkg.lock create mode 100644 tests/runtime/moonbit/http-body-trailers/deps/clocks.wit create mode 100644 tests/runtime/moonbit/http-body-trailers/deps/http.wit create mode 100644 tests/runtime/moonbit/http-body-trailers/runner.mbt create mode 100644 tests/runtime/moonbit/http-body-trailers/test.mbt create mode 100644 tests/runtime/moonbit/http-body-trailers/test.wit create mode 100644 tests/runtime/moonbit/local-async-primitives/runner.mbt create mode 100644 tests/runtime/moonbit/local-async-primitives/test.mbt create mode 100644 tests/runtime/moonbit/local-async-primitives/test.wit create mode 100644 tests/runtime/moonbit/nested-future-stream/runner.rs create mode 100644 tests/runtime/moonbit/nested-future-stream/test.mbt create mode 100644 tests/runtime/moonbit/nested-future-stream/test.wit create mode 100644 tests/runtime/moonbit/resource-payloads/leaf.mbt create mode 100644 tests/runtime/moonbit/resource-payloads/leaf.rs create mode 100644 tests/runtime/moonbit/resource-payloads/runner.mbt create mode 100644 tests/runtime/moonbit/resource-payloads/test.mbt create mode 100644 tests/runtime/moonbit/resource-payloads/test.wit create mode 100644 tests/runtime/moonbit/stream-write-cancel/holder-service.rs create mode 100644 tests/runtime/moonbit/stream-write-cancel/runner.rs create mode 100644 tests/runtime/moonbit/stream-write-cancel/test.mbt create mode 100644 tests/runtime/moonbit/stream-write-cancel/test.wit create mode 100644 tests/runtime/moonbit/wasi-cli-p3-stdout/deps/cli.wit create mode 100644 tests/runtime/moonbit/wasi-cli-p3-stdout/runner.mbt create mode 100644 tests/runtime/moonbit/wasi-cli-p3-stdout/test.mbt create mode 100644 tests/runtime/moonbit/wasi-cli-p3-stdout/test.wit diff --git a/crates/moonbit/CONTEXT.md b/crates/moonbit/CONTEXT.md new file mode 100644 index 000000000..8add75ee5 --- /dev/null +++ b/crates/moonbit/CONTEXT.md @@ -0,0 +1,34 @@ +# MoonBit Bindings Context + +This context records shared language for the MoonBit binding generator. Async +future and stream vocabulary is split into [Async Glossary](docs/async-glossary.md). + +## Language + +**MoonBit binding**: +Generated MoonBit source that exposes WIT imports to MoonBit code or adapts +MoonBit exports to the component ABI. + +**Component adapter path**: +The current implementation path where MoonBit emits core wasm and `wasm-tools` +converts it into a component using adapter imports and exports. +_Avoid_: direct component generation + +## Async Design + +- [Async design contract](docs/async-design.md) +- [Async glossary](docs/async-glossary.md) +- [ADR 0001: FFI-boundary conversion](docs/adr/0001-async-ffi-boundary-conversion.md) +- [ADR 0002: local Future/Promise pair](docs/adr/0002-local-future-promise.md) + +The implementation targets the official upstream generator architecture. WIT +`future` and `stream` remain distinct from local MoonBit `Future` and `Stream`; +generated code converts them only at concrete FFI positions whose intrinsic +names are supplied by `wit-parser`. Local `Future::new()` returns a +MoonBit-only Future/Promise pair, and local `Semaphore` coordinates coroutines; +neither can select or own a component endpoint from `T`. Async support is always +available in the MoonBit generator, but endpoint-free synchronous worlds do not +emit its runtime or wrappers. Component endpoint wrappers are +generated-code-only, enforce a single in-flight operation, and retain operation +buffers until cancellation or completion is observed. Each top-level component +task has its own waitable set and scheduler state. diff --git a/crates/moonbit/docs/adr/0001-async-ffi-boundary-conversion.md b/crates/moonbit/docs/adr/0001-async-ffi-boundary-conversion.md new file mode 100644 index 000000000..1cc18ec12 --- /dev/null +++ b/crates/moonbit/docs/adr/0001-async-ffi-boundary-conversion.md @@ -0,0 +1,141 @@ +# Keep MoonBit async values detached from component futures and streams + +Status: accepted; implemented in the current branch + +MoonBit `Future[T]` handles and local `Stream[T]` values are over arbitrary +MoonBit types, while component `future` and `stream` are ABI values over +WIT-representable payloads. MoonBit async bindings will keep those concepts +separate: generated FFI-boundary code converts between local async values and +component endpoints only at concrete WIT positions where the endpoint operations +and payload lift/lower code are known. + +For MVP, component `future` maps to a one-shot `Future[T]` handle. The handle +represents a ready value or an owned local source computation with +value-discarding drop cleanup. For an incoming component future, generated code +supplies a source closure that captures the raw readable handle and directly +binds the concrete WIT position's read/cancel/drop intrinsics. The generic local +type has no CM-specific variant or operation table. A public local `Promise[T]` +may settle the same Future state, but it is not a component writable endpoint. + +## Consequences + +- `Stream::new()` creates local MoonBit values only. +- `Future::new()` creates a local `Future[T]` / `Promise[T]` pair only. It does + not select or invoke a component `future.new` intrinsic. +- Endpoint operation tables are not part of the target design. Generated + position-specific helpers call canonical intrinsics directly and keep raw + handle state inside generated source closures or producer tasks. +- Component intrinsic module and field names are generated through + `wit-parser`'s `WasmImport::{FutureIntrinsic, StreamIntrinsic}` API. The + MoonBit generator does not reproduce position indices, export prefixes, + unit-payload names, or async-lower prefixes by string formatting. +- The Rust generator boundary owns a recursive conversion plan for each + function. Ordinary lift/lower code requests position-specific helpers from + that plan instead of naming runtime endpoint types or type-shaped tables. +- The existing async generator boundary is retained. The static recursive + rewrite happens behind it without changing ordinary lift/lower ownership or + the endpoint-free sync path. +- User-facing bindings expose local `Future[T]` handles and local `Stream[T]` + for ordinary async composition. +- A nested WIT shape such as `future>>` maps to + `Future[Future[Stream[T]]]`. Only the current layer's readable handle appears + at each canonical payload stage. Generated recursive lift/lower functions bind + each layer to its own function-position intrinsic and apply generated + commit/reject dispositions at every transfer boundary. +- Recursive lower retains its canonical buffer and prepared producer state until + the transfer reports a disposition. Commit starts producer work only after + ownership transfers. Stream progress commits the accepted prefix and retries + the same lowered suffix; an abandoned suffix is rejected exactly once. +- A freshly-created component future is not fully rollbackable. Canonical ABI + permits its writable end to be dropped only after a write succeeds or a write + reports that the reader was dropped. If an outer transfer rejects a nested + future readable, generated code drops that readable but must still drive the + paired writer with the local future value until its write observes `dropped`. + A cancelled write is not settlement and is retried while the component task + remains alive. +- Stream batches whose element type recursively contains a future use a + one-element staging window for MVP. This bounds settlement obligations created + before downstream acceptance without changing the public stream API. +- Generated stream producers remain prepared when their component pair is + created. Commit starts normal pumping. Parent rejection drops both + untransferred component ends and performs state-aware local rejection: incoming + component sources close immediately, buffered values use configured cleanup or + generated cleanup as fallback, and an unstarted local producer is discarded + without executing user code. `Stream::produce` accepts an optional + `on_unstarted_drop` callback for resources captured by that branch and a + separate per-element cleanup for values written after it starts. +- Canonical `backpressure.inc/dec` controls admission of new async component + tasks. It is not tied to future/stream bridge lifetime and is not called + implicitly by the MoonBit runtime. Endpoint read/write suspension provides + data-flow backpressure independently. +- Async import argument settlement distinguishes `cancelled-before-started` + from every state in which the callee may have started. The former recursively + rejects owned resources and endpoints; the latter only reclaims guest-owned + canonical list allocations. +- Local future MVP has consuming `Future::get()` and value-discarding async + `Future::drop()`. Strong cancellation belongs to `Task` and `TaskGroup`, not + to a future-specific result type. `Future::drop()` is explicit cleanup, not a + direct alias for component `future.drop-*`, and futures that may discard + completed WIT payloads carry generated payload cleanup logic. +- Outgoing component `future` producer tasks must settle their raw writable + handle by writing a real value or by attempting the write and observing reader + drop. The MVP does not fabricate default values to satisfy unwritten futures. + If user code produces a ready `T`, generated code may write immediately; if + user code returns a pending `Future[T]`, the bridge exists before the CM + `future.write` operation starts because the component boundary already needs a + readable end to return. +- After an outgoing component `future` readable end is committed, or after a + parent transfer rejects and locally drops it, the bridge shields producer work + from ordinary task/subtask cancellation. Component-task cancellation is + cooperative: it may resolve the cancelled call with `task.cancel`, but it does + not forcibly destroy shielded settlement work. Only instance teardown or a + trap can abandon the writer without settlement. Peer reader drop is + loss-of-interest, not task cancellation; before `future.write` has started, + the bridge does nothing special for it. If a later write reports `dropped`, + the bridge cleans the value and settles the writer. +- If that local future never produces `T`, the Component Model provides no + generic close-without-value operation. This is an explicit liveness limit; the + binding does not fabricate a default value or pretend an idle writer can be + dropped safely. The same limit applies when the paired readable was created + for a nested payload but the outer transfer rejected it before ownership + crossed the boundary. +- Local stream MVP has `Sink::close()` for graceful producer close and async + `Stream::drop()` or `Stream[T]` drop for consumer loss-of-interest. It does + not expose public `Sink::cancel()` because component `stream` has no distinct + generic producer-failure signal to preserve across the boundary. +- Local stream state keeps producer close separate from reader drop. Producer + close preserves buffered `FixedArray[T]` chunks for draining; reader drop + discards unread chunks and uses an explicit `Stream::new_with_cleanup` or + `Stream::produce(cleanup=...)` callback for payloads that need resource + cleanup. +- Local stream capacity is measured in elements. Zero is strict rendezvous and + a positive value is a hard bound on accepted unread values; local streams are + never implicitly unbounded. Waiting readers and writers use direct FIFO + handoff with completion-versus-cancellation race handling. +- An incoming component stream uses a generated demand-driven source whose + reads directly call its concrete site intrinsics. It does not start an eager + pump into a local stream pipe. +- Forwarding component endpoints without reading them is not the default path and + needs an explicit advanced API if we decide to support it. +- Runtime validation includes real `wasi:cli@0.3.0` stream output and a real + `wasi:http@0.3.0` handler response whose body stream, trailers future, and + transmission completion continue after the handler returns the response. +- Local stream validation covers strict rendezvous, bounded buffering, cancelled + waiter removal, completion-versus-cancellation ownership races, local + producer reads, and unread resource cleanup. +- Async export stubs intentionally expose + `background_group : @async-core.TaskGroup[Unit]`. It adapts the mismatch + between MoonBit structured completion and component task return. Generated code + publishes component task return when the user function produces its result, + then keeps the underlying MoonBit task group alive for hook-style post-return + work. That work remains structurally owned and bounded by the component task + or instance lifetime, but cannot change the already-published export result. +- Lowering local `Future[T]` and `Stream[T]` values requires an active component + async task scope, including recursive occurrences inside structured payloads. + A sync WIT import called in that scope prepares its endpoint arguments and + commits those same handles immediately after the core call returns. Sync export + results, and sync imports called without an active scope, remain unsupported. + Incoming lift remains lazy and does not require a producer task until user code + later reads in an async scope. Scope-free sync lowering requires cooperative + component-thread support and must not be faked by using the stackless callback + ABI for a non-async WIT function. diff --git a/crates/moonbit/docs/adr/0002-local-future-promise.md b/crates/moonbit/docs/adr/0002-local-future-promise.md new file mode 100644 index 000000000..96d847644 --- /dev/null +++ b/crates/moonbit/docs/adr/0002-local-future-promise.md @@ -0,0 +1,43 @@ +# Add a local Future/Promise pair + +Status: accepted; implemented in the current branch + +MoonBit needs a producer-facing one-shot primitive for local control-flow cycles +that cannot be expressed as `Future::from(async () -> T)`. The motivating case +is `wasi:http@0.3.0` request handling: `consume-body` takes a Future reporting +processing completion before it returns the request body whose later processing +determines that completion. + +`Future::new()` returns `(Future[T], Promise[T])` and +`Future::new_with_cleanup(cleanup)` adds explicit value-discard cleanup. The +Promise can complete with a value, fail with a MoonBit error, or close without a +value. It is local coordination state only. It does not create, wrap, or own a +component `future` endpoint, and it works for arbitrary MoonBit `T`. + +## Consequences + +- `Promise::complete(value)` returns `true` only when the reader accepts + ownership. If the Future was already dropped, it returns `false` and the + caller retains `value`. +- `Future::drop()` after accepted completion runs the cleanup supplied to + `new_with_cleanup`. The plain `new()` constructor is appropriate only when + discarded `T` needs no explicit cleanup. +- `Promise::fail(error)` makes local `Future::get()` raise that error. + `Promise::close()` makes it raise `PromiseClosed`. +- Settlement is one-shot. Repeating `complete`, `fail`, or `close` after a + successful settlement is a programmer error. +- Dropping or otherwise abandoning a still-pending Promise does not implicitly + close its Future because MoonBit has no generic deterministic destructor. The + producer must explicitly complete, fail, or close it. +- Task cancellation that reaches a waiting reader before settlement drops the + reader, so later completion returns `false`. Once completion assigns the value + and wakes the reader, completion wins a simultaneous cancellation race and the + reader receives the value. +- Explicit `Future::drop()` follows the same race rule while `get()` is pending: + dropping before settlement wakes the reader with `Cancelled`, while an + already-assigned value or error remains owned by the waiting reader. +- A local failure or close cannot settle an already-exposed component future + without a value. If such an outcome is expected across WIT, it belongs in the + payload type, for example `Future[Result[V, E]]`. +- Generated FFI-boundary code remains solely responsible for creating concrete + component future pairs and satisfying their writable-end settlement rules. diff --git a/crates/moonbit/docs/async-design.md b/crates/moonbit/docs/async-design.md new file mode 100644 index 000000000..589a10515 --- /dev/null +++ b/crates/moonbit/docs/async-design.md @@ -0,0 +1,367 @@ +# MoonBit Component Async Design + +Status: MVP implemented on the upstream generator architecture. + +This document is the design contract for MoonBit component-model `async`, +`future`, and `stream` bindings. It intentionally omits implementation +history. Terms are defined in the [async glossary](async-glossary.md). The two +main decisions and their detailed consequences are recorded separately: + +- [ADR 0001: keep local async values detached from component endpoints](adr/0001-async-ffi-boundary-conversion.md) +- [ADR 0002: add a local Future/Promise pair](adr/0002-local-future-promise.md) + +## Goals + +- Give MoonBit code small, local async types that work for arbitrary MoonBit + payloads. +- Convert those values to component endpoints only in generated code for a + concrete WIT function position. +- Preserve canonical ABI ownership through completion, partial progress, + cancellation, peer drop, and recursive endpoint payloads. +- Keep ordinary synchronous binding generation independent of async support. +- Align coroutine and structured-concurrency behavior with + `moonbitlang/async` where the component protocol does not require a + difference. + +The MVP does not include component `error-context`, public BYOB/`read_into`, an +advanced raw-endpoint forwarding interface, or scope-free production of +component futures and streams from synchronous WIT calls. + +## Constraints + +1. MoonBit emits core wasm. `wasm-tools` adapts it into a component. +2. Future and stream intrinsics are identified by the containing WIT function + and canonical endpoint index. A payload type `T` cannot select an intrinsic. + Every intrinsic name must therefore come from `wit-parser`. +3. A component `future` or `stream` value is the readable end. Only + generated `future.new` or `stream.new` calls create its paired writable end. +4. Canonical read and write operations borrow ABI memory until an immediate or + terminal waitable result. Stream operations may transfer only a prefix. +5. MoonBit cannot enforce linear endpoint ownership or borrowed-view lifetimes. + The interface must keep invalid states private and make resource cleanup + explicit where static enforcement is unavailable. +6. A component future has no generic successful close-without-value operation. + Once a writer is exposed, it must write a real value or attempt that write + and observe reader drop before the writable end can be dropped. +7. Stackless callback exports can resume suspended work only inside an active + component async task scope. + +## Architecture + +The design has three layers. + +### Local runtime + +The generated `@async-core` package owns source-language concepts: + +- `Future[T]`, `Promise[T]`, `Stream[T]`, and `Sink[T]`; +- `Task[T]`, `TaskGroup[T]`, cancellation shielding, and coroutine scheduling; +- `Semaphore`, `Mutex`, and `CondVar`; +- payload-independent waitable event decoding. + +It does not contain generic component endpoint types, endpoint vtables, or a +generic `future.new`/`stream.new` operation. + +### Recursive generator plan + +For each WIT function, the Rust generator builds a recursive endpoint plan from +the canonical type shape and `wit-parser`'s ordered future/stream occurrences. +Each plan node records: + +- whether it is a future or stream; +- its position-specific intrinsic names from `wit-parser`; +- child endpoint occurrences in its payload; +- payload lift, lower, commit, reject, and cleanup behavior. + +This is generator data, not a runtime table. + +### Generated site helpers + +Generated `ffi.mbt` code owns raw readable and writable handles, canonical ABI +buffers, and copy-operation state. A helper directly calls the intrinsics for +one plan node and converts between that endpoint and a local value. + +The deletion boundary remains important: endpoint-free synchronous worlds do +not emit the async runtime or wrappers, and their generated output remains +unchanged as async support evolves. + +## Public Interface + +The main local interface is: + +```mbt +async fn Future::get(self : Future[T]) -> T +async fn Future::drop(self : Future[T]) -> Unit +fn Future::ready(value : T) -> Future[T] +fn Future::ready_with_cleanup(value : T, cleanup : (T) -> Unit) -> Future[T] +fn Future::from( + producer : async () -> T, + on_unstarted_drop? : () -> Unit, +) -> Future[T] + +fn Future::new() -> (Future[T], Promise[T]) +fn Future::new_with_cleanup( + cleanup : (T) -> Unit, +) -> (Future[T], Promise[T]) +fn Promise::complete(self : Promise[T], value : T) -> Bool +fn Promise::fail(self : Promise[T], error : Error) -> Bool +fn Promise::close(self : Promise[T]) -> Bool + +fn Stream::new(capacity? : Int = 0) -> (Stream[T], Sink[T]) +fn Stream::new_with_cleanup( + cleanup : (T) -> Unit, + capacity? : Int = 0, +) -> (Stream[T], Sink[T]) +fn Stream::produce( + producer : async (Sink[T]) -> Unit, + cleanup? : (T) -> Unit, + on_unstarted_drop? : () -> Unit, +) -> Stream[T] +async fn Stream::read(self : Stream[T], max : Int) -> FixedArray[T]? +async fn Stream::drop(self : Stream[T]) -> Unit + +async fn Sink::write(self : Sink[T], values : ArrayView[T]) -> Int +async fn Sink::write_all(self : Sink[T], values : ArrayView[T]) -> Bool +fn Sink::is_open(self : Sink[T]) -> Bool +async fn Sink::close(self : Sink[T]) -> Unit +``` + +`Sink[Byte]` additionally provides `write_bytes(BytesView)` and +`write_all_bytes(BytesView)` so immutable `Bytes` do not require an `Array` +conversion. + +`Future[T]` is one-shot. `Promise[T]` is local coordination, never a component +writable endpoint. Expected cross-component failure belongs in `T`, for example +`Future[Result[V, E]]`; local `Promise::fail` and `Promise::close` cannot settle +an exposed component future without a value. + +Streams support both directions without exposing endpoint machinery: + +- consumers pull owned `FixedArray[T]` chunks from `Stream[T]`; +- producers push immutable `ArrayView[T]` values into `Sink[T]`; +- the runtime copies a bounded staging window before suspension; +- capacity is measured in elements, with zero meaning strict rendezvous; +- `write` returns the consumed prefix length. When cleanup is configured, + consumption includes values accepted by the peer and staged values recursively + cleaned after peer drop; +- `is_open` distinguishes a fully accepted write from one that consumed values + while closing, and `write_all` returns whether the endpoint remained open + through the operation; +- `Sink::close` is graceful producer completion and preserves buffered values; +- `Stream::drop` is consumer loss-of-interest and cleans unread owned values. + +There is no public `Sink::cancel`. Component streams have no distinct generic +producer-failure signal to preserve. Such failure belongs in the payload or +surrounding WIT protocol. + +Async export implementations also receive: + +```mbt +background_group : @async-core.TaskGroup[Unit] +``` + +The generated adapter publishes component task return when the implementation +returns its result, then allows this group to finish hook-style work. The work +remains structurally owned in MoonBit but cannot alter the published result. + +## Boundary Conversion + +The same four conversions cover import parameters/results and export +parameters/results: + +| Component value | Local value | Generated owner | +| --- | --- | --- | +| Incoming `future` readable | lazy `Future[T]` source | source closure owns readable and read state | +| Outgoing `Future[T]` | new component future pair | producer task owns writable and settlement | +| Incoming `stream` readable | demand-driven `Stream[T]` source | source closure owns readable and read state | +| Outgoing `Stream[T]` | new component stream pair | producer task owns writable and staged writes | + +Incoming conversion is lazy and creates no endpoint pair. `Future::get` or +`Stream::read` calls the concrete site's read intrinsic when the user requests +data. Dropping the local value calls the same site's cancel/drop path if needed. + +Outgoing conversion first prepares a pair. The readable end may be nested in a +larger canonical payload, so producer work starts only after that payload's +ownership disposition is known: + +- commit means the peer owns the readable end and starts normal production; +- reject cleans an untransferred stream pair and local stream state; +- rejecting a future drops its readable end but still drives the paired writer + until a write observes that reader drop. + +### Recursive endpoints + +A WIT value such as: + +```wit +future>> +``` + +maps directly to: + +```mbt +Future[Future[Stream[T]]] +``` + +The local types contain no CM index. The recursive generator plan binds each +layer to its own site. Reading the outer future reveals the next readable +handle; reading that future reveals the stream handle. Conversion is staged, +not flattened at the original function boundary. + +Every lowered nested payload has explicit commit and reject actions. For a +partial `stream>` write, the accepted prefix belongs to the peer; a +staged rejected suffix is settled by generated cleanup, while an unstaged tail +is cleaned by `Sink`. All three are included in the consumed prefix returned to +the caller, which must not retry any of them. The MVP stages one such element at +a time so backpressure cannot create an unbounded number of unsettled future +writers. + +## Runtime Invariants + +### Ownership + +- Every raw endpoint has exactly one owner: generated source state, generated + producer state, an in-flight canonical transfer, or the peer. +- A copy-operation buffer remains live until an immediate or terminal result. + Calling `cancel-*` alone does not return the buffer. +- A completed stream copy transfers exactly its canonical reported prefix. Any + staged suffix is rejected and recursively cleaned exactly once; `Sink::write` + reports both portions as consumed so aliases cannot retry rejected values. +- Generated cleanup recursively follows the active record, tuple, variant, + list, resource, future, and stream shape. +- Rejecting a materialized local stream installs generated payload cleanup on + its pipe before waking writers, so buffered values, pending writer values, + and writes attempted after rejection are each cleaned exactly once. +- `Future::new_with_cleanup`, `Stream::new_with_cleanup`, and + `Stream::produce(cleanup=...)` clean accepted local values discarded before + consumption. Variants without element cleanup are appropriate only when + discarded `T` needs no explicit cleanup. +- Lazy `Future::from` and `Stream::produce` may carry `on_unstarted_drop` for + captured resources. `Stream::produce` separately accepts per-element cleanup + for values written after production starts. + +Ownership through `ArrayView[T]` is a best-effort contract because MoonBit +cannot prevent aliases. Callers must treat the consumed prefix as moved, whether +the peer accepted it or generated cleanup rejected it. `write_all` additionally +assumes disposal responsibility for the unstaged tail and cleans it before +returning `false` or propagating an error. + +### Cancellation + +Three events must remain distinct: + +- MoonBit task cancellation cooperatively stops local work; +- endpoint copy cancellation recovers an in-flight operation buffer; +- peer drop reports loss-of-interest through a read or write result. + +An incoming read cancelled by MoonBit first issues the concrete endpoint +cancel operation, waits for its terminal event, reclaims the buffer, and only +then drops the readable end. `CondVar` provides direct cleanup notification; +the runtime must not poll by repeatedly yielding. + +The generated source records the component task that registered an in-flight +read. If another export drops that source, cancellation is routed to the +original task's waitable set before the reader is woken. + +An outgoing stream serializes writes and close with `Mutex`, because the +canonical ABI permits only one active operation on an endpoint. MoonBit task +cancellation issues the concrete `stream.cancel-write`, waits for buffer +ownership to return, commits any transferred prefix, rejects the staged suffix, +and then drops the writable end. Unlike a future, a stream can terminate without +inventing a payload value. + +An exposed outgoing future writer is a settlement obligation. Its producer is +shielded from ordinary task/subtask cancellation until it writes one real value +or a write reports reader drop. If its local future never produces `T`, no +generic cleanup can settle it. The writer may remain pending until instance +teardown; the binding does not fabricate a default value. + +### Scheduling + +- Each top-level component task owns a distinct waitable set and scheduler + queue, preventing concurrent exports from consuming each other's events. +- A scheduling round runs only work ready at its start, preserving an event-loop + poll opportunity between rounds. +- Repeated wakes are deduplicated. +- A task cancelled before first execution still enters its body so entry cleanup + can be installed. +- `TaskGroup` failure propagation and cancellation shielding follow + `moonbitlang/async` semantics. +- Canonical `backpressure.inc/dec` controls admission of component tasks. It is + not tied to endpoint producer lifetime; read/write suspension already provides + data-flow backpressure. + +## Synchronous WIT Functions + +WIT sync functions remain sync. The generator does not silently give them a +stackless async callback ABI. + +Incoming endpoint lift is lazy, so a sync function can receive a readable end +without immediately starting a copy. Reading it later still requires an async +task scope. + +Lowering a local `Future` or `Stream` requires an active component async task +scope. A sync import called from such a scope is supported: generated code +prepares endpoint arguments, performs the core call, and commits those handles +immediately afterward. Scope-free sync imports and sync export results that +create producer work are unsupported. Supporting them requires cooperative +component threads, not a callback-ABI workaround. + +Raw endpoint identity forwarding is also not implicit. A future advanced +interface may provide it explicitly without weakening the ordinary local types. + +## `moonbitlang/async` Alignment + +The runtime is audited against `moonbitlang/async` main at commit `18533c8d`. +The continuation primitive, cancellation races, shielding, wake behavior, +fairness, `Task`, `TaskGroup`, `Semaphore`, `Mutex`, and `CondVar` semantics are +kept aligned. + +Intentional differences are limited to the component environment: + +- component waitable sets replace the platform event loop; +- scheduler state is partitioned by waitable set; +- generated bridge work is owned by the component task's waitable set, not a + user `TaskGroup`, so lazy producers can continue after the export returns; +- public timers, retry, async queues, `spawn_loop`, and `pause` are omitted; +- local Future/Promise and Stream/Sink encode WIT ownership needs not provided + by `moonbitlang/async`. + +## Supported MVP + +Implemented and covered by composed runtime tests: + +- async imports and exports; +- local Future/Promise and Stream/Sink coordination; +- incoming and outgoing component futures and streams; +- nested endpoint payloads in both directions; +- partial stream progress, concurrent writes, cancellation races, and resource + payload cleanup; +- concurrent component-task isolation; +- `wasi:cli@0.3.0` stream output; +- `wasi:http@0.3.0` body, trailers, and post-response background work; +- deletion guards proving endpoint-free sync generation is unchanged without + async support. + +Known limits: + +- component `error-context` is out of scope; +- public BYOB/`read_into` is deferred; +- scope-free sync lowering is unsupported; +- fixed-length WIT lists use runtime-checked `FixedArray[T]`; combining one + with a future or stream in either nesting direction is not yet supported; +- a future producer that never produces a value cannot settle its component + writer generically; +- explicitly captured resources require `on_unstarted_drop` before lazy + production; lazy streams of managed values also require per-element cleanup + after production starts; +- generated glue and user implementations still share a MoonBit package, so + `#internal` is documentation control rather than an access boundary. + +## Deferred Decisions + +1. Whether named WIT future/stream aliases should become distinct MoonBit names. +2. Whether to expose a site-aware endpoint-forwarding interface. +3. Whether `Sink::close` needs a separate downstream `finish`/`flush` operation. +4. Whether measured workloads justify BYOB or larger nested-future stream + staging windows. diff --git a/crates/moonbit/docs/async-glossary.md b/crates/moonbit/docs/async-glossary.md new file mode 100644 index 000000000..3807c883f --- /dev/null +++ b/crates/moonbit/docs/async-glossary.md @@ -0,0 +1,488 @@ +# MoonBit Async Glossary + +This glossary records the vocabulary used by the MoonBit async future and stream +design notes. The naming rule is: + +- lowercase code-form names, `future` and `stream`, mean component-model WIT + types and operations; +- uppercase code-form names, `Future` and `Stream`, mean MoonBit-owned local + async types; +- `CM` describes component-model concepts in prose. The target runtime does not + expose reusable generic `CMFuture*` or `CMStream*` wrapper types; raw endpoint + handles and their direct operations stay inside generated site helpers. + +## Component Model Language + +**Component `future`**: +The component-model `future` WIT type. In a WIT signature it transfers +ownership of a readable end, not a MoonBit-local computation. +_Avoid_: `Future`, local Future + +**Component `stream`**: +The component-model `stream` WIT type. In a WIT signature it transfers +ownership of a readable end, not a MoonBit-owned `Stream` buffer. +_Avoid_: `Stream`, local Stream + +**Readable end**: +The endpoint of a component `future` or component `stream` that can be read and +transferred through WIT. + +**Writable end**: +The endpoint created with `future.new` or `stream.new` that stays in the +producing component and writes values to the paired readable end. + +**Async task scope**: +The component-model task context that lets stackless MoonBit async export code +join waitables, receive callback events, and resume suspended bridge +continuations. +_Avoid_: global event loop + +**Background group**: +The `background_group : @async-core.TaskGroup[Unit]` parameter intentionally +exposed by an async MoonBit export. The generated adapter publishes component +task return when the user function produces its result, then keeps the +underlying MoonBit task group alive for this hook-style work. The work remains +structurally owned inside MoonBit and bounded by the component task or instance +lifetime, but its later outcome cannot change the already-published export +result. +_Avoid_: leaked task group, detached global task + +**Async core package**: +The generated `@async-core` MoonBit package. It exposes the local `Future[T]`, +`Promise[T]`, `Stream[T]`, `Sink[T]`, `Task[T]`, `TaskGroup[T]`, `Semaphore`, +`Mutex`, and `CondVar` API while keeping component-model endpoint handles and +scheduler machinery private. +Interface-specific `ffi.mbt` files remain generated boundary implementations +and are not this package. + +**Sync lower producer gap**: +The unsupported case where boundary code without an active component async task +scope must lower a local Future or Stream, including recursive occurrences in a +sync import parameter or sync export result. The producer must continue after +the call returns, but the stackless callback ABI cannot resume it. This requires +cooperative component-thread support. Incoming lift is not this case because it +can remain lazy until a later async read. +_Avoid_: hidden async export + +## MoonBit Binding Language + +**CM future endpoint**: +A conceptual component `future` readable or writable handle owned by generated +boundary code. Its operations are statically bound to a concrete function site, +not stored in a generic MoonBit operation table. +_Avoid_: `Future`, local Future + +**CM stream endpoint**: +A conceptual component `stream` readable or writable handle owned by generated +boundary code. Its operations are statically bound to a concrete function site, +not stored in a generic MoonBit operation table. +_Avoid_: `Stream`, local Stream + +**Endpoint site**: +One component `future` or component `stream` node in a concrete WIT function's +canonical type traversal. A site binds its payload plan and intrinsic names to +that function context. It is generator metadata, not a runtime value. +_Avoid_: payload type alone + +**Recursive boundary plan**: +The generator-owned tree that connects an endpoint site to the endpoint sites +inside its payload. It drives direct lift/lower code and recursive cleanup for +records, variants, collections, nested futures, and nested streams. +_Avoid_: endpoint vtable, flat type lookup + +**Generated readable source**: +A private source closure produced when lifting an incoming component endpoint. +It exclusively owns one raw readable handle and directly calls that site's +read, cancel-read, and drop-readable intrinsics. A future source plugs into local +`Future[T]`; a stream source implements demand-driven pulls for local +`Stream[T]`. +_Avoid_: `CMFutureReader[T]`, `CMStreamReader[T]` + +**Generated writable state**: +A private prepared producer state created when lowering a local Future or Stream. +It exclusively owns one raw writable handle and binds directly to that site's +write/drop intrinsics. Lowering does not start user computation. Commit starts a +normal producer task; rejection starts future writer settlement when required or +discards a prepared stream without transferring its readable end. A future state +carries a settlement obligation from the moment `future.new` succeeds. +_Avoid_: `CMFutureWriter[T]`, `CMStreamWriter[T]` + +**Staged nested endpoint payload**: +The rule that only the current endpoint layer's readable handle appears in its +canonical payload. For `future>>`, the function boundary carries +the outer future handle, reading it carries the inner future handle, and reading +that carries the stream handle. +_Avoid_: flattened endpoint graph + +**Transfer commit/reject**: +Generated ownership handling after lowering a payload containing resources or +nested endpoints. A successful transfer commits the transferred values to the +peer. A rejected ordinary resource is cleaned. A prepared stream drops its +untransferred component pair and rejects guest-owned local state without +pretending the peer accepted values. A rejected future drops its readable end, +but its paired writer and local source remain owned until a later write reaches +settlement. A partial stream transfer commits the accepted prefix. Generated +code retries the remainder of the current staging window while the peer remains +open; peer drop or write cancellation rejects that staged remainder exactly +once. Values beyond the staging window remain caller-owned. +_Avoid_: unconditional cleanup + +**Future writer settlement obligation**: +The non-rollbackable obligation created by `future.new`. The writable end can be +dropped only after a successful write or after a write reports that the readable +end was dropped. Dropping a still-guest-owned readable end does not settle its +paired writer; the generated producer must still obtain a real payload and +attempt the write. A `cancelled` write has returned its ABI buffer but has not +settled the writer. +_Avoid_: future rollback + +**Prepared lower payload**: +The private combination of a canonical ABI value or buffer, unstarted generated +producer state, and recursive `commit`/`reject` paths. Commit starts producer work +only after ownership transfers. The same lowered buffer is retained across +blocked or partial writes. For a staging window, the accepted prefix is +committed and the staged suffix is retried from the same buffer or rejected. +Both portions are consumed from the public `Sink` input; only the unstaged tail +remains caller-owned. It is not a user-facing MoonBit type. +_Avoid_: lowered value alone + +**Local Future**: +A MoonBit-only one-shot handle, expected to be spelled `Future[T]`, that wraps an +owned source computation plus cancellation/drop state. Its generic state has no +CM endpoint variant and does not require `T` to be representable in WIT. A +generated readable source may capture a raw handle inside its private closure, +but the local runtime cannot inspect or forward it. +_Avoid_: component `future`, future endpoint + +**One-shot async thunk**: +A MoonBit `async () -> T` value wrapped by local `Future[T]` or generated bridge +code to read or produce exactly one value. It is the computation inside the +handle, not the public component `future` representation by itself. +_Avoid_: future handle + +**Future state cell**: +The local mutable runtime state owned by a `Future[T]` handle. Depending on the +constructor, it records a ready value, an owned source, or a shared local +Future/Promise settlement cell. It does not contain CM-specific states or own +writable ends. +_Avoid_: endpoint table + +**Owned Future source**: +The generic pending source stored by local `Future[T]`. It has a one-shot async +`run` operation and may have an explicit unstarted cleanup action. Ordinary +MoonBit futures use a local thunk; generated incoming futures use a readable +source whose closure owns the endpoint. +_Avoid_: pending CM state + +**Unstarted producer cleanup**: +The optional `on_unstarted_drop : () -> Unit` action attached to a lazy local +`Future::from` or `Stream::produce`. It owns resources captured by the producer +only until the producer starts. Drop or prepared-transfer rejection invokes it +without executing the producer; get, read, or commit clears it before producer +execution. It is not a cancellation handler for an already-running task. +_Avoid_: producer finalizer, implicit cancellation + +**Bridge-owned future write**: +The separate generated task state that drives a local `Future[T]` computation +into an outgoing component `future`. This state owns the raw writable handle; +the local `Future[T]` does not. +_Avoid_: local Promise, component future writer + +**Future writer settlement obligation**: +The rule that once an outgoing component future pair has exposed its readable +end, the generated task owning the writable handle must eventually write one +real value or attempt the write and observe that the reader was dropped before +the writable end can be dropped. It cannot be satisfied by silently dropping +the writer. +_Avoid_: writer close + +**In-flight future write**: +A generated writable operation that has already called component `future.write` +and is waiting for the `FUTURE_WRITE` waitable event. If the peer +drops the readable end while this operation is in flight, the event reports +`dropped` and returns ownership of the lowered payload buffer to the writer. +_Avoid_: bridge task in flight + +**Pre-write future writer**: +A generated task that owns the writable end but has not started component +`future.write` because the payload value is not available yet. The +bridge task may be running or suspended on local work in this phase, but there +is no CM write operation waiting in a waitable set. Reader drop is observed when +the later `future.write` attempt returns or reports `dropped`. +_Avoid_: idle writer, in-flight future write + +**Bridge-shielded future task**: +An outgoing future bridge task that is protected from ordinary task +cancellation after its readable end has crossed the component boundary. Peer +loss-of-interest does not cancel it; the bridge keeps running until it satisfies +the future writer settlement obligation, unless the whole component instance is +trapping or being torn down. +_Avoid_: cancellable bridge + +**Generated future site helper**: +A generated function that directly calls `future.new` and the other future +intrinsics for one concrete WIT function site. It returns or consumes raw handles +only within generated boundary code. +_Avoid_: local `Future::new()`, generic component endpoint factory + +**Local Promise**: +A MoonBit-only producer side paired with a local `Future[T]` by +`Future::new()` or `Future::new_with_cleanup()`. `complete` transfers a value +only if the reader still exists; `fail` and `close` settle local waiters without +a value. It never creates or owns a component `future` writable end. If its +paired Future later crosses an FFI boundary, generated code creates and owns the +separate component endpoint pair. +_Avoid_: future writer + +**Local Semaphore**: +A MoonBit-only counting semaphore used for coroutine coordination. Waiters are +FIFO. If a release assigns a permit before cancellation resumes the waiter, the +permit wins that race and acquire succeeds so the permit is not lost. It is not +a component-model waitable or backpressure counter. +_Avoid_: waitable, `backpressure.inc` + +**Local Mutex**: +A MoonBit-only FIFO mutex implemented by a one-permit Local Semaphore. Its +acquire operation has the same cancellation race semantics as the semaphore. +Generated stream writers use it to serialize writes and close without polling +the scheduler. +_Avoid_: component stream lock + +**Local Condition Variable**: +A MoonBit-only `CondVar` used for direct task notification. If a signal has +already been assigned when cancellation resumes a waiter, the signal wins so +it cannot be lost. Generated future and stream sources use it to wait for +in-flight component read cancellation to return the operation buffer. +_Avoid_: waitable-set event, predicate polling + +**Local Stream**: +A MoonBit-only stream type, expected to be spelled `Stream[T]`, that coordinates +MoonBit coroutines or wraps a generic demand-driven source without a CM-specific +state variant. A generated readable source may capture a raw component handle, +but the local runtime cannot inspect or forward it, and `T` need not be +representable in WIT. +_Avoid_: component `stream`, stream endpoint + +**Local Sink**: +The producer side of a local Stream. It is not a component `stream` writable end. +_Avoid_: stream writer + +**Producer close**: +The graceful local-stream terminal action performed by `Sink::close()`. It means +no more values will be written, while already-buffered values remain readable. +_Avoid_: producer cancel + +**Consumer stream drop**: +The local-stream terminal action performed when the `Stream[T]` side is dropped +or explicitly consumed by async `Stream::drop()`. It means the consumer has lost +interest, so buffered values still owned by the local stream are cleaned and +producer-side waiters are woken. Bridge internals may cancel in-flight endpoint +copy operations to recover buffers, but the user-facing operation is not hard +cancellation of a producer. +_Avoid_: `Stream::cancel`, stream close + +**Producer failure**: +A possible future local-stream operation where the producer ends the stream with +a failure state instead of graceful EOF. It is not `unreachable`, a wasm trap, or +process abort. It is not part of the MVP public `Sink[T]` interface because +component `stream` does not carry a distinct generic producer-failure signal. +_Avoid_: producer abort, sink close, trap + +**Stream pipe**: +The shared runtime state behind a local `Stream[T]` and `Sink[T]` pair. It owns +bounded local chunk storage, FIFO waiting readers and writers, and separate +writer-close and reader-drop state. Capacity zero means strict rendezvous; +positive capacity is the maximum number of accepted unread elements. It does +not own CM stream endpoints. +_Avoid_: stream endpoint + +**Owned stream chunk**: +An owned, exact-length batch of stream items returned by local `Stream[T]` +reads. The expected representation is `FixedArray[T]`; local stream +implementation should pass chunks by ownership instead of assembling them in a +growable `Array[T]`. +_Avoid_: ABI buffer + +**Borrowed stream chunk**: +A temporary view of stream items, such as `ArrayView[T]`, accepted by the MVP +local `Sink[T]` write API. Before a write operation suspends, the stream runtime +must materialize the relevant view window into owned stream storage or an owned +canonical ABI buffer. +_Avoid_: stream storage + +**Staged write window**: +The bounded part of a borrowed stream chunk that one awaited write operation +materializes into owned storage. MVP `Sink[T]` writes stage at most one window +per awaited operation; `write_all` loops over windows. +_Avoid_: whole input buffer + +**CM operation buffer**: +The canonical ABI buffer supplied to an in-flight component `future` or +component `stream` read/write operation. The buffer is not reusable by MoonBit +until the operation completes or cancellation/drop is observed through the +terminal waitable event. +_Avoid_: local stream buffer + +**Accepted stream prefix**: +The prefix length reported by a completed or terminal stream copy event. Values +inside this prefix have semantically transferred across the stream operation. +For runtime-owned buffers, the source side must not clean them again. For +caller-owned MoonBit values, this is a best-effort API contract rather than a +type-system-enforced lifetime. Values outside the prefix remain owned by the +side that staged the buffer. +_Avoid_: whole chunk + +**Stream cleanup invariant**: +After every stream read, write, close, cancel, peer drop, or partial transfer, +each value is caller-owned, stream-owned, bridge-operation-owned, transferred to +the component peer, or cleaned exactly once. +_Avoid_: best-effort cleanup + +**Local producer close**: +`Sink::close()` stops local writes and marks graceful EOF. Buffered chunks remain +stream-owned and can still be drained before `Stream::read` returns `None`. +_Avoid_: reader drop, cancellation + +**Local reader drop**: +`Stream::drop()` records local consumer loss-of-interest. It wakes blocked +writers, discards unread chunks, and invokes the cleanup operation supplied by +`Stream::new_with_cleanup` or `Stream::produce(cleanup=...)` for each unread +owned value. A suspended +`Sink::write` owns one bounded staged `FixedArray`; `write_all` uses the same +cleanup for any remaining suffix that never entered the stream buffer. +_Avoid_: producer close, hard cancellation + +**Generated stream adapter**: +Boundary-generated state connecting local and component stream semantics. An +incoming adapter is a lazy readable source and starts no pump task. An outgoing +adapter is a producer task that owns the raw writable handle and pulls from a +local `Stream[T]`. +_Avoid_: local stream state + +**Incoming stream demand policy**: +The rule that a generated incoming stream source issues one component read only +for a corresponding local read demand. It does not prefetch into a local pipe. +_Avoid_: unbounded prefetch + +**Source cleanup hook**: +An owned cleanup action stored with a generic local source. Explicit local +cleanup, such as async `Future::drop` or `Stream::drop`, runs the generated hook +so an idle endpoint is dropped directly or an in-flight copy is cancelled and +observed before buffers and handles are released. +_Avoid_: destructor + +**BYOB stream read**: +A "bring your own buffer" read shape where the caller supplies storage to fill. +This is useful for component ABI buffers and specialized fast paths, but should +not be part of the MVP local `Stream[T]` interface for arbitrary `T`. + +**Local-component bridge**: +Generated FFI-boundary code that adapts between a one-shot async thunk or local +Stream and component future/stream endpoints. It exists only for a concrete WIT +site whose payload has generated recursive lift/lower operations. + +**Async generator boundary**: +The Rust generator layer that decides which MoonBit async runtime package, +endpoint bridge helpers, and public async type names to emit. This boundary +lets the old async runtime be peeled out and the replacement runtime added back +without scattering runtime-specific decisions across ordinary lift/lower code. +_Avoid_: inline async codegen + +**Mergeable prototype**: +A production-intended implementation slice built after the async generator +boundary exists. It is allowed to be experimental in scope, but it must use the +real generator boundary, carry tests, and be suitable to harden in place if the +shape proves correct. It is not throwaway code. +_Avoid_: throwaway prototype + +**ABI-compatible payload**: +A MoonBit type that is the generated representation of a WIT type and has the +payload lift/lower operations needed at a specific FFI boundary. +_Avoid_: arbitrary `T` + +**Bridge task**: +Runtime-owned async work that drives a local-component bridge after a readable +end has been returned or passed to another component. + +**Bridge continuation**: +The suspended MoonBit coroutine captured while a local-component bridge is +waiting on a component-model waitable operation. Reader drop, writer drop, and +task cancellation must be routed through this continuation rather than modeled +only as ordinary value destruction. +_Avoid_: destructor + +**Endpoint copy cancellation**: +Cancellation of one in-flight component `future` or component `stream` read or +write operation. This returns ownership of the operation's buffer when the +cancelled event is observed. It is not the same thing as cancelling the MoonBit +coroutine that requested the operation. +_Avoid_: task cancellation + +**Hard cancellation**: +A MoonBit-side request to stop local async work with strong semantics. Examples +include task/subtask cancellation and component teardown or failure. Hard +cancellation may cancel a producer coroutine or cancel and observe an in-flight +endpoint copy operation. It is not the same as the peer dropping a component +endpoint. +_Avoid_: peer drop, consumer loss-of-interest + +**Peer loss-of-interest**: +The opposite component endpoint was dropped. When an endpoint operation observes +this, the component result is `dropped`, not `cancelled`. If there is no endpoint +operation that can observe it yet, such as a pre-write outgoing future writer, +the bridge must not infer hard cancellation from it. +_Avoid_: task cancellation + +**Bridge cancellation**: +A hard cancellation of the MoonBit bridge continuation that is driving +conversion between local async work and a component endpoint. For streams and +in-flight future operations, this may cancel endpoint copy work. For a pre-write +outgoing component future writer, peer readable-end drop is not bridge +cancellation; it is observed only if a later `future.write` reports `dropped`. +_Avoid_: endpoint copy cancellation + +**Future drop**: +The async user-facing operation that consumes a local `Future[T]` handle and +runs its explicit cleanup protocol. If the handle is backed by an unread component +readable end and no read is in flight, this cleanup may call +`future.drop-readable`; otherwise it may need to cancel and observe cleanup +first. If cancellation races with completion, `Future::drop` cleans the produced +value using the future's payload cleanup operation. It is not a direct alias for +component `future.drop-*`, and it is not an automatic destructor. +_Avoid_: read cancellation + +**Payload cleanup operation**: +A generated or stored function that cleans a produced payload value when a +future or stream operation owns that value but will not return it to user code. +For WIT payloads, this operation is generated from lift/lower cleanup logic. A +generic local `Future[T]` or `Stream[T]` cannot invent this operation for +arbitrary `T`; local constructors therefore accept it explicitly through +`Future::ready_with_cleanup`, `Stream::new_with_cleanup`, and +`Stream::produce(cleanup=...)` when needed. +_Avoid_: generic destructor + +## Adapter Generation Language + +**Adapter-generated intrinsic**: +A canonical ABI import generated by the core-wasm-to-component adapter path. For +component `future` and component `stream`, these intrinsics are identified by +the containing function and a discovered component `future` or component +`stream` position. + +**Function-position index**: +The enumeration index assigned to a component `future` or component `stream` +discovered in one concrete WIT function. It is not derivable from the MoonBit +payload type alone. +_Avoid_: parameter index, type index + +**Endpoint operation table**: +The rejected design where adapter intrinsics and payload callbacks are stored in +a runtime record selected for a generic endpoint wrapper. The target design emits +direct site-specific calls instead. +_Avoid_: vtable + +**Endpoint factory**: +A generated site helper that directly creates a component future or stream pair +because it is tied to a concrete WIT function position and therefore knows the +adapter-generated intrinsic names. +_Avoid_: local `Future::new()`, local `Stream::new()`, generic endpoint factory diff --git a/crates/moonbit/src/async/async_abi.mbt b/crates/moonbit/src/async/async_abi.mbt index f4491a0a5..e15b80413 100644 --- a/crates/moonbit/src/async/async_abi.mbt +++ b/crates/moonbit/src/async/async_abi.mbt @@ -33,14 +33,10 @@ fn SubTask::from(code : Int) -> SubTask { } ///| -/// None : the subtask is blocked -fn SubTask::cancel(self : SubTask) -> SubTaskState? { - let result = subtask_cancel(self.handle) - if result == -1 { - None - } else { - Some(SubTaskState::from(subtask_cancel(self.handle))) - } +/// The baseline intrinsic cooperatively waits in the component model and +/// returns only after the subtask reaches a terminal state. +fn SubTask::cancel(self : SubTask) -> SubTaskState { + SubTaskState::from(subtask_cancel(self.handle)) } // #endregion @@ -101,7 +97,12 @@ fn Events::new(code : EventCode, i : Int, j : Int) -> Events { // #region waitable set ///| -struct WaitableSet(Int) derive(Eq, Show, Hash) +#borrow(array) +extern "wasm" fn int_array2ptr(array : FixedArray[Int]) -> Int = + #|(func (param i32) (result i32) local.get 0) + +///| +struct WaitableSet(Int) derive(Eq, Hash) ///| fn WaitableSet::new() -> WaitableSet { @@ -113,6 +114,13 @@ fn WaitableSet::drop(self : Self) -> Unit { waitable_set_drop(self.0) } +///| +fn WaitableSet::poll(self : Self) -> (Events, Int) { + let payload : FixedArray[Int] = FixedArray::make(2, 0) + let event = waitable_set_poll(self.0, int_array2ptr(payload)) + (Events::new(EventCode::from(event), payload[0], payload[1]), payload[0]) +} + // #endregion // #region Future @@ -120,14 +128,14 @@ fn WaitableSet::drop(self : Self) -> Unit { ///| priv enum FutureReadResult { Completed = 0 - Cancelled = 1 + Cancelled = 2 } ///| fn FutureReadResult::from(int : Int) -> FutureReadResult { match int { 0 => Completed - 1 => Cancelled + 2 => Cancelled _ => panic() } } @@ -193,7 +201,6 @@ priv enum CallbackCode { Completed Yield Wait(WaitableSet) - Poll(WaitableSet) } ///| @@ -202,18 +209,16 @@ fn CallbackCode::encode(self : Self) -> Int { Completed => 0 Yield => 1 Wait(id) => 2 | (id.0 << 4) - Poll(id) => 3 | (id.0 << 4) } } ///| -fn CallbackCode::decode(int : Int) -> CallbackCode { +fn CallbackCode::_decode(int : Int) -> CallbackCode { let id = int >> 4 match int & 0xf { 0 => Completed 1 => Yield 2 => Wait(id) - 3 => Poll(id) _ => panic() } } @@ -223,42 +228,48 @@ fn CallbackCode::decode(int : Int) -> CallbackCode { // #region Component async primitives ///| -/// Return whether is cancelled. -/// Use for non-callback implementation. -fn _yield() -> Bool = "$root" "[cancellable][yield]" +fn subtask_cancel(id : Int) -> Int = "$root" "[subtask-cancel]" ///| -fn _backpressure_inc() = "$root" "[backpressure-inc]" +fn subtask_drop(id : Int) = "$root" "[subtask-drop]" ///| -fn _backpressure_dec() = "$root" "[backpressure-dec]" +fn tls_set(tls : Int) = "$root" "[context-set-0]" ///| -fn subtask_cancel(id : Int) -> Int = "$root" "[subtask-cancel]" +fn tls_get() -> Int = "$root" "[context-get-0]" ///| -fn subtask_drop(id : Int) = "$root" "[subtask-drop]" +fn task_cancel() = "[export]$root" "[task-cancel]" ///| -pub fn context_set(task : Int) = "$root" "[context-set-0]" +fn waitable_set_new() -> Int = "$root" "[waitable-set-new]" ///| -pub fn context_get() -> Int = "$root" "[context-get-0]" +fn waitable_set_drop(set : Int) = "$root" "[waitable-set-drop]" ///| -fn tls_set(tls : Int) = "$root" "[context-set-0]" +/// This poll is deliberately non-cancellable. Component-task cancellation is +/// delivered by the stackless callback instead of being consumed here. +fn waitable_set_poll(set : Int, payload : Int) -> Int = "$root" "[waitable-set-poll]" ///| -fn tls_get() -> Int = "$root" "[context-get-0]" +fn task_wakeup_stream_new() -> Int64 = "$root" "[stream-new-unit]" ///| -pub fn task_cancel() = "[export]$root" "[task-cancel]" +fn task_wakeup_stream_read(handle : Int, ptr : Int, len : Int) -> Int = "$root" "[async-lower][stream-read-unit]" ///| -fn waitable_set_new() -> Int = "$root" "[waitable-set-new]" +fn task_wakeup_stream_write(handle : Int, ptr : Int, len : Int) -> Int = "$root" "[async-lower][stream-write-unit]" ///| -fn waitable_set_drop(set : Int) = "$root" "[waitable-set-drop]" +fn task_wakeup_stream_cancel_read(handle : Int) -> Int = "$root" "[stream-cancel-read-unit]" + +///| +fn task_wakeup_stream_drop_readable(handle : Int) = "$root" "[stream-drop-readable-unit]" + +///| +fn task_wakeup_stream_drop_writable(handle : Int) = "$root" "[stream-drop-writable-unit]" ///| fn waitable_join(waitable : Int, set : Int) = "$root" "[waitable-join]" diff --git a/crates/moonbit/src/async/async_primitive.mbt b/crates/moonbit/src/async/async_primitive.mbt index b603b1cc9..4f97aa4ce 100644 --- a/crates/moonbit/src/async/async_primitive.mbt +++ b/crates/moonbit/src/async/async_primitive.mbt @@ -13,9 +13,9 @@ // limitations under the License. ///| -async fn[T] async_suspend( - cb : ((T) -> Unit, (Cancelled) -> Unit) -> Unit, -) -> T raise Cancelled = "%async.suspend" +async fn[T, E : Error] async_suspend( + cb : ((T) -> Unit, (E) -> Unit) -> Unit, +) -> T raise E = "%async.suspend" ///| fn run_async(f : async () -> Unit noraise) = "%async.run" diff --git a/crates/moonbit/src/async/cond_var.mbt b/crates/moonbit/src/async/cond_var.mbt new file mode 100644 index 000000000..39dac21a8 --- /dev/null +++ b/crates/moonbit/src/async/cond_var.mbt @@ -0,0 +1,72 @@ +// Copyright 2025 International Digital Economy Academy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +///| +priv struct CondVarWaiter { + // Once a signal is assigned, cancellation must not lose it before the + // waiter resumes. This matches moonbitlang/async's condition variable. + mut woken : Bool + mut coro : Coroutine? +} + +///| +/// A condition variable for synchronization between tasks. +pub struct CondVar { + priv waiters : @deque.Deque[CondVarWaiter] +} + +///| +/// Create a new condition variable. +#alias(new, deprecated) +pub fn CondVar::CondVar() -> CondVar { + { waiters: Deque([]) } +} + +///| +/// Wait until the condition variable is signaled. +pub async fn CondVar::wait(self : CondVar) -> Unit { + let waiter = { woken: false, coro: Some(current_coroutine()) } + self.waiters.push_back(waiter) + suspend() catch { + _ if waiter.woken => () + err => { + waiter.coro = None + raise err + } + } +} + +///| +/// Wake the first waiting task. Does nothing when no task is waiting. +pub fn CondVar::signal(self : CondVar) -> Unit { + while self.waiters.pop_front() is Some(waiter) { + if waiter.coro is Some(coro) { + waiter.woken = true + coro.wake() + break + } + } +} + +///| +/// Wake all waiting tasks. Does nothing when no task is waiting. +pub fn CondVar::broadcast(self : CondVar) -> Unit { + for waiter in self.waiters { + if waiter.coro is Some(coro) { + waiter.woken = true + coro.wake() + } + } + self.waiters.clear() +} diff --git a/crates/moonbit/src/async/coroutine.mbt b/crates/moonbit/src/async/coroutine.mbt index cfd962695..6f10baae1 100644 --- a/crates/moonbit/src/async/coroutine.mbt +++ b/crates/moonbit/src/async/coroutine.mbt @@ -17,75 +17,74 @@ priv enum State { Done Fail(Error) Running - Suspend(ok_cont~ : (Unit) -> Unit, err_cont~ : (Cancelled) -> Unit) + Suspend(ok_cont~ : (Unit) -> Unit, err_cont~ : (Error) -> Unit) } ///| struct Coroutine { coro_id : Int + waitable_set : WaitableSet mut state : State mut shielded : Bool mut cancelled : Bool mut ready : Bool + mut spawner : ((async () -> Unit) -> Unit)? downstream : Set[Coroutine] } ///| -impl Eq for Coroutine with equal(c1, c2) { +impl Eq for Coroutine with fn equal(c1, c2) { c1.coro_id == c2.coro_id } ///| -impl Hash for Coroutine with hash_combine(self, hasher) { +impl Hash for Coroutine with fn hash_combine(self, hasher) { self.coro_id.hash_combine(hasher) } ///| fn Coroutine::wake(self : Coroutine) -> Unit { - self.ready = true - scheduler.run_later.push_back(self) + if !self.ready { + self.ready = true + enqueue(self) + signal_component_task(self.waitable_set) + } } ///| pub fn is_being_cancelled() -> Bool { let coro = current_coroutine() - coro.cancelled && not(coro.shielded) + coro.cancelled && !coro.shielded +} + +///| +pub fn check_cancellation() -> Unit raise { + if is_being_cancelled() { + raise Cancelled::Cancelled + } } ///| -pub(all) suberror Cancelled derive(Show) +pub(all) suberror Cancelled derive(Debug) ///| fn Coroutine::cancel(self : Coroutine) -> Unit { self.cancelled = true - if not(self.shielded || self.ready) { + if !self.shielded { self.wake() } } ///| -pub async fn pause() -> Unit raise Cancelled { +async fn suspend() -> Unit { guard scheduler.curr_coro is Some(coro) - if coro.cancelled && not(coro.shielded) { + if coro.cancelled && !coro.shielded { raise Cancelled::Cancelled } - async_suspend(fn(ok_cont, err_cont) { - guard coro.state is Running - coro.state = Suspend(ok_cont~, err_cont~) - coro.ready = true - scheduler.run_later.push_back(coro) - }) -} - -///| -pub async fn suspend() -> Unit raise Cancelled { - guard scheduler.curr_coro is Some(coro) - if coro.cancelled && not(coro.shielded) { - raise Cancelled::Cancelled - } - scheduler.blocking += 1 + let schedule = task_schedule(coro.waitable_set) + schedule.blocking += 1 defer { - scheduler.blocking -= 1 + schedule.blocking -= 1 } async_suspend(fn(ok_cont, err_cont) { guard coro.state is Running @@ -94,18 +93,24 @@ pub async fn suspend() -> Unit raise Cancelled { } ///| -fn spawn(f : async () -> Unit) -> Coroutine { +fn spawn_owned( + f : async () -> Unit, + waitable_set : WaitableSet, + inherited_spawner : ((async () -> Unit) -> Unit)?, +) -> Coroutine { scheduler.coro_id += 1 let coro = { state: Running, ready: true, shielded: true, - downstream: Set::new(), + downstream: Set([]), coro_id: scheduler.coro_id, + waitable_set, cancelled: false, + spawner: inherited_spawner, } fn run(_) { - run_async(fn() { + run_async(() => { coro.shielded = false try f() catch { err => coro.state = Fail(err) @@ -120,10 +125,18 @@ fn spawn(f : async () -> Unit) -> Coroutine { } coro.state = Suspend(ok_cont=run, err_cont=_ => ()) - scheduler.run_later.push_back(coro) + enqueue(coro) coro } +///| +fn spawn(f : async () -> Unit) -> Coroutine { + match scheduler.curr_coro { + Some(parent) => spawn_owned(f, parent.waitable_set, parent.spawner) + None => spawn_owned(f, current_waitableset(), None) + } +} + ///| fn Coroutine::unwrap(self : Coroutine) -> Unit raise { match self.state { @@ -136,7 +149,7 @@ fn Coroutine::unwrap(self : Coroutine) -> Unit raise { ///| async fn Coroutine::wait(target : Coroutine) -> Unit { guard scheduler.curr_coro is Some(coro) - guard not(physical_equal(coro, target)) + guard !physical_equal(coro, target) match target.state { Done => return Fail(err) => raise err @@ -162,7 +175,10 @@ fn Coroutine::check_error(coro : Coroutine) -> Unit raise { } ///| -pub async fn protect_from_cancel(f : async () -> Unit) -> Unit { +pub async fn[X] protect_from_cancel( + f : async () -> X, + resume_on_cancel? : Bool = false, +) -> X { guard scheduler.curr_coro is Some(coro) if coro.shielded { // already in a shield, do nothing @@ -172,9 +188,10 @@ pub async fn protect_from_cancel(f : async () -> Unit) -> Unit { defer { coro.shielded = false } - f() - if coro.cancelled { + let result = f() + if !resume_on_cancel && coro.cancelled { raise Cancelled::Cancelled } + result } } diff --git a/crates/moonbit/src/async/ev.mbt b/crates/moonbit/src/async/ev.mbt index 23c642cdd..e08383271 100644 --- a/crates/moonbit/src/async/ev.mbt +++ b/crates/moonbit/src/async/ev.mbt @@ -14,15 +14,34 @@ ///| priv struct EventLoop { - subscribes : Map[Int, Subscriber] + subscribes : Map[WaitableSet, Map[Int, Subscriber]] + wakeups : Map[WaitableSet, TaskWakeup] tasks : Map[WaitableSet, Coroutine] + owned_coroutines : Map[WaitableSet, @set.Set[Coroutine]] finished : Map[WaitableSet, Bool] + resolved : Map[WaitableSet, Bool] + cancellations : Map[WaitableSet, TaskCancellation] + post_return_flush : Map[WaitableSet, Bool] +} + +///| +priv struct TaskWakeup { + reader : Int + writer : Int + mut reading : Bool + mut signaled : Bool } ///| priv struct Subscriber { mut event : Events? - coro : Coroutine + coro : @set.Set[Coroutine] +} + +///| +priv enum TaskCancellation { + Requested + Acknowledged } ///| @@ -31,86 +50,464 @@ fn current_waitableset() -> WaitableSet { } ///| +fn subscribers_for(waitable_set : WaitableSet) -> Map[Int, Subscriber] { + match ev.subscribes.get(waitable_set) { + Some(subscribers) => subscribers + None => { + let subscribers = Map([]) + ev.subscribes.set(waitable_set, subscribers) + subscribers + } + } +} + +///| +fn clear_subscribers(waitable_set : WaitableSet) -> Unit { + if ev.subscribes.get(waitable_set) is Some(subscribers) { + subscribers.each(fn(waitable_id, _subscriber) { + waitable_join(waitable_id, 0) + }) + ev.subscribes.remove(waitable_set) + } +} + +///| +fn finish_waitableset(waitable_set : WaitableSet) -> Unit { + ev.tasks.remove(waitable_set) + ev.owned_coroutines.remove(waitable_set) + ev.finished.remove(waitable_set) + ev.resolved.remove(waitable_set) + ev.cancellations.remove(waitable_set) + ev.post_return_flush.remove(waitable_set) + drop_task_wakeup(waitable_set) + clear_subscribers(waitable_set) + forget_schedule(waitable_set) + waitable_set.drop() + tls_set(0) +} + +///| +fn acknowledge_cancellation(waitable_set : WaitableSet) -> Unit { + guard ev.cancellations.get(waitable_set) is Some(Requested) else { return } + guard ev.finished.get(waitable_set) is Some(true) else { return } + guard ev.tasks.get(waitable_set) is Some(coro) else { return } + match coro.state { + Done => () // The generated wrapper already called task.return. + Fail(_) => + if !(ev.resolved.get(waitable_set) is Some(true)) { + task_cancel() + ev.resolved.set(waitable_set, true) + } + Running | Suspend(_) => return + } + ev.cancellations.set(waitable_set, Acknowledged) +} + +///| +fn notify_subscriber( + waitable_set : WaitableSet, + waitable_id : Int, + event : Events, +) -> Unit { + if ev.wakeups.get(waitable_set) is Some(wakeup) && + wakeup.reader == waitable_id { + guard event is StreamRead(i, { progress: 1, copy_result: Completed }) && + i == waitable_id + wakeup.reading = false + wakeup.signaled = false + remove_subscriber(waitable_set, waitable_id) + waitable_join(waitable_id, 0) + return + } + guard ev.subscribes.get(waitable_set) is Some(subscribers) + guard subscribers.get(waitable_id) is Some(subscriber) + subscriber.event = Some(event) + subscriber.coro.each(Coroutine::wake) + // Keep the delivered event discoverable until its waiter consumes it. A + // local cancellation can race with the wakeup, and cancelling an already + // terminal canonical operation would trap. + waitable_join(waitable_id, 0) +} + +///| +fn next_callback(waitable_set : WaitableSet) -> Int { + if ev.post_return_flush.get(waitable_set) is Some(true) { + ev.post_return_flush.remove(waitable_set) + reschedule(waitable_set) + } + acknowledge_cancellation(waitable_set) + if ev.finished.get(waitable_set) is Some(true) && no_more_work(waitable_set) { + let resolved = ev.resolved.get(waitable_set) is Some(true) + let failed = match ev.tasks.get(waitable_set) { + Some(coro) => coro.state is Fail(_) + None => false + } + finish_waitableset(waitable_set) + if !resolved { + if failed { + abort("async export failed before task return") + } else { + abort("async export completed without task return") + } + } + return CallbackCode::Completed.encode() + } + if has_immediately_ready_task(waitable_set) { + if ev.subscribes.get(waitable_set) is Some(subscribers) && + !subscribers.is_empty() { + let (event, waitable_id) = waitable_set.poll() + match event { + None => () + TaskCancelled => panic() + _ => notify_subscriber(waitable_set, waitable_id, event) + } + } + tls_set(waitable_set.0) + return CallbackCode::Yield.encode() + } + tls_set(waitable_set.0) + arm_task_wakeup(waitable_set) + CallbackCode::Wait(waitable_set.0).encode() +} + +///| +#internal(wit_bindgen, "generated binding code only") +#doc(hidden) pub fn with_waitableset(f : async () -> Unit) -> Int { let waitable_set = WaitableSet::new() + let owned = @set.Set([]) tls_set(waitable_set.0) - let coro = spawn(async fn() -> Unit { - defer ev.finished.set(waitable_set, true) - f() - }) + let coro = spawn_owned( + async fn() -> Unit { + let coro = current_coroutine() + defer owned.remove(coro) + defer ev.finished.set(waitable_set, true) + f() + }, + waitable_set, + None, + ) ev.tasks.set(waitable_set, coro) + owned.add(coro) + ev.owned_coroutines.set(waitable_set, owned) ev.finished.set(waitable_set, false) - reschedule() - if ev.finished.get(waitable_set) is Some(true) { - ev.tasks.remove(waitable_set) - ev.finished.remove(waitable_set) - waitable_set.drop() - return CallbackCode::Completed.encode() - } else { - return CallbackCode::Wait(waitable_set.0).encode() - } + ev.resolved.set(waitable_set, false) + reschedule(waitable_set) + next_callback(waitable_set) +} + +///| +/// Record that the generated export wrapper has resolved the component-model +/// task. One additional fair scheduling round lets the enclosing MoonBit task +/// group observe its body completing without draining runnable background work. +#internal(wit_bindgen, "generated binding code only") +#doc(hidden) +pub fn task_returned() -> Unit { + let waitable_set = current_waitableset() + guard ev.tasks.get(waitable_set) is Some(_) + ev.post_return_flush.set(waitable_set, true) + ev.resolved.set(waitable_set, true) } ///| +#internal(wit_bindgen, "generated binding code only") +#doc(hidden) pub fn cb(event : Int, waitable_id : Int, code : Int) -> Int { let waitable_set = current_waitableset() let events = Events::new(EventCode::from(event), waitable_id, code) + let preserve_task_wakeup = match events { + StreamRead(i, _) => + match ev.wakeups.get(waitable_set) { + Some(wakeup) => wakeup.reader == i + None => false + } + _ => false + } + cancel_task_wakeup_read(waitable_set, preserve=preserve_task_wakeup) match events { + None => { + reschedule(waitable_set) + next_callback(waitable_set) + } TaskCancelled => { guard ev.tasks.get(waitable_set) is Some(coro) + ev.cancellations.set(waitable_set, Requested) coro.cancel() - reschedule() - if ev.finished.get(waitable_set) is Some(true) { - ev.tasks.remove(waitable_set) - ev.finished.remove(waitable_set) - waitable_set.drop() - task_cancel() - return CallbackCode::Completed.encode() - } else { - // Unlikely to reach here - return CallbackCode::Wait(waitable_set.0).encode() + if ev.owned_coroutines.get(waitable_set) is Some(owned) { + owned.each(Coroutine::cancel) } + reschedule(waitable_set) + next_callback(waitable_set) } _ => { - let sub = ev.subscribes.get(waitable_id) - guard sub is Some(subscriber) - subscriber.event = Some(events) - subscriber.coro.wake() - reschedule() - if ev.finished.get(waitable_set) is Some(true) { - ev.tasks.remove(waitable_set) - ev.finished.remove(waitable_set) - waitable_set.drop() - return CallbackCode::Completed.encode() - } else { - return CallbackCode::Wait(waitable_set.0).encode() + notify_subscriber(waitable_set, waitable_id, events) + reschedule(waitable_set) + next_callback(waitable_set) + } + } +} + +///| +let ev : EventLoop = { + subscribes: Map([]), + wakeups: Map([]), + tasks: Map([]), + owned_coroutines: Map([]), + finished: Map([]), + resolved: Map([]), + cancellations: Map([]), + post_return_flush: Map([]), +} + +///| +/// Spawn a coroutine owned by the current component-model async task. +/// +/// This is intended for runtime bridge work such as lowering local futures and +/// streams to component-model handles. The coroutine does not participate in +/// `TaskGroup` structured concurrency, but it is cancelled when the current +/// component-model task is cancelled and keeps the waitable-set alive until it +/// terminates. +#internal(wit_bindgen, "generated binding code only") +#doc(hidden) +pub fn spawn_component_task_current(f : async () -> Unit) -> Unit { + let waitable_set = current_waitableset() + guard ev.tasks.get(waitable_set) is Some(_) + guard ev.owned_coroutines.get(waitable_set) is Some(owned) + let coro = spawn_owned( + async fn() -> Unit { + let coro = current_coroutine() + defer owned.remove(coro) + f() + }, + waitable_set, + Some(spawn_component_task_current), + ) + owned.add(coro) +} + +///| +#internal(wit_bindgen, "generated binding code only") +#doc(hidden) +pub fn has_component_task_scope() -> Bool { + let waitable_set = current_waitableset() + ev.tasks.get(waitable_set) is Some(_) && + ev.owned_coroutines.get(waitable_set) is Some(_) +} + +///| +#internal(wit_bindgen, "generated binding code only") +#doc(hidden) +pub fn current_component_task_token() -> Int { + current_waitableset().0 +} + +///| +fn detach_waitable(waitable_id : Int) -> Unit { + let waitable_set = current_waitableset() + if ev.subscribes.get(waitable_set) is Some(subscribers) { + if subscribers.get(waitable_id) is Some(subscriber) { + subscriber.coro.clear() + subscribers.remove(waitable_id) + } + if subscribers.is_empty() { + ev.subscribes.remove(waitable_set) + } + } + waitable_join(waitable_id, 0) +} + +///| +fn remove_subscriber(waitable_set : WaitableSet, waitable_id : Int) -> Unit { + if ev.subscribes.get(waitable_set) is Some(subscribers) { + subscribers.remove(waitable_id) + if subscribers.is_empty() { + ev.subscribes.remove(waitable_set) + } + } +} + +///| +fn task_wakeup(waitable_set : WaitableSet) -> TaskWakeup { + match ev.wakeups.get(waitable_set) { + Some(wakeup) => wakeup + None => { + let pair = task_wakeup_stream_new() + let wakeup = { + reader: pair.to_int(), + writer: (pair >> 32).to_int(), + reading: false, + signaled: false, } + ev.wakeups.set(waitable_set, wakeup) + wakeup } } } ///| -let ev : EventLoop = { subscribes: {}, tasks: {}, finished: {} } +fn arm_task_wakeup(waitable_set : WaitableSet) -> Unit { + let wakeup = task_wakeup(waitable_set) + if wakeup.reading { + return + } + let result = task_wakeup_stream_read(wakeup.reader, 0, 1) + guard result == -1 + let subscribers = subscribers_for(waitable_set) + guard subscribers.get(wakeup.reader) is None + subscribers.set(wakeup.reader, { event: None, coro: @set.Set([]) }) + waitable_join(wakeup.reader, waitable_set.0) + wakeup.reading = true + wakeup.signaled = false +} + +///| +fn signal_component_task(waitable_set : WaitableSet) -> Unit { + guard ev.wakeups.get(waitable_set) is Some(wakeup) else { return } + if !wakeup.reading || wakeup.signaled { + return + } + let result = StreamResult::from(task_wakeup_stream_write(wakeup.writer, 0, 1)) + guard result.progress == 1 && result.copy_result is Completed + wakeup.signaled = true +} + +///| +fn cancel_task_wakeup_read( + waitable_set : WaitableSet, + preserve~ : Bool, +) -> Unit { + guard ev.wakeups.get(waitable_set) is Some(wakeup) else { return } + if !wakeup.reading || preserve { + return + } + waitable_join(wakeup.reader, 0) + remove_subscriber(waitable_set, wakeup.reader) + ignore(task_wakeup_stream_cancel_read(wakeup.reader)) + wakeup.reading = false + wakeup.signaled = false +} + +///| +fn drop_task_wakeup(waitable_set : WaitableSet) -> Unit { + guard ev.wakeups.get(waitable_set) is Some(wakeup) else { return } + if wakeup.reading { + waitable_join(wakeup.reader, 0) + remove_subscriber(waitable_set, wakeup.reader) + ignore(task_wakeup_stream_cancel_read(wakeup.reader)) + } + task_wakeup_stream_drop_readable(wakeup.reader) + task_wakeup_stream_drop_writable(wakeup.writer) + ev.wakeups.remove(waitable_set) +} + +///| +fn cancel_waitable_event( + owner_task : Int, + waitable_id : Int, + cancel : () -> Events, +) -> Events { + let waitable_set = WaitableSet(owner_task) + guard ev.subscribes.get(waitable_set) is Some(subscribers) + guard subscribers.get(waitable_id) is Some(subscriber) + + // Baseline cancel-read is synchronous and traps while its endpoint belongs + // to a waitable set. Detach before invoking the generated intrinsic. + waitable_join(waitable_id, 0) + let event = match subscriber.event { + Some(event) => event + None => cancel() + } + subscriber.event = Some(event) + subscriber.coro.each(Coroutine::wake) + remove_subscriber(waitable_set, waitable_id) + event +} + +///| +/// Complete an operation immediately or suspend until its terminal waitable +/// event arrives. This is the single owner of subscription registration and +/// detachment for future and stream copy operations. +async fn waitable_event(waitable_id : Int, immediate : Events?) -> Events { + let waitable_set = current_waitableset() + match immediate { + Some(event) => { + if ev.subscribes.get(waitable_set) is Some(subscribers) { + if subscribers.get(waitable_id) is Some(subscriber) { + subscriber.event = Some(event) + subscriber.coro.each(Coroutine::wake) + subscriber.coro.clear() + waitable_join(waitable_id, 0) + } + remove_subscriber(waitable_set, waitable_id) + } + event + } + None => { + let subscribers = subscribers_for(waitable_set) + let subscriber = if subscribers.get(waitable_id) is Some(subscriber) { + subscriber + } else { + let subscriber = { event: None, coro: @set.Set([]) } + subscribers.set(waitable_id, subscriber) + subscriber + } + guard subscriber.event is None + waitable_join(waitable_id, waitable_set.0) + let coro = current_coroutine() + subscriber.coro.add(coro) + defer subscriber.coro.remove(coro) + let event = try suspend() catch { + err => + match subscriber.event { + // Completion owns the canonical buffer once its event is + // delivered, even if local cancellation wakes the same coroutine. + Some(event) => event + None => raise err + } + } noraise { + _ => subscriber.event.unwrap() + } + remove_subscriber(waitable_set, waitable_id) + event + } + } +} ///| +#internal(wit_bindgen, "generated binding code only") +#doc(hidden) pub async fn suspend_for_subtask( val : Int, - cleanup_after_started : () -> Unit, + settle_lowered_arguments : (Bool) -> Unit, + drop_returned_result : () -> Unit, ) -> Unit { let task = SubTask::from(val) - defer subtask_drop(task.handle) let mut cleaned = false - // Helper: ensure cleanup is called once we've moved past Starting state - fn ensure_cleanup(state : SubTaskState) -> Unit { - if not(cleaned) && !(state is Starting) { - cleanup_after_started() + // Once the subtask leaves Starting, canonical arguments are either + // transferred or rejected. Generated code owns the corresponding action. + fn settle_arguments(state : SubTaskState) -> Unit { + if !cleaned && !(state is Starting) { cleaned = true + settle_lowered_arguments(state is Cancelled_before_started) } } + // Immediate completion without a handle. + if task.handle == 0 { + settle_arguments(task.state) + match task.state { + Returned => return + Cancelled_before_started => raise SubTaskCancelled(before_started=true) + Cancelled_before_returned => raise SubTaskCancelled(before_started=false) + _ => panic() + } + } + + defer subtask_drop(task.handle) + // Initial state, return if finished - ensure_cleanup(task.state) + settle_arguments(task.state) match task.state { Returned => return Cancelled_before_started => raise SubTaskCancelled(before_started=true) @@ -118,40 +515,64 @@ pub async fn suspend_for_subtask( _ => () } - // Create subscriber to wait for events - let subscriber = { event: None, coro: current_coroutine() } - ev.subscribes.set(task.handle, subscriber) - defer ev.subscribes.remove(task.handle) - waitable_join(task.handle, current_waitableset().0) - defer waitable_join(task.handle, 0) - for { + // A subtask can report intermediate states, so register the same subscriber + // again after each callback until a terminal state arrives. + let waitable_set = current_waitableset() + let set = @set.Set([]) + let subscriber = { event: None, coro: set } + let coro = current_coroutine() + set.add(coro) + defer { + set.remove(coro) + detach_waitable(task.handle) + } + for ;; { + let subscribers = subscribers_for(waitable_set) + guard subscribers.get(task.handle) is None + subscribers.set(task.handle, subscriber) + waitable_join(task.handle, waitable_set.0) suspend() catch { Cancelled::Cancelled => // Cancel the subtask return protect_from_cancel(() => { - subscriber.event = task - .cancel() - .map(state => Subtask(task.handle, state)) - while subscriber.event is None { - suspend() + detach_waitable(task.handle) + // A terminal event can race with local cancellation after waking + // this coroutine. Once delivered, cancelling the subtask again + // traps, so consume that event before requesting cancellation. + if subscriber.event is Some(Subtask(i, state)) { + guard i == task.handle + settle_arguments(state) + match state { + Returned => { + drop_returned_result() + return + } + Cancelled_before_started | Cancelled_before_returned => + raise Cancelled::Cancelled + Starting | Started => subscriber.event = None + } } - guard subscriber.event is Some(Subtask(i, state)) && i == task.handle - ensure_cleanup(state) + let state = task.cancel() + settle_arguments(state) match state { - Returned => return - Cancelled_before_started => - raise SubTaskCancelled(before_started=true) - Cancelled_before_returned => - raise SubTaskCancelled(before_started=false) - _ => panic() // should not happen + Returned => { + drop_returned_result() + return + } + Cancelled_before_started | Cancelled_before_returned => + raise Cancelled::Cancelled + Starting | Started => panic() } }) + err => raise err } + remove_subscriber(waitable_set, task.handle) + // Subsequent state, return if finished if subscriber.event is Some(Subtask(i, state)) { guard i == task.handle - ensure_cleanup(state) + settle_arguments(state) match state { Returned => return Cancelled_before_started => raise SubTaskCancelled(before_started=true) @@ -164,56 +585,105 @@ pub async fn suspend_for_subtask( } ///| +#internal(wit_bindgen, "generated binding code only") +#doc(hidden) pub async fn suspend_for_future_read(idx : Int, val : Int) -> Unit { - let result = if val == -1 { - let subscriber = { event: None, coro: current_coroutine() } - ev.subscribes.set(idx, subscriber) - defer ev.subscribes.remove(idx) - waitable_join(idx, current_waitableset().0) - defer waitable_join(idx, 0) - suspend() - guard subscriber.event is Some(FutureRead(i, result)) && i == idx - result + let event = if val == -1 { + waitable_event(idx, None) } else { - FutureReadResult::from(val) + let result = FutureReadResult::from(val) + waitable_event(idx, Some(FutureRead(idx, result))) } + guard event is FutureRead(i, result) && i == idx match result { Completed => return - Cancelled => raise FutureReadCancelled + Cancelled => raise FutureReadError::Cancelled } } ///| -pub async fn suspend_for_future_write(idx : Int) -> Bool { - let subscriber = { event: None, coro: current_coroutine() } - ev.subscribes.set(idx, subscriber) - defer ev.subscribes.remove(idx) - waitable_join(idx, current_waitableset().0) - defer waitable_join(idx, 0) - suspend() - guard subscriber.event is Some(FutureWrite(i, result)) && i == idx +#internal(wit_bindgen, "generated binding code only") +#doc(hidden) +pub fn cancel_future_read( + owner_task : Int, + idx : Int, + cancel : () -> Int, +) -> Bool { + let event = cancel_waitable_event(owner_task, idx, () => { + FutureRead(idx, FutureReadResult::from(cancel())) + }) + guard event is FutureRead(i, result) && i == idx match result { Completed => true - Dropped => false - Cancelled => raise FutureWriteCancelled + Cancelled => false + } +} + +///| +#internal(wit_bindgen, "generated binding code only") +#doc(hidden) +pub fn cancel_stream_read( + owner_task : Int, + idx : Int, + cancel : () -> Int, +) -> Int { + let event = cancel_waitable_event(owner_task, idx, () => { + StreamRead(idx, StreamResult::from(cancel())) + }) + guard event is StreamRead(i, result) && i == idx + result.progress +} + +///| +#internal(wit_bindgen, "generated binding code only") +#doc(hidden) +pub fn cancel_stream_write(idx : Int, cancel : () -> Int) -> Int { + let event = cancel_waitable_event(current_component_task_token(), idx, () => { + StreamWrite(idx, StreamResult::from(cancel())) + }) + guard event is StreamWrite(i, result) && i == idx + result.progress +} + +///| +#internal(wit_bindgen, "generated binding code only") +#doc(hidden) +pub async fn suspend_for_future_write_terminal(idx : Int, val : Int) -> Bool? { + let event = if val == -1 { + waitable_event(idx, None) + } else { + let result = FutureWriteResult::from(val) + waitable_event(idx, Some(FutureWrite(idx, result))) + } + guard event is FutureWrite(i, result) && i == idx + match result { + Completed => Some(true) + Dropped => Some(false) + Cancelled => None } } ///| +#internal(wit_bindgen, "generated binding code only") +#doc(hidden) +pub async fn suspend_for_future_write(idx : Int, val : Int) -> Bool { + match suspend_for_future_write_terminal(idx, val) { + Some(transferred) => transferred + None => raise FutureWriteCancelled + } +} + +///| +#internal(wit_bindgen, "generated binding code only") +#doc(hidden) pub async fn suspend_for_stream_read(idx : Int, val : Int) -> (Int, Bool) { - let { progress, copy_result } = if val == -1 { - // Blocked, wait for event - let subscriber = { event: None, coro: current_coroutine() } - ev.subscribes.set(idx, subscriber) - defer ev.subscribes.remove(idx) - waitable_join(idx, current_waitableset().0) - defer waitable_join(idx, 0) - suspend() - guard subscriber.event is Some(StreamRead(i, result)) && i == idx - result + let event = if val == -1 { + waitable_event(idx, None) } else { - StreamResult::from(val) + let result = StreamResult::from(val) + waitable_event(idx, Some(StreamRead(idx, result))) } + guard event is StreamRead(i, { progress, copy_result }) && i == idx match copy_result { Completed => return (progress, false) Dropped => return (progress, true) @@ -227,38 +697,39 @@ pub async fn suspend_for_stream_read(idx : Int, val : Int) -> (Int, Bool) { } ///| +#internal(wit_bindgen, "generated binding code only") +#doc(hidden) pub async fn suspend_for_stream_write(idx : Int, val : Int) -> (Int, Bool) { - let { progress, copy_result } = if val != -1 { - // Not blocked - StreamResult::from(val) + let event = if val == -1 { + waitable_event(idx, None) } else { - // Blocked, wait for event - let subscriber = { event: None, coro: current_coroutine() } - ev.subscribes.set(idx, subscriber) - defer ev.subscribes.remove(idx) - waitable_join(idx, current_waitableset().0) - defer waitable_join(idx, 0) - suspend() - guard subscriber.event is Some(StreamWrite(i, result)) && i == idx - result + let result = StreamResult::from(val) + waitable_event(idx, Some(StreamWrite(idx, result))) } + guard event is StreamWrite(i, { progress, copy_result }) && i == idx match copy_result { Completed => return (progress, false) Dropped => return (progress, true) - Cancelled => - if progress > 0 { - return (progress, false) - } else { - raise StreamWriteCancelled - } + Cancelled => return (progress, false) } } ///| pub suberror OpCancelled { SubTaskCancelled(before_started~ : Bool) - StreamWriteCancelled StreamReadCancelled FutureWriteCancelled - FutureReadCancelled +} + +///| +pub(all) suberror FutureReadError { + Cancelled + Dropped +} + +///| +#internal(wit_bindgen, "generated binding code only") +pub(all) suberror EndpointBusy { + Read + Write } diff --git a/crates/moonbit/src/async/moon.pkg.json b/crates/moonbit/src/async/moon.pkg.json index b7a5c45c3..ae0b479d4 100644 --- a/crates/moonbit/src/async/moon.pkg.json +++ b/crates/moonbit/src/async/moon.pkg.json @@ -1 +1,9 @@ -{ "warn-list": "-44", "supported-targets": ["wasm"] } \ No newline at end of file +{ + "warn-list": "-44", + "import": [ + { "path": "moonbitlang/core/deque", "alias": "deque" }, + { "path": "moonbitlang/core/ref", "alias": "ref" }, + { "path": "moonbitlang/core/set", "alias": "set" } + ], + "supported-targets": "+wasm" +} diff --git a/crates/moonbit/src/async/mutex.mbt b/crates/moonbit/src/async/mutex.mbt new file mode 100644 index 000000000..90197d047 --- /dev/null +++ b/crates/moonbit/src/async/mutex.mbt @@ -0,0 +1,44 @@ +// Copyright 2025 International Digital Economy Academy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +///| +/// A mutex for synchronization between tasks. +#valtype +pub struct Mutex { + priv semaphore : Semaphore +} + +///| +/// Create a mutex in the released state. +pub fn Mutex::Mutex() -> Mutex { + { semaphore: Semaphore(1) } +} + +///| +/// Acquire the mutex in FIFO order. +pub async fn Mutex::acquire(self : Mutex) -> Unit { + self.semaphore.acquire() +} + +///| +/// Try to acquire the mutex without waiting. +pub fn Mutex::try_acquire(self : Mutex) -> Bool { + self.semaphore.try_acquire() +} + +///| +/// Release the mutex. The mutex must already be acquired. +pub fn Mutex::release(self : Mutex) -> Unit { + self.semaphore.release() +} diff --git a/crates/moonbit/src/async/promise.mbt b/crates/moonbit/src/async/promise.mbt new file mode 100644 index 000000000..ec72b21b7 --- /dev/null +++ b/crates/moonbit/src/async/promise.mbt @@ -0,0 +1,200 @@ +// Copyright 2025 International Digital Economy Academy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +///| +priv enum PromiseState[X] { + Pending + Completed(X) + Failed(Error) + Closed + ReaderDropped + Consumed +} + +///| +priv struct PromiseCell[X] { + mut state : PromiseState[X] + ready : Semaphore + cleanup : ((X) -> Unit)? + mut reading : Bool +} + +///| +/// The local producer side of a one-shot `Future[X]`. +/// +/// This is a MoonBit coordination value, not a component-model future writable +/// endpoint. Conversion to a component future remains generated at an FFI site. +pub struct Promise[X] { + priv cell : Ref[PromiseCell[X]] +} + +///| +pub(all) suberror PromiseClosed derive(Debug) + +///| +fn[X] new_future_promise(cleanup : ((X) -> Unit)?) -> (Future[X], Promise[X]) { + let cell : Ref[PromiseCell[X]] = { + val: { + state: Pending, + ready: Semaphore(1, initial_value=0), + cleanup, + reading: false, + }, + } + (Future::{ inner: Promised(cell) }, Promise::{ cell, }) +} + +///| +/// Create a local one-shot Future/Promise pair. +pub fn[X] Future::new() -> (Future[X], Promise[X]) { + new_future_promise(None) +} + +///| +/// Create a local one-shot Future/Promise pair with value-discard cleanup. +/// +/// The cleanup callback runs when the promise completed successfully but the +/// future is dropped before consuming the value. +pub fn[X] Future::new_with_cleanup( + cleanup : (X) -> Unit, +) -> (Future[X], Promise[X]) { + new_future_promise(Some(cleanup)) +} + +///| +/// Complete the paired future with `value`. +/// +/// `true` means the future accepted ownership. `false` means its reader was +/// already dropped and the caller retains ownership of `value`. +pub fn[X] Promise::complete(self : Promise[X], value : X) -> Bool { + match self.cell.val.state { + Pending => { + self.cell.val.state = Completed(value) + self.cell.val.ready.release() + true + } + ReaderDropped => false + Completed(_) | Failed(_) | Closed | Consumed => + abort("promise already settled") + } +} + +///| +/// Complete the paired future with an error raised by `Future::get`. +pub fn[X] Promise::fail(self : Promise[X], error : Error) -> Bool { + match self.cell.val.state { + Pending => { + self.cell.val.state = Failed(error) + self.cell.val.ready.release() + true + } + ReaderDropped => false + Completed(_) | Failed(_) | Closed | Consumed => + abort("promise already settled") + } +} + +///| +/// Close the paired future without producing a value. +/// +/// A local waiter observes `PromiseClosed`. An outgoing component future still +/// cannot represent this terminal state and follows the bridge's no-value +/// failure policy if it has already crossed an FFI boundary. +pub fn[X] Promise::close(self : Promise[X]) -> Bool { + match self.cell.val.state { + Pending => { + self.cell.val.state = Closed + self.cell.val.ready.release() + true + } + ReaderDropped => false + Completed(_) | Failed(_) | Closed | Consumed => + abort("promise already settled") + } +} + +///| +fn[X] consume_promised_future(cell : Ref[PromiseCell[X]]) -> X raise { + match cell.val.state { + Completed(value) => { + cell.val.state = Consumed + value + } + Failed(error) => { + cell.val.state = Consumed + raise error + } + Closed => { + cell.val.state = Consumed + raise PromiseClosed + } + Pending => panic() + ReaderDropped => raise Cancelled::Cancelled + Consumed => abort("future already consumed") + } +} + +///| +async fn[X] promised_future_get(cell : Ref[PromiseCell[X]]) -> X { + if cell.val.reading { + abort("future read already in progress") + } + match cell.val.state { + Pending => { + cell.val.reading = true + cell.val.ready.acquire() catch { + err => { + cell.val.reading = false + if cell.val.state is Pending { + cell.val.state = ReaderDropped + } + raise err + } + } + cell.val.reading = false + consume_promised_future(cell) + } + Completed(_) | Failed(_) | Closed => consume_promised_future(cell) + ReaderDropped => consume_promised_future(cell) + Consumed => abort("future already consumed") + } +} + +///| +fn[X] promised_future_drop(cell : Ref[PromiseCell[X]]) -> Unit { + if cell.val.reading { + match cell.val.state { + Pending => { + cell.val.state = ReaderDropped + cell.val.ready.release() + } + // Once settlement assigned a result, the waiting reader owns it. + Completed(_) | Failed(_) | Closed => () + ReaderDropped | Consumed => () + } + return + } + match cell.val.state { + Pending => cell.val.state = ReaderDropped + Completed(value) => { + cell.val.state = Consumed + match cell.val.cleanup { + Some(cleanup) => cleanup(value) + None => () + } + } + Failed(_) | Closed => cell.val.state = Consumed + ReaderDropped | Consumed => () + } +} diff --git a/crates/moonbit/src/async/scheduler.mbt b/crates/moonbit/src/async/scheduler.mbt index c99947b54..0b50070b4 100644 --- a/crates/moonbit/src/async/scheduler.mbt +++ b/crates/moonbit/src/async/scheduler.mbt @@ -16,42 +16,76 @@ priv struct Scheduler { mut coro_id : Int mut curr_coro : Coroutine? + tasks : Map[WaitableSet, TaskSchedule] +} + +///| +priv struct TaskSchedule { mut blocking : Int run_later : @deque.Deque[Coroutine] } ///| -let scheduler : Scheduler = { - coro_id: 0, - curr_coro: None, - blocking: 0, - run_later: @deque.new(), -} +let scheduler : Scheduler = { coro_id: 0, curr_coro: None, tasks: Map([]) } ///| -pub fn current_coroutine() -> Coroutine { +fn current_coroutine() -> Coroutine { scheduler.curr_coro.unwrap() } ///| -pub fn has_immediately_ready_task() -> Bool { - !scheduler.run_later.is_empty() +fn task_schedule(waitable_set : WaitableSet) -> TaskSchedule { + match scheduler.tasks.get(waitable_set) { + Some(schedule) => schedule + None => { + let schedule = { blocking: 0, run_later: Deque([]) } + scheduler.tasks.set(waitable_set, schedule) + schedule + } + } +} + +///| +fn enqueue(coro : Coroutine) -> Unit { + task_schedule(coro.waitable_set).run_later.push_back(coro) +} + +///| +fn has_immediately_ready_task(waitable_set : WaitableSet) -> Bool { + match scheduler.tasks.get(waitable_set) { + Some(schedule) => !schedule.run_later.is_empty() + None => false + } +} + +///| +fn no_more_work(waitable_set : WaitableSet) -> Bool { + match scheduler.tasks.get(waitable_set) { + Some(schedule) => schedule.blocking == 0 && schedule.run_later.is_empty() + None => true + } } ///| -pub fn no_more_work() -> Bool { - scheduler.blocking == 0 && scheduler.run_later.is_empty() +fn forget_schedule(waitable_set : WaitableSet) -> Unit { + scheduler.tasks.remove(waitable_set) } ///| -pub fn reschedule() -> Unit { - while scheduler.run_later.pop_front() is Some(coro) { +/// Run one fair scheduling round for a component task. Coroutines woken or +/// spawned during this round are left for the next round so the component +/// event loop gets a chance to poll completed waitables. +fn reschedule(waitable_set : WaitableSet) -> Unit { + guard scheduler.tasks.get(waitable_set) is Some(schedule) else { return } + let count = schedule.run_later.length() + for _ in 0.. Semaphore { + if size <= 0 { + abort("size of semaphore must be positive") + } + guard initial_value >= 0 && initial_value <= size + { value: initial_value, size, waiters: Deque([]) } +} + +///| +/// Release one permit. Waiting coroutines acquire permits in FIFO order. +/// +/// Each call must correspond one-to-one with a successful `acquire`. Calling +/// `release` without owning a permit is invalid. +pub fn Semaphore::release(self : Semaphore) -> Unit { + if self.value >= self.size { + abort("semaphore: too many release") + } + while self.waiters.pop_front() is Some(waiter) { + if waiter.coro is Some(coro) { + waiter.acquired = true + coro.wake() + break + } + } nobreak { + self.value += 1 + } +} + +///| +/// Acquire one permit, suspending in FIFO order when none is available. +pub async fn Semaphore::acquire(self : Semaphore) -> Unit { + if self.value > 0 { + self.value -= 1 + } else { + let waiter = { coro: Some(current_coroutine()), acquired: false } + self.waiters.push_back(waiter) + suspend() catch { + _ if waiter.acquired => () + err => { + waiter.coro = None + raise err + } + } + } +} + +///| +/// Try to acquire one permit without suspending. +pub fn Semaphore::try_acquire(self : Semaphore) -> Bool { + if self.value > 0 { + self.value -= 1 + true + } else { + false + } +} diff --git a/crates/moonbit/src/async/task.mbt b/crates/moonbit/src/async/task.mbt index 676936293..20e18e056 100644 --- a/crates/moonbit/src/async/task.mbt +++ b/crates/moonbit/src/async/task.mbt @@ -15,9 +15,9 @@ ///| /// `Task[X]` represents a running task with result type `X`, /// it can be used to wait and retrieve the result value of the task. -struct Task_[X] { - value : Ref[X?] - coro : Coroutine +pub struct Task[X] { + priv value : Ref[X?] + priv coro : Coroutine } ///| @@ -25,7 +25,7 @@ struct Task_[X] { /// If the task fails, `wait` will also fail with the same error. /// /// If the current task is cancelled, `wait` return immediately with error. -pub async fn[X] Task_::wait(self : Task_[X]) -> X { +pub async fn[X] Task::wait(self : Task[X]) -> X { self.coro.wait() self.value.val.unwrap() } @@ -36,15 +36,13 @@ pub async fn[X] Task_::wait(self : Task_[X]) -> X { /// If the task already failed, `try_wait` will fail immediately. /// If the task is still running, `try_wait` returns `None`. /// `try_wait` is a synchoronous function: it never blocks. -pub fn[X] Task_::try_wait(self : Task_[X]) -> X? raise { +pub fn[X] Task::try_wait(self : Task[X]) -> X? raise { self.coro.check_error() self.value.val } ///| /// Cancel a task. Subsequent attempt to wait for the task will receive error. -/// Note that if the task is *not* spawned with `allow_failure=true`, -/// the whole task group will fail too. -pub fn[X] Task_::cancel(self : Task_[X]) -> Unit { +pub fn[X] Task::cancel(self : Task[X]) -> Unit { self.coro.cancel() } diff --git a/crates/moonbit/src/async/task_group.mbt b/crates/moonbit/src/async/task_group.mbt index 03384b941..66186eb9e 100644 --- a/crates/moonbit/src/async/task_group.mbt +++ b/crates/moonbit/src/async/task_group.mbt @@ -31,19 +31,15 @@ priv enum TaskGroupState { /// /// The type parameter `X` in `TaskGroup[X]` is the result type of the group, /// see `with_task_group` for more detail. -struct TaskGroup[X] { - children : Set[Coroutine] - parent : Coroutine - mut waiting : Int - mut state : TaskGroupState - mut result : X? - group_defer : Array[async () -> Unit] +pub struct TaskGroup[X] { + priv children : Set[Coroutine] + priv parent : Coroutine + priv mut waiting : Int + priv mut state : TaskGroupState + priv mut result : X? + priv group_defer : Array[async () -> Unit] } -///| -#deprecated("this error is no longer emitted") -pub suberror AlreadyTerminated derive(Show) - ///| fn[X] TaskGroup::spawn_coroutine( self : TaskGroup[X], @@ -51,17 +47,17 @@ fn[X] TaskGroup::spawn_coroutine( no_wait~ : Bool, allow_failure~ : Bool, ) -> Coroutine { - guard self.state is Running else { + guard self.state is Running || !self.children.is_empty() else { abort("trying to spawn from a terminated task group") } - if not(no_wait) { + if !no_wait { self.waiting += 1 } async fn worker() { let coro = current_coroutine() defer { self.children.remove(coro) - if not(no_wait) { + if !no_wait { self.waiting -= 1 if self.waiting == 0 && self.state is Running { for child in self.children { @@ -74,16 +70,16 @@ fn[X] TaskGroup::spawn_coroutine( self.parent.wake() } } - guard self.state is Running else { } f() catch { err if allow_failure => raise err + Cancelled::Cancelled as err if is_being_cancelled() => raise err err => { if self.state is Running { for child in self.children { child.cancel() } self.state = Fail(err) - } else if not(err is Cancelled::Cancelled) { + } else { self.state = Fail(err) } raise err @@ -93,6 +89,9 @@ fn[X] TaskGroup::spawn_coroutine( let coro = spawn(worker) self.children.add(coro) + if !(self.state is Running) { + coro.cancel() + } coro } @@ -100,14 +99,23 @@ fn[X] TaskGroup::spawn_coroutine( /// Spawn a child task in a task group, and run it asynchronously in the background. /// /// Unless `no_wait` (`false` by default) is `true`, -/// the whole task group will only exit after this child task terminates. +/// the task group will wait for this task to complete before normal exit. +/// No matter what the value of `no_wait` is, +/// `with_task_group` will only return after all child tasks terminate. +/// The task will be cancelled automatically if it is still running +/// when the task group wishes to terminate. /// /// Unless `allow_failure` (`false` by default) is `true`, -/// Ithe whole task group will also fail if the spawned task fails, +/// the whole task group will also fail if the spawned task fails, /// other tasks in the group will be cancelled in this case. /// -/// If the task group is already cancelled or has been terminated, -/// `spawn_bg` will fail with error and the child task will not be spawned. +/// Cancelled tasks are not considered failing, although they raise a +/// cancellation error. A different error raised during cancellation is still +/// considered a failure. +/// +/// A task spawned while the group is cancelling still starts, but begins in a +/// cancelled state so its entry cleanup can run. Spawning after the group has +/// terminated or from a group defer is invalid and aborts. /// /// It is undefined whether the child task will start running immediately /// before `spawn_bg` returns. @@ -126,14 +134,23 @@ pub fn[X] TaskGroup::spawn_bg( /// and retrieved using `.wait()`, or cancelled using `.cancel()`. /// /// Unless `no_wait` (`false` by default) is `true`, -/// the whole task group will only exit after this child task terminates. +/// the task group will wait for this task to complete before normal exit. +/// No matter what the value of `no_wait` is, +/// `with_task_group` will only return after all child tasks terminate. +/// The task will be cancelled automatically if it is still running +/// when the task group wishes to terminate. /// /// Unless `allow_failure` (`false` by default) is `true`, -/// Ithe whole task group will also fail if the spawned task fails, +/// the whole task group will also fail if the spawned task fails, /// other tasks in the group will be cancelled in this case. /// -/// If the task group is already cancelled or has been terminated, -/// `spawn` will fail with error and the child task will not be spawned. +/// Cancelled tasks are not considered failing, although they raise a +/// cancellation error. A different error raised during cancellation is still +/// considered a failure. +/// +/// A task spawned while the group is cancelling still starts, but begins in a +/// cancelled state so its entry cleanup can run. Spawning after the group has +/// terminated or from a group defer is invalid and aborts. /// /// It is undefined whether the child task will start running immediately /// before `spawn` returns. @@ -142,7 +159,7 @@ pub fn[G, X] TaskGroup::spawn( f : async () -> X, no_wait? : Bool = false, allow_failure? : Bool = false, -) -> Task_[X] { +) -> Task[X] { let value = @ref.new(Option::None) let coro = self.spawn_coroutine( () => value.val = Some(f()), @@ -154,7 +171,8 @@ pub fn[G, X] TaskGroup::spawn( ///| /// Attach a defer block, represented as a cleanup function, to a task group. -/// The clenaup function will be invoked when the group terminates. +/// The cleanup function runs after every child task has terminated. Spawning a +/// new child from a group defer is invalid and aborts. /// Group scoped defer blocks are executed in FILO order, just like normal `defer`. /// `with_task_group` will only exit after all group defer blocks terminate. /// @@ -183,36 +201,39 @@ pub fn[X] TaskGroup::add_defer( /// `with_task_group` will return the result of `f`. pub async fn[X] with_task_group(f : async (TaskGroup[X]) -> X) -> X { let tg = { - children: Set::new(), + children: Set([]), parent: current_coroutine(), waiting: 0, state: Running, result: None, group_defer: [], } - tg.spawn_bg(fn() { + let curr = current_coroutine() + let prev = curr.spawner + curr.spawner = Some(fn(child) { tg.spawn_bg(child) }) + defer { + curr.spawner = prev + } + tg.spawn_bg(() => { let value = f(tg) if tg.result is None { tg.result = Some(value) } }) - if not(tg.children.is_empty()) { - suspend() catch { - err => - if tg.state is Running { - tg.state = Fail(err) - for child in tg.children { - child.cancel() - } + suspend() catch { + err => { + if tg.state is Running { + tg.state = Fail(err) + } + if !tg.children.is_empty() { + for child in tg.children { + child.cancel() } + protect_from_cancel(() => suspend(), resume_on_cancel=true) + } } } - if not(tg.children.is_empty()) { - protect_from_cancel(() => suspend()) catch { - _ => () - } - } - tg.children.clear() + guard tg.children.is_empty() while tg.group_defer.pop() is Some(defer_block) { defer_block() catch { err => if tg.state is Done { tg.state = Fail(err) } @@ -238,12 +259,11 @@ pub fn[X] TaskGroup::return_immediately( } if self.state is Running { self.state = Done - let curr_coro = current_coroutine() for child in self.children { - if child != curr_coro { - child.cancel() - } + child.cancel() } } - raise Cancelled::Cancelled + if is_being_cancelled() { + raise Cancelled::Cancelled + } } diff --git a/crates/moonbit/src/async/trait.mbt b/crates/moonbit/src/async/trait.mbt index ad2c20b76..1413502d0 100644 --- a/crates/moonbit/src/async/trait.mbt +++ b/crates/moonbit/src/async/trait.mbt @@ -1,50 +1,908 @@ ///| -pub(all) struct FutureR[X](async () -> X) +pub struct Sink[X] { + priv write : async (ArrayView[X]) -> Int + priv close : async () -> Unit + priv is_open : () -> Bool + priv has_cleanup : () -> Bool + priv cleanup : ((X) -> Unit)? +} + +///| +let sink_write_window_size : Int = 64 + +///| +/// The write callback must either raise before consuming any value or return +/// the number consumed. It must not raise after partial consumption because +/// `ArrayView` cannot carry an ownership offset through an exception. +#internal(wit_bindgen, "generated binding code only") +pub fn[X] Sink::from_callbacks( + write : async (ArrayView[X]) -> Int, + close : async () -> Unit, + is_open : () -> Bool, + cleanup : ((X) -> Unit)?, +) -> Sink[X] { + { write, close, is_open, has_cleanup: () => cleanup is Some(_), cleanup } +} + +///| +/// Consumes up to one bounded window and returns the number of values consumed. +/// +/// Consumption includes values accepted by the peer and values recursively +/// cleaned after the peer closes. Check `is_open()` to distinguish those cases. +pub async fn[X] Sink::write(self : Sink[X], data : ArrayView[X]) -> Int { + if data.length() == 0 { + return 0 + } + let length = if data.length() < sink_write_window_size { + data.length() + } else { + sink_write_window_size + } + // The callback may suspend, so it must not retain the caller's borrowed view. + let buffer = FixedArray::makei(length, i => data[i]) + let written = (self.write)(buffer[:]) + guard written >= 0 && written <= length + if !(self.is_open)() && written < length && (self.has_cleanup)() { + self.cleanup_unaccepted(buffer[:], written) + return length + } + written +} + +///| +/// Writes one bounded window from an immutable byte view. +/// +/// This is the byte-stream counterpart of `write(ArrayView[T])`. MoonBit keeps +/// `BytesView` and `ArrayView[Byte]` as distinct types, so byte strings need a +/// separate entry point until the language provides a shared read-only view. +pub async fn Sink::write_bytes(self : Sink[Byte], data : BytesView) -> Int { + if data.length() == 0 { + return 0 + } + let length = if data.length() < sink_write_window_size { + data.length() + } else { + sink_write_window_size + } + let buffer = FixedArray::makei(length, i => data[i]) + let written = (self.write)(buffer[:]) + guard written >= 0 && written <= length + if !(self.is_open)() && written < length && (self.has_cleanup)() { + self.cleanup_unaccepted(buffer[:], written) + return length + } + written +} + +///| +/// Returns whether the sink can still accept values from its peer. +pub fn[X] Sink::is_open(self : Sink[X]) -> Bool { + (self.is_open)() +} + +///| +fn[X] Sink::cleanup_unaccepted( + self : Sink[X], + data : ArrayView[X], + offset : Int, +) -> Unit { + match self.cleanup { + Some(cleanup) => + for i in offset.. () + } +} + +///| +fn Sink::cleanup_unaccepted_bytes( + self : Sink[Byte], + data : BytesView, + offset : Int, +) -> Unit { + match self.cleanup { + Some(cleanup) => + for i in offset.. () + } +} + +///| +/// Writes the entire slice to the sink unless the sink closes first. +/// +/// Returns `true` if all items were written while the sink remained open and +/// `false` if the endpoint closed during the operation. When the sink has a +/// cleanup callback, every item was either accepted or cleaned and must not be +/// retried. Producers of explicitly managed resources must use such a sink. +pub async fn[X] Sink::write_all(self : Sink[X], data : ArrayView[X]) -> Bool { + let mut offset = 0 + for ;; { + if offset >= data.length() { + return true + } + let written = self.write(data[offset:]) catch { + err => { + self.cleanup_unaccepted(data, offset) + raise err + } + } + offset = offset + written + if !self.is_open() { + self.cleanup_unaccepted(data, offset) + return false + } + guard written > 0 + } +} + +///| +/// Writes the entire byte view unless the sink closes first. +pub async fn Sink::write_all_bytes(self : Sink[Byte], data : BytesView) -> Bool { + let mut offset = 0 + for ;; { + if offset >= data.length() { + return true + } + let written = self.write_bytes(data[offset:]) catch { + err => { + self.cleanup_unaccepted_bytes(data, offset) + raise err + } + } + offset = offset + written + if !self.is_open() { + self.cleanup_unaccepted_bytes(data, offset) + return false + } + guard written > 0 + } +} + +///| +pub async fn[X] Sink::close(self : Sink[X]) -> Unit { + (self.close)() +} + +///| +priv struct LocalFutureState[X] { + mut value : X? + mut closed : Bool + cleanup : ((X) -> Unit)? +} + +///| +fn[X] local_future_close(state : Ref[LocalFutureState[X]]) -> Unit { + if state.val.closed { + return + } + match state.val.value { + Some(value) => { + state.val.value = None + match state.val.cleanup { + Some(cleanup) => cleanup(value) + None => () + } + } + None => () + } + state.val.closed = true +} + +///| +fn[X] local_future_get(state : Ref[LocalFutureState[X]]) -> X raise { + match state.val.value { + Some(value) => { + state.val.value = None + state.val.closed = true + value + } + None => raise Cancelled::Cancelled + } +} ///| -pub(all) struct StreamR[X] { - read : async (Int) -> ArrayView[X]? +priv struct StreamPipeReader[X] { + max : Int + mut value : FixedArray[X]? + mut ended : Bool + mut coro : Coroutine? +} + +///| +priv struct StreamPipeWriter[X] { + mut value : FixedArray[X]? + mut accepted : Int + mut coro : Coroutine? +} + +///| +priv struct StreamPipe[X] { + capacity : Int + chunks : @deque.Deque[FixedArray[X]] + mut buffered : Int + mut writer_closed : Bool + mut reader_dropped : Bool + mut cleanup : ((X) -> Unit)? + mut head : FixedArray[X]? + mut head_pos : Int + readers : @deque.Deque[StreamPipeReader[X]] + writers : @deque.Deque[StreamPipeWriter[X]] +} + +///| +fn[X] wake_stream_pipe_reader( + reader : StreamPipeReader[X], + value : FixedArray[X]?, + ended : Bool, +) -> Unit { + match reader.coro { + Some(coro) => { + reader.value = value + reader.ended = ended + reader.coro = None + coro.wake() + } + None => () + } +} + +///| +fn[X] wake_stream_pipe_writer( + writer : StreamPipeWriter[X], + accepted : Int, +) -> Unit { + match writer.coro { + Some(coro) => { + writer.accepted = accepted + writer.value = None + writer.coro = None + coro.wake() + } + None => writer.value = None + } +} + +///| +fn[X] take_stream_pipe_reader( + pipe : Ref[StreamPipe[X]], +) -> StreamPipeReader[X]? { + while pipe.val.readers.pop_front() is Some(reader) { + if reader.coro is Some(_) { + return Some(reader) + } + } + None +} + +///| +fn[X] take_stream_pipe_writer( + pipe : Ref[StreamPipe[X]], +) -> StreamPipeWriter[X]? { + while pipe.val.writers.pop_front() is Some(writer) { + if writer.coro is Some(_) && writer.value is Some(_) { + return Some(writer) + } + } + None +} + +///| +fn[X] fill_stream_pipe_from_writers(pipe : Ref[StreamPipe[X]]) -> Unit { + if pipe.val.capacity <= 0 || pipe.val.writer_closed || pipe.val.reader_dropped { + return + } + for ;; { + let available = pipe.val.capacity - pipe.val.buffered + if available <= 0 { + return + } + guard take_stream_pipe_writer(pipe) is Some(writer) else { return } + guard writer.value is Some(data) else { continue } + let take = if available < data.length() { available } else { data.length() } + let chunk = FixedArray::makei(take, i => data[i]) + pipe.val.chunks.push_back(chunk) + pipe.val.buffered = pipe.val.buffered + take + wake_stream_pipe_writer(writer, take) + } +} + +///| +fn[X] stream_pipe_drop_reader(pipe : Ref[StreamPipe[X]]) -> Unit { + stream_pipe_drop_reader_with_cleanup(pipe, pipe.val.cleanup) +} + +///| +fn[X] stream_pipe_drop_reader_with_cleanup( + pipe : Ref[StreamPipe[X]], + cleanup : ((X) -> Unit)?, +) -> Unit { + if pipe.val.reader_dropped { + return + } + match cleanup { + Some(cleanup) => { + match pipe.val.head { + Some(head) => + for i in pipe.val.head_pos.. () + } + while pipe.val.chunks.pop_front() is Some(chunk) { + for value in chunk { + cleanup(value) + } + } + } + None => () + } + pipe.val.head = None + pipe.val.head_pos = 0 + pipe.val.chunks.clear() + pipe.val.buffered = 0 + pipe.val.reader_dropped = true + while pipe.val.readers.pop_front() is Some(reader) { + wake_stream_pipe_reader(reader, None, true) + } + while pipe.val.writers.pop_front() is Some(writer) { + let consumed = match (cleanup, writer.value) { + (Some(cleanup), Some(data)) => { + for value in data { + cleanup(value) + } + data.length() + } + _ => 0 + } + wake_stream_pipe_writer(writer, consumed) + } +} + +///| +fn[X] stream_pipe_reject_reader( + pipe : Ref[StreamPipe[X]], + fallback : (X) -> Unit, +) -> Unit { + if pipe.val.cleanup is None { + pipe.val.cleanup = Some(fallback) + } + stream_pipe_drop_reader(pipe) +} + +///| +fn[X] stream_pipe_close_writer(pipe : Ref[StreamPipe[X]]) -> Unit { + if pipe.val.writer_closed { + return + } + pipe.val.writer_closed = true + while pipe.val.writers.pop_front() is Some(writer) { + wake_stream_pipe_writer(writer, 0) + } + if pipe.val.buffered == 0 { + while pipe.val.readers.pop_front() is Some(reader) { + wake_stream_pipe_reader(reader, None, true) + } + } +} + +///| +async fn[X] stream_pipe_write( + pipe : Ref[StreamPipe[X]], + data : ArrayView[X], +) -> Int { + if data.length() == 0 || pipe.val.writer_closed || pipe.val.reader_dropped { + return 0 + } + match take_stream_pipe_reader(pipe) { + Some(reader) => { + let take = if reader.max < data.length() { + reader.max + } else { + data.length() + } + let chunk = FixedArray::makei(take, i => data[i]) + wake_stream_pipe_reader(reader, Some(chunk), false) + return take + } + None => () + } + if pipe.val.capacity > 0 && pipe.val.buffered < pipe.val.capacity { + let available = pipe.val.capacity - pipe.val.buffered + let take = if available < data.length() { available } else { data.length() } + let chunk = FixedArray::makei(take, i => data[i]) + pipe.val.chunks.push_back(chunk) + pipe.val.buffered = pipe.val.buffered + take + return take + } + let writer = StreamPipeWriter::{ + value: Some(FixedArray::makei(data.length(), i => data[i])), + accepted: 0, + coro: Some(current_coroutine()), + } + pipe.val.writers.push_back(writer) + suspend() catch { + _ if writer.coro is None => return writer.accepted + err => { + writer.coro = None + writer.value = None + raise err + } + } + writer.value = None + writer.accepted +} + +///| +async fn[X] stream_pipe_read( + pipe : Ref[StreamPipe[X]], + count : Int, +) -> FixedArray[X]? { + if count <= 0 { + return Some([]) + } + for ;; { + if pipe.val.reader_dropped { + return None + } + match pipe.val.head { + Some(head) => { + let available = head.length() - pipe.val.head_pos + let take = if count < available { count } else { available } + let result = FixedArray::makei(take, i => head[pipe.val.head_pos + i]) + pipe.val.head_pos = pipe.val.head_pos + take + pipe.val.buffered = pipe.val.buffered - take + if pipe.val.head_pos >= head.length() { + pipe.val.head = None + pipe.val.head_pos = 0 + } + fill_stream_pipe_from_writers(pipe) + return Some(result) + } + None => () + } + if pipe.val.chunks.pop_front() is Some(chunk) { + pipe.val.head = Some(chunk) + pipe.val.head_pos = 0 + continue + } + match take_stream_pipe_writer(pipe) { + Some(writer) => { + guard writer.value is Some(data) else { continue } + let take = if count < data.length() { count } else { data.length() } + let result = FixedArray::makei(take, i => data[i]) + wake_stream_pipe_writer(writer, take) + return Some(result) + } + None => () + } + if pipe.val.writer_closed { + return None + } + let reader = StreamPipeReader::{ + max: count, + value: None, + ended: false, + coro: Some(current_coroutine()), + } + pipe.val.readers.push_back(reader) + suspend() catch { + err => { + reader.coro = None + match reader.value { + Some(value) => return Some(value) + None if reader.ended => return None + None => raise err + } + } + } + match reader.value { + Some(value) => return Some(value) + None if reader.ended => return None + None => () + } + } +} + +///| +priv struct FutureSource[X] { + get : async () -> X close : async () -> Unit + close_sync : () -> Unit +} + +///| +priv struct FutureProducerState[X] { + mut producer : (async () -> X)? + mut on_unstarted_drop : (() -> Unit)? +} + +///| +priv enum FutureState[X] { + Source(FutureSource[X]) + Local(Ref[LocalFutureState[X]]) + Pending(Ref[FutureProducerState[X]]) + Promised(Ref[PromiseCell[X]]) +} + +///| +pub struct Future[X] { + priv inner : FutureState[X] +} + +///| +pub fn[X] Future::ready_with_cleanup( + value : X, + cleanup : (X) -> Unit, +) -> Future[X] { + let state : Ref[LocalFutureState[X]] = { + val: { value: Some(value), closed: false, cleanup: Some(cleanup) }, + } + Future::{ inner: Local(state) } +} + +///| +pub fn[X] Future::ready(value : X) -> Future[X] { + let state : Ref[LocalFutureState[X]] = { + val: { value: Some(value), closed: false, cleanup: None }, + } + Future::{ inner: Local(state) } +} + +///| +/// Creates a lazy future from an async producer. +/// +/// `on_unstarted_drop`, when present, releases resources captured by a producer +/// that is discarded before its first `get`. Once the producer starts, it owns +/// its runtime cleanup and this callback is never invoked. +pub fn[X] Future::from( + producer : async () -> X, + on_unstarted_drop? : () -> Unit, +) -> Future[X] { + Future::{ + inner: Pending({ val: { producer: Some(producer), on_unstarted_drop } }), + } +} + +///| +#internal(wit_bindgen, "generated binding code only") +pub fn[X] Future::from_source( + get : async () -> X, + close : async () -> Unit, + close_sync : () -> Unit, +) -> Future[X] { + Future::{ inner: Source({ get, close, close_sync }) } +} + +///| +pub async fn[X] Future::get(self : Future[X]) -> X { + match self.inner { + Source(source) => (source.get)() + Local(state) => local_future_get(state) + Pending(state) => + match state.val.producer { + Some(producer) => { + state.val.producer = None + state.val.on_unstarted_drop = None + producer() + } + None => raise Cancelled::Cancelled + } + Promised(cell) => promised_future_get(cell) + } +} + +///| +pub async fn[X] Future::drop(self : Future[X]) -> Unit noraise { + match self.inner { + Source(source) => (source.close)() catch { _ => () } + Local(state) => local_future_close(state) + Pending(state) => { + let cleanup = state.val.on_unstarted_drop + state.val.producer = None + state.val.on_unstarted_drop = None + match cleanup { + Some(cleanup) => cleanup() + None => () + } + } + Promised(cell) => promised_future_drop(cell) + } +} + +///| +#internal(wit_bindgen, "generated binding code only") +pub fn[X] Future::drop_sync(self : Future[X]) -> Unit { + match self.inner { + Source(source) => (source.close_sync)() + Local(state) => local_future_close(state) + Pending(state) => { + let cleanup = state.val.on_unstarted_drop + state.val.producer = None + state.val.on_unstarted_drop = None + match cleanup { + Some(cleanup) => cleanup() + None => () + } + } + Promised(cell) => promised_future_drop(cell) + } } ///| -pub(all) struct StreamW[X] { - write : async (ArrayView[X]) -> Int +priv struct StreamProducerState[X] { + mut producer : (async (Sink[X]) -> Unit)? + mut cleanup : ((X) -> Unit)? + mut on_unstarted_drop : (() -> Unit)? + mut pipe : Ref[StreamPipe[X]]? +} + +///| +priv struct StreamSource[X] { + read : async (Int) -> FixedArray[X]? close : async () -> Unit + close_sync : () -> Unit } ///| -struct OutStream[X] { - mut stream : StreamW[X]? - mut coroutine : Coroutine? -} derive(Default) +priv enum StreamState[X] { + Source(StreamSource[X]) + Local(Ref[StreamPipe[X]]) + Outgoing(Ref[StreamProducerState[X]]) +} ///| -pub async fn[X] OutStream::get_stream(self : OutStream[X]) -> StreamW[X] { - if self.stream is Some(s) { - return s - } else { - guard self.coroutine is None - self.coroutine = Some(current_coroutine()) - suspend() catch { - e => { - if self.stream is Some(s) { - (s.close)() +pub struct Stream[X] { + priv inner : StreamState[X] +} + +///| +fn[X] new_stream_pipe( + capacity : Int, + cleanup : ((X) -> Unit)?, +) -> Ref[StreamPipe[X]] { + if capacity < 0 { + abort("stream capacity must not be negative") + } + { + val: { + capacity, + chunks: @deque.Deque([]), + buffered: 0, + writer_closed: false, + reader_dropped: false, + cleanup, + head: None, + head_pos: 0, + readers: @deque.Deque([]), + writers: @deque.Deque([]), + }, + } +} + +///| +fn[X] stream_pipe_sink(pipe : Ref[StreamPipe[X]]) -> Sink[X] { + Sink::{ + write: (data : ArrayView[X]) => stream_pipe_write(pipe, data), + close: () => stream_pipe_close_writer(pipe), + is_open: () => !pipe.val.writer_closed && !pipe.val.reader_dropped, + has_cleanup: () => pipe.val.cleanup is Some(_), + cleanup: Some(fn(value) { + match pipe.val.cleanup { + Some(cleanup) => cleanup(value) + None => () + } + }), + } +} + +///| +fn[X] new_local_stream( + capacity : Int, + cleanup : ((X) -> Unit)?, +) -> (Stream[X], Sink[X]) { + let pipe = new_stream_pipe(capacity, cleanup) + let stream = Stream::{ inner: Local(pipe) } + let sink = stream_pipe_sink(pipe) + (stream, sink) +} + +///| +fn spawn_stream_producer(task : async () -> Unit) -> Unit { + let current = current_coroutine() + match current.spawner { + Some(spawner) => spawner(task) + None => { + let _ = spawn(task) + } + } +} + +///| +fn[X] materialize_stream_producer( + state : Ref[StreamProducerState[X]], +) -> Ref[StreamPipe[X]] { + match state.val.pipe { + Some(pipe) => pipe + None => { + let pipe = new_stream_pipe(0, state.val.cleanup) + state.val.cleanup = None + state.val.pipe = Some(pipe) + match state.val.producer { + Some(producer) => { + state.val.producer = None + state.val.on_unstarted_drop = None + let sink = stream_pipe_sink(pipe) + spawn_stream_producer(async fn() { + producer(sink) catch { + _ => { + sink.close() catch { + _ => () + } + return + } + } + sink.close() + }) } - raise e + None => stream_pipe_close_writer(pipe) } + pipe } - self.stream.unwrap() } } ///| -pub fn[X] OutStream::put_stream( - self : OutStream[X], - stream : StreamW[X], -) -> Unit { - self.stream = Some(stream) - if self.coroutine is Some(coro) { - coro.wake() +pub fn[X] Stream::new(capacity? : Int = 0) -> (Stream[X], Sink[X]) { + new_local_stream(capacity, None) +} + +///| +/// Creates a local stream whose unread values are cleaned up when its readable +/// end is dropped. +pub fn[X] Stream::new_with_cleanup( + cleanup : (X) -> Unit, + capacity? : Int = 0, +) -> (Stream[X], Sink[X]) { + new_local_stream(capacity, Some(cleanup)) +} + +///| +/// Creates a lazy stream from an async producer. +/// +/// `cleanup`, when present, releases buffered or pending values if the readable +/// end is dropped after the producer starts. Producers of explicitly managed +/// resources should provide it. +/// +/// `on_unstarted_drop`, when present, releases resources captured by a producer +/// that is discarded before its first read or before a prepared component +/// transfer commits. Once the producer starts, it owns its runtime cleanup and +/// this callback is never invoked. +pub fn[X] Stream::produce( + producer : async (Sink[X]) -> Unit, + cleanup? : (X) -> Unit, + on_unstarted_drop? : () -> Unit, +) -> Stream[X] { + Stream::{ + inner: Outgoing({ + val: { producer: Some(producer), cleanup, on_unstarted_drop, pipe: None }, + }), + } +} + +///| +#internal(wit_bindgen, "generated binding code only") +pub fn[X] Stream::from_source( + read : async (Int) -> FixedArray[X]?, + close : async () -> Unit, + close_sync : () -> Unit, +) -> Stream[X] { + Stream::{ inner: Source({ read, close, close_sync }) } +} + +///| +pub async fn[X] Stream::read(self : Stream[X], count : Int) -> FixedArray[X]? { + match self.inner { + Source(source) => (source.read)(count) + Local(pipe) => stream_pipe_read(pipe, count) + Outgoing(state) => + stream_pipe_read(materialize_stream_producer(state), count) + } +} + +///| +pub async fn[X] Stream::drop(self : Stream[X]) -> Unit noraise { + match self.inner { + Source(source) => (source.close)() catch { _ => () } + Local(pipe) => stream_pipe_drop_reader(pipe) + Outgoing(state) => + match state.val.pipe { + Some(pipe) => stream_pipe_drop_reader(pipe) + None => { + let cleanup = state.val.on_unstarted_drop + state.val.producer = None + state.val.cleanup = None + state.val.on_unstarted_drop = None + match cleanup { + Some(cleanup) => cleanup() + None => () + } + } + } + } +} + +///| +#internal(wit_bindgen, "generated binding code only") +pub fn[X] Stream::drop_sync(self : Stream[X]) -> Unit { + match self.inner { + Source(source) => (source.close_sync)() + Local(pipe) => stream_pipe_drop_reader(pipe) + Outgoing(state) => + match state.val.pipe { + Some(pipe) => stream_pipe_drop_reader(pipe) + None => { + let cleanup = state.val.on_unstarted_drop + state.val.producer = None + state.val.cleanup = None + state.val.on_unstarted_drop = None + match cleanup { + Some(cleanup) => cleanup() + None => () + } + } + } + } +} + +///| +#internal(wit_bindgen, "generated binding code only") +pub fn[X] Stream::take_producer(self : Stream[X]) -> (async (Sink[X]) -> Unit)? { + match self.inner { + Outgoing(state) => + match state.val.pipe { + Some(_) => None + None => { + let producer = state.val.producer + state.val.producer = None + state.val.cleanup = None + state.val.on_unstarted_drop = None + producer + } + } + Source(_) | Local(_) => None + } +} + +///| +/// Rejects a stream that was prepared for a component-model transfer but was +/// never transferred. Component sources are closed immediately and buffered +/// local values are cleaned synchronously. An unstarted producer is discarded +/// without running it; its optional `on_unstarted_drop` callback owns cleanup of +/// captured resources. +#internal(wit_bindgen, "generated binding code only") +pub async fn[X] Stream::reject( + self : Stream[X], + cleanup : (X) -> Unit, +) -> Unit noraise { + match self.inner { + Source(source) => (source.close)() catch { _ => () } + Local(pipe) => stream_pipe_reject_reader(pipe, cleanup) + Outgoing(state) => + match state.val.pipe { + Some(pipe) => stream_pipe_reject_reader(pipe, cleanup) + None => { + let cleanup = state.val.on_unstarted_drop + state.val.producer = None + state.val.cleanup = None + state.val.on_unstarted_drop = None + match cleanup { + Some(cleanup) => cleanup() + None => () + } + } + } } } diff --git a/crates/moonbit/src/async_support.rs b/crates/moonbit/src/async_support.rs index 47ec5933e..128d41afd 100644 --- a/crates/moonbit/src/async_support.rs +++ b/crates/moonbit/src/async_support.rs @@ -1,80 +1,297 @@ -use std::{ - collections::{HashMap, HashSet}, - fmt::Write, -}; +use std::{collections::HashSet, fmt::Write, mem, ops::Range}; use heck::{ToSnakeCase, ToUpperCamelCase}; use wit_bindgen_core::{ - Files, Source, - abi::{self, WasmSignature}, - uwriteln, - wit_parser::{Function, Param, Resolve, Type, TypeDefKind, TypeId}, + AsyncFilterSet, Direction, Files, Ns, Source, + abi::{self, AbiVariant, WasmSignature, WasmType}, + uwrite, uwriteln, + wit_parser::{ + Function, FutureIntrinsic, LiftLowerAbi, Mangling, ManglingAndAbi, Param, Resolve, + StreamIntrinsic, Type, TypeDefKind, TypeId, WasmExport, WasmExportKind, WasmImport, + WorldKey, + }, }; use crate::{ - FFI, FFI_DIR, indent, - pkg::{MoonbitSignature, ToMoonBitIdent}, + ffi, indent, + pkg::{ASYNC_CORE_DIR, MoonbitSignature, ToMoonBitIdent}, }; -use super::{FunctionBindgen, InterfaceGenerator, PayloadFor}; +use super::FunctionBindgen; +use super::InterfaceGenerator; +use super::wasm_type; + +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +enum PayloadFor { + Future, + Stream, +} + +impl PayloadFor { + fn label(self) -> &'static str { + match self { + PayloadFor::Future => "future", + PayloadFor::Stream => "stream", + } + } +} + +#[derive(Clone, Debug, Default)] +pub(crate) struct AsyncFunctionState { + task_return: AsyncTaskReturnState, + endpoint_sites: Vec, + endpoint_uses: Vec, + next_endpoint: usize, +} + +#[derive(Clone, Copy, Debug, Default)] +struct EndpointUse { + lift: bool, + lower: bool, + lower_committed: bool, +} + +#[derive(Clone, Copy, Debug)] +enum EndpointOperation { + Lift, + Lower, + LowerCommitted, +} + +#[derive(Clone, Debug)] +enum AsyncTaskReturnState { + None, + Generating { + prev_src: String, + prev_needs_cleanup_list: bool, + return_param: String, + return_value: String, + }, + Emitted { + params: Vec<(WasmType, String)>, + body: String, + needs_cleanup_list: bool, + return_param: String, + return_value: String, + }, +} + +impl Default for AsyncTaskReturnState { + fn default() -> Self { + Self::None + } +} + +#[derive(Clone, Debug)] +struct AsyncEndpointSite { + kind: PayloadFor, + ty: TypeId, + index: usize, + binding_name: String, + symbol_name: String, + payload_sites: Range, + intrinsics: AsyncEndpointIntrinsics, +} + +#[derive(Clone, Debug)] +struct AsyncIntrinsicName { + module: String, + field: String, +} + +#[derive(Clone, Debug)] +struct AsyncEndpointIntrinsics { + new: AsyncIntrinsicName, + read: AsyncIntrinsicName, + write: AsyncIntrinsicName, + cancel_read: AsyncIntrinsicName, + cancel_write: AsyncIntrinsicName, + drop_readable: AsyncIntrinsicName, + drop_writable: AsyncIntrinsicName, +} + +#[derive(Clone, Debug)] +pub(crate) struct AsyncFunctionPlan { + endpoint_sites: Vec, +} + +impl AsyncFunctionPlan { + fn new(endpoint_sites: Vec) -> Self { + Self { endpoint_sites } + } + + pub(crate) fn state(&self) -> AsyncFunctionState { + AsyncFunctionState::from_sites(self.endpoint_sites.clone()) + } + + pub(crate) fn has_endpoints(&self) -> bool { + !self.endpoint_sites.is_empty() + } + + fn payload_sites(&self, site: &AsyncEndpointSite) -> Vec { + self.endpoint_sites[site.payload_sites.clone()].to_vec() + } + + fn endpoint_uses(&self, state: &AsyncFunctionState) -> Vec { + assert_eq!(self.endpoint_sites.len(), state.endpoint_uses.len()); + let mut uses = state.endpoint_uses.clone(); + for (site_index, site) in self.endpoint_sites.iter().enumerate().rev() { + let owner = uses[site_index]; + for index in site.payload_sites.clone() { + uses[index].lift |= owner.lift || owner.lower; + uses[index].lower |= owner.lower; + } + } + uses + } +} + +impl AsyncFunctionState { + fn from_sites(endpoint_sites: Vec) -> Self { + let endpoint_uses = vec![EndpointUse::default(); endpoint_sites.len()]; + Self { + endpoint_sites, + endpoint_uses, + next_endpoint: 0, + ..Self::default() + } + } + + fn next_site( + &mut self, + kind: PayloadFor, + ty: TypeId, + operation: EndpointOperation, + ) -> AsyncEndpointSite { + let Some(offset) = self.endpoint_sites[self.next_endpoint..] + .iter() + .position(|site| site.kind == kind && site.ty == ty) + else { + unreachable!("missing async endpoint site for {kind:?} {ty:?}") + }; + let index = self.next_endpoint + offset; + self.next_endpoint = index + 1; + match operation { + EndpointOperation::Lift => self.endpoint_uses[index].lift = true, + EndpointOperation::Lower => self.endpoint_uses[index].lower = true, + EndpointOperation::LowerCommitted => { + self.endpoint_uses[index].lower = true; + self.endpoint_uses[index].lower_committed = true; + } + } + self.endpoint_sites[index].clone() + } +} + +pub(crate) struct AsyncImportPlan { + is_async: bool, +} + +impl AsyncImportPlan { + pub(crate) fn mangling_and_abi(&self) -> ManglingAndAbi { + if self.is_async { + ManglingAndAbi::Legacy(LiftLowerAbi::AsyncCallback) + } else { + ManglingAndAbi::Legacy(LiftLowerAbi::Sync) + } + } + + pub(crate) fn abi_variant(&self) -> AbiVariant { + self.mangling_and_abi().import_variant() + } + + pub(crate) fn signature_is_async(&self) -> bool { + self.is_async + } + + pub(crate) fn is_async(&self) -> bool { + self.is_async + } +} + +pub(crate) struct AsyncImportBody { + pub(crate) src: String, + pub(crate) needs_cleanup_list: bool, + pub(crate) state: AsyncFunctionState, +} + +pub(crate) struct AsyncExportPlan { + is_async: bool, +} + +impl AsyncExportPlan { + pub(crate) fn mangling_and_abi(&self) -> ManglingAndAbi { + if self.is_async { + ManglingAndAbi::Legacy(LiftLowerAbi::AsyncCallback) + } else { + ManglingAndAbi::Legacy(LiftLowerAbi::Sync) + } + } + + pub(crate) fn abi_variant(&self) -> AbiVariant { + self.mangling_and_abi().export_variant() + } + + pub(crate) fn signature_is_async(&self) -> bool { + self.is_async + } -const ASYNC_PRIMITIVE: &str = include_str!("./ffi/async_primitive.mbt"); -const ASYNC_FUTURE: &str = include_str!("./ffi/future.mbt"); -const ASYNC_WASM_PRIMITIVE: &str = include_str!("./ffi/wasm_primitive.mbt"); -const ASYNC_WAITABLE_SET: &str = include_str!("./ffi/waitable_task.mbt"); -const ASYNC_SUBTASK: &str = include_str!("./ffi/subtask.mbt"); + pub(crate) fn is_async(&self) -> bool { + self.is_async + } +} -// NEW Async Impl const ASYNC_ABI: &str = include_str!("./async/async_abi.mbt"); -const ASYNC_CORO: &str = include_str!("./async/coroutine.mbt"); +const ASYNC_COND_VAR: &str = include_str!("./async/cond_var.mbt"); +const ASYNC_COROUTINE: &str = include_str!("./async/coroutine.mbt"); const ASYNC_EV: &str = include_str!("./async/ev.mbt"); +const ASYNC_MUTEX: &str = include_str!("./async/mutex.mbt"); +const ASYNC_PRIMITIVE: &str = include_str!("./async/async_primitive.mbt"); +const ASYNC_PROMISE: &str = include_str!("./async/promise.mbt"); const ASYNC_SCHEDULER: &str = include_str!("./async/scheduler.mbt"); +const ASYNC_SEMAPHORE: &str = include_str!("./async/semaphore.mbt"); const ASYNC_TASK: &str = include_str!("./async/task.mbt"); const ASYNC_TASK_GROUP: &str = include_str!("./async/task_group.mbt"); const ASYNC_TRAIT: &str = include_str!("./async/trait.mbt"); -const ASYNC_PKG_JSON: &str = include_str!("./async/moon.pkg.json"); -const ASYNC_PRIM: &str = include_str!("./async/async_primitive.mbt"); +const ASYNC_PKG: &str = include_str!("./async/moon.pkg.json"); struct Segment<'a> { name: &'a str, src: &'a str, } -const ASYNC_UTILS: [&Segment; 5] = [ +const ASYNC_UTILS: [&Segment; 12] = [ &Segment { name: "async_primitive", src: ASYNC_PRIMITIVE, }, &Segment { - name: "async_future", - src: ASYNC_FUTURE, + name: "async_abi", + src: ASYNC_ABI, }, &Segment { - name: "async_wasm_primitive", - src: ASYNC_WASM_PRIMITIVE, + name: "async_coroutine", + src: ASYNC_COROUTINE, }, &Segment { - name: "async_waitable_set", - src: ASYNC_WAITABLE_SET, + name: "async_ev", + src: ASYNC_EV, }, &Segment { - name: "async_subtask", - src: ASYNC_SUBTASK, + name: "async_cond_var", + src: ASYNC_COND_VAR, }, -]; - -const ASYNC_IMPL: [&Segment; 8] = [ &Segment { - name: "async_abi", - src: ASYNC_ABI, + name: "async_semaphore", + src: ASYNC_SEMAPHORE, }, &Segment { - name: "async_coro", - src: ASYNC_CORO, + name: "async_mutex", + src: ASYNC_MUTEX, }, &Segment { - name: "async_ev", - src: ASYNC_EV, + name: "async_promise", + src: ASYNC_PROMISE, }, &Segment { name: "async_scheduler", @@ -92,221 +309,480 @@ const ASYNC_IMPL: [&Segment; 8] = [ name: "async_trait", src: ASYNC_TRAIT, }, - &Segment { - name: "async_primitive", - src: ASYNC_PRIM, - }, ]; -pub(crate) const ASYNC_DIR: &str = "async"; - #[derive(Default)] pub(crate) struct AsyncSupport { - is_async: bool, - futures: HashMap>, + runtime_required: bool, + endpoints: HashSet, +} + +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +struct AsyncEndpointKey { + binding_name: String, + index: usize, + kind: PayloadFor, +} + +#[derive(Clone, Debug)] +struct AsyncEndpointNames { + binding_name: String, +} + +impl AsyncEndpointNames { + fn import(resolve: &Resolve, interface: Option<&WorldKey>, func: &Function) -> Self { + let (module, func_name) = resolve.wasm_import_name( + ManglingAndAbi::Legacy(LiftLowerAbi::Sync), + WasmImport::Func { interface, func }, + ); + let binding_name = format!("{module}#{func_name}"); + Self { binding_name } + } + + fn export(resolve: &Resolve, interface: Option<&WorldKey>, func: &Function) -> Self { + let export_name = resolve.wasm_export_name( + ManglingAndAbi::Legacy(LiftLowerAbi::Sync), + WasmExport::Func { + interface, + func, + kind: WasmExportKind::Normal, + }, + ); + Self { + binding_name: format!("[export]{export_name}"), + } + } } impl AsyncSupport { - pub(crate) fn mark_async(&mut self) { - self.is_async = true; + pub(crate) fn import_plan( + &mut self, + async_filter: &mut AsyncFilterSet, + resolve: &Resolve, + module: Option<&WorldKey>, + func: &Function, + ) -> AsyncImportPlan { + let is_async = async_filter.is_async(resolve, module, func, true); + if is_async { + self.runtime_required = true; + } + AsyncImportPlan { is_async } + } + + pub(crate) fn export_plan( + &mut self, + async_filter: &mut AsyncFilterSet, + resolve: &Resolve, + interface: Option<&WorldKey>, + func: &Function, + ) -> AsyncExportPlan { + let is_async = async_filter.is_async(resolve, interface, func, false); + if is_async { + self.runtime_required = true; + } + AsyncExportPlan { is_async } + } + + pub(crate) fn require_runtime(&mut self) { + self.runtime_required = true; } - pub(crate) fn register_future_or_stream(&mut self, module: &str, ty: TypeId) -> bool { - self.futures - .entry(module.to_string()) - .or_default() - .insert(ty) + fn register_future_or_stream( + &mut self, + binding_name: &str, + index: usize, + kind: PayloadFor, + ) -> bool { + self.endpoints.insert(AsyncEndpointKey { + binding_name: binding_name.to_string(), + index, + kind, + }) } - pub(crate) fn emit_utils(&self, files: &mut Files, version: &str) { - if !self.is_async && self.futures.is_empty() { + pub(crate) fn emit_runtime_files(&self, files: &mut Files, version: &str) { + if !self.runtime_required && self.endpoints.is_empty() { return; } let mut body = Source::default(); wit_bindgen_core::generated_preamble(&mut body, version); - body.push_str(FFI); - files.push(&format!("{FFI_DIR}/top.mbt"), indent(&body).as_bytes()); + files.push( + &format!("{ASYNC_CORE_DIR}/top.mbt"), + indent(&body).as_bytes(), + ); ASYNC_UTILS.iter().for_each(|s| { files.push( - &format!("{FFI_DIR}/{}.mbt", s.name), - indent(s.src).as_bytes(), - ); - }); - ASYNC_IMPL.iter().for_each(|s| { - files.push( - &format!("{ASYNC_DIR}/{}.mbt", s.name), - indent(s.src).as_bytes(), + &format!("{ASYNC_CORE_DIR}/{}.mbt", s.name), + s.src.as_bytes(), ); }); files.push( - &format!("{ASYNC_DIR}/moon.pkg.json"), - indent(ASYNC_PKG_JSON).as_bytes(), + &format!("{ASYNC_CORE_DIR}/moon.pkg.json"), + ASYNC_PKG.as_bytes(), ); - files.push( - &format!("{FFI_DIR}/moon.pkg.json"), - "{ \"warn-list\": \"-44\", \"supported-targets\": [\"wasm\"] }".as_bytes(), + } +} + +fn async_endpoint_sites( + resolve: &Resolve, + names: &AsyncEndpointNames, + interface: Option<&WorldKey>, + func: &Function, + exported: bool, +) -> Vec { + let mut sites = func + .find_futures_and_streams(resolve) + .into_iter() + .enumerate() + .map(|(index, ty)| { + let kind = match &resolve.types[ty].kind { + TypeDefKind::Future(_) => PayloadFor::Future, + TypeDefKind::Stream(_) => PayloadFor::Stream, + _ => unreachable!(), + }; + async_endpoint_site(resolve, names, interface, func, exported, index, kind, ty) + }) + .collect::>(); + + for index in 0..sites.len() { + let payload_type = match &resolve.types[sites[index].ty].kind { + TypeDefKind::Future(payload) | TypeDefKind::Stream(payload) => payload.as_ref(), + _ => unreachable!(), + }; + let Some(payload_type) = payload_type else { + sites[index].payload_sites = index..index; + continue; + }; + let mut payload_types = Vec::new(); + find_futures_and_streams_in_type(resolve, payload_type, &mut payload_types); + let start = index + .checked_sub(payload_types.len()) + .expect("nested async endpoint payload sites must precede their owner"); + assert_eq!( + sites[start..index] + .iter() + .map(|site| site.ty) + .collect::>(), + payload_types, + "upstream async endpoint order changed" ); + sites[index].payload_sites = start..index; } + + sites } -/// Async-specific helpers used by `InterfaceGenerator` to keep the main -/// visitor implementation focused on shared lowering/lifting logic. -impl<'a> InterfaceGenerator<'a> { - /// Builds the MoonBit body for async imports, wiring wasm subtasks into the - /// runtime and lowering/lifting payloads as needed. - #[allow(dead_code, reason = "used by follow-up async generation work")] - pub(super) fn generate_async_import_function( - &mut self, - func: &Function, - mbt_sig: MoonbitSignature, - sig: &WasmSignature, - ) -> String { - let mut body = String::default(); - let mut lower_params = Vec::new(); - let mut lower_results = Vec::new(); +fn async_endpoint_site( + resolve: &Resolve, + names: &AsyncEndpointNames, + interface: Option<&WorldKey>, + func: &Function, + exported: bool, + index: usize, + kind: PayloadFor, + ty: TypeId, +) -> AsyncEndpointSite { + let stem = endpoint_stem(&names.binding_name, index, kind); + AsyncEndpointSite { + kind, + ty, + index, + binding_name: names.binding_name.clone(), + symbol_name: stem.to_upper_camel_case(), + payload_sites: index..index, + intrinsics: async_endpoint_intrinsics(resolve, interface, func, exported, kind, ty), + } +} - if sig.indirect_params { - match &func.params[..] { - [] => {} - [_] => { - lower_params.push("_lower_ptr".into()); - } - multiple_params => { - let params = multiple_params.iter().map(|Param { ty, .. }| ty); - let offsets = self.world_gen.sizes.field_offsets(params.clone()); - let elem_info = self.world_gen.sizes.params(params); - body.push_str(&format!( - r#" - let _lower_ptr : Int = {ffi}malloc({}) - "#, - elem_info.size.size_wasm32(), - ffi = self - .world_gen - .pkg_resolver - .qualify_package(self.name, FFI_DIR) - )); +#[derive(Clone, Copy)] +enum EndpointIntrinsic { + New, + Read, + Write, + CancelRead, + CancelWrite, + DropReadable, + DropWritable, +} - for ((offset, ty), name) in offsets.iter().zip( - multiple_params - .iter() - .map(|Param { name, .. }| name.to_moonbit_ident()), - ) { - let result = self.lower_to_memory( - &format!("_lower_ptr + {}", offset.size_wasm32()), - &name, - ty, - self.name, - ); - body.push_str(&result); - } +impl EndpointIntrinsic { + fn async_lowered(self) -> bool { + matches!(self, Self::Read | Self::Write) + } - lower_params.push("_lower_ptr".into()); - } - } - } else { - let mut f = FunctionBindgen::new(self, Box::new([])); - for (name, ty) in mbt_sig.params.iter() { - lower_params.extend(abi::lower_flat( - f.interface_gen.resolve, - &mut f, - name.clone(), - ty, - )); - } - lower_results.push(f.src.clone()); + fn future(self) -> FutureIntrinsic { + match self { + Self::New => FutureIntrinsic::New, + Self::Read => FutureIntrinsic::Read, + Self::Write => FutureIntrinsic::Write, + Self::CancelRead => FutureIntrinsic::CancelRead, + Self::CancelWrite => FutureIntrinsic::CancelWrite, + Self::DropReadable => FutureIntrinsic::DropReadable, + Self::DropWritable => FutureIntrinsic::DropWritable, } + } - let func_name = func.name.to_upper_camel_case(); + fn stream(self) -> StreamIntrinsic { + match self { + Self::New => StreamIntrinsic::New, + Self::Read => StreamIntrinsic::Read, + Self::Write => StreamIntrinsic::Write, + Self::CancelRead => StreamIntrinsic::CancelRead, + Self::CancelWrite => StreamIntrinsic::CancelWrite, + Self::DropReadable => StreamIntrinsic::DropReadable, + Self::DropWritable => StreamIntrinsic::DropWritable, + } + } +} - let ffi = self - .world_gen - .pkg_resolver - .qualify_package(self.name, FFI_DIR); +fn async_endpoint_intrinsics( + resolve: &Resolve, + interface: Option<&WorldKey>, + func: &Function, + exported: bool, + kind: PayloadFor, + ty: TypeId, +) -> AsyncEndpointIntrinsics { + let intrinsic_ty = match &resolve.types[ty].kind { + TypeDefKind::Future(None) | TypeDefKind::Stream(None) => None, + TypeDefKind::Future(Some(_)) | TypeDefKind::Stream(Some(_)) => Some(ty), + _ => unreachable!(), + }; + let name = |intrinsic: EndpointIntrinsic| { + let import = match kind { + PayloadFor::Future => WasmImport::FutureIntrinsic { + interface, + func, + ty: intrinsic_ty, + intrinsic: intrinsic.future(), + exported, + async_: intrinsic.async_lowered(), + }, + PayloadFor::Stream => WasmImport::StreamIntrinsic { + interface, + func, + ty: intrinsic_ty, + intrinsic: intrinsic.stream(), + exported, + async_: intrinsic.async_lowered(), + }, + }; + let (module, field) = + resolve.wasm_import_name(ManglingAndAbi::Legacy(LiftLowerAbi::Sync), import); + AsyncIntrinsicName { module, field } + }; - let call_import = |params: &Vec| { - format!( - r#" - let _subtask_code = wasmImport{func_name}({}) - let _subtask_status = {ffi}SubtaskStatus::decode(_subtask_code) - let _subtask = @ffi.Subtask::from_handle(_subtask_status.handle(), code=_subtask_code) - - let task = @ffi.current_task() - task.add_waitable(_subtask, @ffi.current_coroutine()) - defer task.remove_waitable(_subtask) - - for {{ - if _subtask.done() || _subtask_status is Returned(_) {{ - break - }} else {{ - @ffi.suspend() - }} - }} + AsyncEndpointIntrinsics { + new: name(EndpointIntrinsic::New), + read: name(EndpointIntrinsic::Read), + write: name(EndpointIntrinsic::Write), + cancel_read: name(EndpointIntrinsic::CancelRead), + cancel_write: name(EndpointIntrinsic::CancelWrite), + drop_readable: name(EndpointIntrinsic::DropReadable), + drop_writable: name(EndpointIntrinsic::DropWritable), + } +} - "#, - params.join(", ") - ) - }; - match &func.result { - Some(ty) => { - lower_params.push("_result_ptr".into()); - let call_import = call_import(&lower_params); - let (lift, lift_result) = &self.lift_from_memory("_result_ptr", ty, self.name); - body.push_str(&format!( - r#" - {} - {} - {call_import} - {lift} - {lift_result} - "#, - lower_results.join("\n"), - &self.malloc_memory("_result_ptr", "1", ty) - )); +fn find_futures_and_streams_in_type(resolve: &Resolve, ty: &Type, results: &mut Vec) { + let Type::Id(id) = ty else { + return; + }; + + match &resolve.types[*id].kind { + TypeDefKind::Resource + | TypeDefKind::Handle(_) + | TypeDefKind::Flags(_) + | TypeDefKind::Enum(_) => {} + TypeDefKind::Record(record) => { + for field in &record.fields { + find_futures_and_streams_in_type(resolve, &field.ty, results); } - None => { - let call_import = call_import(&lower_params); - body.push_str(&call_import); + } + TypeDefKind::Tuple(tuple) => { + for ty in &tuple.types { + find_futures_and_streams_in_type(resolve, ty, results); + } + } + TypeDefKind::Variant(variant) => { + for case in &variant.cases { + if let Some(ty) = &case.ty { + find_futures_and_streams_in_type(resolve, ty, results); + } + } + } + TypeDefKind::Option(ty) + | TypeDefKind::List(ty) + | TypeDefKind::FixedLengthList(ty, ..) + | TypeDefKind::Type(ty) => find_futures_and_streams_in_type(resolve, ty, results), + TypeDefKind::Map(key, value) => { + find_futures_and_streams_in_type(resolve, key, results); + find_futures_and_streams_in_type(resolve, value, results); + } + TypeDefKind::Result(result) => { + if let Some(ty) = &result.ok { + find_futures_and_streams_in_type(resolve, ty, results); + } + if let Some(ty) = &result.err { + find_futures_and_streams_in_type(resolve, ty, results); + } + } + TypeDefKind::Future(payload) => { + if let Some(ty) = payload { + find_futures_and_streams_in_type(resolve, ty, results); + } + results.push(*id); + } + TypeDefKind::Stream(payload) => { + if let Some(ty) = payload { + find_futures_and_streams_in_type(resolve, ty, results); } + results.push(*id); } + TypeDefKind::Unknown => unreachable!(), + } +} + +fn endpoint_stem(binding_name: &str, index: usize, kind: PayloadFor) -> String { + format!( + "{}_{}_{}", + moonbit_identifier_stem(binding_name), + kind.label(), + index, + ) +} + +fn moonbit_identifier_stem(input: &str) -> String { + input + .chars() + .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' }) + .collect::() + .to_snake_case() +} - body.to_string() +fn background_group_name(func: &Function) -> String { + let mut names = Ns::default(); + for param in &func.params { + names.tmp(¶m.name.to_moonbit_ident()); } + names.tmp("background_group") +} - /// Ensures async futures and streams referenced by `func` have their helper - /// import tables generated for the given module prefix. - pub(super) fn generation_futures_and_streams_import( +/// Async-specific helpers used by `InterfaceGenerator` to keep the main +/// visitor implementation focused on shared lowering/lifting logic. +impl<'a> InterfaceGenerator<'a> { + pub(super) fn add_async_export_stub_parameter( &mut self, - prefix: &str, func: &Function, - module: &str, + is_async: bool, + params: &mut Vec, ) { - let module = format!("{prefix}{module}"); - for (index, ty) in func - .find_futures_and_streams(self.resolve) - .into_iter() - .enumerate() - { - let func_name = &func.name; + if is_async && matches!(self.direction, Direction::Export) { + let ffi = self + .world_gen + .pkg_resolver + .qualify_package(self.name, ASYNC_CORE_DIR); + let background_group = background_group_name(func); + params.push(format!("{background_group} : {ffi}TaskGroup[Unit]")); + } + } - match &self.resolve.types[ty].kind { + pub(super) fn emit_async_export_wrapper( + &mut self, + plan: &AsyncExportPlan, + func: &Function, + func_name: &str, + params: &str, + result_type: &str, + cleanup_list: &str, + body: &str, + ) -> bool { + if !plan.is_async() { + return false; + } + let ffi = self + .world_gen + .pkg_resolver + .qualify_package(self.name, ASYNC_CORE_DIR); + let background_group = background_group_name(func); + uwrite!( + self.ffi, + r#" + #doc(hidden) + pub fn {func_name}({params}) -> {result_type} {{ + return {ffi}with_waitableset(async fn() {{ + {ffi}with_task_group(async fn({background_group}) {{ + {cleanup_list} + {body} + }}) + }}) + }} + "#, + ); + true + } + + pub(super) fn import_async_function_plan( + &self, + interface: Option<&WorldKey>, + func: &Function, + ) -> AsyncFunctionPlan { + let names = AsyncEndpointNames::import(self.resolve, interface, func); + AsyncFunctionPlan::new(async_endpoint_sites( + self.resolve, + &names, + interface, + func, + false, + )) + } + + pub(super) fn export_async_function_plan( + &self, + interface: Option<&WorldKey>, + func: &Function, + ) -> AsyncFunctionPlan { + let names = AsyncEndpointNames::export(self.resolve, interface, func); + AsyncFunctionPlan::new(async_endpoint_sites( + self.resolve, + &names, + interface, + func, + true, + )) + } + + pub(super) fn generate_async_import_body( + &mut self, + endpoint_plan: &AsyncFunctionPlan, + func: &Function, + mbt_sig: &MoonbitSignature, + sig: &WasmSignature, + ) -> AsyncImportBody { + self.generate_async_import_function(endpoint_plan, func, mbt_sig, sig) + } + + pub(super) fn emit_future_stream_helpers( + &mut self, + plan: &AsyncFunctionPlan, + state: &AsyncFunctionState, + ) { + let endpoint_uses = plan.endpoint_uses(state); + for (site, endpoint_use) in plan.endpoint_sites.iter().zip(endpoint_uses) { + match &self.resolve.types[site.ty].kind { TypeDefKind::Future(payload_type) => { - self.r#generate_async_future_or_stream_import( - PayloadFor::Future, - &module, - index, - func_name, - ty, + self.generate_async_future_or_stream_import( + site, + endpoint_use, + plan.payload_sites(site), payload_type.as_ref(), ); } TypeDefKind::Stream(payload_type) => { - self.r#generate_async_future_or_stream_import( - PayloadFor::Stream, - &module, - index, - func_name, - ty, + self.generate_async_future_or_stream_import( + site, + endpoint_use, + plan.payload_sites(site), payload_type.as_ref(), ); } @@ -315,195 +791,1449 @@ impl<'a> InterfaceGenerator<'a> { } } - fn generate_async_future_or_stream_import( + pub(super) fn emit_async_export_callback( &mut self, - payload_for: PayloadFor, - module: &str, - index: usize, - func_name: &str, - ty: TypeId, - result_type: Option<&Type>, - ) { - if !self - .world_gen - .async_support - .register_future_or_stream(module, ty) - { - return; + plan: &AsyncExportPlan, + interface: Option<&WorldKey>, + func: &Function, + camel_name: &str, + async_state: AsyncFunctionState, + ) -> bool { + if !plan.is_async() { + return false; } - let result = match result_type { - Some(ty) => self.world_gen.pkg_resolver.type_name(self.name, ty), + + let export_func_name = self + .world_gen + .export_ns + .tmp(&format!("wasmExportAsync{camel_name}")); + let AsyncTaskReturnState::Emitted { + body: task_return_body, + needs_cleanup_list: task_return_needs_cleanup, + params: task_return_params, + return_param, + return_value, + } = async_state.task_return + else { + unreachable!() + }; + let (task_return_module, task_return_name, _) = + func.task_return_import(self.resolve, interface, Mangling::Legacy); + let callback_export_name = self.resolve.wasm_export_name( + plan.mangling_and_abi(), + WasmExport::Func { + interface, + func, + kind: WasmExportKind::Callback, + }, + ); + let task_return_param_tys = task_return_params + .iter() + .enumerate() + .map(|(idx, (ty, _expr))| format!("p{}: {}", idx, wasm_type(*ty))) + .collect::>() + .join(", "); + let task_return_param_exprs = task_return_params + .iter() + .map(|(_ty, expr)| expr.as_str()) + .collect::>() + .join(", "); + let helper_package = self.name; + let return_ty = match &func.result { + Some(result) => self + .world_gen + .pkg_resolver + .type_name(helper_package, result) + .to_string(), None => "Unit".into(), }; - - let type_name = self + let return_expr = match return_ty.as_str() { + "Unit" => "".into(), + _ => format!("{return_param}: Ref[{return_ty}?]",), + }; + let cleanup_list = if task_return_needs_cleanup { + self.ffi_imports.insert(ffi::FREE); + " + let cleanup_list : Array[Int] = [] + " + } else { + "" + }; + let cleanup = if task_return_needs_cleanup { + " + cleanup_list.each(mbt_ffi_free) + " + } else { + "" + }; + let ffi = self .world_gen .pkg_resolver - .type_name(self.name, &Type::Id(ty)); - let name = result.to_upper_camel_case(); - let kind = match payload_for { - PayloadFor::Future => "future", - PayloadFor::Stream => "stream", - }; - let table_name = format!("{}_{}_table", type_name.to_snake_case(), kind); - let camel_kind = kind.to_upper_camel_case(); - let payload_len_arg = match payload_for { - PayloadFor::Future => "", - PayloadFor::Stream => " ,length : Int", + .qualify_package(helper_package, ASYNC_CORE_DIR); + let task_return = format!( + r#" + {cleanup_list} + {task_return_body} + {export_func_name}TaskReturn({task_return_param_exprs}) + {cleanup} + {ffi}task_returned() + "# + ); + let task_return = match return_ty.as_str() { + "Unit" => task_return, + _ => format!( + r#" + match {return_param}.val {{ + Some({return_value}) => {{ + {task_return} + }} + None => () + }} + "# + ), }; + let snake_func_name = func.name.to_moonbit_ident().to_string(); - let payload_lift_func = match payload_for { - PayloadFor::Future => "", - PayloadFor::Stream => "List", - }; + uwriteln!( + self.ffi, + r#" + fn {export_func_name}TaskReturn({task_return_param_tys}) = "{task_return_module}" "{task_return_name}" + + fn {snake_func_name}_task_return({return_expr}) -> Unit {{ + {task_return} + }} + "# + ); + + uwriteln!( + self.ffi, + r#" + #doc(hidden) + pub fn {export_func_name}(event_raw: Int, waitable: Int, code: Int) -> Int {{ + {ffi}cb(event_raw, waitable, code) + }} + "# + ); + + let gen_dir = self.world_gen.opts.gen_dir.clone(); + let package = self + .world_gen + .pkg_resolver + .qualify_package(&gen_dir, self.name); + let export = format!( + r#" + #doc(hidden) + pub fn {export_func_name}(event_raw: Int, waitable: Int, code: Int) -> Int {{ + {package}{export_func_name}(event_raw, waitable, code) + }} + "#, + ); + self.world_gen + .export + .insert(callback_export_name, (export_func_name.clone(), export)); + + true + } + + /// Builds the MoonBit body for async imports, wiring wasm subtasks into the + /// runtime and lowering/lifting payloads as needed. + fn generate_async_import_function( + &mut self, + endpoint_plan: &AsyncFunctionPlan, + func: &Function, + mbt_sig: &MoonbitSignature, + sig: &WasmSignature, + ) -> AsyncImportBody { + let mut body = String::default(); + let mut lower_params = Vec::new(); + let mut lower_results = Vec::new(); + let mut async_state = endpoint_plan.state(); + let mut needs_cleanup_list = false; + self.ffi_imports.insert(ffi::FREE); let ffi = self .world_gen .pkg_resolver - .qualify_package(self.name, FFI_DIR); - - let mut dealloc_list; - let malloc; - let lift; - let lower; - let lift_result; - let lift_list: String; - let lower_list: String; - if let Some(result_type) = result_type { - (lift, lift_result) = self.lift_from_memory("ptr", result_type, module); - lower = self.lower_to_memory("ptr", "value", result_type, module); - dealloc_list = self.deallocate_lists( - std::slice::from_ref(result_type), - &[String::from("ptr")], - true, - module, - ); - lift_list = self.list_lift_from_memory( - "ptr", - "length", - &format!("wasm{name}{kind}Lift"), - result_type, - ); - lower_list = - self.list_lower_to_memory(&format!("wasm{name}{kind}Lower"), "value", result_type); + .qualify_package(self.name, ASYNC_CORE_DIR); - malloc = self.malloc_memory("ptr", "length", result_type); + if sig.indirect_params { + match &func.params[..] { + [] => {} + [Param { name, ty, .. }] => { + body.push_str(&self.malloc_memory("_lower_ptr", "1", ty, self.name)); + body.push_str("\ndefer mbt_ffi_free(_lower_ptr)\n"); + body.push_str(&self.lower_to_memory( + "_lower_ptr", + &name.to_moonbit_ident(), + ty, + self.name, + &mut async_state, + )); + lower_params.push("_lower_ptr".into()); + } + multiple_params => { + let params = multiple_params.iter().map(|Param { ty, .. }| ty); + let offsets = self.world_gen.sizes.field_offsets(params.clone()); + let elem_info = self.world_gen.sizes.params(params); + self.ffi_imports.insert(ffi::MALLOC); + body.push_str(&format!( + r#" + let _lower_ptr : Int = mbt_ffi_malloc({}) + defer mbt_ffi_free(_lower_ptr) + "#, + elem_info.size.size_wasm32(), + )); + + for ((offset, ty), name) in offsets.iter().zip( + multiple_params + .iter() + .map(|Param { name, .. }| name.to_moonbit_ident()), + ) { + let result = self.lower_to_memory( + &format!("_lower_ptr + {}", offset.size_wasm32()), + &name, + ty, + self.name, + &mut async_state, + ); + body.push_str(&result); + } - if dealloc_list.is_empty() { - dealloc_list = "let _ = ptr".to_string(); + lower_params.push("_lower_ptr".into()); + } } } else { - lift = "let _ = ptr".to_string(); - lower = "let _ = (ptr, value)".to_string(); - dealloc_list = "let _ = ptr".to_string(); - malloc = "let ptr = 0;".into(); - lift_result = "".into(); - lift_list = "FixedArray::make(length, Unit::default())".into(); - lower_list = "0".into(); - } - - let (mut lift_func, mut lower_func) = if result_type - .is_some_and(|ty| self.is_list_canonical(self.resolve, ty)) - && matches!(payload_for, PayloadFor::Stream) + let mut f = FunctionBindgen::new(self, Box::new([])) + .with_async_state(mem::take(&mut async_state)) + .without_block_cleanup(); + for (name, ty) in mbt_sig.params.iter() { + lower_params.extend(abi::lower_flat( + f.interface_gen.resolve, + &mut f, + name.clone(), + ty, + )); + } + lower_results.push(f.src.clone()); + needs_cleanup_list = f.needs_cleanup_list; + async_state = f.async_state; + } + + if !sig.indirect_params { + let mut stable_params = Vec::with_capacity(lower_params.len()); + let bindings = lower_params + .iter() + .enumerate() + .map(|(index, value)| { + let name = format!("_lower_arg{index}"); + stable_params.push(name.clone()); + format!("let {name} = {value}") + }) + .collect::>() + .join("\n"); + lower_results.push(bindings); + lower_params = stable_params; + } + + let argument_types = func + .params + .iter() + .map(|Param { ty, .. }| *ty) + .collect::>(); + let argument_operands = lower_params.clone(); + let (commit_arguments, commit_state) = self.commit_lists_and_endpoints_with_state( + &argument_types, + &argument_operands, + sig.indirect_params, + self.name, + endpoint_plan.endpoint_sites.clone(), + ); + let (reject_arguments, rejection_state) = self.deallocate_lists_and_own_with_state( + &argument_types, + &argument_operands, + sig.indirect_params, + self.name, + endpoint_plan.endpoint_sites.clone(), + ); + for (used, rejected) in async_state + .endpoint_uses + .iter_mut() + .zip(rejection_state.endpoint_uses) + { + used.lift |= rejected.lift; + used.lower |= rejected.lower; + } + for (used, committed) in async_state + .endpoint_uses + .iter_mut() + .zip(commit_state.endpoint_uses) { - ("".into(), "".into()) + used.lift |= committed.lift; + used.lower |= committed.lower; + } + let commit_arguments = if commit_arguments.trim().is_empty() { + "()".to_string() } else { - ( - format!( + commit_arguments + }; + let reject_arguments = if reject_arguments.trim().is_empty() { + "()".to_string() + } else { + reject_arguments + }; + let settle_arguments = format!( + r#" + fn(before_started) {{ + if before_started {{ + {reject_arguments} + }} else {{ + {commit_arguments} + }} + }} + "# + ); + let func_name = func.name.to_upper_camel_case(); + + let call_import = |params: &Vec, drop_returned_result: &str| { + format!( + r#" + let _subtask_code = wasmImport{func_name}({}) + {ffi}suspend_for_subtask( + _subtask_code, + {settle_arguments}, + fn() {{ + {drop_returned_result} + }}, + ) + + "#, + params.join(", ") + ) + }; + match &func.result { + Some(ty) => { + lower_params.push("_result_ptr".into()); + let (drop_returned_result, drop_result_state) = self + .deallocate_lists_and_own_with_state( + std::slice::from_ref(ty), + &["_result_ptr".into()], + true, + self.name, + endpoint_plan.endpoint_sites.clone(), + ); + for (used, dropped) in async_state + .endpoint_uses + .iter_mut() + .zip(drop_result_state.endpoint_uses) + { + used.lift |= dropped.lift; + used.lower |= dropped.lower; + } + let call_import = call_import(&lower_params, &drop_returned_result); + let (lift, lift_result) = + &self.lift_from_memory("_result_ptr", ty, self.name, &mut async_state); + body.push_str(&format!( r#" - fn wasm{name}{kind}Lift(ptr: Int) -> {result} {{ + {} + {} + defer mbt_ffi_free(_result_ptr) + {call_import} {lift} {lift_result} + "#, + lower_results.join("\n"), + &self.malloc_memory("_result_ptr", "1", ty, self.name) + )); + } + None => { + let call_import = call_import(&lower_params, "()"); + body.push_str(&format!("{}\n{call_import}", lower_results.join("\n"))); + } + } + + AsyncImportBody { + src: body, + needs_cleanup_list, + state: async_state, + } + } + + fn generate_async_future_or_stream_import( + &mut self, + site: &AsyncEndpointSite, + endpoint_use: EndpointUse, + payload_sites: Vec, + result_type: Option<&Type>, + ) { + if !self.world_gen.async_support.register_future_or_stream( + &site.binding_name, + site.index, + site.kind, + ) { + return; + } + let helper_package = self.name.to_string(); + let result = match result_type { + Some(ty) => self.world_gen.pkg_resolver.type_name(&helper_package, ty), + None => "Unit".into(), + }; + + let symbol_name = &site.symbol_name; + let payload_len_arg = match site.kind { + PayloadFor::Future => "", + PayloadFor::Stream => " ,length : Int", + }; + let new_module = &site.intrinsics.new.module; + let new_field = &site.intrinsics.new.field; + let read_module = &site.intrinsics.read.module; + let read_field = &site.intrinsics.read.field; + let write_module = &site.intrinsics.write.module; + let write_field = &site.intrinsics.write.field; + let cancel_read_module = &site.intrinsics.cancel_read.module; + let cancel_read_field = &site.intrinsics.cancel_read.field; + let cancel_write_module = &site.intrinsics.cancel_write.module; + let cancel_write_field = &site.intrinsics.cancel_write.field; + let drop_readable_module = &site.intrinsics.drop_readable.module; + let drop_readable_field = &site.intrinsics.drop_readable.field; + let drop_writable_module = &site.intrinsics.drop_writable.module; + let drop_writable_field = &site.intrinsics.drop_writable.field; + let ffi = self + .world_gen + .pkg_resolver + .qualify_package(&helper_package, ASYNC_CORE_DIR); + + let elem_size = result_type + .map(|ty| self.world_gen.sizes.size(ty).size_wasm32()) + .unwrap_or(0); + let read_chunk_owns_buffer = + result_type.is_some_and(|ty| self.is_list_canonical(self.resolve, ty)); + let staging_window = if payload_sites.is_empty() { 64 } else { 1 }; + + let (lift, lift_result, lower, malloc, lift_list, commit, reject, free_outer) = + if let Some(result_type) = result_type { + let mut payload_state = AsyncFunctionState::from_sites(payload_sites.clone()); + let (lift, lift_result) = + self.lift_from_memory("ptr", result_type, &helper_package, &mut payload_state); + let mut payload_state = AsyncFunctionState::from_sites(payload_sites.clone()); + let lower = self.lower_to_memory( + "ptr", + "value", + result_type, + &helper_package, + &mut payload_state, + ); + let (commit, _) = self.commit_lists_and_endpoints_with_state( + std::slice::from_ref(result_type), + &[String::from("elem_ptr")], + true, + &helper_package, + payload_sites.clone(), + ); + let reject = self.deallocate_lists_and_own( + std::slice::from_ref(result_type), + &[String::from("elem_ptr")], + true, + &helper_package, + payload_sites.clone(), + ); + let lift_list = self.list_lift_from_memory( + "ptr", + "length", + &format!("wasm{symbol_name}Lift"), + result_type, + &helper_package, + ); + let malloc = self.malloc_memory("ptr", "length", result_type, &helper_package); + self.ffi_imports.insert(ffi::FREE); + ( + lift, + lift_result, + lower, + malloc, + lift_list, + commit, + reject, + "mbt_ffi_free(ptr)".to_string(), + ) + } else { + ( + "let _ = ptr".into(), + "".into(), + "let _ = (ptr, value)".into(), + "let ptr = 0".into(), + "FixedArray::make(length, Unit::default())".into(), + String::new(), + String::new(), + "let _ = ptr".into(), + ) + }; + + let commit = if commit.trim().is_empty() { + "let _ = (ptr, start, length)".to_string() + } else { + format!( + r#" + for i = start; i < start + length; i = i + 1 {{ + let elem_ptr = ptr + i * {elem_size} + {commit} }} "# - ), - format!( - r#" - fn wasm{name}{kind}Lower(value: {result}, ptr: Int) -> Unit {{ - {lower} + ) + }; + let reject = if reject.trim().is_empty() { + "let _ = (ptr, start, length)".to_string() + } else { + format!( + r#" + for i = start; i < start + length; i = i + 1 {{ + let elem_ptr = ptr + i * {elem_size} + {reject} }} "# - ), ) }; - if matches!(payload_for, PayloadFor::Stream) { - lift_func.push_str(&format!( + let lift_func = (endpoint_use.lift + && !(matches!(site.kind, PayloadFor::Stream) && read_chunk_owns_buffer)) + .then(|| { + format!( + r#" + fn wasm{symbol_name}Lift(ptr: Int) -> {result} {{ + {lift} + {lift_result} + }} + "# + ) + }) + .unwrap_or_default(); + let lower_func = endpoint_use + .lower + .then(|| { + format!( + r#" + fn wasm{symbol_name}Lower(value: {result}, ptr: Int) -> Unit {{ + {lower} + }} + "# + ) + }) + .unwrap_or_default(); + let list_lift_func = (endpoint_use.lift && matches!(site.kind, PayloadFor::Stream)) + .then(|| { + format!( + r#" + fn wasm{symbol_name}ListLift( + ptr: Int, + length: Int, + ) -> FixedArray[{result}] {{ + {lift_list} + }} + "# + ) + }) + .unwrap_or_default(); + + let lift_intrinsics = endpoint_use + .lift + .then(|| { + format!( + r#" + fn wasmImport{symbol_name}Read(handle : Int, buffer_ptr : Int{payload_len_arg}) -> Int = "{read_module}" "{read_field}" + fn wasmImport{symbol_name}CancelRead(handle : Int) -> Int = "{cancel_read_module}" "{cancel_read_field}" + "# + ) + }) + .unwrap_or_default(); + let drop_readable_intrinsic = (endpoint_use.lift || endpoint_use.lower) + .then(|| { + format!( + r#" + fn wasmImport{symbol_name}DropReadable(handle : Int) = "{drop_readable_module}" "{drop_readable_field}" + "# + ) + }) + .unwrap_or_default(); + let cancel_write_intrinsic = (endpoint_use.lower + && matches!(site.kind, PayloadFor::Stream)) + .then(|| { + format!( r#" - fn wasm{name}{kind}ListLift(ptr: Int, length: Int) -> FixedArray[{result}] {{ - {lift_list} + fn wasmImport{symbol_name}CancelWrite(handle : Int) -> Int = "{cancel_write_module}" "{cancel_write_field}" + "#, + ) + }) + .unwrap_or_default(); + let lower_intrinsics = endpoint_use + .lower + .then(|| { + format!( + r#" + fn wasmImport{symbol_name}New() -> UInt64 = "{new_module}" "{new_field}" + fn wasmImport{symbol_name}Write(handle : Int, buffer_ptr : Int{payload_len_arg}) -> Int = "{write_module}" "{write_field}" + {cancel_write_intrinsic} + fn wasmImport{symbol_name}DropWritable(handle : Int) = "{drop_writable_module}" "{drop_writable_field}" + "# + ) + }) + .unwrap_or_default(); + + let commit_func = endpoint_use + .lower + .then(|| { + format!( + r#" + fn wasm{symbol_name}Commit( + ptr: Int, + start: Int, + length: Int, + ) -> Unit {{ + {commit} + }} + "# + ) + }) + .unwrap_or_default(); + let reject_future_read = format!("wasm{symbol_name}Reject(self.read_buffer, 0, 1)"); + let reject_stream_read = format!("wasm{symbol_name}Reject(self.read_buffer, 0, progress)"); + + let bridge_func = match site.kind { + PayloadFor::Future => { + let reject_prepared = (endpoint_use.lift && endpoint_use.lower) + .then(|| { + format!( + r#" + if wasm{symbol_name}FutureRejectPrepared(self.handle) {{ + self.closed = true + return + }} + "# + ) + }) + .unwrap_or_default(); + let reject_prepared_func = (endpoint_use.lift && endpoint_use.lower) + .then(|| { + format!( + r#" +fn wasm{symbol_name}FutureRejectPrepared(handle: Int) -> Bool {{ + guard wasm{symbol_name}FutureProducers.get(handle) is Some(producer) else {{ + return false + }} + wasm{symbol_name}FutureProducers.remove(handle) + let writer = producer.writer + wasmImport{symbol_name}DropReadable(handle) + {ffi}spawn_component_task_current(async fn() {{ + defer wasmImport{symbol_name}DropWritable(writer) + {ffi}protect_from_cancel( + () => {{ + let value = producer.future.get() + let ptr = wasm{symbol_name}Malloc(1) + wasm{symbol_name}Lower(value, ptr) + let terminal : Ref[Bool?] = {{ val: None }} + while terminal.val is None {{ + terminal.val = {ffi}suspend_for_future_write_terminal( + writer, + wasmImport{symbol_name}Write(writer, ptr), + ) }} - "# - )); + guard terminal.val is Some(transferred) + if transferred {{ + wasm{symbol_name}Commit(ptr, 0, 1) + }} else {{ + wasm{symbol_name}Reject(ptr, 0, 1) + }} + {free_outer} + }}, + resume_on_cancel=true, + ) catch {{ + _ => abort("rejected component future producer ended without a value") + }} + }}) + true +}} + "# + ) + }) + .unwrap_or_default(); + let lift_bridge = endpoint_use + .lift + .then(|| { + format!( + r#" +priv struct Wasm{symbol_name}FutureSource {{ + handle : Int + mut closed : Bool + mut reading : Bool + mut read_task : Int + mut read_buffer : Int + mut read_discarding : Bool + mut read_cleanup_done : Bool + read_cleanup : {ffi}CondVar +}} - lower_func.push_str(&format!( - r#" - fn wasm{name}{kind}ListLower(value: FixedArray[{result}]) -> Int {{ - {lower_list} +fn Wasm{symbol_name}FutureSource::finish_read( + self : Wasm{symbol_name}FutureSource, +) -> Unit {{ + self.reading = false + self.read_task = 0 + self.read_buffer = 0 + self.read_discarding = false + // Keep completion latched until a later read starts so every waiter woken + // by the broadcast can observe it. +}} + +async fn Wasm{symbol_name}FutureSource::wait_for_read_cleanup( + self : Wasm{symbol_name}FutureSource, +) -> Unit noraise {{ + {ffi}protect_from_cancel( + () => {{ + while !self.read_cleanup_done {{ + self.read_cleanup.wait() + }} + }}, + resume_on_cancel=true, + ) catch {{ + _ => () + }} +}} + +async fn Wasm{symbol_name}FutureSource::cancel_active_read( + self : Wasm{symbol_name}FutureSource, +) -> Unit noraise {{ + if !self.reading {{ + return + }} + if self.read_discarding {{ + self.wait_for_read_cleanup() + return + }} + self.read_discarding = true + let completed = Ref(false) + {ffi}protect_from_cancel( + () => {{ + completed.val = {ffi}cancel_future_read( + self.read_task, + self.handle, + () => wasmImport{symbol_name}CancelRead(self.handle), + ) + }}, + resume_on_cancel=true, + ) catch {{ + _ => () + }} + if completed.val && self.read_buffer != 0 {{ + {reject_future_read} + }} + self.read_cleanup_done = true + self.read_cleanup.broadcast() +}} + +async fn Wasm{symbol_name}FutureSource::close( + self : Wasm{symbol_name}FutureSource, +) -> Unit noraise {{ + if self.closed {{ + return + }} + if self.reading {{ + self.cancel_active_read() + }} + self.closed = true + wasmImport{symbol_name}DropReadable(self.handle) +}} + +fn Wasm{symbol_name}FutureSource::close_sync( + self : Wasm{symbol_name}FutureSource, +) -> Unit {{ + if self.closed || self.reading {{ + return + }} + {reject_prepared} + self.closed = true + wasmImport{symbol_name}DropReadable(self.handle) +}} + +async fn Wasm{symbol_name}FutureSource::read( + self : Wasm{symbol_name}FutureSource, +) -> {result} {{ + if self.closed {{ + raise {ffi}FutureReadError::Dropped + }} + if self.reading {{ + raise {ffi}EndpointBusy::Read + }} + let ptr = wasm{symbol_name}Malloc(1) + defer {{ {free_outer} }} + self.reading = true + self.read_task = {ffi}current_component_task_token() + self.read_buffer = ptr + self.read_discarding = false + self.read_cleanup_done = false + {ffi}suspend_for_future_read( + self.handle, + wasmImport{symbol_name}Read(self.handle, ptr), + ) catch {{ + err => {{ + if self.read_discarding {{ + self.wait_for_read_cleanup() + self.finish_read() + raise {ffi}FutureReadError::Dropped + }} + if err is {ffi}Cancelled::Cancelled {{ + self.cancel_active_read() + if !self.closed {{ + self.closed = true + wasmImport{symbol_name}DropReadable(self.handle) }} - "# - )); + }} + self.finish_read() + raise err + }} + }} + if self.read_discarding {{ + self.wait_for_read_cleanup() + self.finish_read() + raise {ffi}FutureReadError::Dropped + }} + let value = wasm{symbol_name}Lift(ptr) + self.finish_read() + if !self.closed {{ + self.closed = true + wasmImport{symbol_name}DropReadable(self.handle) + }} + value +}} + +fn wasm{symbol_name}FutureLift(handle: Int) -> {ffi}Future[{result}] {{ + let source = Wasm{symbol_name}FutureSource::{{ + handle, + closed: false, + reading: false, + read_task: 0, + read_buffer: 0, + read_discarding: false, + read_cleanup_done: false, + read_cleanup: {ffi}CondVar(), + }} + {ffi}Future::from_source( + () => source.read(), + () => source.close(), + () => source.close_sync(), + ) +}} +"# + ) + }) + .unwrap_or_default(); + let lower_committed = endpoint_use + .lower_committed + .then(|| { + format!( + r#" +fn wasm{symbol_name}FutureLowerCommitted(future: {ffi}Future[{result}]) -> Int {{ + let reader = wasm{symbol_name}FutureLower(future) + wasm{symbol_name}FutureCommit(reader) + reader +}} + "# + ) + }) + .unwrap_or_default(); + let lower_bridge = if endpoint_use.lower { + format!( + r#" +priv struct Wasm{symbol_name}FutureProducer {{ + future : {ffi}Future[{result}] + writer : Int +}} + +let wasm{symbol_name}FutureProducers : Map[Int, Wasm{symbol_name}FutureProducer] = Map([]) + +{reject_prepared_func} + +fn wasm{symbol_name}FutureCommit(handle: Int) -> Unit {{ + guard wasm{symbol_name}FutureProducers.get(handle) is Some(producer) else {{ + return + }} + wasm{symbol_name}FutureProducers.remove(handle) + let writer = producer.writer + if !{ffi}has_component_task_scope() {{ + abort("component future producer requires an async task scope") + }} + {ffi}spawn_component_task_current(async fn() {{ + {ffi}protect_from_cancel( + () => {{ + let value = producer.future.get() + let ptr = wasm{symbol_name}Malloc(1) + wasm{symbol_name}Lower(value, ptr) + let terminal : Ref[Bool?] = {{ val: None }} + while terminal.val is None {{ + terminal.val = {ffi}suspend_for_future_write_terminal( + writer, + wasmImport{symbol_name}Write(writer, ptr), + ) + }} + guard terminal.val is Some(transferred) + if transferred {{ + wasm{symbol_name}Commit(ptr, 0, 1) + }} else {{ + wasm{symbol_name}Reject(ptr, 0, 1) + }} + {free_outer} + wasmImport{symbol_name}DropWritable(writer) + }}, + resume_on_cancel=true, + ) catch {{ + _ => abort("component future producer ended without a value") + }} + }}) +}} + +fn wasm{symbol_name}FutureLower(future: {ffi}Future[{result}]) -> Int {{ + if !{ffi}has_component_task_scope() {{ + abort("component future producer requires an async task scope") + }} + let pair = wasmImport{symbol_name}New() + let reader = pair.to_int() + let writer = (pair >> 32).to_int() + wasm{symbol_name}FutureProducers.set(reader, {{ future, writer }}) + reader +}} + +{lower_committed} +"# + ) + } else { + String::new() + }; + format!("{lift_bridge}{lower_bridge}") + } + PayloadFor::Stream => { + let reject_prepared = (endpoint_use.lift && endpoint_use.lower) + .then(|| { + format!( + r#" + if wasm{symbol_name}StreamRejectPrepared(self.handle) {{ + self.closed = true + return + }} + "# + ) + }) + .unwrap_or_default(); + let reject_prepared_func = (endpoint_use.lift && endpoint_use.lower) + .then(|| { + format!( + r#" +fn wasm{symbol_name}StreamRejectPrepared(handle: Int) -> Bool {{ + guard wasm{symbol_name}StreamProducers.get(handle) is Some(prepared) else {{ + return false + }} + wasm{symbol_name}StreamProducers.remove(handle) + let writer = prepared.writer + wasmImport{symbol_name}DropReadable(handle) + wasmImport{symbol_name}DropWritable(writer) + {ffi}spawn_component_task_current(async fn() {{ + {ffi}protect_from_cancel( + () => {{ + prepared.stream.reject((value : {result}) => {{ + let ptr = wasm{symbol_name}Malloc(1) + wasm{symbol_name}Lower(value, ptr) + wasm{symbol_name}Reject(ptr, 0, 1) + {free_outer} + }}) + }}, + resume_on_cancel=true, + ) catch {{ + _ => () + }} + }}) + true +}} + "# + ) + }) + .unwrap_or_default(); + let lift_bridge = endpoint_use + .lift + .then(|| { + format!( + r#" +priv struct Wasm{symbol_name}StreamSource {{ + handle : Int + mut closed : Bool + mut reading : Bool + mut read_task : Int + mut read_buffer : Int + mut read_discarding : Bool + mut read_cleanup_done : Bool + read_cleanup : {ffi}CondVar +}} + +fn Wasm{symbol_name}StreamSource::finish_read( + self : Wasm{symbol_name}StreamSource, +) -> Unit {{ + self.reading = false + self.read_task = 0 + self.read_buffer = 0 + self.read_discarding = false + // Keep completion latched until a later read starts so every waiter woken + // by the broadcast can observe it. +}} + +async fn Wasm{symbol_name}StreamSource::wait_for_read_cleanup( + self : Wasm{symbol_name}StreamSource, +) -> Unit noraise {{ + {ffi}protect_from_cancel( + () => {{ + while !self.read_cleanup_done {{ + self.read_cleanup.wait() + }} + }}, + resume_on_cancel=true, + ) catch {{ + _ => () + }} +}} + +async fn Wasm{symbol_name}StreamSource::cancel_active_read( + self : Wasm{symbol_name}StreamSource, +) -> Unit noraise {{ + if !self.reading {{ + return + }} + if self.read_discarding {{ + self.wait_for_read_cleanup() + return + }} + self.read_discarding = true + let progress_ref = Ref(0) + {ffi}protect_from_cancel( + () => {{ + progress_ref.val = {ffi}cancel_stream_read( + self.read_task, + self.handle, + () => wasmImport{symbol_name}CancelRead(self.handle), + ) + }}, + resume_on_cancel=true, + ) catch {{ + _ => () + }} + let progress = progress_ref.val + if progress > 0 && self.read_buffer != 0 {{ + {reject_stream_read} + }} + self.read_cleanup_done = true + self.read_cleanup.broadcast() +}} + +async fn Wasm{symbol_name}StreamSource::close( + self : Wasm{symbol_name}StreamSource, +) -> Unit noraise {{ + if self.closed {{ + return + }} + if self.reading {{ + self.cancel_active_read() + }} + self.closed = true + wasmImport{symbol_name}DropReadable(self.handle) +}} + +fn Wasm{symbol_name}StreamSource::close_sync( + self : Wasm{symbol_name}StreamSource, +) -> Unit {{ + if self.closed || self.reading {{ + return + }} + {reject_prepared} + self.closed = true + wasmImport{symbol_name}DropReadable(self.handle) +}} + +async fn Wasm{symbol_name}StreamSource::read( + self : Wasm{symbol_name}StreamSource, + count : Int, +) -> FixedArray[{result}]? {{ + if self.closed {{ + return None + }} + if count <= 0 {{ + return Some([]) + }} + if self.reading {{ + raise {ffi}EndpointBusy::Read + }} + let ptr = wasm{symbol_name}Malloc(count) + let mut owns_buffer = false + defer {{ + if !owns_buffer {{ + {free_outer} + }} + }} + self.reading = true + self.read_task = {ffi}current_component_task_token() + self.read_buffer = ptr + self.read_discarding = false + self.read_cleanup_done = false + let (progress, end) = {ffi}suspend_for_stream_read( + self.handle, + wasmImport{symbol_name}Read(self.handle, ptr, count), + ) catch {{ + err => {{ + if self.read_discarding {{ + self.wait_for_read_cleanup() + self.finish_read() + return None + }} + if err is {ffi}Cancelled::Cancelled {{ + self.cancel_active_read() + if !self.closed {{ + self.closed = true + wasmImport{symbol_name}DropReadable(self.handle) + }} + }} + self.finish_read() + raise err + }} + }} + if self.read_discarding {{ + self.wait_for_read_cleanup() + self.finish_read() + return None + }} + if progress == 0 {{ + self.finish_read() + if end {{ + self.close() + return None + }} + return Some([]) + }} + let values = wasm{symbol_name}ListLift(ptr, progress) + owns_buffer = {read_chunk_owns_buffer} + self.finish_read() + if end {{ + self.close() + }} + Some(values) +}} + +fn wasm{symbol_name}StreamLift(handle: Int) -> {ffi}Stream[{result}] {{ + let source = Wasm{symbol_name}StreamSource::{{ + handle, + closed: false, + reading: false, + read_task: 0, + read_buffer: 0, + read_discarding: false, + read_cleanup_done: false, + read_cleanup: {ffi}CondVar(), + }} + {ffi}Stream::from_source( + (count) => source.read(count), + () => source.close(), + () => source.close_sync(), + ) +}} +"# + ) + }) + .unwrap_or_default(); + let lower_committed = endpoint_use + .lower_committed + .then(|| { + format!( + r#" +fn wasm{symbol_name}StreamLowerCommitted(stream: {ffi}Stream[{result}]) -> Int {{ + let reader = wasm{symbol_name}StreamLower(stream) + wasm{symbol_name}StreamCommit(reader) + reader +}} + "# + ) + }) + .unwrap_or_default(); + let lower_bridge = if endpoint_use.lower { + format!( + r#" +priv struct Wasm{symbol_name}StreamProducer {{ + stream : {ffi}Stream[{result}] + writer : Int +}} + +let wasm{symbol_name}StreamProducers : Map[Int, Wasm{symbol_name}StreamProducer] = Map([]) + +{reject_prepared_func} + +fn wasm{symbol_name}StreamCommit(handle: Int) -> Unit {{ + guard wasm{symbol_name}StreamProducers.get(handle) is Some(prepared) else {{ + return + }} + wasm{symbol_name}StreamProducers.remove(handle) + let stream = prepared.stream + let writer = prepared.writer + if !{ffi}has_component_task_scope() {{ + abort("component stream producer requires an async task scope") + }} + let producer = stream.take_producer() + {ffi}spawn_component_task_current(async fn() {{ + let writer_closed = Ref(false) + let writer_lock = {ffi}Mutex() + let close_writer = fn() -> Unit {{ + if !writer_closed.val {{ + writer_closed.val = true + wasmImport{symbol_name}DropWritable(writer) + }} + }} + defer close_writer() + let cleanup_value = fn(value : {result}) -> Unit {{ + let ptr = wasm{symbol_name}Malloc(1) + wasm{symbol_name}Lower(value, ptr) + wasm{symbol_name}Reject(ptr, 0, 1) + {free_outer} + }} + let sink = {ffi}Sink::from_callbacks( + (data : ArrayView[{result}]) => {{ + writer_lock.acquire() + defer writer_lock.release() + if writer_closed.val || data.length() == 0 {{ + return 0 + }} + let data_len = if data.length() < {staging_window} {{ + data.length() + }} else {{ + {staging_window} + }} + let ptr = wasm{symbol_name}Malloc(data_len) + for i = 0; i < data_len; i = i + 1 {{ + let value : {result} = data[i] + wasm{symbol_name}Lower(value, ptr + i * {elem_size}) + }} + let settle_staging = fn(transferred: Int) -> Unit {{ + wasm{symbol_name}Commit(ptr, 0, transferred) + if transferred < data_len {{ + wasm{symbol_name}Reject( + ptr, + transferred, + data_len - transferred, + ) + }} + {free_outer} + }} + let mut total = 0 + let mut dropped = false + while total < data_len {{ + let (progress, end) = {ffi}suspend_for_stream_write( + writer, + wasmImport{symbol_name}Write( + writer, + ptr + total * {elem_size}, + data_len - total, + ), + ) catch {{ + _ => {{ + total = total + {ffi}cancel_stream_write( + writer, + () => wasmImport{symbol_name}CancelWrite(writer), + ) + settle_staging(total) + close_writer() + return data_len + }} + }} + total = total + progress + if end {{ + dropped = true + break + }} + }} + settle_staging(total) + if dropped {{ + close_writer() + }} + // The canonical writer accepted `total`; generated cleanup + // consumed the rest of this staging window. + data_len + }}, + () => {{ + writer_lock.acquire() + defer writer_lock.release() + close_writer() + }}, + () => !writer_closed.val, + Some(cleanup_value), + ) + let relay_source = producer is None + let run_producer = async fn() -> Unit {{ + match producer {{ + Some(producer) => {{ + producer(sink) + sink.close() + }} + None => + for ;; {{ + match stream.read({staging_window}) {{ + Some(data) => + if !sink.write_all(data[:]) {{ + stream.reject(cleanup_value) + return + }} + None => {{ + sink.close() + return + }} + }} + }} + }} + }} + run_producer() catch {{ + _ => + if relay_source {{ + {ffi}protect_from_cancel( + () => stream.reject(cleanup_value), + resume_on_cancel=true, + ) catch {{ + _ => () + }} + }} + }} + }}) +}} + +fn wasm{symbol_name}StreamLower(stream: {ffi}Stream[{result}]) -> Int {{ + if !{ffi}has_component_task_scope() {{ + abort("component stream producer requires an async task scope") + }} + let pair = wasmImport{symbol_name}New() + let reader = pair.to_int() + let writer = (pair >> 32).to_int() + wasm{symbol_name}StreamProducers.set(reader, {{ stream, writer }}) + reader +}} + +{lower_committed} +"# + ) + } else { + String::new() + }; + format!("{lift_bridge}{lower_bridge}") + } }; uwriteln!( self.ffi, r#" -fn wasmImport{name}{kind}New() -> UInt64 = "{module}" "[{kind}-new-{index}]{func_name}" -fn wasmImport{name}{kind}Read(handle : Int, buffer_ptr : Int{payload_len_arg}) -> Int = "{module}" "[async-lower][{kind}-read-{index}]{func_name}" -fn wasmImport{name}{kind}Write(handle : Int, buffer_ptr : Int{payload_len_arg}) -> Int = "{module}" "[async-lower][{kind}-write-{index}]{func_name}" -fn wasmImport{name}{kind}CancelRead(handle : Int) -> Int = "{module}" "[{kind}-cancel-read-{index}]{func_name}" -fn wasmImport{name}{kind}CancelWrite(handle : Int) -> Int = "{module}" "[{kind}-cancel-write-{index}]{func_name}" -fn wasmImport{name}{kind}DropReadable(handle : Int) = "{module}" "[{kind}-drop-readable-{index}]{func_name}" -fn wasmImport{name}{kind}DropWritable(handle : Int) = "{module}" "[{kind}-drop-writable-{index}]{func_name}" -fn wasm{name}{kind}Deallocate(ptr: Int) -> Unit {{ - {dealloc_list} -}} -fn wasm{name}{kind}Malloc(length: Int) -> Int {{ +{lift_intrinsics} +{drop_readable_intrinsic} +{lower_intrinsics} + +fn wasm{symbol_name}Malloc(length: Int) -> Int {{ {malloc} ptr }} -fn {table_name}() -> {ffi}{camel_kind}VTable[{result}] {{ - {ffi}{camel_kind}VTable::new( - wasmImport{name}{kind}New, - wasmImport{name}{kind}Read, - wasmImport{name}{kind}Write, - wasmImport{name}{kind}CancelRead, - wasmImport{name}{kind}CancelWrite, - wasmImport{name}{kind}DropReadable, - wasmImport{name}{kind}DropWritable, - wasm{name}{kind}Malloc, - wasm{name}{kind}Deallocate, - wasm{name}{kind}{payload_lift_func}Lift, - wasm{name}{kind}{payload_lift_func}Lower, - ) +{commit_func} + +fn wasm{symbol_name}Reject( + ptr: Int, + start: Int, + length: Int, +) -> Unit {{ + {reject} }} + {lift_func} {lower_func} -"# +{list_lift_func} +{bridge_func} +"#, ); } - fn deallocate_lists( + fn deallocate_lists_and_own( &mut self, types: &[Type], operands: &[String], indirect: bool, - _module: &str, + package: &str, + endpoint_sites: Vec, ) -> String { - let mut f = FunctionBindgen::new(self, Box::new([])); - abi::deallocate_lists_in_types(f.interface_gen.resolve, types, operands, indirect, &mut f); - f.src + self.deallocate_lists_and_own_with_state(types, operands, indirect, package, endpoint_sites) + .0 } - fn lift_from_memory(&mut self, address: &str, ty: &Type, _module: &str) -> (String, String) { - let mut f = FunctionBindgen::new(self, Box::new([])); + fn deallocate_lists_and_own_with_state( + &mut self, + types: &[Type], + operands: &[String], + indirect: bool, + package: &str, + endpoint_sites: Vec, + ) -> (String, AsyncFunctionState) { + let mut f = FunctionBindgen::new(self, Box::new([])) + .with_type_context(package) + .with_async_state(AsyncFunctionState::from_sites(endpoint_sites)) + .with_sync_endpoint_drop(); + abi::deallocate_lists_and_own_in_types( + f.interface_gen.resolve, + types, + operands, + indirect, + &mut f, + ); + (f.src, f.async_state) + } + + fn commit_lists_and_endpoints_with_state( + &mut self, + types: &[Type], + operands: &[String], + indirect: bool, + package: &str, + endpoint_sites: Vec, + ) -> (String, AsyncFunctionState) { + let mut f = FunctionBindgen::new(self, Box::new([])) + .with_type_context(package) + .with_async_state(AsyncFunctionState::from_sites(endpoint_sites)) + .with_endpoint_commit(); + abi::deallocate_lists_and_own_in_types( + f.interface_gen.resolve, + types, + operands, + indirect, + &mut f, + ); + (f.src, f.async_state) + } + + fn lift_from_memory( + &mut self, + address: &str, + ty: &Type, + package: &str, + async_state: &mut AsyncFunctionState, + ) -> (String, String) { + let mut f = FunctionBindgen::new(self, Box::new([])) + .with_type_context(package) + .with_async_state(mem::take(async_state)); let result = abi::lift_from_memory(f.interface_gen.resolve, &mut f, address.into(), ty); + *async_state = f.async_state; (f.src, result) } - fn lower_to_memory(&mut self, address: &str, value: &str, ty: &Type, _module: &str) -> String { - let mut f = FunctionBindgen::new(self, Box::new([])); + fn lower_to_memory( + &mut self, + address: &str, + value: &str, + ty: &Type, + package: &str, + async_state: &mut AsyncFunctionState, + ) -> String { + let mut f = FunctionBindgen::new(self, Box::new([])) + .with_type_context(package) + .with_async_state(mem::take(async_state)) + .without_block_cleanup(); abi::lower_to_memory( f.interface_gen.resolve, &mut f, @@ -511,16 +2241,15 @@ fn {table_name}() -> {ffi}{camel_kind}VTable[{result}] {{ value.into(), ty, ); + *async_state = f.async_state; f.src } - fn malloc_memory(&mut self, address: &str, length: &str, ty: &Type) -> String { + fn malloc_memory(&mut self, address: &str, length: &str, ty: &Type, package: &str) -> String { let size = self.world_gen.sizes.size(ty).size_wasm32(); - let ffi = self - .world_gen - .pkg_resolver - .qualify_package(self.name, FFI_DIR); - format!("let {address} = {ffi}malloc({size} * {length});") + let _ = package; + self.ffi_imports.insert(ffi::MALLOC); + format!("let {address} = mbt_ffi_malloc({size} * {length});") } fn is_list_canonical(&self, _resolve: &Resolve, element: &Type) -> bool { @@ -536,26 +2265,26 @@ fn {table_name}() -> {ffi}{camel_kind}VTable[{result}] {{ length: &str, lift_func: &str, ty: &Type, + package: &str, ) -> String { - let ffi = self - .world_gen - .pkg_resolver - .qualify_package(self.name, FFI_DIR); + let _ = package; if self.is_list_canonical(self.resolve, ty) { if ty == &Type::U8 { - return format!("{ffi}ptr2bytes({address}, {length})"); + self.ffi_imports.insert(ffi::PTR2BYTES); + return format!("mbt_ffi_ptr2bytes({address}, {length})"); } - let ty = match ty { - Type::U32 => "uint", - Type::U64 => "uint64", - Type::S32 => "int", - Type::S64 => "int64", - Type::F32 => "float", - Type::F64 => "double", + let (ty, builtin) = match ty { + Type::U32 => ("uint", ffi::PTR2UINT_ARRAY), + Type::U64 => ("uint64", ffi::PTR2UINT64_ARRAY), + Type::S32 => ("int", ffi::PTR2INT_ARRAY), + Type::S64 => ("int64", ffi::PTR2INT64_ARRAY), + Type::F32 => ("float", ffi::PTR2FLOAT_ARRAY), + Type::F64 => ("double", ffi::PTR2DOUBLE_ARRAY), _ => unreachable!(), }; - return format!("{ffi}ptr2{ty}_array({address}, {length})"); + self.ffi_imports.insert(builtin); + return format!("mbt_ffi_ptr2{ty}_array({address}, {length})"); } let size = self.world_gen.sizes.size(ty).size_wasm32(); format!( @@ -570,40 +2299,400 @@ fn {table_name}() -> {ffi}{camel_kind}VTable[{result}] {{ "# ) } +} - fn list_lower_to_memory(&mut self, lower_func: &str, value: &str, ty: &Type) -> String { - // Align the address, moonbit only supports wasm32 for now - let ffi = self - .world_gen - .pkg_resolver - .qualify_package(self.name, FFI_DIR); - if self.is_list_canonical(self.resolve, ty) { - if ty == &Type::U8 { - return format!("{ffi}bytes2ptr({value})"); +impl<'a, 'b> FunctionBindgen<'a, 'b> { + pub(super) fn with_type_context(mut self, type_context: &str) -> Self { + self.type_context = type_context.to_string(); + self + } + + pub(super) fn without_block_cleanup(mut self) -> Self { + self.suppress_block_cleanup = true; + self + } + + pub(super) fn with_sync_endpoint_drop(mut self) -> Self { + self.sync_endpoint_drop = true; + self + } + + pub(super) fn with_endpoint_commit(mut self) -> Self { + self.commit_endpoints = true; + self + } + + pub(super) fn with_async_state(mut self, async_state: AsyncFunctionState) -> Self { + self.async_state = async_state; + self + } + + pub(super) fn with_sync_import_commit(mut self, argument_types: Vec) -> Self { + self.sync_import_argument_types = Some(argument_types); + self + } + + pub(super) fn commit_sync_import_arguments( + &mut self, + sig: &WasmSignature, + operands: &[String], + ) { + let Some(argument_types) = self.sync_import_argument_types.clone() else { + return; + }; + if self.async_state.endpoint_sites.is_empty() { + return; + } + + let argument_count = if sig.indirect_params { + 1 + } else { + operands.len() - usize::from(sig.retptr) + }; + let argument_operands = operands[..argument_count].to_vec(); + let sites = self.async_state.endpoint_sites.clone(); + let previous_state = + mem::replace(&mut self.async_state, AsyncFunctionState::from_sites(sites)); + let previous_commit_endpoints = mem::replace(&mut self.commit_endpoints, true); + let previous_preserve_allocations = + mem::replace(&mut self.preserve_guest_allocations, true); + + abi::deallocate_lists_and_own_in_types( + self.interface_gen.resolve, + &argument_types, + &argument_operands, + sig.indirect_params, + self, + ); + + let commit_state = mem::replace(&mut self.async_state, previous_state); + self.commit_endpoints = previous_commit_endpoints; + self.preserve_guest_allocations = previous_preserve_allocations; + for (used, committed) in self + .async_state + .endpoint_uses + .iter_mut() + .zip(commit_state.endpoint_uses) + { + used.lift |= committed.lift; + used.lower |= committed.lower; + } + } + + pub(super) fn emit_async_call_interface( + &mut self, + func: &Function, + operands: &[String], + results: &mut Vec, + ) { + let name = self.interface_gen.world_gen.pkg_resolver.func_call( + &self.type_context, + func, + &self.func_interface, + ); + let task_return_name = format!("{}_task_return", func.name.to_moonbit_ident()); + + let mut args = operands.to_vec(); + if matches!(self.interface_gen.direction, Direction::Export) { + args.push(background_group_name(func)); + } + let args = args.join(", "); + let (return_param, return_value) = match func.result { + Some(ty) => { + let return_param = self.locals.tmp("return_result"); + let return_value = self.locals.tmp("return_value"); + let task_return_type = self + .interface_gen + .world_gen + .pkg_resolver + .type_name(&self.type_context, &ty); + results.push(return_value.clone()); + uwrite!( + self.src, + r#" + let {return_param}: Ref[{task_return_type}?] = Ref(None) + {return_param}.val = Some({name}({args})) + {task_return_name}({return_param}) + "#, + ); + (return_param, return_value) + } + None => { + uwrite!( + self.src, + r#" + {name}({args}) + {task_return_name}() + "#, + ); + (String::new(), String::new()) } + }; + assert!(matches!( + self.async_state.task_return, + AsyncTaskReturnState::None + )); + self.async_state.task_return = AsyncTaskReturnState::Generating { + prev_src: mem::take(&mut self.src), + prev_needs_cleanup_list: mem::replace(&mut self.needs_cleanup_list, false), + return_param, + return_value, + }; + } - let ty = match ty { - Type::U32 => "uint", - Type::U64 => "uint64", - Type::S32 => "int", - Type::S64 => "int64", - Type::F32 => "float", - Type::F64 => "double", + pub(super) fn emit_future_lift( + &mut self, + ty: TypeId, + operands: &[String], + results: &mut Vec, + ) { + let result = self.locals.tmp("result"); + let op = &operands[0]; + let site = self + .async_state + .next_site(PayloadFor::Future, ty, EndpointOperation::Lift); + let lift_name = format!("wasm{}FutureLift", site.symbol_name); + + if self.commit_endpoints { + let commit_name = format!("wasm{}FutureCommit", site.symbol_name); + uwriteln!(self.src, r#"{commit_name}({op});"#); + results.push("()".into()); + return; + } + + uwriteln!(self.src, r#"let {result} = {lift_name}({op});"#,); + + results.push(result); + } + + pub(super) fn emit_future_lower( + &mut self, + ty: TypeId, + operands: &[String], + results: &mut Vec, + ) { + let committed = matches!( + self.async_state.task_return, + AsyncTaskReturnState::Generating { .. } + ); + let operation = if committed { + EndpointOperation::LowerCommitted + } else { + EndpointOperation::Lower + }; + let site = self + .async_state + .next_site(PayloadFor::Future, ty, operation); + let suffix = if committed { + "FutureLowerCommitted" + } else { + "FutureLower" + }; + let lower_name = format!("wasm{}{suffix}", site.symbol_name); + let op = &operands[0]; + results.push(format!("{lower_name}({op})")); + } + + pub(super) fn capture_task_return(&mut self, params: &[WasmType], operands: &[String]) { + let (body, needs_cleanup_list, return_param, return_value) = + match &mut self.async_state.task_return { + AsyncTaskReturnState::Generating { + prev_src, + prev_needs_cleanup_list, + return_param, + return_value, + } => { + mem::swap(&mut self.src, prev_src); + let needs_cleanup_list = + mem::replace(&mut self.needs_cleanup_list, *prev_needs_cleanup_list); + ( + mem::take(prev_src), + needs_cleanup_list, + return_param.clone(), + return_value.clone(), + ) + } _ => unreachable!(), }; - return format!("{ffi}{ty}_array2ptr({value})"); + assert_eq!(params.len(), operands.len()); + self.async_state.task_return = AsyncTaskReturnState::Emitted { + body, + needs_cleanup_list, + params: params + .iter() + .zip(operands) + .map(|(a, b)| (*a, b.clone())) + .collect(), + return_param, + return_value, + }; + } + + pub(super) fn emit_stream_lower( + &mut self, + ty: TypeId, + operands: &[String], + results: &mut Vec, + ) { + let committed = matches!( + self.async_state.task_return, + AsyncTaskReturnState::Generating { .. } + ); + let operation = if committed { + EndpointOperation::LowerCommitted + } else { + EndpointOperation::Lower + }; + let site = self + .async_state + .next_site(PayloadFor::Stream, ty, operation); + let suffix = if committed { + "StreamLowerCommitted" + } else { + "StreamLower" + }; + let lower_name = format!("wasm{}{suffix}", site.symbol_name); + let op = &operands[0]; + results.push(format!("{lower_name}({op})")); + } + + pub(super) fn emit_stream_lift( + &mut self, + ty: TypeId, + operands: &[String], + results: &mut Vec, + ) { + let result = self.locals.tmp("result"); + let op = &operands[0]; + let site = self + .async_state + .next_site(PayloadFor::Stream, ty, EndpointOperation::Lift); + let lift_name = format!("wasm{}StreamLift", site.symbol_name); + + if self.commit_endpoints { + let commit_name = format!("wasm{}StreamCommit", site.symbol_name); + uwriteln!(self.src, r#"{commit_name}({op});"#); + results.push("()".into()); + return; } - let size = self.world_gen.sizes.size(ty).size_wasm32(); - format!( - r#" - let address = {ffi}malloc(({value}).length() * {size}); - for index = 0; index < ({value}).length(); index = index + 1 {{ - let ptr = (address) + (index * {size}); - let value = {value}[index]; - {lower_func}(value, ptr); - }} - address - "# - ) + + uwriteln!(self.src, r#"let {result} = {lift_name}({op});"#,); + + results.push(result); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use wit_bindgen_core::wit_parser::{Docs, FunctionKind, Stability, TypeDef, TypeOwner}; + + fn future_type(resolve: &mut Resolve, payload: Option) -> TypeId { + resolve.types.alloc(TypeDef { + name: None, + kind: TypeDefKind::Future(payload), + owner: TypeOwner::None, + docs: Docs::default(), + stability: Stability::Unknown, + span: Default::default(), + external_id: None, + }) + } + + fn test_function(params: Vec<(&str, Type)>, result: Option) -> Function { + Function { + name: "f".into(), + kind: FunctionKind::Freestanding, + params: params + .into_iter() + .map(|(name, ty)| Param { + name: name.to_string(), + ty, + span: Default::default(), + }) + .collect(), + result, + docs: Docs::default(), + stability: Stability::Unknown, + span: Default::default(), + external_id: None, + } + } + + #[test] + fn duplicate_future_sites_have_distinct_symbols() { + let mut resolve = Resolve::default(); + let future = future_type(&mut resolve, Some(Type::U32)); + let func = test_function( + vec![("a", Type::Id(future)), ("b", Type::Id(future))], + Some(Type::Id(future)), + ); + + let names = AsyncEndpointNames::import(&resolve, None, &func); + let sites = async_endpoint_sites(&resolve, &names, None, &func, false); + assert_eq!(sites.len(), 3); + assert_eq!(sites[0].ty, future); + assert_eq!(sites[1].ty, future); + assert_eq!(sites[2].ty, future); + assert_ne!(sites[0].symbol_name, sites[1].symbol_name); + assert_ne!(sites[1].symbol_name, sites[2].symbol_name); + + let first_symbol = sites[0].symbol_name.clone(); + let second_symbol = sites[1].symbol_name.clone(); + let third_symbol = sites[2].symbol_name.clone(); + let mut state = AsyncFunctionState::from_sites(sites); + + assert_eq!( + state + .next_site(PayloadFor::Future, future, EndpointOperation::Lower) + .symbol_name, + first_symbol + ); + assert_eq!( + state + .next_site(PayloadFor::Future, future, EndpointOperation::Lower) + .symbol_name, + second_symbol + ); + assert_eq!( + state + .next_site(PayloadFor::Future, future, EndpointOperation::Lower) + .symbol_name, + third_symbol + ); + } + + #[test] + fn nested_payload_sites_keep_their_original_indices() { + let mut resolve = Resolve::default(); + let inner = future_type(&mut resolve, Some(Type::U32)); + let outer = future_type(&mut resolve, Some(Type::Id(inner))); + let func = test_function(vec![("a", Type::Id(outer)), ("b", Type::Id(outer))], None); + + let names = AsyncEndpointNames::import(&resolve, None, &func); + let sites = async_endpoint_sites(&resolve, &names, None, &func, false); + assert_eq!( + sites.iter().map(|site| site.index).collect::>(), + vec![0, 1, 2, 3] + ); + assert_eq!( + sites.iter().map(|site| site.ty).collect::>(), + vec![inner, outer, inner, outer] + ); + + let plan = AsyncFunctionPlan::new(sites.clone()); + let first_outer_payload = plan.payload_sites(&sites[1]); + let second_outer_payload = plan.payload_sites(&sites[3]); + + assert_eq!(first_outer_payload[0].index, 0); + assert_eq!(second_outer_payload[0].index, 2); + + let mut state = AsyncFunctionState::from_sites(second_outer_payload); + assert_eq!( + state + .next_site(PayloadFor::Future, inner, EndpointOperation::Lift) + .symbol_name, + sites[2].symbol_name + ); } } diff --git a/crates/moonbit/src/ffi/async_primitive.mbt b/crates/moonbit/src/ffi/async_primitive.mbt index 3a15c1330..11cea7e12 100644 --- a/crates/moonbit/src/ffi/async_primitive.mbt +++ b/crates/moonbit/src/ffi/async_primitive.mbt @@ -78,7 +78,7 @@ pub fn Coroutine::cancel(self : Coroutine) -> Unit { } ///| -pub async fn pause() -> Unit { +async fn pause() -> Unit { guard scheduler.curr_coro is Some(coro) if coro.cancelled && not(coro.shielded) { raise Cancelled::Cancelled diff --git a/crates/moonbit/src/ffi/future.mbt b/crates/moonbit/src/ffi/future.mbt deleted file mode 100644 index b8b0c19a3..000000000 --- a/crates/moonbit/src/ffi/future.mbt +++ /dev/null @@ -1,540 +0,0 @@ -///| -pub struct FutureVTable[T] { - new : () -> UInt64 - read : (Int, Int) -> Int - write : (Int, Int) -> Int - cancel_read : (Int) -> Int - cancel_write : (Int) -> Int - drop_readable : (Int) -> Unit - drop_writable : (Int) -> Unit - malloc : (Int) -> Int - free : (Int) -> Unit - lift : (Int) -> T - lower : (T, Int) -> Unit -} - -///| -pub fn[T] FutureVTable::new( - new : () -> UInt64, - read : (Int, Int) -> Int, - write : (Int, Int) -> Int, - cancel_read : (Int) -> Int, - cancel_write : (Int) -> Int, - drop_readable : (Int) -> Unit, - drop_writable : (Int) -> Unit, - malloc : (Int) -> Int, - free : (Int) -> Unit, - lift : (Int) -> T, - lower : (T, Int) -> Unit, -) -> FutureVTable[T] { - { - new, - read, - write, - cancel_read, - cancel_write, - drop_readable, - drop_writable, - malloc, - free, - lift, - lower, - } -} - -///| -pub fn[T] new_future( - vtable : FutureVTable[T], -) -> (FutureReader[T], FutureWriter[T]) { - let handle = (vtable.new)() - let left_handle = handle.to_int() - let right_handle = (handle >> 32).to_int() - ( - FutureReader::new(left_handle, vtable), - FutureWriter::new(right_handle, vtable), - ) -} - -///| -pub struct FutureReader[T] { - handle : Int - vtable : FutureVTable[T] - mut code : Int? - mut dropped : Bool - memory_refs : Array[Int] -} - -///| -pub fn[T] FutureReader::new( - handle : Int, - vtable : FutureVTable[T], -) -> FutureReader[T] { - { handle, vtable, code: None, memory_refs: [], dropped: false } -} - -///| -pub impl[T] Waitable for FutureReader[T] with update(self, code~ : Int) -> Unit { - self.code = Some(code) -} - -///| -pub impl[T] Eq for FutureReader[T] with equal(self, other) -> Bool { - self.handle == other.handle -} - -///| -pub impl[T] Waitable for FutureReader[T] with handle(self) -> Int { - self.handle -} - -///| -pub impl[T] Waitable for FutureReader[T] with cancel(self) -> Unit { - if self.code is Some(code) && WaitableStatus::decode(code) is Cancelled(_) { - return - } - self.code = Some((self.vtable.cancel_read)(self.handle)) -} - -///| -pub impl[T] Waitable for FutureReader[T] with drop(self) -> Bool { - _async_debug("stream-reader-drop(\{self.handle})") - if self.dropped { - return false - } - (self.vtable.drop_readable)(self.handle) - self.dropped = true - for ptr in self.memory_refs { - self.free(ptr) - } - true -} - -///| -pub impl[T] Waitable for FutureReader[T] with done(self) -> Bool { - match self.code { - Some(c) => - match WaitableStatus::decode(c) { - Completed(_) | Dropped(_) | Cancelled(_) => true - Blocking => false - } - None => false - } -} - -///| -pub fn[T] FutureReader::malloc(self : FutureReader[T]) -> Int { - let ptr = (self.vtable.malloc)(1) - ptr -} - -///| -pub fn[T] FutureReader::free(self : FutureReader[T], ptr : Int) -> Unit { - (self.vtable.free)(ptr) -} - -///| -pub fn[T] FutureReader::lift(self : FutureReader[T], ptr : Int) -> T { - let res = (self.vtable.lift)(ptr) - res -} - -///| -pub fn[T] FutureReader::lower_read(self : FutureReader[T], ptr : Int) -> Int { - (self.vtable.read)(self.handle, ptr) -} - -///| -pub async fn[T] FutureReader::read(self : FutureReader[T]) -> T { - let buf_ptr = self.malloc() - self.memory_refs.push(buf_ptr) - self.code = Some(self.lower_read(buf_ptr)) - _async_debug("future-read(\{self.handle}) -> \{self.code.unwrap()}") - // register this waitable to the current task - let task = current_task() - task.add_waitable(self, current_coroutine()) - defer task.remove_waitable(self) - - // wait until ready - for { - let status = WaitableStatus::decode(self.code.unwrap()) - match status { - Cancelled(_) | Dropped(_) => raise Cancelled::Cancelled - Completed(_) => break - Blocking => suspend() - } - } - // when receive event, continue this coroutine - let value = self.lift(buf_ptr) - return value -} - -///| -pub struct FutureWriter[T] { - handle : Int - vtable : FutureVTable[T] - mut code : Int? - mut dropped : Bool - memory_refs : Array[Int] -} - -///| -pub fn[T] FutureWriter::new( - handle : Int, - vtable : FutureVTable[T], -) -> FutureWriter[T] { - { handle, vtable, code: None, memory_refs: [], dropped: false } -} - -///| -pub impl[T] Waitable for FutureWriter[T] with update(self, code~ : Int) -> Unit { - self.code = Some(code) -} - -///| -pub impl[T] Eq for FutureWriter[T] with equal(self, other) -> Bool { - self.handle == other.handle -} - -///| -pub impl[T] Waitable for FutureWriter[T] with handle(self) -> Int { - self.handle -} - -///| -pub impl[T] Waitable for FutureWriter[T] with cancel(self) -> Unit { - if self.code is Some(code) && WaitableStatus::decode(code) is Cancelled(_) { - return - } - self.code = Some((self.vtable.cancel_write)(self.handle)) -} - -///| -pub impl[T] Waitable for FutureWriter[T] with drop(self) -> Bool { - _async_debug("stream-writer-drop(\{self.handle})") - if self.dropped { - return false - } - (self.vtable.drop_writable)(self.handle) - self.dropped = true - for ptr in self.memory_refs { - self.free(ptr) - } - true -} - -///| -pub impl[T] Waitable for FutureWriter[T] with done(self) -> Bool { - match self.code { - Some(c) => - match WaitableStatus::decode(c) { - Completed(_) | Dropped(_) | Cancelled(_) => true - Blocking => false - } - None => false - } -} - -///| -pub fn[T] FutureWriter::malloc(self : FutureWriter[T]) -> Int { - (self.vtable.malloc)(1) -} - -///| -pub fn[T] FutureWriter::free(self : FutureWriter[T], ptr : Int) -> Unit { - (self.vtable.free)(ptr) -} - -///| -pub fn[T] FutureWriter::lower( - self : FutureWriter[T], - value : T, - ptr : Int, -) -> Unit { - (self.vtable.lower)(value, ptr) -} - -///| -pub fn[T] FutureWriter::lower_write(self : FutureWriter[T], ptr : Int) -> Int { - (self.vtable.write)(self.handle, ptr) -} - -///| -pub async fn[T] FutureWriter::write(self : FutureWriter[T], value : T) -> Unit { - // register this waitable to the current task - let task = current_task() - task.add_waitable(self, current_coroutine()) - defer task.remove_waitable(self) - let buf_ptr = self.malloc() - self.memory_refs.push(buf_ptr) - self.lower(value, buf_ptr) - self.code = Some(self.lower_write(buf_ptr)) - defer self.free(buf_ptr) - - // wait until ready - for { - let status = WaitableStatus::decode(self.code.unwrap()) - match status { - Cancelled(_) | Dropped(_) => raise Cancelled::Cancelled - Completed(_) => break - Blocking => suspend() - } - } - // when receive event, continue this coroutine - return -} - -///| -pub suberror StreamCancelled (Int, Cancelled) derive(Show) - -///| -pub struct StreamVTable[T] { - new : () -> UInt64 - read : (Int, Int, Int) -> Int - write : (Int, Int, Int) -> Int - cancel_read : (Int) -> Int - cancel_write : (Int) -> Int - drop_readable : (Int) -> Unit - drop_writable : (Int) -> Unit - malloc : (Int) -> Int - free : (Int) -> Unit - lift : (Int, Int) -> FixedArray[T] - lower : (FixedArray[T]) -> Int -} - -///| -pub fn[T] StreamVTable::new( - new : () -> UInt64, - read : (Int, Int, Int) -> Int, - write : (Int, Int, Int) -> Int, - cancel_read : (Int) -> Int, - cancel_write : (Int) -> Int, - drop_readable : (Int) -> Unit, - drop_writable : (Int) -> Unit, - malloc : (Int) -> Int, - free : (Int) -> Unit, - lift : (Int, Int) -> FixedArray[T], - lower : (FixedArray[T]) -> Int, -) -> StreamVTable[T] { - { - new, - read, - write, - cancel_read, - cancel_write, - drop_readable, - drop_writable, - malloc, - free, - lift, - lower, - } -} - -///| -pub fn[T] new_stream( - vtable : StreamVTable[T], -) -> (StreamReader[T], StreamWriter[T]) { - let handle = (vtable.new)() - let left_handle = handle.to_int() - let right_handle = (handle >> 32).to_int() - ( - StreamReader::new(left_handle, vtable), - StreamWriter::new(right_handle, vtable), - ) -} - -///| -pub struct StreamReader[T] { - handle : Int - vtable : StreamVTable[T] - mut code : Int? - mut dropped : Bool - memory_refs : Array[Int] -} - -///| -pub impl[T] Waitable for StreamReader[T] with update(self, code~ : Int) -> Unit { - self.code = Some(code) -} - -///| -pub impl[T] Eq for StreamReader[T] with equal(self, other) -> Bool { - self.handle == other.handle -} - -///| -pub impl[T] Waitable for StreamReader[T] with handle(self) -> Int { - self.handle -} - -///| -pub impl[T] Waitable for StreamReader[T] with cancel(self) -> Unit { - if self.code is Some(code) && WaitableStatus::decode(code) is Cancelled(_) { - return - } - self.code = Some((self.vtable.cancel_read)(self.handle)) -} - -///| -pub impl[T] Waitable for StreamReader[T] with drop(self) -> Bool { - _async_debug("stream-reader-drop(\{self.handle})") - if self.dropped { - return false - } - (self.vtable.drop_readable)(self.handle) - self.dropped = true - for ptr in self.memory_refs { - (self.vtable.free)(ptr) - } - true -} - -///| -pub impl[T] Waitable for StreamReader[T] with done(self) -> Bool { - match self.code { - Some(c) => - match WaitableStatus::decode(c) { - Completed(_) | Dropped(_) | Cancelled(_) => true - Blocking => false - } - None => false - } -} - -///| -pub fn[T] StreamReader::new( - handle : Int, - vtable : StreamVTable[T], -) -> StreamReader[T] { - { handle, vtable, code: None, memory_refs: [], dropped: false } -} - -///| -pub async fn[T] StreamReader::read( - self : StreamReader[T], - buffer : FixedArray[T], - offset? : Int = 0, - length : Int, -) -> Int { - // register this waitable to the current task - let task = current_task() - task.add_waitable(self, current_coroutine()) - defer task.remove_waitable(self) - let buf_ptr = (self.vtable.malloc)(length) - self.code = Some((self.vtable.read)(self.handle, buf_ptr, length)) - _async_debug("stream-read(\{self.handle}) -> \{self.code.unwrap()}") - for { - let status = WaitableStatus::decode(self.code.unwrap()) - match status { - Completed(n) => { - let read_result = (self.vtable.lift)(buf_ptr, n) - for i in 0.. { - let read_result = (self.vtable.lift)(buf_ptr, n) - for i in 0.. suspend() - } - } -} - -///| -pub struct StreamWriter[T] { - handle : Int - vtable : StreamVTable[T] - mut code : Int? - mut dropped : Bool - memory_refs : Array[Int] -} - -///| -pub impl[T] Waitable for StreamWriter[T] with update(self, code~ : Int) -> Unit { - self.code = Some(code) -} - -///| -pub impl[T] Eq for StreamWriter[T] with equal(self, other) -> Bool { - self.handle == other.handle -} - -///| -pub impl[T] Waitable for StreamWriter[T] with handle(self) -> Int { - self.handle -} - -///| -pub impl[T] Waitable for StreamWriter[T] with cancel(self) -> Unit { - if self.code is Some(code) && WaitableStatus::decode(code) is Cancelled(_) { - return - } - self.code = Some((self.vtable.cancel_write)(self.handle)) -} - -///| -pub impl[T] Waitable for StreamWriter[T] with drop(self) -> Bool { - _async_debug("stream-writer-drop(\{self.handle})") - let task = current_task() - let coro = task.children.get(self.handle) - if coro is Some((_, coro)) { - coro.cancel() - coro.wake() - } - if self.dropped { - return false - } - (self.vtable.drop_writable)(self.handle) - self.dropped = true - for ptr in self.memory_refs { - (self.vtable.free)(ptr) - } - true -} - -///| -pub impl[T] Waitable for StreamWriter[T] with done(self) -> Bool { - match self.code { - Some(c) => - match WaitableStatus::decode(c) { - Completed(_) | Dropped(_) | Cancelled(_) => true - Blocking => false - } - None => false - } -} - -///| -pub fn[T] StreamWriter::new( - handle : Int, - vtable : StreamVTable[T], -) -> StreamWriter[T] { - { handle, vtable, code: None, memory_refs: [], dropped: false } -} - -///| -pub async fn[T] StreamWriter::write( - self : StreamWriter[T], - buffer : FixedArray[T], -) -> Int { - // register this waitable to the current task - let task = current_task() - task.add_waitable(self, current_coroutine()) - defer task.remove_waitable(self) - let write_buf = (self.vtable.lower)(buffer) - self.code = Some((self.vtable.write)(self.handle, write_buf, buffer.length())) - for { - let status = WaitableStatus::decode(self.code.unwrap()) - match status { - Completed(n) => return n - Cancelled(n) | Dropped(n) => - raise StreamCancelled::StreamCancelled((n, Cancelled::Cancelled)) - Blocking => suspend() - } - } -} diff --git a/crates/moonbit/src/lib.rs b/crates/moonbit/src/lib.rs index 7939d6146..99189d9f8 100644 --- a/crates/moonbit/src/lib.rs +++ b/crates/moonbit/src/lib.rs @@ -1,6 +1,6 @@ use anyhow::Result; use core::panic; -use heck::{ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase}; +use heck::{ToShoutySnakeCase, ToUpperCamelCase}; use std::{ collections::{HashMap, HashSet}, fmt::Write, @@ -15,12 +15,14 @@ use wit_bindgen_core::{ wit_parser::{ Alignment, ArchitectureSize, Docs, Enum, Flags, FlagsRepr, Function, Int, InterfaceId, LiftLowerAbi, ManglingAndAbi, Param, Record, Resolve, ResourceIntrinsic, Result_, - SizeAlign, Tuple, Type, TypeId, Variant, WasmExport, WasmExportKind, WasmImport, WorldId, - WorldKey, + SizeAlign, Tuple, Type, TypeDefKind, TypeId, Variant, WasmExport, WasmExportKind, + WasmImport, WorldId, WorldKey, }, }; -use crate::async_support::AsyncSupport; +use wit_bindgen_core::wit_parser::WorldItem; + +use crate::async_support::{AsyncFunctionState, AsyncSupport}; use crate::pkg::{Imports, MoonbitSignature, PkgResolver, ToMoonBitIdent, ToMoonBitTypeIdent}; mod async_support; @@ -39,10 +41,6 @@ mod pkg; // We use AsyncCallback ABI for async functions // TODO: Export will share the type signatures with the import by using a newtype alias -pub(crate) const FFI_DIR: &str = "ffi"; - -pub(crate) const FFI: &str = include_str!("./ffi/ffi.mbt"); - const VERSION: &str = env!("CARGO_PKG_VERSION"); #[derive(Default, Debug, Clone)] @@ -115,11 +113,6 @@ impl InterfaceFragment { } } -enum PayloadFor { - Future, - Stream, -} - #[derive(Default)] pub struct MoonBit { opts: Opts, @@ -245,6 +238,15 @@ impl MoonBit { /// final export FFI module. Async helpers are emitted when required. impl WorldGenerator for MoonBit { fn preprocess(&mut self, resolve: &Resolve, world: WorldId) -> Result<()> { + if world_contains_endpoint_fixed_length_list_combination(resolve, world) { + anyhow::bail!( + "MoonBit async bindings do not yet support combining future or stream types with fixed-length lists" + ); + } + if world_contains_future_or_stream(resolve, world) { + self.async_support.require_runtime(); + } + self.pkg_resolver.resolve = resolve.clone(); self.project_name = self .opts @@ -518,7 +520,7 @@ impl WorldGenerator for MoonBit { fn finish(&mut self, _resolve: &Resolve, _id: WorldId, files: &mut Files) -> Result<()> { // If async is used, export async utils - self.async_support.emit_utils(files, VERSION); + self.async_support.emit_runtime_files(files, VERSION); // Export project files if !self.opts.ignore_stub && !self.opts.ignore_module_file { @@ -590,100 +592,82 @@ impl InterfaceGenerator<'_> { } fn import(&mut self, func: &Function) { - // Determine if the function is async - let async_ = self - .world_gen - .opts - .async_ - .is_async(self.resolve, self.interface, func, false); - if async_ { - self.world_gen.async_support.mark_async(); - } - - let ffi_import_name = format!("wasmImport{}", func.name.to_upper_camel_case()); - let mut bindgen = FunctionBindgen::new( - self, - func.params - .iter() - .map(|Param { name, .. }| name.to_moonbit_ident()) - .collect(), - ); - - abi::call( - bindgen.interface_gen.resolve, - AbiVariant::GuestImport, - LiftLower::LowerArgsLiftResults, + let async_plan = self.world_gen.async_support.import_plan( + &mut self.world_gen.opts.async_, + self.resolve, + self.interface, func, - &mut bindgen, - false, ); + let variant = async_plan.abi_variant(); + let endpoint_plan = self.import_async_function_plan(self.interface, func); + let wasm_sig = self.resolve.wasm_signature(variant, func); + let mbt_sig = self.world_gen.pkg_resolver.mbt_sig(self.name, func, false); + let (src, needs_cleanup_list, endpoint_state) = if async_plan.is_async() { + let body = self.generate_async_import_body(&endpoint_plan, func, &mbt_sig, &wasm_sig); + (body.src, body.needs_cleanup_list, body.state) + } else { + let mut bindgen = FunctionBindgen::new( + self, + func.params + .iter() + .map(|Param { name, .. }| name.to_moonbit_ident()) + .collect(), + ) + .with_async_state(endpoint_plan.state()); + if endpoint_plan.has_endpoints() { + bindgen = bindgen.with_sync_import_commit( + func.params.iter().map(|Param { ty, .. }| *ty).collect(), + ); + } - let mut src = bindgen.src.clone(); + abi::call( + bindgen.interface_gen.resolve, + AbiVariant::GuestImport, + LiftLower::LowerArgsLiftResults, + func, + &mut bindgen, + false, + ); + (bindgen.src, bindgen.needs_cleanup_list, bindgen.async_state) + }; - let cleanup_list = if bindgen.needs_cleanup_list { + let cleanup_list = if needs_cleanup_list { "let cleanup_list : Array[Int] = []" } else { "" }; - let mbt_sig = self.world_gen.pkg_resolver.mbt_sig(self.name, func, false); - let sig = self.sig_string(&mbt_sig, async_); - - // Generate the core wasm abi - let wasm_sig = self.resolve.wasm_signature( - if async_ { - AbiVariant::GuestImportAsync - } else { - AbiVariant::GuestImport - }, - func, - ); let (import_module, import_name) = self.resolve.wasm_import_name( - ManglingAndAbi::Legacy(if async_ { - LiftLowerAbi::AsyncCallback - } else { - LiftLowerAbi::Sync - }), + async_plan.mangling_and_abi(), WasmImport::Func { interface: self.interface, func, }, ); - { - let result_type = match &wasm_sig.results[..] { - [] => "".into(), - [result] => format!("-> {}", wasm_type(*result)), - _ => unimplemented!("multi-value results are not supported yet"), - }; - - let params = wasm_sig - .params - .iter() - .enumerate() - .map(|(i, param)| format!("p{i} : {}", wasm_type(*param))) - .collect::>() - .join(", "); - - uwriteln!( - self.ffi, - r#" - fn {ffi_import_name}({params}) {result_type} = "{import_module}" "{import_name}" - "# - ); - } + let result_type = match &wasm_sig.results[..] { + [] => "".into(), + [result] => format!("-> {}", wasm_type(*result)), + _ => unimplemented!("multi-value results are not supported yet"), + }; + let params = wasm_sig + .params + .iter() + .enumerate() + .map(|(i, param)| format!("p{i} : {}", wasm_type(*param))) + .collect::>() + .join(", "); + let ffi_import_name = format!("wasmImport{}", func.name.to_upper_camel_case()); + uwriteln!( + self.ffi, + r#" + fn {ffi_import_name}({params}) {result_type} = "{import_module}" "{import_name}" + "# + ); - // Generate the MoonBit wrapper - if async_ { - let interface_name = match self.interface { - Some(key) => self.resolve.name_world_key(key), - None => "$root".into(), - }; - self.generation_futures_and_streams_import("", func, &interface_name); - src = self.generate_async_import_function(func, mbt_sig, &wasm_sig); - } + self.emit_future_stream_helpers(&endpoint_plan, &endpoint_state); print_docs(&mut self.src, &func.docs); - + let sig = self.sig_string(func, &mbt_sig, async_plan.signature_is_async()); uwrite!( self.src, r#" @@ -696,43 +680,31 @@ impl InterfaceGenerator<'_> { } fn export(&mut self, func: &Function) { - // Determine if is async - let async_ = self - .world_gen - .opts - .async_ - .is_async(self.resolve, self.interface, func, false); - if async_ { - self.world_gen.async_support.mark_async(); - } - - // Generate declarations for user implementations. - { - let mbt_sig = self.world_gen.pkg_resolver.mbt_sig(self.name, func, false); - let func_sig = self.sig_string(&mbt_sig, async_); - - print_docs(&mut self.src, &func.docs); - uwrite!( - self.src, - r#" - declare {func_sig} - "# - ); - } - - // Generate the caller function - let variant = if async_ { - AbiVariant::GuestExportAsync - } else { - AbiVariant::GuestExport - }; - + let async_plan = self.world_gen.async_support.export_plan( + &mut self.world_gen.opts.async_, + self.resolve, + self.interface, + func, + ); + let variant = async_plan.abi_variant(); let sig = self.resolve.wasm_signature(variant, func); + let mbt_sig = self.world_gen.pkg_resolver.mbt_sig(self.name, func, false); + let func_sig = self.sig_string(func, &mbt_sig, async_plan.signature_is_async()); + + print_docs(&mut self.src, &func.docs); + uwrite!( + self.src, + r#" + declare {func_sig} + "# + ); + let endpoint_plan = self.export_async_function_plan(self.interface, func); let mut bindgen = FunctionBindgen::new( self, (0..sig.params.len()).map(|i| format!("p{i}")).collect(), - ); + ) + .with_async_state(endpoint_plan.state()); abi::call( bindgen.interface_gen.resolve, @@ -740,15 +712,15 @@ impl InterfaceGenerator<'_> { LiftLower::LiftArgsLowerResults, func, &mut bindgen, - async_, + async_plan.is_async(), ); - // TODO: adapt async cleanup - assert!(!bindgen.needs_cleanup_list); - - // Async functions deferred task return - let deferred_task_return = bindgen.deferred_task_return.clone(); - + let cleanup_list = if bindgen.needs_cleanup_list { + "let cleanup_list : Array[Int] = []" + } else { + "" + }; + let async_state = bindgen.async_state.clone(); let src = bindgen.src; let result_type = match &sig.results[..] { @@ -775,36 +747,36 @@ impl InterfaceGenerator<'_> { .collect::>() .join(", "); - // Async functions return type - let interface_name = match self.interface { - Some(key) => Some(self.resolve.name_world_key(key)), - None => None, - }; - - let module_name = interface_name.as_deref().unwrap_or("$root"); - self.r#generation_futures_and_streams_import("[export]", func, module_name); - - uwrite!( - self.ffi, - r#" - #doc(hidden) - pub fn {func_name}({params}) -> {result_type} {{ - {src} - }} - "#, - ); let export_name = self.resolve.wasm_export_name( - ManglingAndAbi::Legacy(if async_ { - LiftLowerAbi::AsyncCallback - } else { - LiftLowerAbi::Sync - }), + async_plan.mangling_and_abi(), WasmExport::Func { interface: self.interface, func, kind: WasmExportKind::Normal, }, ); + self.emit_future_stream_helpers(&endpoint_plan, &async_state); + + if !self.emit_async_export_wrapper( + &async_plan, + func, + &func_name, + ¶ms, + result_type, + cleanup_list, + &src, + ) { + uwrite!( + self.ffi, + r#" + #doc(hidden) + pub fn {func_name}({params}) -> {result_type} {{ + {cleanup_list} + {src} + }} + "#, + ); + } let export = format!( r#" @@ -826,104 +798,14 @@ impl InterfaceGenerator<'_> { .export .insert(export_name, (func_name, export)); - // If async, we also need a callback function and a task_return intrinsic - if async_ { - let export_func_name = self - .world_gen - .export_ns - .tmp(&format!("wasmExportAsync{camel_name}")); - let DeferredTaskReturn::Emitted { - body: task_return_body, - params: task_return_params, - return_param, - } = deferred_task_return - else { - unreachable!() - }; - let export_name = self.resolve.wasm_export_name( - ManglingAndAbi::Legacy(LiftLowerAbi::AsyncCallback), - WasmExport::Func { - interface: self.interface, - func, - kind: WasmExportKind::Callback, - }, - ); - - let task_return_param_tys = task_return_params - .iter() - .enumerate() - .map(|(idx, (ty, _expr))| format!("p{}: {}", idx, wasm_type(*ty))) - .collect::>() - .join(", "); - let task_return_param_exprs = task_return_params - .iter() - .map(|(_ty, expr)| expr.as_str()) - .collect::>() - .join(", "); - let return_ty = match &func.result { - Some(result) => self - .world_gen - .pkg_resolver - .type_name(self.name, result) - .to_string(), - None => "Unit".into(), - }; - let return_expr = match return_ty.as_str() { - "Unit" => "".into(), - _ => format!("{return_param}: {return_ty}",), - }; - let snake_func_name = func.name.to_moonbit_ident().to_string(); - let ffi = self - .world_gen - .pkg_resolver - .qualify_package(self.name, FFI_DIR); - - let (task_return_module, task_return_name) = self.resolve.wasm_import_name( - ManglingAndAbi::Legacy(LiftLowerAbi::AsyncCallback), - WasmImport::Func { - interface: self.interface, - func, - }, - ); - - uwriteln!( - self.src, - r#" - fn {export_func_name}TaskReturn({task_return_param_tys}) = "{task_return_module}" "{task_return_name}" - - pub fn {snake_func_name}_task_return({return_expr}) -> Unit {{ - {task_return_body} - {export_func_name}TaskReturn({task_return_param_exprs}) - }} - "# - ); - - uwriteln!( - self.ffi, - r#" - pub fn {export_func_name}(event_raw: Int, waitable: Int, code: Int) -> Int {{ - {ffi}callback(event_raw, waitable, code) - }} - "# - ); - let export = format!( - r#" - pub fn {export_func_name}(event_raw: Int, waitable: Int, code: Int) -> Int {{ - {}{snake_func_name}_callback(event_raw, waitable, code) - }} - "#, - self.world_gen - .pkg_resolver - .qualify_package(self.world_gen.opts.gen_dir.as_str(), self.name), - ); - - self.world_gen - .export - .insert(export_name, (export_func_name.clone(), export)); - } - - // If post return is needed, generate it - if abi::guest_export_needs_post_return(self.resolve, func) { + if !self.emit_async_export_callback( + &async_plan, + self.interface, + func, + &camel_name, + async_state, + ) && abi::guest_export_needs_post_return(self.resolve, func) + { let params = sig .results .iter() @@ -987,8 +869,8 @@ impl InterfaceGenerator<'_> { } } - fn sig_string(&mut self, sig: &MoonbitSignature, async_: bool) -> String { - let params = sig + fn sig_string(&mut self, func: &Function, sig: &MoonbitSignature, async_: bool) -> String { + let mut params = sig .params .iter() .map(|(name, ty)| { @@ -997,6 +879,8 @@ impl InterfaceGenerator<'_> { }) .collect::>(); + self.add_async_export_stub_parameter(func, async_, &mut params); + let params = params.join(", "); let result_type = match &sig.result_type { None => "Unit".into(), @@ -1034,25 +918,34 @@ impl<'a> wit_bindgen_core::InterfaceGenerator<'a> for InterfaceGenerator<'a> { .collect::>() .join("; "); + let contains_endpoint = record + .fields + .iter() + .any(|field| type_contains_future_or_stream(self.resolve, &field.ty)); let mut deriviation: Vec<_> = Vec::new(); - if self.derive_opts.derive_debug { + if self.derive_opts.derive_debug && !contains_endpoint { deriviation.push("Debug") } - if self.derive_opts.derive_show { + if self.derive_opts.derive_show && !contains_endpoint { deriviation.push("Show") } - if self.derive_opts.derive_eq { + if self.derive_opts.derive_eq && !contains_endpoint { deriviation.push("Eq") } + let derivation = if deriviation.is_empty() { + String::new() + } else { + format!(" derive({})", deriviation.join(", ")) + }; + uwrite!( self.src, " pub(all) struct {name} {{ {parameters} - }} derive({}) - ", - deriviation.join(", ") + }}{derivation} + " ); } @@ -1326,14 +1219,19 @@ impl<'a> wit_bindgen_core::InterfaceGenerator<'a> for InterfaceGenerator<'a> { .collect::>() .join("\n "); + let contains_endpoint = variant + .cases + .iter() + .filter_map(|case| case.ty.as_ref()) + .any(|ty| type_contains_future_or_stream(self.resolve, ty)); let mut deriviation: Vec<_> = Vec::new(); - if self.derive_opts.derive_debug { + if self.derive_opts.derive_debug && !contains_endpoint { deriviation.push("Debug") } - if self.derive_opts.derive_show { + if self.derive_opts.derive_show && !contains_endpoint { deriviation.push("Show") } - if self.derive_opts.derive_eq { + if self.derive_opts.derive_eq && !contains_endpoint { deriviation.push("Eq") } let declaration = if self.derive_opts.derive_error && name.contains("Error") { @@ -1342,14 +1240,19 @@ impl<'a> wit_bindgen_core::InterfaceGenerator<'a> for InterfaceGenerator<'a> { "enum" }; + let derivation = if deriviation.is_empty() { + String::new() + } else { + format!(" derive({})", deriviation.join(", ")) + }; + uwrite!( self.src, " pub(all) {declaration} {name} {{ {cases} - }} derive({}) - ", - deriviation.join(", ") + }}{derivation} + " ); } @@ -1464,11 +1367,11 @@ impl<'a> wit_bindgen_core::InterfaceGenerator<'a> for InterfaceGenerator<'a> { } fn type_future(&mut self, _id: TypeId, _name: &str, _ty: &Option, _docs: &Docs) { - unimplemented!() // Not needed + // Rendered inline by `PkgResolver::type_name`. } fn type_stream(&mut self, _id: TypeId, _name: &str, _ty: &Option, _docs: &Docs) { - unimplemented!() // Not needed + // Rendered inline by `PkgResolver::type_name`. } fn type_builtin(&mut self, _id: TypeId, _name: &str, _ty: &Type, _docs: &Docs) { @@ -1490,22 +1393,10 @@ struct BlockStorage { cleanup: Vec, } -#[derive(Clone, Debug)] -enum DeferredTaskReturn { - None, - Generating { - prev_src: String, - return_param: String, - }, - Emitted { - params: Vec<(WasmType, String)>, - body: String, - return_param: String, - }, -} - struct FunctionBindgen<'a, 'b> { interface_gen: &'b mut InterfaceGenerator<'a>, + type_context: String, + func_interface: String, params: Box<[String]>, src: String, locals: Ns, @@ -1514,7 +1405,12 @@ struct FunctionBindgen<'a, 'b> { payloads: Vec, cleanup: Vec, needs_cleanup_list: bool, - deferred_task_return: DeferredTaskReturn, + suppress_block_cleanup: bool, + preserve_guest_allocations: bool, + sync_endpoint_drop: bool, + commit_endpoints: bool, + sync_import_argument_types: Option>, + async_state: AsyncFunctionState, } impl<'a, 'b> FunctionBindgen<'a, 'b> { @@ -1526,8 +1422,11 @@ impl<'a, 'b> FunctionBindgen<'a, 'b> { params.iter().for_each(|str| { locals.tmp(str); }); + let type_context = r#gen.name.to_string(); Self { interface_gen: r#gen, + func_interface: type_context.clone(), + type_context, params, src: String::new(), locals, @@ -1536,7 +1435,12 @@ impl<'a, 'b> FunctionBindgen<'a, 'b> { payloads: Vec::new(), cleanup: Vec::new(), needs_cleanup_list: false, - deferred_task_return: DeferredTaskReturn::None, + suppress_block_cleanup: false, + preserve_guest_allocations: false, + sync_endpoint_drop: false, + commit_endpoints: false, + sync_import_argument_types: None, + async_state: AsyncFunctionState::default(), } } @@ -1711,21 +1615,14 @@ impl<'a, 'b> FunctionBindgen<'a, 'b> { self.interface_gen .world_gen .pkg_resolver - .type_constructor(self.interface_gen.name, ty) + .type_constructor(&self.type_context, ty) } fn resolve_type_name(&mut self, ty: &Type) -> String { self.interface_gen .world_gen .pkg_resolver - .type_name(self.interface_gen.name, ty) - } - - fn resolve_pkg(&mut self, pkg: &str) -> String { - self.interface_gen - .world_gen - .pkg_resolver - .qualify_package(self.interface_gen.name, pkg) + .type_name(&self.type_context, ty) } fn use_ffi(&mut self, str: &'static str) { @@ -2367,78 +2264,38 @@ impl Bindgen for FunctionBindgen<'_, '_> { }; let func_name = name.to_upper_camel_case(); - - let operands = operands.join(", "); + let call_operands = if self.sync_import_argument_types.is_some() { + operands + .iter() + .map(|operand| { + let stable = self.locals.tmp("lower_arg"); + uwriteln!(self.src, "let {stable} = {operand}"); + stable + }) + .collect::>() + } else { + operands.clone() + }; + let arguments = call_operands.join(", "); // TODO: handle this to support async functions - uwriteln!(self.src, "{assignment} wasmImport{func_name}({operands});"); + uwriteln!(self.src, "{assignment} wasmImport{func_name}({arguments});"); + self.commit_sync_import_arguments(sig, &call_operands); } Instruction::CallInterface { func, async_ } => { + if *async_ { + self.emit_async_call_interface(func, operands, results); + return; + } + let name = self.interface_gen.world_gen.pkg_resolver.func_call( - self.interface_gen.name, + &self.type_context, func, - self.interface_gen.name, + &self.func_interface, ); let args = operands.join(", "); - if *async_ { - let (async_func_result, task_return_result, task_return_type) = - match func.result { - Some(ty) => { - let res = self.locals.tmp("return_result"); - (res.clone(), res, self.resolve_type_name(&ty)) - } - None => ("_ignore".into(), "".into(), "Unit".into()), - }; - - if func.result.is_some() { - results.push(async_func_result.clone()); - } - let ffi = self.resolve_pkg(FFI_DIR); - uwrite!( - self.src, - r#" - let task = {ffi}current_task(); - let _ = task.with_waitable_set(fn(task) {{ - let {async_func_result}: Ref[{task_return_type}?] = Ref::new(None) - task.wait(fn() {{ - {async_func_result}.val = Some({name}({args})); - }}) - for {{ - if task.no_wait() && {async_func_result}.val is Some({async_func_result}){{ - {name}_task_return({task_return_result}); - break; - }} else {{ - {ffi}suspend() catch {{ - _ => {{ - {ffi}task_cancel(); - }} - }} - }} - }} - }}) - if task.is_fail() is Some({ffi}Cancelled::Cancelled) {{ - {ffi}task_cancel(); - return {ffi}CallbackCode::Exit.encode() - }} - if task.is_done() {{ - return {ffi}CallbackCode::Exit.encode() - }} - return {ffi}CallbackCode::Wait(task.handle()).encode() - "#, - ); - assert!(matches!( - self.deferred_task_return, - DeferredTaskReturn::None - )); - self.deferred_task_return = DeferredTaskReturn::Generating { - prev_src: mem::take(&mut self.src), - return_param: async_func_result.to_string(), - }; - return; - } - let assignment = match func.result { None => "let _ = ".into(), Some(ty) => { @@ -2651,13 +2508,17 @@ impl Bindgen for FunctionBindgen<'_, '_> { } Instruction::GuestDeallocate { .. } => { - self.use_ffi(ffi::FREE); - uwriteln!(self.src, "mbt_ffi_free({})", operands[0]) + if !self.preserve_guest_allocations { + self.use_ffi(ffi::FREE); + uwriteln!(self.src, "mbt_ffi_free({})", operands[0]) + } } Instruction::GuestDeallocateString => { - self.use_ffi(ffi::FREE); - uwriteln!(self.src, "mbt_ffi_free({})", operands[0]) + if !self.preserve_guest_allocations { + self.use_ffi(ffi::FREE); + uwriteln!(self.src, "mbt_ffi_free({})", operands[0]) + } } Instruction::GuestDeallocateVariant { blocks } => { @@ -2722,8 +2583,10 @@ impl Bindgen for FunctionBindgen<'_, '_> { ); } - self.use_ffi(ffi::FREE); - uwriteln!(self.src, "mbt_ffi_free({address})",); + if !self.preserve_guest_allocations { + self.use_ffi(ffi::FREE); + uwriteln!(self.src, "mbt_ffi_free({address})",); + } } Instruction::Flush { amt } => { @@ -2731,80 +2594,42 @@ impl Bindgen for FunctionBindgen<'_, '_> { } Instruction::FutureLift { ty, .. } => { - let result = self.locals.tmp("result"); - let op = &operands[0]; - // let qualifier = self.r#gen.qualify_package(self.func_interface); - let ty = self.resolve_type_name(&Type::Id(*ty)); - let ffi = self - .interface_gen - .world_gen - .pkg_resolver - .qualify_package(self.interface_gen.name, FFI_DIR); - - let snake_name = format!("static_{}_future_table", ty.to_snake_case(),); - - uwriteln!( - self.src, - r#"let {result} = {ffi}FutureReader::new({op}, {snake_name});"#, - ); - - results.push(result); + self.emit_future_lift(*ty, operands, results); } - Instruction::FutureLower { .. } => { - let op = &operands[0]; - results.push(format!("{op}.handle")); + Instruction::FutureLower { ty, .. } => { + self.emit_future_lower(*ty, operands, results); } Instruction::AsyncTaskReturn { params, .. } => { - let (body, return_param) = match &mut self.deferred_task_return { - DeferredTaskReturn::Generating { - prev_src, - return_param, - } => { - mem::swap(&mut self.src, prev_src); - (mem::take(prev_src), return_param.clone()) - } - _ => unreachable!(), - }; - assert_eq!(params.len(), operands.len()); - self.deferred_task_return = DeferredTaskReturn::Emitted { - body, - params: params - .iter() - .zip(operands) - .map(|(a, b)| (*a, b.clone())) - .collect(), - return_param, - }; + self.capture_task_return(params, operands); } - Instruction::StreamLower { .. } => { - let op = &operands[0]; - results.push(format!("{op}.handle")); + Instruction::StreamLower { ty, .. } => { + self.emit_stream_lower(*ty, operands, results); } Instruction::StreamLift { ty, .. } => { - let result = self.locals.tmp("result"); - let op = &operands[0]; - let qualifier = self.resolve_pkg(self.interface_gen.name); - let ty = self.resolve_type_name(&Type::Id(*ty)); - let ffi = self.resolve_pkg(FFI_DIR); - let snake_name = format!( - "static_{}_stream_table", - ty.replace(&qualifier, "").to_snake_case(), - ); - - uwriteln!( - self.src, - r#"let {result} = {ffi}StreamReader::new({op}, {snake_name});"#, - ); - - results.push(result); + self.emit_stream_lift(*ty, operands, results); } - Instruction::ErrorContextLower { .. } - | Instruction::ErrorContextLift { .. } - | Instruction::DropHandle { .. } => todo!(), + Instruction::DropHandle { ty } => { + let is_endpoint = match ty { + Type::Id(id) => matches!( + &self.interface_gen.resolve.types[*id].kind, + TypeDefKind::Future(_) | TypeDefKind::Stream(_) + ), + _ => false, + }; + if !self.commit_endpoints { + let method = if self.sync_endpoint_drop && is_endpoint { + "drop_sync" + } else { + "drop" + }; + uwriteln!(self.src, "{}.{method}()", operands[0]); + } + } + Instruction::ErrorContextLower { .. } | Instruction::ErrorContextLift { .. } => todo!(), Instruction::FixedLengthListLift { element: _, size, @@ -2824,13 +2649,18 @@ impl Bindgen for FunctionBindgen<'_, '_> { size, id: _, } => { + uwriteln!( + self.src, + "if ({}).length() != {size} {{ panic() }}", + operands[0] + ); for i in 0..(*size as usize) { results.push(format!("({})[{i}]", operands[0])); } } Instruction::FixedLengthListLowerToMemory { element, - size: _, + size: fixed_length, id: _, } => { let Block { @@ -2847,7 +2677,8 @@ impl Bindgen for FunctionBindgen<'_, '_> { uwrite!( self.src, " - for {index} = 0; {index} < ({vec}).length(); {index} = {index} + 1 {{ + if ({vec}).length() != {fixed_length} {{ panic() }} + for {index} = 0; {index} < {fixed_length}; {index} = {index} + 1 {{ let iter_elem = ({vec})[{index}] let iter_base = ({target}) + ({index} * {size}) {body} @@ -3000,8 +2831,10 @@ impl Bindgen for FunctionBindgen<'_, '_> { ); } - self.use_ffi(ffi::FREE); - uwriteln!(self.src, "mbt_ffi_free({address})",); + if !self.preserve_guest_allocations { + self.use_ffi(ffi::FREE); + uwriteln!(self.src, "mbt_ffi_free({address})",); + } } } } @@ -3034,7 +2867,7 @@ impl Bindgen for FunctionBindgen<'_, '_> { fn finish_block(&mut self, operands: &mut Vec) { let BlockStorage { body, cleanup } = self.block_storage.pop().unwrap(); - if !self.cleanup.is_empty() { + if !self.cleanup.is_empty() && !self.suppress_block_cleanup { self.needs_cleanup_list = true; self.use_ffi(ffi::FREE); @@ -3121,6 +2954,218 @@ fn flags_repr(flags: &Flags) -> Int { } } +fn type_contains_future_or_stream(resolve: &Resolve, ty: &Type) -> bool { + let Type::Id(id) = ty else { + return false; + }; + + match &resolve.types[*id].kind { + TypeDefKind::Future(_) | TypeDefKind::Stream(_) => true, + TypeDefKind::Record(record) => record + .fields + .iter() + .any(|field| type_contains_future_or_stream(resolve, &field.ty)), + TypeDefKind::Tuple(tuple) => tuple + .types + .iter() + .any(|ty| type_contains_future_or_stream(resolve, ty)), + TypeDefKind::Variant(variant) => variant + .cases + .iter() + .filter_map(|case| case.ty.as_ref()) + .any(|ty| type_contains_future_or_stream(resolve, ty)), + TypeDefKind::Option(ty) + | TypeDefKind::List(ty) + | TypeDefKind::FixedLengthList(ty, _) + | TypeDefKind::Type(ty) => type_contains_future_or_stream(resolve, ty), + TypeDefKind::Map(key, value) => { + type_contains_future_or_stream(resolve, key) + || type_contains_future_or_stream(resolve, value) + } + TypeDefKind::Result(result) => result + .ok + .iter() + .chain(result.err.iter()) + .any(|ty| type_contains_future_or_stream(resolve, ty)), + TypeDefKind::Resource + | TypeDefKind::Handle(_) + | TypeDefKind::Flags(_) + | TypeDefKind::Enum(_) => false, + TypeDefKind::Unknown => unreachable!(), + } +} + +fn type_contains_endpoint_fixed_length_list_combination( + resolve: &Resolve, + ty: &Type, + inside_fixed_length_list: bool, + inside_endpoint: bool, +) -> bool { + let Type::Id(id) = ty else { + return false; + }; + + match &resolve.types[*id].kind { + TypeDefKind::Future(payload) | TypeDefKind::Stream(payload) => { + inside_fixed_length_list + || payload.as_ref().is_some_and(|ty| { + type_contains_endpoint_fixed_length_list_combination(resolve, ty, false, true) + }) + } + TypeDefKind::FixedLengthList(ty, _) => { + inside_endpoint + || type_contains_endpoint_fixed_length_list_combination(resolve, ty, true, false) + } + TypeDefKind::Record(record) => record.fields.iter().any(|field| { + type_contains_endpoint_fixed_length_list_combination( + resolve, + &field.ty, + inside_fixed_length_list, + inside_endpoint, + ) + }), + TypeDefKind::Tuple(tuple) => tuple.types.iter().any(|ty| { + type_contains_endpoint_fixed_length_list_combination( + resolve, + ty, + inside_fixed_length_list, + inside_endpoint, + ) + }), + TypeDefKind::Variant(variant) => variant + .cases + .iter() + .filter_map(|case| case.ty.as_ref()) + .any(|ty| { + type_contains_endpoint_fixed_length_list_combination( + resolve, + ty, + inside_fixed_length_list, + inside_endpoint, + ) + }), + TypeDefKind::Option(ty) | TypeDefKind::List(ty) | TypeDefKind::Type(ty) => { + type_contains_endpoint_fixed_length_list_combination( + resolve, + ty, + inside_fixed_length_list, + inside_endpoint, + ) + } + TypeDefKind::Map(key, value) => { + type_contains_endpoint_fixed_length_list_combination( + resolve, + key, + inside_fixed_length_list, + inside_endpoint, + ) || type_contains_endpoint_fixed_length_list_combination( + resolve, + value, + inside_fixed_length_list, + inside_endpoint, + ) + } + TypeDefKind::Result(result) => result.ok.iter().chain(result.err.iter()).any(|ty| { + type_contains_endpoint_fixed_length_list_combination( + resolve, + ty, + inside_fixed_length_list, + inside_endpoint, + ) + }), + TypeDefKind::Resource + | TypeDefKind::Handle(_) + | TypeDefKind::Flags(_) + | TypeDefKind::Enum(_) => false, + TypeDefKind::Unknown => unreachable!(), + } +} + +fn world_contains_endpoint_fixed_length_list_combination( + resolve: &Resolve, + world: WorldId, +) -> bool { + let item_contains_endpoint = |item: &WorldItem| match item { + WorldItem::Function(func) => { + func.params.iter().any(|param| { + type_contains_endpoint_fixed_length_list_combination( + resolve, ¶m.ty, false, false, + ) + }) || func.result.as_ref().is_some_and(|ty| { + type_contains_endpoint_fixed_length_list_combination(resolve, ty, false, false) + }) + } + WorldItem::Interface { id, .. } => { + let interface = &resolve.interfaces[*id]; + interface.types.values().any(|id| { + type_contains_endpoint_fixed_length_list_combination( + resolve, + &Type::Id(*id), + false, + false, + ) + }) || interface.functions.values().any(|func| { + func.params.iter().any(|param| { + type_contains_endpoint_fixed_length_list_combination( + resolve, ¶m.ty, false, false, + ) + }) || func.result.as_ref().is_some_and(|ty| { + type_contains_endpoint_fixed_length_list_combination(resolve, ty, false, false) + }) + }) + } + WorldItem::Type { id, .. } => type_contains_endpoint_fixed_length_list_combination( + resolve, + &Type::Id(*id), + false, + false, + ), + }; + let world = &resolve.worlds[world]; + world + .imports + .values() + .chain(world.exports.values()) + .any(item_contains_endpoint) +} + +fn world_contains_future_or_stream(resolve: &Resolve, world: WorldId) -> bool { + let item_contains_endpoint = |item: &WorldItem| match item { + WorldItem::Function(func) => { + func.params + .iter() + .any(|param| type_contains_future_or_stream(resolve, ¶m.ty)) + || func + .result + .as_ref() + .is_some_and(|ty| type_contains_future_or_stream(resolve, ty)) + } + WorldItem::Interface { id, .. } => { + let interface = &resolve.interfaces[*id]; + interface + .types + .values() + .any(|id| type_contains_future_or_stream(resolve, &Type::Id(*id))) + || interface.functions.values().any(|func| { + func.params + .iter() + .any(|param| type_contains_future_or_stream(resolve, ¶m.ty)) + || func + .result + .as_ref() + .is_some_and(|ty| type_contains_future_or_stream(resolve, ty)) + }) + } + WorldItem::Type { id, .. } => type_contains_future_or_stream(resolve, &Type::Id(*id)), + }; + let world = &resolve.worlds[world]; + world + .imports + .values() + .chain(world.exports.values()) + .any(item_contains_endpoint) +} + fn indent(code: &str) -> Source { let mut indented = Source::default(); let mut was_empty = false; @@ -3155,3 +3200,706 @@ fn print_docs(src: &mut String, docs: &Docs) { } } } + +#[cfg(test)] +mod tests { + use super::*; + + fn try_generate_with_opts(wit: &str, world: &str, opts: Opts) -> Result { + let mut resolve = Resolve::default(); + let pkg = resolve.push_str("test.wit", wit).unwrap(); + let world = resolve.select_world(&[pkg], Some(world)).unwrap(); + let mut files = Files::default(); + let mut generator = MoonBit { + opts, + ..MoonBit::default() + }; + generator.generate(&mut resolve, world, &mut files)?; + Ok(files) + } + + fn try_generate(wit: &str, world: &str) -> Result { + try_generate_with_opts( + wit, + world, + Opts { + gen_dir: "gen".into(), + ..Opts::default() + }, + ) + } + + fn generate(wit: &str, world: &str) -> Files { + try_generate(wit, world).unwrap() + } + + fn file<'a>(files: &'a Files, path: &str) -> &'a str { + let contents = files + .iter() + .find_map(|(name, contents)| (name == path).then_some(contents)) + .unwrap_or_else(|| { + let names = files + .iter() + .map(|(name, _)| name) + .collect::>() + .join(", "); + std::panic!("missing generated file `{path}`; generated: {names}") + }); + std::str::from_utf8(contents).unwrap() + } + + #[test] + fn endpoint_free_sync_generation_matches_golden() { + let files = generate( + r#" + package a:b; + + world runner { + import add: func(a: u32, b: u32) -> u32; + export echo: func(value: u32) -> u32; + } + "#, + "runner", + ); + + // This exact-output fingerprint runs with both async adapters. Ignore only + // the release-version preamble, which is unrelated to generated ABI. + let mut entries = files.iter().collect::>(); + entries.sort_by_key(|(name, _)| *name); + let fingerprints = entries + .into_iter() + .map(|(name, contents)| { + let contents = if contents.starts_with(b"// Generated by") { + let preamble_end = contents.iter().position(|byte| *byte == b'\n').unwrap() + 1; + &contents[preamble_end..] + } else { + contents + }; + let hash = contents + .iter() + .fold(0xcbf29ce484222325_u64, |mut hash, byte| { + hash ^= u64::from(*byte); + hash.wrapping_mul(0x100000001b3) + }); + (name.to_string(), hash) + }) + .collect::>(); + + assert_eq!( + fingerprints, + vec![ + ("gen/ffi.mbt".into(), 10220319382745692950), + ("gen/moon.pkg.json".into(), 15894505084782869543), + ("gen/world/runner/ffi.mbt".into(), 14715999128234894449), + ("gen/world/runner/moon.pkg.json".into(), 6361049410124596525,), + ("gen/world/runner/top.mbt".into(), 12192865914091673515,), + ("moon.mod.json".into(), 14111159726816684443), + ("world/runner/ffi_import.mbt".into(), 17812050158059242657,), + ("world/runner/import.mbt".into(), 5430383198437179961), + ("world/runner/moon.pkg.json".into(), 6361049410124596525,), + ] + ); + } + + #[test] + fn sync_world_does_not_emit_async_runtime_or_wrappers() { + let files = generate( + r#" + package a:b; + + world runner { + import add: func(a: u32, b: u32) -> u32; + } + "#, + "runner", + ); + + let import = file(&files, "world/runner/import.mbt"); + assert!(import.contains("pub fn add(")); + for async_name in [ + "pub async fn", + "with_waitableset", + "TaskGroup", + "background_group", + "CMFuture", + "CMStream", + ] { + assert!(!import.contains(async_name)); + } + assert!( + files + .iter() + .all(|(name, _)| !name.starts_with("async-core/async_")) + ); + } + + #[test] + fn async_filters_respect_import_export_direction() { + let wit = r#" + package a:b; + world runner { import run: func(); } + "#; + + let mut import_opts = Opts { + gen_dir: "gen".into(), + ..Opts::default() + }; + import_opts.async_.push("import:run"); + let import_files = try_generate_with_opts(wit, "runner", import_opts).unwrap(); + let import = file(&import_files, "world/runner/import.mbt"); + let import_ffi = file(&import_files, "world/runner/ffi_import.mbt"); + assert!(import.contains("pub async fn run("), "{import}"); + assert!(import_ffi.contains("[async-lower]run"), "{import_ffi}"); + + let mut export_opts = Opts { + gen_dir: "gen".into(), + ..Opts::default() + }; + export_opts.async_.push("export:run"); + let export_files = try_generate_with_opts(wit, "runner", export_opts).unwrap(); + let import = file(&export_files, "world/runner/import.mbt"); + let import_ffi = file(&export_files, "world/runner/ffi_import.mbt"); + assert!(import.contains("pub fn run("), "{import}"); + assert!(!import.contains("pub async fn run("), "{import}"); + assert!(!import_ffi.contains("[async-lower]run"), "{import_ffi}"); + } + + #[test] + fn async_import_indirect_params_emit_malloc_builtin() { + let files = generate( + r#" + package a:b; + + interface types { + resource descriptor { + run: async func(first: string, second: list); + } + } + + world bindings { import types; } + "#, + "bindings", + ); + + let ffi = file(&files, "interface/a/b/types/ffi.mbt"); + assert!(ffi.contains("extern \"wasm\" fn mbt_ffi_malloc"), "{ffi}"); + } + + #[test] + fn type_only_endpoints_emit_async_runtime() { + let files = generate( + r#" + package a:b; + + interface types { + record payload { ready: future } + } + + world runner { import types; } + "#, + "runner", + ); + + let source = file(&files, "interface/a/b/types/top.mbt"); + assert!(source.contains("@async-core.Future[UInt]"), "{source}"); + file(&files, "async-core/moon.pkg.json"); + file(&files, "async-core/async_trait.mbt"); + } + + #[test] + fn endpoint_container_types_omit_value_derives() { + let mut resolve = Resolve::default(); + let pkg = resolve + .push_str( + "test.wit", + r#" + package a:b; + + interface types { + record plain { value: u32 } + record nested { value: option> } + variant streamed { none, value(list>) } + use-types: func(a: plain, b: nested, c: streamed); + } + + world runner { import types; } + "#, + ) + .unwrap(); + let world = resolve.select_world(&[pkg], Some("runner")).unwrap(); + let mut files = Files::default(); + let mut generator = MoonBit { + opts: Opts { + derive: DeriveOpts { + derive_debug: true, + derive_show: true, + derive_eq: true, + derive_error: false, + }, + gen_dir: "gen".into(), + ..Opts::default() + }, + ..MoonBit::default() + }; + generator.generate(&mut resolve, world, &mut files).unwrap(); + + let source = file(&files, "interface/a/b/types/top.mbt"); + assert!( + source.contains("struct Plain {\n value : UInt\n} derive(Debug, Show, Eq)"), + "{source}" + ); + assert!( + source.contains("struct Nested {\n value : @async-core.Future[UInt]?\n}"), + "{source}" + ); + assert!( + !source.contains("struct Nested {\n value : @async-core.Future[UInt]?\n} derive(") + ); + assert!( + source.contains( + "Streamed {\n None\n Value(Array[@async-core.Stream[String]])\n}" + ), + "{source}" + ); + assert!(!source.contains( + "Streamed {\n None\n Value(Array[@async-core.Stream[String]])\n} derive(" + )); + } + + #[test] + fn sync_functions_with_endpoints_remain_sync() { + let files = generate( + r#" + package a:b; + + world runner { + import exchange: func( + input: stream, + ready: future, + ) -> tuple, future>; + } + "#, + "runner", + ); + + let import = file(&files, "world/runner/import.mbt"); + assert!(import.contains("pub fn exchange(")); + assert!(import.contains("@async-core.Stream[Byte]")); + assert!(import.contains("@async-core.Future[UInt]")); + assert!(!import.contains("pub async fn exchange")); + assert!(!import.contains("background_group")); + let call = import.find("wasmImportExchange").unwrap(); + let after_call = &import[call..]; + assert!(after_call.contains("StreamCommit"), "{import}"); + assert!(after_call.contains("FutureCommit"), "{import}"); + assert_eq!(import.matches("StreamLower(input)").count(), 1, "{import}"); + assert_eq!(import.matches("FutureLower(ready)").count(), 1, "{import}"); + assert!( + files + .iter() + .any(|(name, _)| name == "async-core/async_trait.mbt") + ); + } + + #[test] + fn async_export_background_group_name_is_deconflicted() { + let files = generate( + r#" + package a:b; + world service { + export handle: async func(background-group: u32); + } + "#, + "service", + ); + let public = file(&files, "gen/world/service/top.mbt"); + assert!( + public.contains( + "background_group : UInt, background_group0 : @async-core.TaskGroup[Unit]" + ), + "{public}" + ); + let wrapper = file(&files, "gen/world/service/ffi.mbt"); + assert!( + wrapper.contains("with_task_group(async fn(background_group0)"), + "{wrapper}" + ); + assert!( + wrapper.contains("handle((p0).reinterpret_as_uint(), background_group0)"), + "{wrapper}" + ); + } + + #[test] + fn async_export_surface_hides_component_model_bridge_types() { + let files = generate( + r#" + package a:b; + + interface handler { + handle: async func( + input: stream, + ready: future, + ) -> tuple, future>; + } + + world service { + export handler; + } + "#, + "service", + ); + + let public = file(&files, "gen/interface/a/b/handler/top.mbt"); + assert!(public.contains("input : @async-core.Stream[Byte]")); + assert!(public.contains("ready : @async-core.Future[UInt]")); + assert!(public.contains("background_group : @async-core.TaskGroup[Unit]")); + for internal_name in [ + "CMFuture", + "CMStream", + "VTable", + "take_cm_handle", + "take_producer", + "from_callbacks", + ] { + assert!(!public.contains(internal_name)); + } + + let wrapper = file(&files, "gen/interface/a/b/handler/ffi.mbt"); + assert!(wrapper.contains("with_task_group(async fn(background_group)")); + let root_wrapper = file(&files, "gen/ffi.mbt"); + for ffi in [wrapper, root_wrapper] { + let lines = ffi.lines().collect::>(); + for (index, line) in lines.iter().enumerate() { + if line.trim_start().starts_with("pub fn wasmExport") { + assert_eq!(lines[index - 1].trim(), "#doc(hidden)", "{ffi}"); + } + } + } + + let coroutine = file(&files, "async-core/async_coroutine.mbt"); + assert!(!coroutine.contains("fn pause()")); + assert!(!coroutine.contains("wait_until")); + let task = file(&files, "async-core/async_task.mbt"); + assert!(task.contains("pub struct Task[X] {\n priv value : Ref[X?]")); + let promise = file(&files, "async-core/async_promise.mbt"); + assert!(promise.contains("pub struct Promise[X]")); + assert!(promise.contains("pub fn[X] Future::new()")); + assert!(promise.contains("pub fn[X] Promise::complete")); + let semaphore = file(&files, "async-core/async_semaphore.mbt"); + assert!(semaphore.contains("pub struct Semaphore")); + assert!(semaphore.contains("pub async fn Semaphore::acquire")); + let cond_var = file(&files, "async-core/async_cond_var.mbt"); + assert!(cond_var.contains("pub struct CondVar")); + assert!(cond_var.contains("pub async fn CondVar::wait")); + let mutex = file(&files, "async-core/async_mutex.mbt"); + assert!(mutex.contains("pub struct Mutex")); + assert!(mutex.contains("pub async fn Mutex::acquire")); + + let async_core = files + .iter() + .filter(|(name, _)| name.starts_with("async-core/")) + .map(|(_, contents)| String::from_utf8_lossy(contents)) + .collect::>() + .join("\n"); + for hidden in [ + "#doc(hidden)\npub fn with_waitableset", + "#doc(hidden)\npub fn cb", + "#doc(hidden)\npub fn spawn_component_task_current", + "#doc(hidden)\npub fn has_component_task_scope", + "#doc(hidden)\npub async fn suspend_for_subtask", + "#doc(hidden)\npub async fn suspend_for_future_read", + "#doc(hidden)\npub async fn suspend_for_future_write", + "#doc(hidden)\npub async fn suspend_for_stream_read", + "#doc(hidden)\npub async fn suspend_for_stream_write", + ] { + assert!( + async_core.contains(hidden), + "runtime helper is public: {hidden}" + ); + } + for raw in [ + "pub extern \"wasm\" fn malloc", + "pub extern \"wasm\" fn load32", + "pub fn context_set", + "pub fn context_get", + "pub fn task_cancel", + "pub fn backpressure_inc", + "pub fn backpressure_dec", + "[backpressure-inc]", + "[backpressure-dec]", + "pub fn current_coroutine", + "pub fn detach_waitable", + "pub fn has_immediately_ready_task", + "pub fn no_more_work", + "pub fn reschedule", + "pub fn spawn(", + "pub fn spawn_bg_current", + "pub async fn suspend()", + ] { + assert!( + !async_core.contains(raw), + "raw async-core API leaked: {raw}" + ); + } + } + + #[test] + fn nested_endpoints_use_static_boundary_helpers() { + let files = generate( + r#" + package test:moonbit-nested; + + interface nested { + relay: async func( + value: future>>, + ) -> future>>; + relay-stream: async func( + value: stream>, + ) -> stream>; + } + + world service { export nested; } + "#, + "service", + ); + + let ffi = file(&files, "gen/interface/test/moonbit-nested/nested/ffi.mbt"); + for site in [ + "RelayStream0StreamSource", + "RelayFuture1FutureSource", + "RelayFuture2FutureSource", + "RelayStream3StreamLower", + "RelayFuture4FutureLower", + "RelayFuture5FutureLower", + ] { + assert!(ffi.contains(site), "missing static endpoint site {site}"); + } + assert!(ffi.contains("[async-lower][future-read-2]relay")); + assert!(ffi.contains("[async-lower][future-write-2]relay")); + assert!(ffi.contains("suspend_for_future_write_terminal")); + assert!(ffi.contains("let data_len = if data.length() < 1")); + assert!(ffi.contains("let writer_lock = @async-core.Mutex()")); + assert!(ffi.contains("writer_lock.acquire()")); + assert!(ffi.contains("defer writer_lock.release()")); + assert!(ffi.contains("read_cleanup : @async-core.CondVar")); + assert!(ffi.contains("self.read_cleanup.broadcast()")); + assert!(ffi.contains("@async-core.cancel_future_read(")); + assert!(ffi.contains("@async-core.cancel_stream_read(")); + assert!(ffi.contains("@async-core.cancel_stream_write(")); + assert!(ffi.contains("[stream-cancel-write-")); + assert!(!ffi.contains("suspend_for_future_cancel_read")); + assert!(!ffi.contains("wait_until")); + assert!(ffi.contains("RelayFuture5FutureLowerCommitted")); + assert!(ffi.contains("RelayFuture4FutureRejectPrepared")); + assert!(ffi.contains("RelayStream3StreamRejectPrepared")); + assert!(!ffi.contains("RelayFuture5FutureRejectPrepared")); + + let generated = files + .iter() + .map(|(name, contents)| format!("{name}\n{}", String::from_utf8_lossy(contents))) + .collect::>() + .join("\n"); + for legacy in [ + "async_cm.mbt", + "CMFutureVTable", + "CMStreamVTable", + "take_cm_handle", + "new_cm_future", + "new_cm_stream", + ] { + assert!( + !generated.contains(legacy), + "legacy bridge leaked: {legacy}" + ); + } + + let imports = generate( + r#" + package test:moonbit-nested; + + interface nested { + relay: async func( + value: future>>, + ) -> future>>; + relay-stream: async func( + value: stream>, + ) -> stream>; + } + + world client { import nested; } + "#, + "client", + ); + let import = file(&imports, "interface/test/moonbit-nested/nested/top.mbt"); + assert!(import.contains("if before_started")); + assert!(import.contains(".drop_sync()")); + assert!(import.contains("defer mbt_ffi_free(_result_ptr)")); + assert_eq!(import.matches("FutureLower(value)").count(), 1, "{import}"); + assert_eq!(import.matches("StreamLower(value)").count(), 1, "{import}"); + assert!(!import.contains("cleanup_list")); + } + + #[test] + fn fixed_length_lists_use_checked_fixed_arrays_but_reject_nested_endpoints() { + let files = generate( + r#" + package test:fixed-array; + + interface api { + type pair = list; + type block = list; + accept: func(value: pair) -> pair; + accept-block: func(value: block); + } + + world client { import api; } + "#, + "client", + ); + let top = file(&files, "interface/test/fixed-array/api/top.mbt"); + assert!(top.contains("FixedArray[UInt]"), "{top}"); + assert!(top.contains(".length() != 2"), "{top}"); + assert!(top.contains(".length() != 20"), "{top}"); + + for unsupported in [ + "type unsupported = list, 1>;", + "type unsupported = future>;", + ] { + let wit = format!( + r#" + package test:fixed-endpoints; + + interface api {{ + {unsupported} + accept: func(value: unsupported); + }} + + world client {{ import api; }} + "# + ); + let error = match try_generate(&wit, "client") { + Ok(_) => std::panic!( + "fixed-length list and async endpoint combinations must be rejected" + ), + Err(error) => error, + }; + assert!( + error + .to_string() + .contains("combining future or stream types with fixed-length lists"), + "{error:#}" + ); + } + } + + #[test] + fn cancelled_returned_async_import_recursively_cleans_result() { + let files = generate( + r#" + package test:cancelled-result; + + interface api { + resource leaf; + record payload { + label: string, + leaves: list, + ready: future, + } + load: async func() -> payload; + } + + world client { import api; } + "#, + "client", + ); + + let import = file(&files, "interface/test/cancelled-result/api/top.mbt"); + assert!( + import.contains("suspend_for_subtask") + && import.contains("fn() {") + && import.contains(".drop_sync()") + && import.contains(".drop()"), + "{import}" + ); + assert!( + import.contains("mbt_ffi_free(mbt_ffi_load32((_result_ptr) + 0))") + && import.contains("mbt_ffi_free(mbt_ffi_load32((_result_ptr) + 8))") + && import.contains("defer mbt_ffi_free(_result_ptr)"), + "cancelled returned result must free its strings, lists, and result area: {import}" + ); + } + + #[test] + fn async_runtime_traps_unhandled_export_failure() { + let files = generate( + r#" + package test:failing-export; + world service { export run: async func(); } + "#, + "service", + ); + + let event_loop = file(&files, "async-core/async_ev.mbt"); + assert!( + event_loop.contains("abort(\"async export failed before task return\")") + && event_loop.contains("if !(ev.resolved.get(waitable_set) is Some(true))"), + "{event_loop}" + ); + } + + #[test] + fn async_runtime_borrows_waitable_poll_payload() { + let files = generate( + r#" + package test:poll-payload; + world service { export run: async func(); } + "#, + "service", + ); + + let abi = file(&files, "async-core/async_abi.mbt"); + assert!( + abi.contains("#borrow(array)\nextern \"wasm\" fn int_array2ptr"), + "{abi}" + ); + assert!(!abi.contains("#owned(array)"), "{abi}"); + } + + #[test] + fn async_runtime_uses_baseline_subtask_cancel() { + let files = generate( + r#" + package test:subtask-cancel; + world service { export run: async func(); } + "#, + "service", + ); + + let abi = file(&files, "async-core/async_abi.mbt"); + assert!( + abi.contains(r#""$root" "[subtask-cancel]""#) + && !abi.contains("[async-lower][subtask-cancel]"), + "{abi}" + ); + } + + #[test] + fn unit_future_intrinsics_use_wit_parser_unit_name() { + let files = generate( + r#" + package a:b; + + world runner { + import exchange: func(value: future) -> future; + } + "#, + "runner", + ); + + let ffi = file(&files, "world/runner/ffi_import.mbt"); + assert!(ffi.contains(r#""$root" "[future-new-unit]exchange""#)); + assert!(ffi.contains(r#""$root" "[async-lower][future-read-unit]exchange""#)); + assert!(ffi.contains(r#""$root" "[future-cancel-read-unit]exchange""#)); + } +} diff --git a/crates/moonbit/src/pkg.rs b/crates/moonbit/src/pkg.rs index ccae5ac18..b98343f23 100644 --- a/crates/moonbit/src/pkg.rs +++ b/crates/moonbit/src/pkg.rs @@ -9,7 +9,7 @@ use wit_bindgen_core::{ }, }; -pub(crate) const FFI_DIR: &str = "ffi"; +pub(crate) const ASYNC_CORE_DIR: &str = "async-core"; #[derive(Default)] pub(crate) struct Imports { @@ -247,9 +247,9 @@ impl PkgResolver { } TypeDefKind::Future(ty) => { - let qualifier = self.qualify_package(this, FFI_DIR); + let qualifier = self.qualify_package(this, ASYNC_CORE_DIR); format!( - "{}FutureReader[{}]", + "{}Future[{}]", qualifier, ty.as_ref() .map(|t| self.type_name(this, t)) @@ -258,9 +258,9 @@ impl PkgResolver { } TypeDefKind::Stream(ty) => { - let qualifier = self.qualify_package(this, FFI_DIR); + let qualifier = self.qualify_package(this, ASYNC_CORE_DIR); format!( - "{}StreamReader[{}]", + "{}Stream[{}]", qualifier, ty.as_ref() .map(|t| self.type_name(this, t)) diff --git a/crates/test/src/moonbit.rs b/crates/test/src/moonbit.rs index 8811314f5..435bc4ae4 100644 --- a/crates/test/src/moonbit.rs +++ b/crates/test/src/moonbit.rs @@ -33,6 +33,10 @@ impl LanguageMethods for MoonBit { ] } + fn codegen_test_variants(&self) -> &[(&str, &[&str])] { + &[("async", &["--async=all"])] + } + fn prepare(&self, runner: &mut Runner) -> anyhow::Result<()> { println!("Testing if MoonBit toolchain exists..."); if runner @@ -125,9 +129,7 @@ impl LanguageMethods for MoonBit { config: &crate::config::WitConfig, _args: &[String], ) -> bool { - // async-resource-func actually works, but most other async tests - // fail during codegen or verification - config.async_ && name != "async-resource-func.wit" + name == "named-fixed-length-list.wit-async" || config.error_context } fn verify(&self, runner: &Runner, verify: &crate::Verify) -> anyhow::Result<()> { diff --git a/tests/runtime/cancel-import/test.mbt b/tests/runtime/cancel-import/test.mbt new file mode 100644 index 000000000..e24774938 --- /dev/null +++ b/tests/runtime/cancel-import/test.mbt @@ -0,0 +1,19 @@ +//@ [lang] +//@ path = 'gen/interface/my/test/i/stub.mbt' + +///| +pub async fn pending_import( + x : @async-core.Future[Unit], + _background_group : @async-core.TaskGroup[Unit], +) -> Unit { + x.get() +} + +///| +pub fn backpressure_set(x : Bool) -> Unit { + if x { + @async-core.backpressure_inc() + } else { + @async-core.backpressure_dec() + } +} diff --git a/tests/runtime/future-cancel-read/test.mbt b/tests/runtime/future-cancel-read/test.mbt index 5354c920e..13d62eaba 100644 --- a/tests/runtime/future-cancel-read/test.mbt +++ b/tests/runtime/future-cancel-read/test.mbt @@ -3,35 +3,35 @@ ///| pub async fn cancel_before_read( - x : @ffi.FutureReader[UInt], -) -> Unit noraise { - let task = @ffi.current_task() - let _ = x.drop() + x : @async-core.Future[UInt], + _background_group : @async-core.TaskGroup[Unit], +) -> Unit { + x.drop() } ///| pub async fn cancel_after_read( - x : @ffi.FutureReader[UInt], -) -> Unit noraise { - let task = @ffi.current_task() - task.spawn( - fn() { - let _ = x.read() catch { _ => raise @ffi.Cancelled::Cancelled } + x : @async-core.Future[UInt], + background_group : @async-core.TaskGroup[Unit], +) -> Unit { + background_group.spawn_bg( + async fn() { + let _ = x.get() catch { _ => raise @async-core.Cancelled::Cancelled } } ) - task.cancel_waitable(x) + x.drop() } ///| pub async fn start_read_then_cancel( - data : @ffi.FutureReader[UInt], - signal : @ffi.FutureReader[Unit], -) -> Unit noraise { - let task = @ffi.current_task() - task.spawn( - fn() { let _ = data.read() catch { _ => raise @ffi.Cancelled::Cancelled } }, - ) - task.spawn( - fn() { signal.read() catch { _ => raise @ffi.Cancelled::Cancelled } }, + data : @async-core.Future[UInt], + signal : @async-core.Future[Unit], + background_group : @async-core.TaskGroup[Unit], +) -> Unit { + background_group.spawn_bg( + async fn() { let _ = data.get() catch { _ => raise @async-core.Cancelled::Cancelled } }, + ) + background_group.spawn_bg( + async fn() { signal.get() catch { _ => raise @async-core.Cancelled::Cancelled } }, ) } diff --git a/tests/runtime/future-cancel-write/test.mbt b/tests/runtime/future-cancel-write/test.mbt index ac2349645..faaaa4144 100644 --- a/tests/runtime/future-cancel-write/test.mbt +++ b/tests/runtime/future-cancel-write/test.mbt @@ -2,16 +2,16 @@ //@ path = 'gen/interface/my/test/i/stub.mbt' ///| -pub fn take_then_drop(x : @ffi.FutureReader[String]) -> Unit { - let _ = x.drop() +pub fn take_then_drop(x : @async-core.Future[String]) -> Unit { + x.drop_sync() } ///| pub async fn read_and_drop( - x : @ffi.FutureReader[String], -) -> Unit noraise { - let task = @ffi.current_task() - let _ = task.spawn(fn() { - let _ = x.read() catch { _ => raise @ffi.Cancelled::Cancelled } + x : @async-core.Future[String], + background_group : @async-core.TaskGroup[Unit], +) -> Unit { + background_group.spawn_bg(async fn() { + let _ = x.get() catch { _ => raise @async-core.Cancelled::Cancelled } }) } diff --git a/tests/runtime/future-close-after-coming-back/test.mbt b/tests/runtime/future-close-after-coming-back/test.mbt index a11ed06d6..b6fc91dcf 100644 --- a/tests/runtime/future-close-after-coming-back/test.mbt +++ b/tests/runtime/future-close-after-coming-back/test.mbt @@ -1,6 +1,6 @@ //@ [lang] //@ path = 'gen/interface/a/b/the-test/stub.mbt' -pub fn f(_param : @ffi.FutureReader[Unit]) -> @ffi.FutureReader[Unit] { +pub fn f(_param : @async-core.Future[Unit]) -> @async-core.Future[Unit] { _param } diff --git a/tests/runtime/moonbit/async-import-cancel/gate.mbt b/tests/runtime/moonbit/async-import-cancel/gate.mbt new file mode 100644 index 000000000..d43e29bac --- /dev/null +++ b/tests/runtime/moonbit/async-import-cancel/gate.mbt @@ -0,0 +1,28 @@ +//@ [lang] +//@ path = 'gen/interface/test/async-import-cancel/gate-control/stub.mbt' + +///| +let return_started : Ref[Bool] = Ref(false) + +///| +let return_released : Ref[Bool] = Ref(false) + +///| +pub fn mark_started() -> Unit { + return_started.val = true +} + +///| +pub fn started() -> Bool { + return_started.val +} + +///| +pub fn release() -> Unit { + return_released.val = true +} + +///| +pub fn released() -> Bool { + return_released.val +} diff --git a/tests/runtime/moonbit/async-import-cancel/runner.mbt b/tests/runtime/moonbit/async-import-cancel/runner.mbt new file mode 100644 index 000000000..f7e76b5c8 --- /dev/null +++ b/tests/runtime/moonbit/async-import-cancel/runner.mbt @@ -0,0 +1,313 @@ +//@ wasmtime-flags = '-Wcomponent-model-async' +//@ [lang] +//@ path = 'gen/world/runner/stub.mbt' +//@ pkg_config = """{ "import": [{ "path": "test/async-import-cancel/async-core", "alias": "async-core" }, { "path": "test/async-import-cancel/interface/test/async-import-cancel/pending", "alias": "pending" }, { "path": "test/async-import-cancel/interface/test/async-import-cancel/gate-control", "alias": "gate-control" }] }""" + +///| +pub async fn run(background_group : @async-core.TaskGroup[Unit]) -> Unit { + guard @pending.leaf_live_count() == 0U else { panic() } + guard @pending.leaf_drop_count() == 0U else { panic() } + + @pending.backpressure_set(true) + let leaf = @pending.Leaf::leaf() + let future = @async-core.Future::ready_with_cleanup(leaf, fn( + value : @pending.Leaf, + ) { + value.drop() + }) + let (lowered, lowered_sink) = @async-core.Stream::new(capacity=1) + let task = background_group.spawn(async fn() -> Unit { + let signal : FixedArray[Unit] = [()] + guard lowered_sink.write_all(signal[:]) else { panic() } + lowered_sink.close() + defer @pending.backpressure_set(false) + @pending.pending(future) + }) + + guard lowered.read(1) is Some(signal) && signal.length() == 1 else { panic() } + guard !@pending.pending_started() else { panic() } + task.cancel() + @pending.settle() + guard !@pending.pending_started() else { panic() } + + for _ in 0..<32 { + if @pending.leaf_live_count() == 0U && @pending.leaf_drop_count() == 1U { + break + } + @pending.settle() + } + guard @pending.leaf_live_count() == 0U else { panic() } + guard @pending.leaf_drop_count() == 1U else { panic() } + + @pending.backpressure_set(true) + let stream_leaf = @pending.Leaf::leaf() + let stream_producer_ran = Ref(false) + let stream = @async-core.Stream::produce( + async fn(sink) { + stream_producer_ran.val = true + let values : FixedArray[@pending.Leaf] = [stream_leaf] + guard sink.write_all(values[:]) else { panic() } + sink.close() + }, + on_unstarted_drop=fn() { stream_leaf.drop() }, + ) + let (stream_lowered, stream_lowered_sink) = @async-core.Stream::new( + capacity=1, + ) + let stream_task = background_group.spawn(async fn() -> Unit { + let signal : FixedArray[Unit] = [()] + guard stream_lowered_sink.write_all(signal[:]) else { panic() } + stream_lowered_sink.close() + defer @pending.backpressure_set(false) + @pending.pending_stream(stream) + }) + + guard stream_lowered.read(1) is Some(signal) && signal.length() == 1 else { + panic() + } + guard !@pending.pending_stream_started() else { panic() } + stream_task.cancel() + @pending.settle() + guard !@pending.pending_stream_started() else { panic() } + guard !stream_producer_ran.val else { panic() } + + for _ in 0..<32 { + if @pending.leaf_live_count() == 0U && @pending.leaf_drop_count() == 2U { + break + } + @pending.settle() + } + guard @pending.leaf_live_count() == 0U else { panic() } + guard @pending.leaf_drop_count() == 2U else { panic() } + + let returned_leaf = @pending.Leaf::leaf() + let (return_started, return_started_sink) = @async-core.Stream::new( + capacity=0, + ) + let returned_task = background_group.spawn(async fn() -> Unit { + let signal : FixedArray[Unit] = [()] + guard return_started_sink.write_all(signal[:]) else { panic() } + return_started_sink.close() + let payload = @pending.return_after_cancel(returned_leaf) + payload.leaf.drop() + }) + guard return_started.read(1) is Some(signal) && signal.length() == 1 else { + panic() + } + guard @gate-control.started() else { panic() } + returned_task.cancel() + @gate-control.release() + + for _ in 0..<32 { + if @pending.leaf_live_count() == 0U && @pending.leaf_drop_count() == 5U { + break + } + @pending.settle() + } + guard @pending.leaf_live_count() == 0U else { panic() } + guard @pending.leaf_drop_count() == 5U else { panic() } + + let cleanup_leaf = fn(value : @pending.Leaf) { value.drop() } + let stream_values : FixedArray[@async-core.Future[@pending.Leaf]] = [ + @async-core.Future::ready_with_cleanup(@pending.Leaf::leaf(), cleanup_leaf), + @async-core.Future::ready_with_cleanup(@pending.Leaf::leaf(), cleanup_leaf), + @async-core.Future::ready_with_cleanup(@pending.Leaf::leaf(), cleanup_leaf), + ] + let cleanup_stream = @async-core.Stream::produce(async fn(sink) { + guard !sink.write_all(stream_values[:]) else { + abort("peer unexpectedly accepted the future stream") + } + sink.close() + }) + @pending.drop_future_stream(cleanup_stream) + for _ in 0..<32 { + if @pending.leaf_live_count() == 0U && @pending.leaf_drop_count() == 8U { + break + } + @pending.settle() + } + guard @pending.leaf_live_count() == 0U else { panic() } + guard @pending.leaf_drop_count() == 8U else { panic() } + + let after_start_future = @async-core.Future::ready_with_cleanup( + @pending.Leaf::leaf(), + cleanup_leaf, + ) + let after_start_stream_leaf = @pending.Leaf::leaf() + let after_start_stream = @async-core.Stream::produce( + async fn(sink) { + let values : FixedArray[@pending.Leaf] = [after_start_stream_leaf] + guard !sink.write_all(values[:]) else { + abort("cancelled callee unexpectedly consumed its stream") + } + sink.close() + }, + on_unstarted_drop=fn() { after_start_stream_leaf.drop() }, + ) + let after_start_task = background_group.spawn(async fn() -> Unit { + @pending.take_after_start({ + direct: @pending.Leaf::leaf(), + future_value: after_start_future, + stream_value: after_start_stream, + }) + }) + while !@pending.take_after_start_started() { + @pending.settle() + } + after_start_task.cancel() + after_start_task.wait() catch { + _ => () + } + for _ in 0..<32 { + if @pending.leaf_live_count() == 0U && @pending.leaf_drop_count() == 11U { + break + } + @pending.settle() + } + guard @pending.leaf_live_count() == 0U else { panic() } + guard @pending.leaf_drop_count() == 11U else { panic() } + + let race_future = @pending.open_race_future() + let (race_read_started, race_read_started_sink) = @async-core.Stream::new( + capacity=1, + ) + let race_read = background_group.spawn(async fn() -> Bool { + let signal : FixedArray[Unit] = [()] + guard race_read_started_sink.write_all(signal[:]) else { panic() } + race_read_started_sink.close() + let value = race_future.get() + value.drop() + true + }) + guard race_read_started.read(1) is Some(signal) && signal.length() == 1 else { + panic() + } + @pending.complete_race_future() + race_read.cancel() + guard race_read.wait() else { panic() } + for _ in 0..<32 { + if @pending.leaf_live_count() == 0U && @pending.leaf_drop_count() == 12U { + break + } + @pending.settle() + } + guard @pending.leaf_live_count() == 0U else { panic() } + guard @pending.leaf_drop_count() == 12U else { panic() } + + let race_stream = @pending.open_race_stream() + let (race_stream_started, race_stream_started_sink) = @async-core.Stream::new( + capacity=1, + ) + let race_stream_read = background_group.spawn(async fn() -> Bool { + let signal : FixedArray[Unit] = [()] + guard race_stream_started_sink.write_all(signal[:]) else { panic() } + race_stream_started_sink.close() + guard race_stream.read(1) is Some(values) && values.length() == 1 else { + return false + } + values[0].drop() + true + }) + guard race_stream_started.read(1) is Some(signal) && signal.length() == 1 else { + panic() + } + @pending.complete_race_stream() + race_stream_read.cancel() + guard race_stream_read.wait() else { panic() } + for _ in 0..<32 { + if @pending.leaf_live_count() == 0U && @pending.leaf_drop_count() == 13U { + break + } + @pending.settle() + } + guard @pending.leaf_live_count() == 0U else { panic() } + guard @pending.leaf_drop_count() == 13U else { panic() } + + let pending_future = @pending.open_future() + let (future_read_started, future_read_started_sink) = @async-core.Stream::new( + capacity=1, + ) + let future_read = background_group.spawn(async fn() -> Unit { + let signal : FixedArray[Unit] = [()] + guard future_read_started_sink.write_all(signal[:]) else { panic() } + future_read_started_sink.close() + let _ = pending_future.get() catch { _ => return } + }) + guard future_read_started.read(1) is Some(signal) && signal.length() == 1 else { + panic() + } + pending_future.drop() + future_read.wait() + guard @pending.open_future_reader_dropped() else { panic() } + + let cancelled_future = @pending.open_future() + let (future_cancel_started, future_cancel_started_sink) = @async-core.Stream::new( + capacity=1, + ) + let future_cancel = background_group.spawn( + async fn() -> Unit { + let signal : FixedArray[Unit] = [()] + guard future_cancel_started_sink.write_all(signal[:]) else { panic() } + future_cancel_started_sink.close() + let _ = cancelled_future.get() + }, + allow_failure=true, + ) + guard future_cancel_started.read(1) is Some(signal) && signal.length() == 1 else { + panic() + } + future_cancel.cancel() + let future_was_cancelled = try future_cancel.wait() catch { + @async-core.Cancelled::Cancelled => true + _ => false + } noraise { + _ => false + } + guard future_was_cancelled else { panic() } + guard @pending.open_future_reader_dropped() else { panic() } + + let pending_stream = @pending.open_stream() + let (stream_read_started, stream_read_started_sink) = @async-core.Stream::new( + capacity=1, + ) + let stream_read = background_group.spawn(async fn() -> Unit { + let signal : FixedArray[Unit] = [()] + guard stream_read_started_sink.write_all(signal[:]) else { panic() } + stream_read_started_sink.close() + let _ = pending_stream.read(1) catch { _ => return } + }) + guard stream_read_started.read(1) is Some(signal) && signal.length() == 1 else { + panic() + } + pending_stream.drop() + stream_read.wait() + guard @pending.open_stream_reader_dropped() else { panic() } + + let cancelled_stream = @pending.open_stream() + let (stream_cancel_started, stream_cancel_started_sink) = @async-core.Stream::new( + capacity=1, + ) + let stream_cancel = background_group.spawn( + async fn() -> Unit { + let signal : FixedArray[Unit] = [()] + guard stream_cancel_started_sink.write_all(signal[:]) else { panic() } + stream_cancel_started_sink.close() + let _ = cancelled_stream.read(1) + }, + allow_failure=true, + ) + guard stream_cancel_started.read(1) is Some(signal) && signal.length() == 1 else { + panic() + } + stream_cancel.cancel() + let stream_was_cancelled = try stream_cancel.wait() catch { + @async-core.Cancelled::Cancelled => true + _ => false + } noraise { + _ => false + } + guard stream_was_cancelled else { panic() } + guard @pending.open_stream_reader_dropped() else { panic() } + guard @pending.leaf_live_count() == 0U else { panic() } + guard @pending.leaf_drop_count() == 17U else { panic() } +} diff --git a/tests/runtime/moonbit/async-import-cancel/test.rs b/tests/runtime/moonbit/async-import-cancel/test.rs new file mode 100644 index 000000000..a432a3b78 --- /dev/null +++ b/tests/runtime/moonbit/async-import-cancel/test.rs @@ -0,0 +1,181 @@ +include!(env!("BINDINGS")); + +use crate::test::async_import_cancel::gate_control; +use crate::exports::test::async_import_cancel::pending::{ + Guest, GuestLeaf, Leaf, OwnedInput, ReturnedPayload, +}; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use std::sync::Mutex; +use wit_bindgen::{ + FutureReader, FutureWriter, StreamReader, StreamResult, StreamWriter, +}; + +struct Component; + +export!(Component); + +static LEAF_LIVE_COUNT: AtomicU32 = AtomicU32::new(0); +static LEAF_DROP_COUNT: AtomicU32 = AtomicU32::new(0); +static PENDING_STARTED: AtomicBool = AtomicBool::new(false); +static PENDING_STREAM_STARTED: AtomicBool = AtomicBool::new(false); +static TAKE_AFTER_START_STARTED: AtomicBool = AtomicBool::new(false); +static OPEN_FUTURE_WRITER: Mutex>> = Mutex::new(None); +static RACE_FUTURE_WRITER: Mutex>> = Mutex::new(None); +static OPEN_STREAM_WRITER: Mutex>> = Mutex::new(None); +static RACE_STREAM_WRITER: Mutex>> = Mutex::new(None); + +struct MyLeaf; + +impl MyLeaf { + fn tracked() -> Self { + LEAF_LIVE_COUNT.fetch_add(1, Ordering::SeqCst); + Self + } +} + +impl Guest for Component { + type Leaf = MyLeaf; + + async fn pending(value: FutureReader) { + PENDING_STARTED.store(true, Ordering::SeqCst); + let _ = value.await; + } + + fn pending_started() -> bool { + PENDING_STARTED.load(Ordering::SeqCst) + } + + async fn pending_stream(mut value: StreamReader) { + PENDING_STREAM_STARTED.store(true, Ordering::SeqCst); + let _ = value.read(Vec::with_capacity(1)).await; + } + + fn pending_stream_started() -> bool { + PENDING_STREAM_STARTED.load(Ordering::SeqCst) + } + + fn open_future() -> FutureReader { + let (writer, reader) = wit_future::new::(|| Leaf::new(MyLeaf::tracked())); + assert!(OPEN_FUTURE_WRITER.lock().unwrap().replace(writer).is_none()); + reader + } + + async fn open_future_reader_dropped() -> bool { + let writer = OPEN_FUTURE_WRITER.lock().unwrap().take().unwrap(); + match writer.write(Leaf::new(MyLeaf::tracked())).await { + Ok(()) => false, + Err(error) => { + drop(error.value); + true + } + } + } + + fn open_race_future() -> FutureReader { + let (writer, reader) = wit_future::new::(|| Leaf::new(MyLeaf::tracked())); + assert!(RACE_FUTURE_WRITER.lock().unwrap().replace(writer).is_none()); + reader + } + + async fn complete_race_future() { + let writer = RACE_FUTURE_WRITER.lock().unwrap().take().unwrap(); + writer + .write(Leaf::new(MyLeaf::tracked())) + .await + .unwrap(); + } + + fn open_stream() -> StreamReader { + let (writer, reader) = wit_stream::new::(); + assert!(OPEN_STREAM_WRITER.lock().unwrap().replace(writer).is_none()); + reader + } + + async fn open_stream_reader_dropped() -> bool { + let mut writer = OPEN_STREAM_WRITER.lock().unwrap().take().unwrap(); + let (result, remaining) = writer + .write(vec![Leaf::new(MyLeaf::tracked())]) + .await; + let dropped = result == StreamResult::Dropped && remaining.remaining() == 1; + drop(remaining); + dropped + } + + fn open_race_stream() -> StreamReader { + let (writer, reader) = wit_stream::new::(); + assert!(RACE_STREAM_WRITER.lock().unwrap().replace(writer).is_none()); + reader + } + + async fn complete_race_stream() { + let mut writer = RACE_STREAM_WRITER.lock().unwrap().take().unwrap(); + let (result, remaining) = writer + .write(vec![Leaf::new(MyLeaf::tracked())]) + .await; + assert_eq!(result, StreamResult::Complete(1)); + assert_eq!(remaining.remaining(), 0); + } + + async fn drop_future_stream(value: StreamReader>) { + drop(value); + } + + async fn take_after_start(_value: OwnedInput) { + TAKE_AFTER_START_STARTED.store(true, Ordering::SeqCst); + loop { + wit_bindgen::yield_async().await; + } + } + + fn take_after_start_started() -> bool { + TAKE_AFTER_START_STARTED.load(Ordering::SeqCst) + } + + async fn return_after_cancel(value: Leaf) -> ReturnedPayload { + gate_control::mark_started(); + while !gate_control::released() { + let _ = wit_bindgen::yield_blocking(); + } + let (ready_writer, ready) = wit_future::new::(|| Leaf::new(MyLeaf::tracked())); + drop(ready_writer); + ReturnedPayload { + label: "returned after cancellation".into(), + leaf: value, + leaves: vec![Leaf::new(MyLeaf::tracked())], + ready, + } + } + + async fn settle() { + let _ = wit_bindgen::yield_blocking(); + } + + fn backpressure_set(value: bool) { + if value { + wit_bindgen::backpressure_inc(); + } else { + wit_bindgen::backpressure_dec(); + } + } + + fn leaf_live_count() -> u32 { + LEAF_LIVE_COUNT.load(Ordering::SeqCst) + } + + fn leaf_drop_count() -> u32 { + LEAF_DROP_COUNT.load(Ordering::SeqCst) + } +} + +impl GuestLeaf for MyLeaf { + fn new() -> Self { + Self::tracked() + } +} + +impl Drop for MyLeaf { + fn drop(&mut self) { + LEAF_LIVE_COUNT.fetch_sub(1, Ordering::SeqCst); + LEAF_DROP_COUNT.fetch_add(1, Ordering::SeqCst); + } +} diff --git a/tests/runtime/moonbit/async-import-cancel/test.wit b/tests/runtime/moonbit/async-import-cancel/test.wit new file mode 100644 index 000000000..1589ed5be --- /dev/null +++ b/tests/runtime/moonbit/async-import-cancel/test.wit @@ -0,0 +1,66 @@ +//@ async = true +//@ dependencies = ['test', 'gate'] + +package test:async-import-cancel; + +interface gate-control { + mark-started: func(); + started: func() -> bool; + release: func(); + released: func() -> bool; +} + +interface pending { + resource leaf { + constructor(); + } + + record returned-payload { + label: string, + leaf: leaf, + leaves: list, + ready: future, + } + + record owned-input { + direct: leaf, + future-value: future, + stream-value: stream, + } + + pending: async func(value: future); + pending-started: func() -> bool; + pending-stream: async func(value: stream); + pending-stream-started: func() -> bool; + open-future: func() -> future; + open-future-reader-dropped: async func() -> bool; + open-race-future: func() -> future; + complete-race-future: async func(); + open-stream: func() -> stream; + open-stream-reader-dropped: async func() -> bool; + open-race-stream: func() -> stream; + complete-race-stream: async func(); + drop-future-stream: async func(value: stream>); + take-after-start: async func(value: owned-input); + take-after-start-started: func() -> bool; + return-after-cancel: async func(value: leaf) -> returned-payload; + settle: async func(); + backpressure-set: func(value: bool); + leaf-live-count: func() -> u32; + leaf-drop-count: func() -> u32; +} + +world test { + import gate-control; + export pending; +} + +world gate { + export gate-control; +} + +world runner { + import pending; + import gate-control; + export run: async func(); +} diff --git a/tests/runtime/moonbit/concurrent-export-isolation/runner.mbt b/tests/runtime/moonbit/concurrent-export-isolation/runner.mbt new file mode 100644 index 000000000..e6478cb77 --- /dev/null +++ b/tests/runtime/moonbit/concurrent-export-isolation/runner.mbt @@ -0,0 +1,133 @@ +//@ wasmtime-flags = '-Wcomponent-model-async' +//@ [lang] +//@ path = 'gen/world/runner/stub.mbt' +//@ pkg_config = """{ "warn-list": "-44", "import": [{ "path": "test/moonbit-concurrent-export-isolation/async-core", "alias": "async-core" }, { "path": "test/moonbit-concurrent-export-isolation/interface/test/moonbit-concurrent-export-isolation/operations", "alias": "operations" }] }""" + +///| +pub async fn run(background_group : @async-core.TaskGroup[Unit]) -> Unit { + let isolated = background_group.spawn( + async fn() -> Bool { @operations.check_context_isolation() }, + allow_failure=true, + ) + let isolated_again = background_group.spawn( + async fn() -> Bool { @operations.check_context_isolation() }, + allow_failure=true, + ) + let completed = background_group.spawn( + async fn() -> UInt { @operations.complete_now() }, + allow_failure=true, + ) + guard isolated.wait() else { panic() } + guard isolated_again.wait() else { panic() } + guard completed.wait() == 42U else { panic() } + + let (gate, gate_sink) = @async-core.Stream::new(capacity=0) + let waiting = background_group.spawn( + async fn() -> Unit { + @operations.wait_for_gate( + @async-core.Future::from(async fn() { + guard gate.read(1) is Some(signal) && signal.length() == 1 + }), + ) + }, + allow_failure=true, + ) + let completing = background_group.spawn( + async fn() -> UInt { + let result = @operations.complete_now() + let signal : FixedArray[Unit] = [()] + guard gate_sink.write_all(signal[:]) else { panic() } + gate_sink.close() + result + }, + allow_failure=true, + ) + guard completing.wait() == 42U else { panic() } + waiting.wait() + + let (release_stream, release_sink) = @async-core.Stream::new(capacity=0) + let held_stream = @operations.hold_stream( + @async-core.Future::from(async fn() { + guard release_stream.read(1) is Some(signal) && signal.length() == 1 else { + abort("stream release signal closed early") + } + }), + ) + let completing_while_stream_is_held = background_group.spawn( + async fn() -> UInt { @operations.complete_now() }, + allow_failure=true, + ) + + // Give the imported call several scheduler turns without releasing the + // stream producer. Component backpressure must not be tied to bridge life. + let (turns, turn_sink) = @async-core.Stream::new(capacity=0) + let turn_writer = background_group.spawn( + async fn() -> Bool { + let turn : FixedArray[Unit] = [()] + for _ in 0..<32 { + guard turn_sink.write_all(turn[:]) else { return false } + } + turn_sink.close() + true + }, + allow_failure=true, + ) + for _ in 0..<32 { + guard turns.read(1) is Some(turn) && turn.length() == 1 else { panic() } + } + guard turn_writer.wait() else { panic() } + let completed_before_release = completing_while_stream_is_held.try_wait() == + Some(42U) + + let release : FixedArray[Unit] = [()] + guard release_sink.write_all(release[:]) else { panic() } + release_sink.close() + guard held_stream.read(1) is Some(chunk) && + chunk.length() == 1 && + chunk[0] == b'x' else { + panic() + } + guard held_stream.read(1) is None else { panic() } + guard completing_while_stream_is_held.wait() == 42U else { panic() } + guard completed_before_release else { panic() } + + let (cross_future, cross_promise) = @async-core.Future::new() + @operations.begin_cross_task_future_read(cross_future) + for _ in 0..<32 { + if @operations.cross_task_future_read_state() >= 1 { + break + } + ignore(@operations.complete_now()) + } + guard @operations.cross_task_future_read_state() == 1 else { panic() } + @operations.drop_cross_task_future_read() + guard cross_promise.complete(()) else { panic() } + for _ in 0..<32 { + if @operations.cross_task_future_read_state() == 2 { + break + } + ignore(@operations.complete_now()) + } + guard @operations.cross_task_future_read_state() == 2 else { panic() } + + let (cross_stream, cross_sink) = @async-core.Stream::new(capacity=0) + @operations.begin_cross_task_stream_read(cross_stream) + for _ in 0..<32 { + if @operations.cross_task_stream_read_state() >= 1 { + break + } + ignore(@operations.complete_now()) + } + guard @operations.cross_task_stream_read_state() == 1 else { panic() } + @operations.drop_cross_task_stream_read() + let discarded : FixedArray[Byte] = [7] + guard cross_sink.write_all(discarded[:]) else { panic() } + cross_sink.close() + for _ in 0..<32 { + if @operations.cross_task_stream_read_state() == 2 { + break + } + ignore(@operations.complete_now()) + } + guard @operations.cross_task_stream_read_state() == 2 else { panic() } +} diff --git a/tests/runtime/moonbit/concurrent-export-isolation/test.mbt b/tests/runtime/moonbit/concurrent-export-isolation/test.mbt new file mode 100644 index 000000000..7aa60a0fd --- /dev/null +++ b/tests/runtime/moonbit/concurrent-export-isolation/test.mbt @@ -0,0 +1,127 @@ +//@ [lang] +//@ path = 'gen/interface/test/moonbit-concurrent-export-isolation/operations/stub.mbt' + +///| +let cross_task_future : Ref[@async-core.Future[Unit]?] = Ref(None) + +///| +let cross_task_future_state : Ref[Byte] = Ref(0) + +///| +let cross_task_stream : Ref[@async-core.Stream[Byte]?] = Ref(None) + +///| +let cross_task_stream_state : Ref[Byte] = Ref(0) + +///| +pub async fn check_context_isolation( + background_group : @async-core.TaskGroup[Unit], +) -> Bool { + let (turns, turn_sink) = @async-core.Stream::new(capacity=0) + background_group.spawn_bg(async fn() { + let turn : FixedArray[Unit] = [()] + for _ in 0..<2048 { + guard turn_sink.write_all(turn[:]) else { return } + } + turn_sink.close() + }) + // Exceed one scheduler callback budget with real suspend/resume cycles so + // another export starts while this coroutine is still in flight. + for _ in 0..<2048 { + guard turns.read(1) is Some(turn) && turn.length() == 1 else { + return false + } + } + true +} + +///| +pub async fn wait_for_gate( + gate : @async-core.Future[Unit], + _background_group : @async-core.TaskGroup[Unit], +) -> Unit { + let _ = gate.get() +} + +///| +pub async fn hold_stream( + gate : @async-core.Future[Unit], + _background_group : @async-core.TaskGroup[Unit], +) -> @async-core.Stream[Byte] { + @async-core.Stream::produce(async fn(sink) { + let _ = gate.get() + guard sink.write_all_bytes(b"x"[:]) else { return } + sink.close() + }) +} + +///| +pub async fn complete_now( + _background_group : @async-core.TaskGroup[Unit], +) -> UInt { + 42 +} + +///| +pub async fn begin_cross_task_future_read( + value : @async-core.Future[Unit], + background_group : @async-core.TaskGroup[Unit], +) -> Unit { + cross_task_future.val = Some(value) + cross_task_future_state.val = 0 + background_group.spawn_bg(async fn() { + cross_task_future_state.val = 1 + match cross_task_future.val { + Some(value) => value.get() catch { _ => () } + None => panic() + } + cross_task_future_state.val = 2 + }) +} + +///| +pub fn cross_task_future_read_state() -> Byte { + cross_task_future_state.val +} + +///| +pub async fn drop_cross_task_future_read( + _background_group : @async-core.TaskGroup[Unit], +) -> Unit { + match cross_task_future.val { + Some(value) => value.drop() + None => panic() + } +} + +///| +pub async fn begin_cross_task_stream_read( + value : @async-core.Stream[Byte], + background_group : @async-core.TaskGroup[Unit], +) -> Unit { + cross_task_stream.val = Some(value) + cross_task_stream_state.val = 0 + background_group.spawn_bg(async fn() { + cross_task_stream_state.val = 1 + match cross_task_stream.val { + Some(value) => ignore(value.read(1)) catch { _ => () } + None => panic() + } + cross_task_stream_state.val = 2 + }) +} + +///| +pub fn cross_task_stream_read_state() -> Byte { + cross_task_stream_state.val +} + +///| +pub async fn drop_cross_task_stream_read( + _background_group : @async-core.TaskGroup[Unit], +) -> Unit { + match cross_task_stream.val { + Some(value) => value.drop() + None => panic() + } +} diff --git a/tests/runtime/moonbit/concurrent-export-isolation/test.wit b/tests/runtime/moonbit/concurrent-export-isolation/test.wit new file mode 100644 index 000000000..fed56dd4d --- /dev/null +++ b/tests/runtime/moonbit/concurrent-export-isolation/test.wit @@ -0,0 +1,25 @@ +//@ async = true +package test:moonbit-concurrent-export-isolation; + +interface operations { + check-context-isolation: async func() -> bool; + wait-for-gate: async func(gate: future); + hold-stream: async func(gate: future) -> stream; + complete-now: async func() -> u32; + begin-cross-task-future-read: async func(value: future); + cross-task-future-read-state: func() -> u8; + drop-cross-task-future-read: async func(); + begin-cross-task-stream-read: async func(value: stream); + cross-task-stream-read-state: func() -> u8; + drop-cross-task-stream-read: async func(); +} + +world test { + export operations; +} + +world runner { + import operations; + + export run: async func(); +} diff --git a/tests/runtime/moonbit/http-background-hook/deps/wasi-cli-0.3.0/package.wit b/tests/runtime/moonbit/http-background-hook/deps/wasi-cli-0.3.0/package.wit new file mode 100644 index 000000000..219ca0a55 --- /dev/null +++ b/tests/runtime/moonbit/http-background-hook/deps/wasi-cli-0.3.0/package.wit @@ -0,0 +1,27 @@ +package wasi:cli@0.3.0; + +interface types { + enum error-code { + io, + illegal-byte-sequence, + pipe, + } +} + +interface stdout { + use types.{error-code}; + + write-via-stream: func(data: stream) -> future>; +} + +interface stderr { + use types.{error-code}; + + write-via-stream: func(data: stream) -> future>; +} + +interface stdin { + use types.{error-code}; + + read-via-stream: func() -> tuple, future>>; +} diff --git a/tests/runtime/moonbit/http-background-hook/deps/wasi-clocks-0.3.0/package.wit b/tests/runtime/moonbit/http-background-hook/deps/wasi-clocks-0.3.0/package.wit new file mode 100644 index 000000000..c70cccb01 --- /dev/null +++ b/tests/runtime/moonbit/http-background-hook/deps/wasi-clocks-0.3.0/package.wit @@ -0,0 +1,42 @@ +package wasi:clocks@0.3.0; + +interface types { + type duration = u64; +} + +interface monotonic-clock { + use types.{duration}; + + type mark = u64; + + now: func() -> mark; + + get-resolution: func() -> duration; + + wait-until: async func(when: mark); + + wait-for: async func(how-long: duration); +} + +interface system-clock { + use types.{duration}; + + record instant { + seconds: s64, + nanoseconds: u32, + } + + now: func() -> instant; + + get-resolution: func() -> duration; +} + +interface timezone { + use system-clock.{instant}; + + iana-id: func() -> option; + + utc-offset: func(when: instant) -> option; + + to-debug-string: func() -> string; +} diff --git a/tests/runtime/moonbit/http-background-hook/deps/wasi-http-0.3.0/package.wit b/tests/runtime/moonbit/http-background-hook/deps/wasi-http-0.3.0/package.wit new file mode 100644 index 000000000..08458f7c6 --- /dev/null +++ b/tests/runtime/moonbit/http-background-hook/deps/wasi-http-0.3.0/package.wit @@ -0,0 +1,509 @@ +package wasi:http@0.3.0; + +/// This interface defines all of the types and methods for implementing HTTP +/// Requests and Responses, as well as their headers, trailers, and bodies. +@since(version = 0.3.0) +interface types { + use wasi:clocks/types@0.3.0.{duration}; + + /// This type corresponds to HTTP standard Methods. + @since(version = 0.3.0) + variant method { + get, + head, + post, + put, + delete, + connect, + options, + trace, + patch, + other(string), + } + + /// This type corresponds to HTTP standard Related Schemes. + @since(version = 0.3.0) + variant scheme { + HTTP, + HTTPS, + other(string), + } + + /// Defines the case payload type for `DNS-error` above: + @since(version = 0.3.0) + record DNS-error-payload { + rcode: option, + info-code: option, + } + + /// Defines the case payload type for `TLS-alert-received` above: + @since(version = 0.3.0) + record TLS-alert-received-payload { + alert-id: option, + alert-message: option, + } + + /// Defines the case payload type for `HTTP-response-{header,trailer}-size` above: + @since(version = 0.3.0) + record field-size-payload { + field-name: option, + field-size: option, + } + + /// These cases are inspired by the IANA HTTP Proxy Error Types: + /// + @since(version = 0.3.0) + variant error-code { + DNS-timeout, + DNS-error(DNS-error-payload), + destination-not-found, + destination-unavailable, + destination-IP-prohibited, + destination-IP-unroutable, + connection-refused, + connection-terminated, + connection-timeout, + connection-read-timeout, + connection-write-timeout, + connection-limit-reached, + TLS-protocol-error, + TLS-certificate-error, + TLS-alert-received(TLS-alert-received-payload), + HTTP-request-denied, + HTTP-request-length-required, + HTTP-request-body-size(option), + HTTP-request-method-invalid, + HTTP-request-URI-invalid, + HTTP-request-URI-too-long, + HTTP-request-header-section-size(option), + HTTP-request-header-size(option), + HTTP-request-trailer-section-size(option), + HTTP-request-trailer-size(field-size-payload), + HTTP-response-incomplete, + HTTP-response-header-section-size(option), + HTTP-response-header-size(field-size-payload), + HTTP-response-body-size(option), + HTTP-response-trailer-section-size(option), + HTTP-response-trailer-size(field-size-payload), + HTTP-response-transfer-coding(option), + HTTP-response-content-coding(option), + HTTP-response-timeout, + HTTP-upgrade-failed, + HTTP-protocol-error, + loop-detected, + configuration-error, + /// This is a catch-all error for anything that doesn't fit cleanly into a + /// more specific case. It also includes an optional string for an + /// unstructured description of the error. Users should not depend on the + /// string for diagnosing errors, as it's not required to be consistent + /// between implementations. + internal-error(option), + } + + /// This type enumerates the different kinds of errors that may occur when + /// setting or appending to a `fields` resource. + @since(version = 0.3.0) + variant header-error { + /// This error indicates that a `field-name` or `field-value` was + /// syntactically invalid when used with an operation that sets headers in a + /// `fields`. + invalid-syntax, + /// This error indicates that a forbidden `field-name` was used when trying + /// to set a header in a `fields`. + forbidden, + /// This error indicates that the operation on the `fields` was not + /// permitted because the fields are immutable. + immutable, + /// This error indicates that the operation would exceed an + /// implementation-defined limit on field sizes. This may apply to + /// an individual `field-value`, a single `field-name` plus all its + /// values, or the total aggregate size of all fields. + size-exceeded, + /// This is a catch-all error for anything that doesn't fit cleanly into a + /// more specific case. Implementations can use this to extend the error + /// type without breaking existing code. It also includes an optional + /// string for an unstructured description of the error. Users should not + /// depend on the string for diagnosing errors, as it's not required to be + /// consistent between implementations. + other(option), + } + + /// This type enumerates the different kinds of errors that may occur when + /// setting fields of a `request-options` resource. + @since(version = 0.3.0) + variant request-options-error { + /// Indicates the specified field is not supported by this implementation. + not-supported, + /// Indicates that the operation on the `request-options` was not permitted + /// because it is immutable. + immutable, + /// This is a catch-all error for anything that doesn't fit cleanly into a + /// more specific case. Implementations can use this to extend the error + /// type without breaking existing code. It also includes an optional + /// string for an unstructured description of the error. Users should not + /// depend on the string for diagnosing errors, as it's not required to be + /// consistent between implementations. + other(option), + } + + /// Field names are always strings. + /// + /// Field names should always be treated as case insensitive by the `fields` + /// resource for the purposes of equality checking. + @since(version = 0.3.0) + type field-name = string; + + /// Field values should always be ASCII strings. However, in + /// reality, HTTP implementations often have to interpret malformed values, + /// so they are provided as a list of bytes. + @since(version = 0.3.0) + type field-value = list; + + /// This following block defines the `fields` resource which corresponds to + /// HTTP standard Fields. Fields are a common representation used for both + /// Headers and Trailers. + /// + /// A `fields` may be mutable or immutable. A `fields` created using the + /// constructor, `from-list`, or `clone` will be mutable, but a `fields` + /// resource given by other means (including, but not limited to, + /// `request.headers`) might be be immutable. In an immutable fields, the + /// `set`, `append`, and `delete` operations will fail with + /// `header-error.immutable`. + /// + /// A `fields` resource should store `field-name`s and `field-value`s in their + /// original casing used to construct or mutate the `fields` resource. The `fields` + /// resource should use that original casing when serializing the fields for + /// transport or when returning them from a method. + /// + /// Implementations may impose limits on individual field values and on total + /// aggregate field section size. Operations that would exceed these limits + /// fail with `header-error.size-exceeded` + @since(version = 0.3.0) + resource fields { + /// Construct an empty HTTP Fields. + /// + /// The resulting `fields` is mutable. + constructor(); + /// Construct an HTTP Fields. + /// + /// The resulting `fields` is mutable. + /// + /// The list represents each name-value pair in the Fields. Names + /// which have multiple values are represented by multiple entries in this + /// list with the same name. + /// + /// The tuple is a pair of the field name, represented as a string, and + /// Value, represented as a list of bytes. In a valid Fields, all names + /// and values are valid UTF-8 strings. However, values are not always + /// well-formed, so they are represented as a raw list of bytes. + /// + /// An error result will be returned if any header or value was + /// syntactically invalid, if a header was forbidden, or if the + /// entries would exceed an implementation size limit. + from-list: static func(entries: list>) -> result; + /// Get all of the values corresponding to a name. If the name is not present + /// in this `fields`, an empty list is returned. However, if the name is + /// present but empty, this is represented by a list with one or more + /// empty field-values present. + get: func(name: field-name) -> list; + /// Returns `true` when the name is present in this `fields`. If the name is + /// syntactically invalid, `false` is returned. + has: func(name: field-name) -> bool; + /// Set all of the values for a name. Clears any existing values for that + /// name, if they have been set. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + /// + /// Fails with `header-error.size-exceeded` if the name or values would + /// exceed an implementation-defined size limit. + set: func(name: field-name, value: list) -> result<_, header-error>; + /// Delete all values for a name. Does nothing if no values for the name + /// exist. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + delete: func(name: field-name) -> result<_, header-error>; + /// Delete all values for a name. Does nothing if no values for the name + /// exist. + /// + /// Returns all values previously corresponding to the name, if any. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + get-and-delete: func(name: field-name) -> result, header-error>; + /// Append a value for a name. Does not change or delete any existing + /// values for that name. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + /// + /// Fails with `header-error.size-exceeded` if the value would exceed + /// an implementation-defined size limit. + append: func(name: field-name, value: field-value) -> result<_, header-error>; + /// Retrieve the full set of names and values in the Fields. Like the + /// constructor, the list represents each name-value pair. + /// + /// The outer list represents each name-value pair in the Fields. Names + /// which have multiple values are represented by multiple entries in this + /// list with the same name. + /// + /// The names and values are always returned in the original casing and in + /// the order in which they will be serialized for transport. + copy-all: func() -> list>; + /// Make a deep copy of the Fields. Equivalent in behavior to calling the + /// `fields` constructor on the return value of `copy-all`. The resulting + /// `fields` is mutable. + clone: func() -> fields; + } + + /// Headers is an alias for Fields. + @since(version = 0.3.0) + type headers = fields; + + /// Trailers is an alias for Fields. + @since(version = 0.3.0) + type trailers = fields; + + /// Represents an HTTP Request. + @since(version = 0.3.0) + resource request { + /// Construct a new `request` with a default `method` of `GET`, and + /// `none` values for `path-with-query`, `scheme`, and `authority`. + /// + /// `headers` is the HTTP Headers for the Request. + /// + /// `contents` is the optional body content stream with `none` + /// representing a zero-length content stream. + /// Once it is closed, `trailers` future must resolve to a result. + /// If `trailers` resolves to an error, underlying connection + /// will be closed immediately. + /// + /// `options` is optional `request-options` resource to be used + /// if the request is sent over a network connection. + /// + /// It is possible to construct, or manipulate with the accessor functions + /// below, a `request` with an invalid combination of `scheme` + /// and `authority`, or `headers` which are not permitted to be sent. + /// It is the obligation of the `handler.handle` implementation + /// to reject invalid constructions of `request`. + /// + /// The returned future resolves to result of transmission of this request. + new: static func(headers: headers, contents: option>, trailers: future, error-code>>, options: option) -> tuple>>; + /// Get the Method for the Request. + get-method: func() -> method; + /// Set the Method for the Request. Fails if the string present in a + /// `method.other` argument is not a syntactically valid method. + set-method: func(method: method) -> result; + /// Get the combination of the HTTP Path and Query for the Request. When + /// `none`, this represents an empty Path and empty Query. + get-path-with-query: func() -> option; + /// Set the combination of the HTTP Path and Query for the Request. When + /// `none`, this represents an empty Path and empty Query. Fails is the + /// string given is not a syntactically valid path and query uri component. + set-path-with-query: func(path-with-query: option) -> result; + /// Get the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. + get-scheme: func() -> option; + /// Set the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. Fails if the + /// string given is not a syntactically valid uri scheme. + set-scheme: func(scheme: option) -> result; + /// Get the authority of the Request's target URI. A value of `none` may be used + /// with Related Schemes which do not require an authority. The HTTP and + /// HTTPS schemes always require an authority. + get-authority: func() -> option; + /// Set the authority of the Request's target URI. A value of `none` may be used + /// with Related Schemes which do not require an authority. The HTTP and + /// HTTPS schemes always require an authority. Fails if the string given is + /// not a syntactically valid URI authority. + set-authority: func(authority: option) -> result; + /// Get the `request-options` to be associated with this request + /// + /// The returned `request-options` resource is immutable: `set-*` operations + /// will fail if invoked. + /// + /// This `request-options` resource is a child: it must be dropped before + /// the parent `request` is dropped, or its ownership is transferred to + /// another component by e.g. `handler.handle`. + get-options: func() -> option; + /// Get the headers associated with the Request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + get-headers: func() -> headers; + /// Get body of the Request. + /// + /// Stream returned by this method represents the contents of the body. + /// Once the stream is reported as closed, callers should await the returned + /// future to determine whether the body was received successfully. + /// The future will only resolve after the stream is reported as closed. + /// + /// This function takes a `res` future as a parameter, which can be used to + /// communicate an error in handling of the request. + /// + /// Note that function will move the `request`, but references to headers or + /// request options acquired from it previously will remain valid. + consume-body: static func(this: request, res: future>) -> tuple, future, error-code>>>; + } + + /// Parameters for making an HTTP Request. Each of these parameters is + /// currently an optional timeout applicable to the transport layer of the + /// HTTP protocol. + /// + /// These timeouts are separate from any the user may use to bound an + /// asynchronous call. + @since(version = 0.3.0) + resource request-options { + /// Construct a default `request-options` value. + constructor(); + /// The timeout for the initial connect to the HTTP Server. + get-connect-timeout: func() -> option; + /// Set the timeout for the initial connect to the HTTP Server. An error + /// return value indicates that this timeout is not supported or that this + /// handle is immutable. + set-connect-timeout: func(duration: option) -> result<_, request-options-error>; + /// The timeout for receiving the first byte of the Response body. + get-first-byte-timeout: func() -> option; + /// Set the timeout for receiving the first byte of the Response body. An + /// error return value indicates that this timeout is not supported or that + /// this handle is immutable. + set-first-byte-timeout: func(duration: option) -> result<_, request-options-error>; + /// The timeout for receiving subsequent chunks of bytes in the Response + /// body stream. + get-between-bytes-timeout: func() -> option; + /// Set the timeout for receiving subsequent chunks of bytes in the Response + /// body stream. An error return value indicates that this timeout is not + /// supported or that this handle is immutable. + set-between-bytes-timeout: func(duration: option) -> result<_, request-options-error>; + /// Make a deep copy of the `request-options`. + /// The resulting `request-options` is mutable. + clone: func() -> request-options; + } + + /// This type corresponds to the HTTP standard Status Code. + @since(version = 0.3.0) + type status-code = u16; + + /// Represents an HTTP Response. + @since(version = 0.3.0) + resource response { + /// Construct a new `response`, with a default `status-code` of `200`. + /// If a different `status-code` is needed, it must be set via the + /// `set-status-code` method. + /// + /// `headers` is the HTTP Headers for the Response. + /// + /// `contents` is the optional body content stream with `none` + /// representing a zero-length content stream. + /// Once it is closed, `trailers` future must resolve to a result. + /// If `trailers` resolves to an error, underlying connection + /// will be closed immediately. + /// + /// The returned future resolves to result of transmission of this response. + new: static func(headers: headers, contents: option>, trailers: future, error-code>>) -> tuple>>; + /// Get the HTTP Status Code for the Response. + get-status-code: func() -> status-code; + /// Set the HTTP Status Code for the Response. Fails if the status-code + /// given is not a valid http status code. + set-status-code: func(status-code: status-code) -> result; + /// Get the headers associated with the Response. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + get-headers: func() -> headers; + /// Get body of the Response. + /// + /// Stream returned by this method represents the contents of the body. + /// Once the stream is reported as closed, callers should await the returned + /// future to determine whether the body was received successfully. + /// The future will only resolve after the stream is reported as closed. + /// + /// This function takes a `res` future as a parameter, which can be used to + /// communicate an error in handling of the response. + /// + /// Note that function will move the `response`, but references to headers + /// acquired from it previously will remain valid. + consume-body: static func(this: response, res: future>) -> tuple, future, error-code>>>; + } +} + +/// This interface defines a handler of HTTP Requests. +/// +/// In a `wasi:http/service` this interface is exported to respond to an +/// incoming HTTP Request with a Response. +/// +/// In `wasi:http/middleware` this interface is both exported and imported as +/// the "downstream" and "upstream" directions of the middleware chain. +@since(version = 0.3.0) +interface handler { + use types.{request, response, error-code}; + + /// This function may be called with either an incoming request read from the + /// network or a request synthesized or forwarded by another component. + handle: async func(request: request) -> result; +} + +/// This interface defines an HTTP client for sending "outgoing" requests. +/// +/// Most components are expected to import this interface to provide the +/// capability to send HTTP requests to arbitrary destinations on a network. +/// +/// The type signature of `client.send` is the same as `handler.handle`. This +/// duplication is currently necessary because some Component Model tooling +/// (including WIT itself) is unable to represent a component importing two +/// instances of the same interface. A `client.send` import may be linked +/// directly to a `handler.handle` export to bypass the network. +@since(version = 0.3.0) +interface client { + use types.{request, response, error-code}; + + /// This function may be used to either send an outgoing request over the + /// network or to forward it to another component. + send: async func(request: request) -> result; +} + +/// The `wasi:http/service` world captures a broad category of HTTP services +/// including web applications, API servers, and proxies. It may be `include`d +/// in more specific worlds such as `wasi:http/middleware`. +@since(version = 0.3.0) +world service { + import wasi:cli/types@0.3.0; + import wasi:cli/stdout@0.3.0; + import wasi:cli/stderr@0.3.0; + import wasi:cli/stdin@0.3.0; + import wasi:clocks/types@0.3.0; + import types; + import client; + import wasi:clocks/monotonic-clock@0.3.0; + import wasi:clocks/system-clock@0.3.0; + @unstable(feature = clocks-timezone) + import wasi:clocks/timezone@0.3.0; + import wasi:random/random@0.3.0; + import wasi:random/insecure@0.3.0; + import wasi:random/insecure-seed@0.3.0; + + export handler; +} +/// The `wasi:http/middleware` world captures HTTP services that forward HTTP +/// Requests to another handler. +/// +/// Components may implement this world to allow them to participate in handler +/// "chains" where a `request` flows through handlers on its way to some terminal +/// `service` and corresponding `response` flows in the opposite direction. +@since(version = 0.3.0) +world middleware { + import wasi:clocks/types@0.3.0; + import types; + import handler; + import wasi:cli/types@0.3.0; + import wasi:cli/stdout@0.3.0; + import wasi:cli/stderr@0.3.0; + import wasi:cli/stdin@0.3.0; + import client; + import wasi:clocks/monotonic-clock@0.3.0; + import wasi:clocks/system-clock@0.3.0; + @unstable(feature = clocks-timezone) + import wasi:clocks/timezone@0.3.0; + import wasi:random/random@0.3.0; + import wasi:random/insecure@0.3.0; + import wasi:random/insecure-seed@0.3.0; + + export handler; +} diff --git a/tests/runtime/moonbit/http-background-hook/deps/wasi-random-0.3.0/package.wit b/tests/runtime/moonbit/http-background-hook/deps/wasi-random-0.3.0/package.wit new file mode 100644 index 000000000..ee31d79e8 --- /dev/null +++ b/tests/runtime/moonbit/http-background-hook/deps/wasi-random-0.3.0/package.wit @@ -0,0 +1,17 @@ +package wasi:random@0.3.0; + +interface random { + get-random-bytes: func(max-len: u64) -> list; + + get-random-u64: func() -> u64; +} + +interface insecure { + get-insecure-random-bytes: func(max-len: u64) -> list; + + get-insecure-random-u64: func() -> u64; +} + +interface insecure-seed { + get-insecure-seed: func() -> tuple; +} diff --git a/tests/runtime/moonbit/http-background-hook/runner-invalid.mbt b/tests/runtime/moonbit/http-background-hook/runner-invalid.mbt new file mode 100644 index 000000000..f9c414822 --- /dev/null +++ b/tests/runtime/moonbit/http-background-hook/runner-invalid.mbt @@ -0,0 +1,51 @@ +//@ wasmtime-flags = '-Wcomponent-model-async -S p3 -S http' +//@ [lang] +//@ path = 'gen/world/runner/stub.mbt' +//@ pkg_config = """{ "warn-list": "-44", "import": [{ "path": "example/background-hook/async-core", "alias": "async-core" }, { "path": "example/background-hook/interface/wasi/http/handler", "alias": "handler" }, { "path": "example/background-hook/interface/wasi/http/types", "alias": "types" }] }""" + +///| +pub async fn run(_background_group : @async-core.TaskGroup[Unit]) -> Unit { + let request_body = @async-core.Stream::produce(async fn(sink) { + guard sink.write_all_bytes(b"build=debug"[:]) else { + abort("request body closed early") + } + sink.close() + }) + let (request, request_transmitted) = @types.Request::new( + @types.Fields::fields(), + Some(request_body), + @async-core.Future::ready(Ok(None)), + None, + ) + guard request.set_method(@types.Method::Post) is Ok(_) else { panic() } + guard request.set_path_with_query(Some("/hooks/build")) is Ok(_) else { + panic() + } + + let response = match @handler.handle(request) { + Ok(response) => response + Err(_) => panic() + } + guard response.get_status_code() == 202U else { panic() } + + let (response_body, response_trailers) = response.consume_body( + @async-core.Future::ready(Ok(())), + ) + guard response_body.read(1) is None else { panic() } + response_body.drop() + match response_trailers.get() { + Ok(None) => () + Ok(Some(fields)) => { + fields.drop() + panic() + } + Err(_) => panic() + } + + match request_transmitted.get() { + Err(@types.ErrorCode::InternalError(Some(message))) => { + guard message == "unexpected hook payload" else { panic() } + } + _ => panic() + } +} diff --git a/tests/runtime/moonbit/http-background-hook/runner.mbt b/tests/runtime/moonbit/http-background-hook/runner.mbt new file mode 100644 index 000000000..71845d74b --- /dev/null +++ b/tests/runtime/moonbit/http-background-hook/runner.mbt @@ -0,0 +1,49 @@ +//@ wasmtime-flags = '-Wcomponent-model-async -S p3 -S http' +//@ [lang] +//@ path = 'gen/world/runner/stub.mbt' +//@ pkg_config = """{ "warn-list": "-44", "import": [{ "path": "example/background-hook/async-core", "alias": "async-core" }, { "path": "example/background-hook/interface/wasi/http/handler", "alias": "handler" }, { "path": "example/background-hook/interface/wasi/http/types", "alias": "types" }] }""" + +///| +pub async fn run(_background_group : @async-core.TaskGroup[Unit]) -> Unit { + let request_body = @async-core.Stream::produce(async fn(sink) { + guard sink.write_all_bytes(b"build=release"[:]) else { + abort("request body closed early") + } + sink.close() + }) + let (request, request_transmitted) = @types.Request::new( + @types.Fields::fields(), + Some(request_body), + @async-core.Future::ready(Ok(None)), + None, + ) + guard request.set_method(@types.Method::Post) is Ok(_) else { panic() } + guard request.set_path_with_query(Some("/hooks/build")) is Ok(_) else { + panic() + } + + let response = match @handler.handle(request) { + Ok(response) => response + Err(_) => panic() + } + guard response.get_status_code() == 202U else { panic() } + + let (response_body, response_trailers) = response.consume_body( + @async-core.Future::ready(Ok(())), + ) + guard response_body.read(1) is None else { panic() } + response_body.drop() + match response_trailers.get() { + Ok(None) => () + Ok(Some(fields)) => { + fields.drop() + panic() + } + Err(_) => panic() + } + + match request_transmitted.get() { + Ok(_) => () + Err(_) => panic() + } +} diff --git a/tests/runtime/moonbit/http-background-hook/test.mbt b/tests/runtime/moonbit/http-background-hook/test.mbt new file mode 100644 index 000000000..b979824bb --- /dev/null +++ b/tests/runtime/moonbit/http-background-hook/test.mbt @@ -0,0 +1,82 @@ +//@ wasmtime-flags = '-Wcomponent-model-async -S p3 -S http' +//@ [lang] +//@ path = 'gen/interface/wasi/http/handler/stub.mbt' +//@ pkg_config = """{ "import": [{ "path": "moonbitlang/core/buffer", "alias": "buffer" }, { "path": "example/background-hook/async-core", "alias": "async-core" }, { "path": "example/background-hook/interface/wasi/http/types", "alias": "types" }] }""" + +///| +pub async fn handle( + request : @types.Request, + background_group : @async-core.TaskGroup[Unit], +) -> Result[@types.Response, @types.ErrorCode] { + guard request.get_method() is @types.Method::Post else { + request.drop() + return Err(@types.ErrorCode::HttpRequestMethodInvalid) + } + guard request.get_path_with_query() == Some("/hooks/build") else { + request.drop() + return Err(@types.ErrorCode::HttpRequestUriInvalid) + } + + let (handling_result, handling_promise) = @async-core.Future::new() + let (request_body, request_trailers) = request.consume_body(handling_result) + let payload = @buffer.Buffer() + for ;; { + match request_body.read(4096) { + Some(chunk) => payload.write_iter(chunk.iter()) + None => break + } + } + request_body.drop() + match request_trailers.get() { + Ok(Some(fields)) => fields.drop() + Ok(None) => () + Err(error) => { + let _ = handling_promise.complete(Err(error)) + return Err(error) + } + } + let payload = payload.to_bytes() + + let (response, response_transmitted) = @types.Response::new( + @types.Fields::fields(), + None, + @async-core.Future::ready(Ok(None)), + ) + guard response.set_status_code(202U) is Ok(_) else { + let error = @types.ErrorCode::InternalError( + Some("failed to set hook response status"), + ) + let _ = handling_promise.complete(Err(error)) + response.drop() + return Err(error) + } + + background_group.spawn_bg(async fn() { + let result = (async fn() -> Result[Unit, @types.ErrorCode] { + match response_transmitted.get() { + Err(error) => return Err(error) + Ok(_) => () + } + let expected = b"build=release" + guard payload.length() == expected.length() else { + return Err( + @types.ErrorCode::InternalError(Some("unexpected hook payload")), + ) + } + for i in 0.. Err( + @types.ErrorCode::InternalError(Some("background hook task failed")), + ) + } + let _ = handling_promise.complete(result) + }) + Ok(response) +} diff --git a/tests/runtime/moonbit/http-background-hook/test.wit b/tests/runtime/moonbit/http-background-hook/test.wit new file mode 100644 index 000000000..04f413b0d --- /dev/null +++ b/tests/runtime/moonbit/http-background-hook/test.wit @@ -0,0 +1,12 @@ +package example:background-hook; + +world test { + include wasi:http/service@0.3.0; +} + +world runner { + import wasi:http/types@0.3.0; + import wasi:http/handler@0.3.0; + + export run: async func(); +} diff --git a/tests/runtime/moonbit/http-background-hook/wkg.lock b/tests/runtime/moonbit/http-background-hook/wkg.lock new file mode 100644 index 000000000..1c6187c52 --- /dev/null +++ b/tests/runtime/moonbit/http-background-hook/wkg.lock @@ -0,0 +1,12 @@ +# This file is automatically generated. +# It is not intended for manual editing. +version = 1 + +[[packages]] +name = "wasi:http" +registry = "wasi.dev" + +[[packages.versions]] +requirement = "=0.3.0" +version = "0.3.0" +digest = "sha256:92cd8f3730c00226dc15626a2e7b21834dd187fc221f09818720d228585bbbf7" diff --git a/tests/runtime/moonbit/http-body-trailers/deps/clocks.wit b/tests/runtime/moonbit/http-body-trailers/deps/clocks.wit new file mode 100644 index 000000000..6522c9fdc --- /dev/null +++ b/tests/runtime/moonbit/http-body-trailers/deps/clocks.wit @@ -0,0 +1,7 @@ +package wasi:clocks@0.3.0; + +@since(version = 0.3.0) +interface types { + @since(version = 0.3.0) + type duration = u64; +} diff --git a/tests/runtime/moonbit/http-body-trailers/deps/http.wit b/tests/runtime/moonbit/http-body-trailers/deps/http.wit new file mode 100644 index 000000000..e453ace5a --- /dev/null +++ b/tests/runtime/moonbit/http-body-trailers/deps/http.wit @@ -0,0 +1,460 @@ +package wasi:http@0.3.0; + +/// This interface defines all of the types and methods for implementing HTTP +/// Requests and Responses, as well as their headers, trailers, and bodies. +@since(version = 0.3.0) +interface types { + use wasi:clocks/types@0.3.0.{duration}; + + /// This type corresponds to HTTP standard Methods. + @since(version = 0.3.0) + variant method { + get, + head, + post, + put, + delete, + connect, + options, + trace, + patch, + other(string), + } + + /// This type corresponds to HTTP standard Related Schemes. + @since(version = 0.3.0) + variant scheme { + HTTP, + HTTPS, + other(string), + } + + /// Defines the case payload type for `DNS-error` above: + @since(version = 0.3.0) + record DNS-error-payload { + rcode: option, + info-code: option, + } + + /// Defines the case payload type for `TLS-alert-received` above: + @since(version = 0.3.0) + record TLS-alert-received-payload { + alert-id: option, + alert-message: option, + } + + /// Defines the case payload type for `HTTP-response-{header,trailer}-size` above: + @since(version = 0.3.0) + record field-size-payload { + field-name: option, + field-size: option, + } + + /// These cases are inspired by the IANA HTTP Proxy Error Types: + /// + @since(version = 0.3.0) + variant error-code { + DNS-timeout, + DNS-error(DNS-error-payload), + destination-not-found, + destination-unavailable, + destination-IP-prohibited, + destination-IP-unroutable, + connection-refused, + connection-terminated, + connection-timeout, + connection-read-timeout, + connection-write-timeout, + connection-limit-reached, + TLS-protocol-error, + TLS-certificate-error, + TLS-alert-received(TLS-alert-received-payload), + HTTP-request-denied, + HTTP-request-length-required, + HTTP-request-body-size(option), + HTTP-request-method-invalid, + HTTP-request-URI-invalid, + HTTP-request-URI-too-long, + HTTP-request-header-section-size(option), + HTTP-request-header-size(option), + HTTP-request-trailer-section-size(option), + HTTP-request-trailer-size(field-size-payload), + HTTP-response-incomplete, + HTTP-response-header-section-size(option), + HTTP-response-header-size(field-size-payload), + HTTP-response-body-size(option), + HTTP-response-trailer-section-size(option), + HTTP-response-trailer-size(field-size-payload), + HTTP-response-transfer-coding(option), + HTTP-response-content-coding(option), + HTTP-response-timeout, + HTTP-upgrade-failed, + HTTP-protocol-error, + loop-detected, + configuration-error, + /// This is a catch-all error for anything that doesn't fit cleanly into a + /// more specific case. It also includes an optional string for an + /// unstructured description of the error. Users should not depend on the + /// string for diagnosing errors, as it's not required to be consistent + /// between implementations. + internal-error(option), + } + + /// This type enumerates the different kinds of errors that may occur when + /// setting or appending to a `fields` resource. + @since(version = 0.3.0) + variant header-error { + /// This error indicates that a `field-name` or `field-value` was + /// syntactically invalid when used with an operation that sets headers in a + /// `fields`. + invalid-syntax, + /// This error indicates that a forbidden `field-name` was used when trying + /// to set a header in a `fields`. + forbidden, + /// This error indicates that the operation on the `fields` was not + /// permitted because the fields are immutable. + immutable, + /// This error indicates that the operation would exceed an + /// implementation-defined limit on field sizes. This may apply to + /// an individual `field-value`, a single `field-name` plus all its + /// values, or the total aggregate size of all fields. + size-exceeded, + /// This is a catch-all error for anything that doesn't fit cleanly into a + /// more specific case. Implementations can use this to extend the error + /// type without breaking existing code. It also includes an optional + /// string for an unstructured description of the error. Users should not + /// depend on the string for diagnosing errors, as it's not required to be + /// consistent between implementations. + other(option), + } + + /// This type enumerates the different kinds of errors that may occur when + /// setting fields of a `request-options` resource. + @since(version = 0.3.0) + variant request-options-error { + /// Indicates the specified field is not supported by this implementation. + not-supported, + /// Indicates that the operation on the `request-options` was not permitted + /// because it is immutable. + immutable, + /// This is a catch-all error for anything that doesn't fit cleanly into a + /// more specific case. Implementations can use this to extend the error + /// type without breaking existing code. It also includes an optional + /// string for an unstructured description of the error. Users should not + /// depend on the string for diagnosing errors, as it's not required to be + /// consistent between implementations. + other(option), + } + + /// Field names are always strings. + /// + /// Field names should always be treated as case insensitive by the `fields` + /// resource for the purposes of equality checking. + @since(version = 0.3.0) + type field-name = string; + + /// Field values should always be ASCII strings. However, in + /// reality, HTTP implementations often have to interpret malformed values, + /// so they are provided as a list of bytes. + @since(version = 0.3.0) + type field-value = list; + + /// This following block defines the `fields` resource which corresponds to + /// HTTP standard Fields. Fields are a common representation used for both + /// Headers and Trailers. + /// + /// A `fields` may be mutable or immutable. A `fields` created using the + /// constructor, `from-list`, or `clone` will be mutable, but a `fields` + /// resource given by other means (including, but not limited to, + /// `request.headers`) might be be immutable. In an immutable fields, the + /// `set`, `append`, and `delete` operations will fail with + /// `header-error.immutable`. + /// + /// A `fields` resource should store `field-name`s and `field-value`s in their + /// original casing used to construct or mutate the `fields` resource. The `fields` + /// resource should use that original casing when serializing the fields for + /// transport or when returning them from a method. + /// + /// Implementations may impose limits on individual field values and on total + /// aggregate field section size. Operations that would exceed these limits + /// fail with `header-error.size-exceeded` + @since(version = 0.3.0) + resource fields { + /// Construct an empty HTTP Fields. + /// + /// The resulting `fields` is mutable. + constructor(); + /// Construct an HTTP Fields. + /// + /// The resulting `fields` is mutable. + /// + /// The list represents each name-value pair in the Fields. Names + /// which have multiple values are represented by multiple entries in this + /// list with the same name. + /// + /// The tuple is a pair of the field name, represented as a string, and + /// Value, represented as a list of bytes. In a valid Fields, all names + /// and values are valid UTF-8 strings. However, values are not always + /// well-formed, so they are represented as a raw list of bytes. + /// + /// An error result will be returned if any header or value was + /// syntactically invalid, if a header was forbidden, or if the + /// entries would exceed an implementation size limit. + from-list: static func(entries: list>) -> result; + /// Get all of the values corresponding to a name. If the name is not present + /// in this `fields`, an empty list is returned. However, if the name is + /// present but empty, this is represented by a list with one or more + /// empty field-values present. + get: func(name: field-name) -> list; + /// Returns `true` when the name is present in this `fields`. If the name is + /// syntactically invalid, `false` is returned. + has: func(name: field-name) -> bool; + /// Set all of the values for a name. Clears any existing values for that + /// name, if they have been set. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + /// + /// Fails with `header-error.size-exceeded` if the name or values would + /// exceed an implementation-defined size limit. + set: func(name: field-name, value: list) -> result<_, header-error>; + /// Delete all values for a name. Does nothing if no values for the name + /// exist. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + delete: func(name: field-name) -> result<_, header-error>; + /// Delete all values for a name. Does nothing if no values for the name + /// exist. + /// + /// Returns all values previously corresponding to the name, if any. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + get-and-delete: func(name: field-name) -> result, header-error>; + /// Append a value for a name. Does not change or delete any existing + /// values for that name. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + /// + /// Fails with `header-error.size-exceeded` if the value would exceed + /// an implementation-defined size limit. + append: func(name: field-name, value: field-value) -> result<_, header-error>; + /// Retrieve the full set of names and values in the Fields. Like the + /// constructor, the list represents each name-value pair. + /// + /// The outer list represents each name-value pair in the Fields. Names + /// which have multiple values are represented by multiple entries in this + /// list with the same name. + /// + /// The names and values are always returned in the original casing and in + /// the order in which they will be serialized for transport. + copy-all: func() -> list>; + /// Make a deep copy of the Fields. Equivalent in behavior to calling the + /// `fields` constructor on the return value of `copy-all`. The resulting + /// `fields` is mutable. + clone: func() -> fields; + } + + /// Headers is an alias for Fields. + @since(version = 0.3.0) + type headers = fields; + + /// Trailers is an alias for Fields. + @since(version = 0.3.0) + type trailers = fields; + + /// Represents an HTTP Request. + @since(version = 0.3.0) + resource request { + /// Construct a new `request` with a default `method` of `GET`, and + /// `none` values for `path-with-query`, `scheme`, and `authority`. + /// + /// `headers` is the HTTP Headers for the Request. + /// + /// `contents` is the optional body content stream with `none` + /// representing a zero-length content stream. + /// Once it is closed, `trailers` future must resolve to a result. + /// If `trailers` resolves to an error, underlying connection + /// will be closed immediately. + /// + /// `options` is optional `request-options` resource to be used + /// if the request is sent over a network connection. + /// + /// It is possible to construct, or manipulate with the accessor functions + /// below, a `request` with an invalid combination of `scheme` + /// and `authority`, or `headers` which are not permitted to be sent. + /// It is the obligation of the `handler.handle` implementation + /// to reject invalid constructions of `request`. + /// + /// The returned future resolves to result of transmission of this request. + new: static func(headers: headers, contents: option>, trailers: future, error-code>>, options: option) -> tuple>>; + /// Get the Method for the Request. + get-method: func() -> method; + /// Set the Method for the Request. Fails if the string present in a + /// `method.other` argument is not a syntactically valid method. + set-method: func(method: method) -> result; + /// Get the combination of the HTTP Path and Query for the Request. When + /// `none`, this represents an empty Path and empty Query. + get-path-with-query: func() -> option; + /// Set the combination of the HTTP Path and Query for the Request. When + /// `none`, this represents an empty Path and empty Query. Fails is the + /// string given is not a syntactically valid path and query uri component. + set-path-with-query: func(path-with-query: option) -> result; + /// Get the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. + get-scheme: func() -> option; + /// Set the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. Fails if the + /// string given is not a syntactically valid uri scheme. + set-scheme: func(scheme: option) -> result; + /// Get the authority of the Request's target URI. A value of `none` may be used + /// with Related Schemes which do not require an authority. The HTTP and + /// HTTPS schemes always require an authority. + get-authority: func() -> option; + /// Set the authority of the Request's target URI. A value of `none` may be used + /// with Related Schemes which do not require an authority. The HTTP and + /// HTTPS schemes always require an authority. Fails if the string given is + /// not a syntactically valid URI authority. + set-authority: func(authority: option) -> result; + /// Get the `request-options` to be associated with this request + /// + /// The returned `request-options` resource is immutable: `set-*` operations + /// will fail if invoked. + /// + /// This `request-options` resource is a child: it must be dropped before + /// the parent `request` is dropped, or its ownership is transferred to + /// another component by e.g. `handler.handle`. + get-options: func() -> option; + /// Get the headers associated with the Request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + get-headers: func() -> headers; + /// Get body of the Request. + /// + /// Stream returned by this method represents the contents of the body. + /// Once the stream is reported as closed, callers should await the returned + /// future to determine whether the body was received successfully. + /// The future will only resolve after the stream is reported as closed. + /// + /// This function takes a `res` future as a parameter, which can be used to + /// communicate an error in handling of the request. + /// + /// Note that function will move the `request`, but references to headers or + /// request options acquired from it previously will remain valid. + consume-body: static func(this: request, res: future>) -> tuple, future, error-code>>>; + } + + /// Parameters for making an HTTP Request. Each of these parameters is + /// currently an optional timeout applicable to the transport layer of the + /// HTTP protocol. + /// + /// These timeouts are separate from any the user may use to bound an + /// asynchronous call. + @since(version = 0.3.0) + resource request-options { + /// Construct a default `request-options` value. + constructor(); + /// The timeout for the initial connect to the HTTP Server. + get-connect-timeout: func() -> option; + /// Set the timeout for the initial connect to the HTTP Server. An error + /// return value indicates that this timeout is not supported or that this + /// handle is immutable. + set-connect-timeout: func(duration: option) -> result<_, request-options-error>; + /// The timeout for receiving the first byte of the Response body. + get-first-byte-timeout: func() -> option; + /// Set the timeout for receiving the first byte of the Response body. An + /// error return value indicates that this timeout is not supported or that + /// this handle is immutable. + set-first-byte-timeout: func(duration: option) -> result<_, request-options-error>; + /// The timeout for receiving subsequent chunks of bytes in the Response + /// body stream. + get-between-bytes-timeout: func() -> option; + /// Set the timeout for receiving subsequent chunks of bytes in the Response + /// body stream. An error return value indicates that this timeout is not + /// supported or that this handle is immutable. + set-between-bytes-timeout: func(duration: option) -> result<_, request-options-error>; + /// Make a deep copy of the `request-options`. + /// The resulting `request-options` is mutable. + clone: func() -> request-options; + } + + /// This type corresponds to the HTTP standard Status Code. + @since(version = 0.3.0) + type status-code = u16; + + /// Represents an HTTP Response. + @since(version = 0.3.0) + resource response { + /// Construct a new `response`, with a default `status-code` of `200`. + /// If a different `status-code` is needed, it must be set via the + /// `set-status-code` method. + /// + /// `headers` is the HTTP Headers for the Response. + /// + /// `contents` is the optional body content stream with `none` + /// representing a zero-length content stream. + /// Once it is closed, `trailers` future must resolve to a result. + /// If `trailers` resolves to an error, underlying connection + /// will be closed immediately. + /// + /// The returned future resolves to result of transmission of this response. + new: static func(headers: headers, contents: option>, trailers: future, error-code>>) -> tuple>>; + /// Get the HTTP Status Code for the Response. + get-status-code: func() -> status-code; + /// Set the HTTP Status Code for the Response. Fails if the status-code + /// given is not a valid http status code. + set-status-code: func(status-code: status-code) -> result; + /// Get the headers associated with the Response. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + get-headers: func() -> headers; + /// Get body of the Response. + /// + /// Stream returned by this method represents the contents of the body. + /// Once the stream is reported as closed, callers should await the returned + /// future to determine whether the body was received successfully. + /// The future will only resolve after the stream is reported as closed. + /// + /// This function takes a `res` future as a parameter, which can be used to + /// communicate an error in handling of the response. + /// + /// Note that function will move the `response`, but references to headers + /// acquired from it previously will remain valid. + consume-body: static func(this: response, res: future>) -> tuple, future, error-code>>>; + } +} + +/// This interface defines a handler of HTTP Requests. +/// +/// In a `wasi:http/service` this interface is exported to respond to an +/// incoming HTTP Request with a Response. +/// +/// In `wasi:http/middleware` this interface is both exported and imported as +/// the "downstream" and "upstream" directions of the middleware chain. +@since(version = 0.3.0) +interface handler { + use types.{request, response, error-code}; + + /// This function may be called with either an incoming request read from the + /// network or a request synthesized or forwarded by another component. + handle: async func(request: request) -> result; +} + +/// This interface defines an HTTP client for sending "outgoing" requests. +/// +/// Most components are expected to import this interface to provide the +/// capability to send HTTP requests to arbitrary destinations on a network. +/// +/// The type signature of `client.send` is the same as `handler.handle`. This +/// duplication is currently necessary because some Component Model tooling +/// (including WIT itself) is unable to represent a component importing two +/// instances of the same interface. A `client.send` import may be linked +/// directly to a `handler.handle` export to bypass the network. +@since(version = 0.3.0) +interface client { + use types.{request, response, error-code}; + + /// This function may be used to either send an outgoing request over the + /// network or to forward it to another component. + send: async func(request: request) -> result; +} diff --git a/tests/runtime/moonbit/http-body-trailers/runner.mbt b/tests/runtime/moonbit/http-body-trailers/runner.mbt new file mode 100644 index 000000000..e18c150a5 --- /dev/null +++ b/tests/runtime/moonbit/http-body-trailers/runner.mbt @@ -0,0 +1,47 @@ +//@ wasmtime-flags = '-Wcomponent-model-async -S p3 -S http' +//@ [lang] +//@ path = 'gen/world/runner/stub.mbt' +//@ pkg_config = """{ "warn-list": "-44", "import": [{ "path": "my/test/async-core", "alias": "async-core" }, { "path": "my/test/interface/wasi/http/handler", "alias": "handler" }, { "path": "my/test/interface/wasi/http/types", "alias": "types" }] }""" + +///| +pub async fn run(_background_group : @async-core.TaskGroup[Unit]) -> Unit { + let (request, request_transmitted) = @types.Request::new( + @types.Fields::fields(), + None, + @async-core.Future::ready(Ok(None)), + None, + ) + let response = match @handler.handle(request) { + Ok(response) => response + Err(_) => panic() + } + + // Completing this future tells the handler that the response was transmitted. + let (body, trailers) = @types.Response::consume_body( + response, + @async-core.Future::ready(Ok(())), + ) + let bytes : Array[Byte] = [] + for ;; { + match body.read(4) { + Some(chunk) => + for i = 0; i < chunk.length(); i = i + 1 { + bytes.push(chunk[i]) + } + None => break + } + } + + let expected = b"hello http p3" + guard bytes.length() == expected.length() else { panic() } + for i = 0; i < expected.length(); i = i + 1 { + guard bytes[i] == expected[i] else { panic() } + } + + match trailers.get() { + Ok(Some(fields)) => fields.drop() + Ok(None) | Err(_) => panic() + } + body.drop() + request_transmitted.drop() +} diff --git a/tests/runtime/moonbit/http-body-trailers/test.mbt b/tests/runtime/moonbit/http-body-trailers/test.mbt new file mode 100644 index 000000000..6748a3c5b --- /dev/null +++ b/tests/runtime/moonbit/http-body-trailers/test.mbt @@ -0,0 +1,66 @@ +//@ [lang] +//@ path = 'gen/interface/wasi/http/handler/stub.mbt' +//@ pkg_config = """{ +//@ "import": [ +//@ { "path": "my/test/async-core", "alias": "async-core" }, +//@ { "path": "my/test/interface/wasi/http/types", "alias": "types" } +//@ ] +//@ }""" + +///| +let body_progress : Ref[UInt] = Ref(0) + +///| +pub async fn handle( + request : @types.Request, + background_group : @async-core.TaskGroup[Unit], +) -> Result[@types.Response, @types.ErrorCode] { + request.drop() + body_progress.val = 0 + let transmitted_done = Ref(false) + let (completed, completed_sink) = @async-core.Stream::new(capacity=2) + + let trailers = @async-core.Future::from(async fn() { + for _ in 0..<2 { + guard completed.read(1) is Some(signal) && signal.length() == 1 else { + abort("response completion signal closed early") + } + } + guard body_progress.val >= 3 && transmitted_done.val else { + abort("response completion signal arrived too early") + } + Ok(Some(@types.Fields::fields())) + }) + + let body = @async-core.Stream::produce(async fn(sink) { + body_progress.val = 1 + let _ = sink.write_all_bytes(b"hello "[:]) + body_progress.val = 2 + let _ = sink.write_all_bytes(b"http p3"[:]) + sink.close() + body_progress.val = 3 + let signal : FixedArray[Unit] = [()] + guard completed_sink.write_all(signal[:]) else { + abort("response completion signal closed early") + } + }) + + let (response, transmitted) = @types.Response::new( + @types.Fields::fields(), + Some(body), + trailers, + ) + background_group.spawn_bg(async fn() { + match transmitted.get() { + Ok(_) => { + transmitted_done.val = true + let signal : FixedArray[Unit] = [()] + guard completed_sink.write_all(signal[:]) else { + abort("response completion signal closed early") + } + } + Err(_) => abort("response transmission failed") + } + }) + Ok(response) +} diff --git a/tests/runtime/moonbit/http-body-trailers/test.wit b/tests/runtime/moonbit/http-body-trailers/test.wit new file mode 100644 index 000000000..10365d6a1 --- /dev/null +++ b/tests/runtime/moonbit/http-body-trailers/test.wit @@ -0,0 +1,14 @@ +package my:test; + +world test { + import wasi:http/types@0.3.0; + + export wasi:http/handler@0.3.0; +} + +world runner { + import wasi:http/types@0.3.0; + import wasi:http/handler@0.3.0; + + export run: async func(); +} diff --git a/tests/runtime/moonbit/local-async-primitives/runner.mbt b/tests/runtime/moonbit/local-async-primitives/runner.mbt new file mode 100644 index 000000000..60adaac55 --- /dev/null +++ b/tests/runtime/moonbit/local-async-primitives/runner.mbt @@ -0,0 +1,9 @@ +//@ wasmtime-flags = '-Wcomponent-model-async' +//@ [lang] +//@ path = 'gen/world/runner/stub.mbt' +//@ pkg_config = """{ "import": [{ "path": "test/moonbit-local-async-primitives/async-core", "alias": "async-core" }, { "path": "test/moonbit-local-async-primitives/interface/test/moonbit-local-async-primitives/operations", "alias": "operations" }] }""" + +///| +pub async fn run(_background_group : @async-core.TaskGroup[Unit]) -> Unit { + guard @operations.exercise() else { panic() } +} diff --git a/tests/runtime/moonbit/local-async-primitives/test.mbt b/tests/runtime/moonbit/local-async-primitives/test.mbt new file mode 100644 index 000000000..3cc39b689 --- /dev/null +++ b/tests/runtime/moonbit/local-async-primitives/test.mbt @@ -0,0 +1,311 @@ +//@ wasmtime-flags = '-Wcomponent-model-async' +//@ [lang] +//@ path = 'gen/interface/test/moonbit-local-async-primitives/operations/stub.mbt' + +///| +suberror ExpectedPromiseFailure derive(Debug) + +///| +priv struct LocalOnlyValue { + marker : Int +} + +///| +pub async fn exercise(background_group : @async-core.TaskGroup[Unit]) -> Bool { + let semaphore = @async-core.Semaphore(1, initial_value=1) + guard semaphore.try_acquire() else { return false } + guard !semaphore.try_acquire() else { return false } + semaphore.release() + semaphore.acquire() + + let mutex = @async-core.Mutex() + guard mutex.try_acquire() else { return false } + guard !mutex.try_acquire() else { return false } + let (mutex_started, mutex_started_promise) = @async-core.Future::new() + let mutex_waiter = background_group.spawn( + async fn() -> Bool { + guard mutex_started_promise.complete(()) else { return false } + mutex.acquire() + mutex.release() + true + }, + allow_failure=true, + ) + mutex_started.get() + mutex.release() + guard mutex_waiter.wait() else { return false } + + let condition = @async-core.CondVar() + let condition_ready = Ref(false) + let (condition_started, condition_started_promise) = @async-core.Future::new() + let condition_waiter = background_group.spawn( + async fn() -> Bool { + guard condition_started_promise.complete(()) else { return false } + while !condition_ready.val { + condition.wait() + } + true + }, + allow_failure=true, + ) + condition_started.get() + condition_ready.val = true + condition.signal() + condition_waiter.cancel() + guard condition_waiter.wait() else { return false } + + let cancellation_condition = @async-core.CondVar() + let (condition_cancel_started, condition_cancel_started_promise) = @async-core.Future::new() + let cancelled_condition_waiter = background_group.spawn( + async fn() -> Unit { + guard condition_cancel_started_promise.complete(()) else { panic() } + cancellation_condition.wait() + }, + allow_failure=true, + ) + condition_cancel_started.get() + cancelled_condition_waiter.cancel() + let condition_wait_was_cancelled = try + cancelled_condition_waiter.wait() + catch { + @async-core.Cancelled::Cancelled => true + _ => false + } noraise { + _ => false + } + guard condition_wait_was_cancelled else { return false } + + let (condition_survivor_started, condition_survivor_started_promise) = @async-core.Future::new() + let condition_survivor = background_group.spawn( + async fn() -> Bool { + guard condition_survivor_started_promise.complete(()) else { + return false + } + cancellation_condition.wait() + true + }, + allow_failure=true, + ) + condition_survivor_started.get() + cancellation_condition.broadcast() + guard condition_survivor.wait() else { return false } + + let (ready_future, ready_promise) = @async-core.Future::new() + guard ready_promise.complete(41) else { return false } + guard ready_future.get() == 41 else { return false } + + let (local_future, local_promise) = @async-core.Future::new() + guard local_promise.complete({ marker: 47 }) else { return false } + guard local_future.get().marker == 47 else { return false } + + let cleaned = Ref(0) + let (discarded_future, discarded_promise) = @async-core.Future::new_with_cleanup(fn( + value : Int, + ) { + cleaned.val = value + }, + ) + guard discarded_promise.complete(42) else { return false } + discarded_future.drop() + discarded_future.drop() + guard cleaned.val == 42 else { return false } + + let (dropped_future, dropped_promise) : ( + @async-core.Future[Int], + @async-core.Promise[Int], + ) = @async-core.Future::new() + dropped_future.drop() + guard !dropped_promise.complete(43) else { return false } + + let (failed_future, failed_promise) : ( + @async-core.Future[Int], + @async-core.Promise[Int], + ) = @async-core.Future::new() + guard failed_promise.fail(ExpectedPromiseFailure) else { return false } + let observed_failure = try failed_future.get() catch { + ExpectedPromiseFailure::ExpectedPromiseFailure => true + _ => false + } noraise { + _ => false + } + guard observed_failure else { return false } + + let (closed_future, closed_promise) : ( + @async-core.Future[Int], + @async-core.Promise[Int], + ) = @async-core.Future::new() + guard closed_promise.close() else { return false } + let observed_close = try closed_future.get() catch { + @async-core.PromiseClosed::PromiseClosed => true + _ => false + } noraise { + _ => false + } + guard observed_close else { return false } + + let (pending_future, pending_promise) = @async-core.Future::new() + let (pending_started, pending_started_promise) = @async-core.Future::new() + let pending_waiter = background_group.spawn( + async fn() -> Int { + guard pending_started_promise.complete(()) else { panic() } + pending_future.get() + }, + allow_failure=true, + ) + pending_started.get() + guard pending_promise.complete(44) else { return false } + guard pending_waiter.wait() == 44 else { return false } + + let (cancelled_future, cancelled_promise) = @async-core.Future::new() + let (cancel_started, cancel_started_promise) = @async-core.Future::new() + let cancelled_waiter = background_group.spawn( + async fn() -> Int { + guard cancel_started_promise.complete(()) else { panic() } + cancelled_future.get() + }, + allow_failure=true, + ) + cancel_started.get() + cancelled_waiter.cancel() + let observed_cancel = try cancelled_waiter.wait() catch { + @async-core.Cancelled::Cancelled => true + _ => false + } noraise { + _ => false + } + guard observed_cancel else { return false } + guard !cancelled_promise.complete(45) else { return false } + + let (dropped_while_reading, dropped_while_reading_promise) = @async-core.Future::new() + let (drop_read_started, drop_read_started_promise) = @async-core.Future::new() + let dropped_reader = background_group.spawn( + async fn() -> Bool { + guard drop_read_started_promise.complete(()) else { panic() } + dropped_while_reading.get() catch { + @async-core.Cancelled::Cancelled => return true + _ => return false + } + false + }, + allow_failure=true, + ) + drop_read_started.get() + dropped_while_reading.drop() + guard dropped_reader.wait() else { return false } + guard !dropped_while_reading_promise.complete(()) else { return false } + + let completion_cleanup = Ref(0) + let (completed_while_reading, completed_while_reading_promise) = @async-core.Future::new_with_cleanup(fn( + value : Int, + ) { + completion_cleanup.val = value + }, + ) + let (completed_read_started, completed_read_started_promise) = @async-core.Future::new() + let completed_reader = background_group.spawn( + async fn() -> Int { + guard completed_read_started_promise.complete(()) else { panic() } + completed_while_reading.get() + }, + allow_failure=true, + ) + completed_read_started.get() + guard completed_while_reading_promise.complete(45) else { return false } + completed_while_reading.drop() + guard completed_reader.wait() == 45 else { return false } + guard completion_cleanup.val == 0 else { return false } + + let (racing_future, racing_promise) = @async-core.Future::new() + let (race_started, race_started_promise) = @async-core.Future::new() + let racing_waiter = background_group.spawn( + async fn() -> Int { + guard race_started_promise.complete(()) else { panic() } + racing_future.get() + }, + allow_failure=true, + ) + race_started.get() + guard racing_promise.complete(46) else { return false } + racing_waiter.cancel() + guard racing_waiter.wait() == 46 else { return false } + + let fifo = @async-core.Semaphore(1, initial_value=0) + let phase = Ref(0) + let (first_started, first_started_promise) = @async-core.Future::new() + let first = background_group.spawn( + async fn() -> Bool { + guard first_started_promise.complete(()) else { return false } + fifo.acquire() + guard phase.val == 0 else { return false } + phase.val = 1 + fifo.release() + true + }, + allow_failure=true, + ) + first_started.get() + let (second_started, second_started_promise) = @async-core.Future::new() + let second = background_group.spawn( + async fn() -> Bool { + guard second_started_promise.complete(()) else { return false } + fifo.acquire() + guard phase.val == 1 else { return false } + phase.val = 2 + true + }, + allow_failure=true, + ) + second_started.get() + fifo.release() + guard first.wait() else { return false } + guard second.wait() else { return false } + guard phase.val == 2 else { return false } + + let cancellable = @async-core.Semaphore(1, initial_value=0) + let (cancelled_acquire_started, cancelled_acquire_started_promise) = @async-core.Future::new() + let cancelled_acquire = background_group.spawn( + async fn() -> Bool { + guard cancelled_acquire_started_promise.complete(()) else { return false } + cancellable.acquire() + false + }, + allow_failure=true, + ) + cancelled_acquire_started.get() + cancelled_acquire.cancel() + let acquire_was_cancelled = try cancelled_acquire.wait() catch { + @async-core.Cancelled::Cancelled => true + _ => false + } noraise { + _ => false + } + guard acquire_was_cancelled else { return false } + + let (survivor_started, survivor_started_promise) = @async-core.Future::new() + let survivor = background_group.spawn( + async fn() -> Bool { + guard survivor_started_promise.complete(()) else { return false } + cancellable.acquire() + true + }, + allow_failure=true, + ) + survivor_started.get() + cancellable.release() + guard survivor.wait() else { return false } + + let assigned = @async-core.Semaphore(1, initial_value=0) + let (assigned_started, assigned_started_promise) = @async-core.Future::new() + let assigned_waiter = background_group.spawn( + async fn() -> Bool { + guard assigned_started_promise.complete(()) else { return false } + assigned.acquire() + true + }, + allow_failure=true, + ) + assigned_started.get() + assigned.release() + assigned_waiter.cancel() + assigned_waiter.wait() +} diff --git a/tests/runtime/moonbit/local-async-primitives/test.wit b/tests/runtime/moonbit/local-async-primitives/test.wit new file mode 100644 index 000000000..8fae0534f --- /dev/null +++ b/tests/runtime/moonbit/local-async-primitives/test.wit @@ -0,0 +1,15 @@ +//@ async = true +package test:moonbit-local-async-primitives; + +interface operations { + exercise: async func() -> bool; +} + +world test { + export operations; +} + +world runner { + import operations; + export run: async func(); +} diff --git a/tests/runtime/moonbit/nested-future-stream/runner.rs b/tests/runtime/moonbit/nested-future-stream/runner.rs new file mode 100644 index 000000000..52842639d --- /dev/null +++ b/tests/runtime/moonbit/nested-future-stream/runner.rs @@ -0,0 +1,144 @@ +//@ wasmtime-flags = '-Wcomponent-model-async' + +include!(env!("BINDINGS")); + +use crate::test::moonbit_nested_future_stream::nested::{ + cancellation_observed, concurrent_writes, post_return_lazy, relay, relay_stream, + resolve_shared, wait_cancellation_observed, wait_cancelled, wait_shared, +}; +use futures::task::noop_waker_ref; +use std::future::Future; +use std::task::Context; +use wit_bindgen::{FutureReader, StreamReader, StreamResult}; + +struct Component; + +export!(Component); + +impl Guest for Component { + async fn run() { + transfers_nested_endpoints().await; + rejects_nested_endpoints().await; + transfers_stream_of_futures().await; + serializes_concurrent_stream_writes().await; + wakes_a_different_component_task().await; + materializes_a_lazy_stream_after_return().await; + cancels_a_moonbit_component_task().await; + } +} + +async fn cancels_a_moonbit_component_task() { + let mut task = Box::pin(wait_cancelled()); + assert!(task + .as_mut() + .poll(&mut Context::from_waker(noop_waker_ref())) + .is_pending()); + drop(task); + wait_cancellation_observed().await; + assert!(cancellation_observed()); +} + +async fn wakes_a_different_component_task() { + let (value, ()) = futures::join!(wait_shared(), resolve_shared(42)); + assert_eq!(value, 42); +} + +async fn materializes_a_lazy_stream_after_return() { + let mut stream = post_return_lazy().await; + let (result, values) = stream.read(Vec::with_capacity(1)).await; + assert_eq!(result, StreamResult::Complete(1)); + assert_eq!(values, [42]); +} + +async fn serializes_concurrent_stream_writes() { + let mut stream = concurrent_writes().await; + for _ in 0..4 { + wit_bindgen::yield_async().await; + } + let (first_result, first) = stream.read(Vec::with_capacity(1)).await; + assert_eq!(first_result, StreamResult::Complete(1)); + let (second_result, second) = stream.read(Vec::with_capacity(1)).await; + assert_eq!(second_result, StreamResult::Complete(1)); + let mut values = vec![first[0], second[0]]; + values.sort(); + assert_eq!(values, [1, 2]); +} + +async fn transfers_stream_of_futures() { + let (mut input_writer, input) = wit_stream::new::>(); + let (value_writer1, value_reader1) = wit_future::new::(|| unreachable!()); + let (value_writer2, value_reader2) = wit_future::new::(|| unreachable!()); + let mut output = relay_stream(input).await; + + let send = async { + let (result, remaining) = input_writer.write(vec![value_reader1]).await; + assert_eq!(result, StreamResult::Complete(1)); + assert_eq!(remaining.remaining(), 0); + value_writer1.write(11).await.unwrap(); + + let (result, remaining) = input_writer.write(vec![value_reader2]).await; + assert_eq!(result, StreamResult::Complete(1)); + assert_eq!(remaining.remaining(), 0); + value_writer2.write(22).await.unwrap(); + drop(input_writer); + }; + let receive = async { + let (result, values) = output.read(Vec::with_capacity(1)).await; + assert_eq!(result, StreamResult::Complete(1)); + assert_eq!(values.into_iter().next().unwrap().await, 11); + + let (result, values) = output.read(Vec::with_capacity(1)).await; + assert_eq!(result, StreamResult::Complete(1)); + assert_eq!(values.into_iter().next().unwrap().await, 22); + }; + let ((), ()) = futures::join!(send, receive); +} + +async fn transfers_nested_endpoints() { + let (mut stream_writer, stream_reader) = wit_stream::new::(); + let (inner_writer, inner_reader) = + wit_future::new::>(|| unreachable!()); + let (outer_writer, outer_reader) = + wit_future::new::>>(|| unreachable!()); + let output = relay(outer_reader).await; + + let send = async { + outer_writer.write(inner_reader).await.unwrap(); + inner_writer.write(stream_reader).await.unwrap(); + let (result, remaining) = stream_writer.write(vec![1, 2, 3]).await; + assert_eq!(result, StreamResult::Complete(3)); + assert_eq!(remaining.remaining(), 0); + drop(stream_writer); + }; + let receive = async { + let mut stream = output.await.await; + let (result, bytes) = stream.read(Vec::with_capacity(3)).await; + assert_eq!(result, StreamResult::Complete(3)); + assert_eq!(bytes, [1, 2, 3]); + }; + let ((), ()) = futures::join!(send, receive); +} + +async fn rejects_nested_endpoints() { + let (mut stream_writer, stream_reader) = wit_stream::new::(); + let (inner_writer, inner_reader) = + wit_future::new::>(|| unreachable!()); + let (outer_writer, outer_reader) = + wit_future::new::>>(|| unreachable!()); + let output = relay(outer_reader).await; + drop(output); + + outer_writer.write(inner_reader).await.unwrap(); + inner_writer.write(stream_reader).await.unwrap(); + let (result, remaining) = stream_writer.write(vec![9]).await; + match result { + StreamResult::Dropped => assert_eq!(remaining.remaining(), 1), + StreamResult::Complete(1) => { + assert_eq!(remaining.remaining(), 0); + let (result, remaining) = stream_writer.write(vec![10]).await; + assert_eq!(result, StreamResult::Dropped); + assert_eq!(remaining.remaining(), 1); + } + result => panic!("unexpected stream write result: {result:?}"), + } +} diff --git a/tests/runtime/moonbit/nested-future-stream/test.mbt b/tests/runtime/moonbit/nested-future-stream/test.mbt new file mode 100644 index 000000000..6dde2fd03 --- /dev/null +++ b/tests/runtime/moonbit/nested-future-stream/test.mbt @@ -0,0 +1,149 @@ +//@ [lang] +//@ path = 'gen/interface/test/moonbit-nested-future-stream/nested/stub.mbt' + +///| +let shared : (@async-core.Future[UInt], @async-core.Promise[UInt]) = @async-core.Future::new() + +///| +let never : (@async-core.Future[Unit], @async-core.Promise[Unit]) = @async-core.Future::new() + +///| +let cancellation_seen : Ref[Bool] = { val: false } + +///| +let cancellation_done : (@async-core.Future[Unit], @async-core.Promise[Unit]) = @async-core.Future::new() + +///| +pub async fn relay( + value : @async-core.Future[@async-core.Future[@async-core.Stream[Byte]]], + _background_group : @async-core.TaskGroup[Unit], +) -> @async-core.Future[@async-core.Future[@async-core.Stream[Byte]]] { + @async-core.Future::from(async fn() { + let inner = value.get() + @async-core.Future::from(async fn() { inner.get() }) + }) +} + +///| +pub async fn relay_stream( + value : @async-core.Stream[@async-core.Future[Byte]], + _background_group : @async-core.TaskGroup[Unit], +) -> @async-core.Stream[@async-core.Future[Byte]] { + value +} + +///| +pub async fn concurrent_writes( + background_group : @async-core.TaskGroup[Unit], +) -> @async-core.Stream[Byte] { + let (first_sinks, first_sinks_sink) : ( + @async-core.Stream[@async-core.Sink[Byte]], + @async-core.Sink[@async-core.Sink[Byte]], + ) = @async-core.Stream::new(capacity=1) + let (second_sinks, second_sinks_sink) : ( + @async-core.Stream[@async-core.Sink[Byte]], + @async-core.Sink[@async-core.Sink[Byte]], + ) = @async-core.Stream::new(capacity=1) + let (first_started, first_started_sink) = @async-core.Stream::new(capacity=0) + let first = background_group.spawn( + async fn() -> Bool { + guard first_sinks.read(1) is Some(sinks) && sinks.length() == 1 else { + return false + } + let signal : FixedArray[Unit] = [()] + guard first_started_sink.write_all(signal[:]) else { return false } + first_started_sink.close() + let data : FixedArray[Byte] = [1] + sinks[0].write_all(data[:]) + }, + allow_failure=true, + ) + let second = background_group.spawn( + async fn() -> Bool { + guard second_sinks.read(1) is Some(sinks) && sinks.length() == 1 else { + return false + } + guard first_started.read(1) is Some(signal) && signal.length() == 1 else { + return false + } + let data : FixedArray[Byte] = [2] + sinks[0].write_all(data[:]) + }, + allow_failure=true, + ) + @async-core.Stream::produce(async fn(sink) { + let sinks : FixedArray[@async-core.Sink[Byte]] = [sink] + guard first_sinks_sink.write_all(sinks[:]) else { + abort("failed to start first component stream writer") + } + first_sinks_sink.close() + guard second_sinks_sink.write_all(sinks[:]) else { + abort("failed to start second component stream writer") + } + second_sinks_sink.close() + guard first.wait() else { abort("first component stream write failed") } + guard second.wait() else { abort("second component stream write failed") } + sink.close() + }) +} + +///| +pub async fn post_return_lazy( + _background_group : @async-core.TaskGroup[Unit], +) -> @async-core.Stream[Byte] { + @async-core.Stream::produce(async fn(sink) { + let nested = @async-core.Stream::produce(async fn(nested_sink) { + let data : FixedArray[Byte] = [42] + guard nested_sink.write_all(data[:]) else { + abort("nested stream closed before accepting its value") + } + nested_sink.close() + }) + guard nested.read(1) is Some(data) && data.length() == 1 else { + abort("nested stream did not produce its value") + } + guard sink.write_all(data[:]) else { + abort("component stream closed before accepting its value") + } + nested.drop() + sink.close() + }) +} + +///| +pub async fn wait_shared( + _background_group : @async-core.TaskGroup[Unit], +) -> UInt { + shared.0.get() +} + +///| +pub async fn resolve_shared( + value : UInt, + _background_group : @async-core.TaskGroup[Unit], +) -> Unit { + guard shared.1.complete(value) else { panic() } +} + +///| +pub async fn wait_cancelled( + _background_group : @async-core.TaskGroup[Unit], +) -> Unit { + defer { + cancellation_seen.val = true + ignore(cancellation_done.1.complete(())) + } + never.0.get() +} + +///| +pub async fn wait_cancellation_observed( + _background_group : @async-core.TaskGroup[Unit], +) -> Unit { + cancellation_done.0.get() +} + +///| +pub fn cancellation_observed() -> Bool { + cancellation_seen.val +} diff --git a/tests/runtime/moonbit/nested-future-stream/test.wit b/tests/runtime/moonbit/nested-future-stream/test.wit new file mode 100644 index 000000000..53720d9b9 --- /dev/null +++ b/tests/runtime/moonbit/nested-future-stream/test.wit @@ -0,0 +1,30 @@ +//@ async = true + +package test:moonbit-nested-future-stream; + +interface nested { + relay: async func( + value: future>>, + ) -> future>>; + + relay-stream: async func( + value: stream>, + ) -> stream>; + + concurrent-writes: async func() -> stream; + post-return-lazy: async func() -> stream; + wait-shared: async func() -> u32; + resolve-shared: async func(value: u32); + wait-cancelled: async func(); + wait-cancellation-observed: async func(); + cancellation-observed: func() -> bool; +} + +world test { + export nested; +} + +world runner { + import nested; + export run: async func(); +} diff --git a/tests/runtime/moonbit/resource-payloads/leaf.mbt b/tests/runtime/moonbit/resource-payloads/leaf.mbt new file mode 100644 index 000000000..b7c1c28a1 --- /dev/null +++ b/tests/runtime/moonbit/resource-payloads/leaf.mbt @@ -0,0 +1,213 @@ +//@ [lang] +//@ path = 'gen/interface/my/test/leaf-interface/stub.mbt' +//@ pkg_config = """{ "warn-list": "-44", "import": [{ "path": "my/test/async-core", "alias": "async-core" }, { "path": "moonbitlang/core/encoding/utf8", "alias": "utf8" }] }""" + +///| +priv struct BodyState { + mut contents : @async-core.Stream[Byte]? + mut trailers : @async-core.Future[Fields]? +} + +///| +priv struct ResponseState { + mut body : BodyState? +} + +///| +let leaf_values : Map[Int, String] = Map([]) + +///| +let fields_values : Map[Int, String] = Map([]) + +///| +let body_values : Map[Int, BodyState] = Map([]) + +///| +let response_values : Map[Int, ResponseState] = Map([]) + +///| +let next_resource_rep : Ref[Int] = Ref(1) + +///| +let leaf_drops : Ref[UInt] = { val: 0 } + +///| +let leaf_live : Ref[UInt] = { val: 0 } + +///| +let fields_live : Ref[UInt] = { val: 0 } + +///| +let body_live : Ref[UInt] = { val: 0 } + +///| +let response_live : Ref[UInt] = { val: 0 } + +///| +pub fn LeafThing::leaf_thing(value : String) -> LeafThing { + let rep = next_resource_rep.val + next_resource_rep.val += 1 + leaf_values[rep] = value + leaf_live.val += 1 + LeafThing::new(rep) +} + +///| +pub fn LeafThing::get(self : LeafThing) -> String { + leaf_values[self.0] +} + +///| +pub fn LeafThing::dtor(self : LeafThing) -> Unit { + leaf_values.remove(self.0) + leaf_drops.val += 1 + leaf_live.val -= 1 +} + +///| +pub fn leaf_drop_count() -> UInt { + leaf_drops.val +} + +///| +pub fn leaf_live_count() -> UInt { + leaf_live.val +} + +///| +pub fn Fields::fields(value : String) -> Fields { + let rep = next_resource_rep.val + next_resource_rep.val += 1 + fields_values[rep] = value + fields_live.val += 1 + Fields::new(rep) +} + +///| +pub fn Fields::get(self : Fields) -> String { + fields_values[self.0] +} + +///| +pub fn Fields::dtor(self : Fields) -> Unit { + fields_values.remove(self.0) + fields_live.val -= 1 +} + +///| +pub fn Body::body( + contents : @async-core.Stream[Byte], + trailers : @async-core.Future[Fields]?, +) -> Body { + let rep = next_resource_rep.val + next_resource_rep.val += 1 + body_values[rep] = { contents: Some(contents), trailers } + body_live.val += 1 + Body::new(rep) +} + +///| +async fn collect_body_state(state : BodyState) -> (String, String?) { + guard state.contents is Some(contents) else { panic() } + state.contents = None + + let bytes : Array[Byte] = [] + for ;; { + match contents.read(16) { + Some(chunk) => + for byte in chunk { + bytes.push(byte) + } + None => break + } + } + contents.drop() + + let trailers = match state.trailers { + Some(future) => { + state.trailers = None + let fields = future.get() + let value = fields_values[fields.rep()] + fields.drop() + future.drop() + Some(value) + } + None => None + } + (@utf8.decode_lossy(Bytes::from_array(bytes[:])[:]), trailers) +} + +///| +pub async fn Body::collect( + self : Body, + _background_group : @async-core.TaskGroup[Unit], +) -> (String, String?) { + collect_body_state(body_values[self.0]) +} + +///| +pub fn Body::dtor(self : Body) -> Unit { + match body_values.get(self.0) { + Some(state) => { + body_values.remove(self.0) + guard state.contents is None && state.trailers is None else { + abort("test body dropped before collect") + } + body_live.val -= 1 + } + None => () + } +} + +///| +pub fn Response::response(body : Body) -> Response { + // Move the owned body's representation into the response before releasing + // its handle. The body remains live until `Response::collect` consumes it. + let body_rep = body.rep() + guard body_values.get(body_rep) is Some(body_state) else { panic() } + body_values.remove(body_rep) + body.drop() + let rep = next_resource_rep.val + next_resource_rep.val += 1 + response_values[rep] = { body: Some(body_state) } + response_live.val += 1 + Response::new(rep) +} + +///| +pub async fn Response::collect( + self : Response, + _background_group : @async-core.TaskGroup[Unit], +) -> (String, String?) { + let state = response_values[self.0] + guard state.body is Some(body_state) else { panic() } + state.body = None + let result = collect_body_state(body_state) + body_live.val -= 1 + result +} + +///| +pub fn Response::dtor(self : Response) -> Unit { + let state = response_values[self.0] + guard state.body is None else { + abort("test response dropped before collect") + } + response_values.remove(self.0) + response_live.val -= 1 +} + +///| +pub fn fields_live_count() -> UInt { + fields_live.val +} + +///| +pub fn body_live_count() -> UInt { + body_live.val +} + +///| +pub fn response_live_count() -> UInt { + response_live.val +} diff --git a/tests/runtime/moonbit/resource-payloads/leaf.rs b/tests/runtime/moonbit/resource-payloads/leaf.rs new file mode 100644 index 000000000..33e3d3d9c --- /dev/null +++ b/tests/runtime/moonbit/resource-payloads/leaf.rs @@ -0,0 +1,144 @@ +include!(env!("BINDINGS")); + +use crate::exports::my::test::leaf_interface::{ + Body, Fields, Guest, GuestBody, GuestFields, GuestLeafThing, GuestResponse, +}; +use std::cell::RefCell; +use std::sync::atomic::{AtomicU32, Ordering}; +use wit_bindgen::{FutureReader, StreamReader}; + +struct Component; + +export!(Component); + +static LEAF_DROP_COUNT: AtomicU32 = AtomicU32::new(0); +static LEAF_LIVE_COUNT: AtomicU32 = AtomicU32::new(0); +static FIELDS_LIVE_COUNT: AtomicU32 = AtomicU32::new(0); +static BODY_LIVE_COUNT: AtomicU32 = AtomicU32::new(0); +static RESPONSE_LIVE_COUNT: AtomicU32 = AtomicU32::new(0); + +struct MyLeafThing { + value: String, +} + +struct MyFields { + value: String, +} + +struct MyBody { + contents: RefCell>>, + trailers: RefCell>>, +} + +struct MyResponse { + body: RefCell>, +} + +impl Guest for Component { + type LeafThing = MyLeafThing; + type Fields = MyFields; + type Body = MyBody; + type Response = MyResponse; + + fn leaf_drop_count() -> u32 { + LEAF_DROP_COUNT.load(Ordering::SeqCst) + } + + fn leaf_live_count() -> u32 { + LEAF_LIVE_COUNT.load(Ordering::SeqCst) + } + + fn fields_live_count() -> u32 { + FIELDS_LIVE_COUNT.load(Ordering::SeqCst) + } + + fn body_live_count() -> u32 { + BODY_LIVE_COUNT.load(Ordering::SeqCst) + } + + fn response_live_count() -> u32 { + RESPONSE_LIVE_COUNT.load(Ordering::SeqCst) + } +} + +impl GuestLeafThing for MyLeafThing { + fn new(s: String) -> Self { + LEAF_LIVE_COUNT.fetch_add(1, Ordering::SeqCst); + Self { value: s } + } + + fn get(&self) -> String { + self.value.clone() + } +} + +impl Drop for MyLeafThing { + fn drop(&mut self) { + LEAF_DROP_COUNT.fetch_add(1, Ordering::SeqCst); + LEAF_LIVE_COUNT.fetch_sub(1, Ordering::SeqCst); + } +} + +impl GuestFields for MyFields { + fn new(s: String) -> Self { + FIELDS_LIVE_COUNT.fetch_add(1, Ordering::SeqCst); + Self { value: s } + } + + fn get(&self) -> String { + self.value.clone() + } +} + +impl Drop for MyFields { + fn drop(&mut self) { + FIELDS_LIVE_COUNT.fetch_sub(1, Ordering::SeqCst); + } +} + +impl GuestBody for MyBody { + fn new(contents: StreamReader, trailers: Option>) -> Self { + BODY_LIVE_COUNT.fetch_add(1, Ordering::SeqCst); + Self { + contents: RefCell::new(Some(contents)), + trailers: RefCell::new(trailers), + } + } + + async fn collect(&self) -> (String, Option) { + let contents = self.contents.borrow_mut().take().unwrap(); + let bytes = contents.collect().await; + let body = String::from_utf8(bytes).unwrap(); + let trailers = match self.trailers.borrow_mut().take() { + Some(trailers) => Some(trailers.await.get::().get()), + None => None, + }; + (body, trailers) + } +} + +impl Drop for MyBody { + fn drop(&mut self) { + BODY_LIVE_COUNT.fetch_sub(1, Ordering::SeqCst); + } +} + +impl GuestResponse for MyResponse { + fn new(body: Body) -> Self { + RESPONSE_LIVE_COUNT.fetch_add(1, Ordering::SeqCst); + Self { + body: RefCell::new(Some(body)), + } + } + + async fn collect(&self) -> (String, Option) { + let body = self.body.borrow_mut().take().unwrap(); + body.get::().collect().await + } +} + +impl Drop for MyResponse { + fn drop(&mut self) { + RESPONSE_LIVE_COUNT.fetch_sub(1, Ordering::SeqCst); + } +} diff --git a/tests/runtime/moonbit/resource-payloads/runner.mbt b/tests/runtime/moonbit/resource-payloads/runner.mbt new file mode 100644 index 000000000..e7770fae1 --- /dev/null +++ b/tests/runtime/moonbit/resource-payloads/runner.mbt @@ -0,0 +1,321 @@ +//@ wasmtime-flags = '-Wcomponent-model-async' +//@ [lang] +//@ path = 'gen/world/runner/stub.mbt' +//@ pkg_config = """{ "warn-list": "-44", "import": [{ "path": "my/test/async-core", "alias": "async-core" }, { "path": "my/test/interface/my/test/leaf-interface", "alias": "leaf-interface" }, { "path": "my/test/interface/my/test/test-interface", "alias": "test-interface" }] }""" + +///| +async fn read_one_leaf( + stream : @async-core.Stream[@leaf-interface.LeafThing], + expected : String, +) -> Unit { + match stream.read(1) { + Some(things) => { + if things.length() != 1 { + for thing in things { + thing.drop() + } + panic() + } + let thing = things[0] + let value = thing.get() + thing.drop() + guard value == expected else { panic() } + } + None => panic() + } +} + +///| +async fn wait_for_leaf_drop_count(expected : UInt) -> Unit { + for _ in 0..<32 { + if @leaf-interface.leaf_drop_count() == expected { + return + } + @test-interface.settle() + } + guard @leaf-interface.leaf_drop_count() == expected else { panic() } +} + +///| +async fn assert_no_live_resources() -> Unit { + for _ in 0..<32 { + if @leaf-interface.leaf_live_count() == 0U && + @leaf-interface.fields_live_count() == 0U && + @leaf-interface.body_live_count() == 0U && + @leaf-interface.response_live_count() == 0U { + return + } + @test-interface.settle() + } + guard @leaf-interface.leaf_live_count() == 0U else { panic() } + guard @leaf-interface.fields_live_count() == 0U else { panic() } + guard @leaf-interface.body_live_count() == 0U else { panic() } + guard @leaf-interface.response_live_count() == 0U else { panic() } +} + +///| +fn nested_leaf_stream( + prefix : String, +) -> @async-core.Stream[@leaf-interface.LeafThing] { + @async-core.Stream::produce(async fn(sink) { + let values : FixedArray[@leaf-interface.LeafThing] = [ + @leaf-interface.LeafThing::leaf_thing(prefix + "-a"), + @leaf-interface.LeafThing::leaf_thing(prefix + "-b"), + ] + let _ = sink.write_all(values[:]) + sink.close() + }) +} + +///| +fn nested_leaf_future( + prefix : String, +) -> @async-core.Future[ + @async-core.Future[@async-core.Stream[@leaf-interface.LeafThing]], +] { + @async-core.Future::ready( + @async-core.Future::ready(nested_leaf_stream(prefix)), + ) +} + +///| +pub async fn run(background_group : @async-core.TaskGroup[Unit]) -> Unit { + { + let before = @leaf-interface.leaf_drop_count() + let output = @test-interface.relay_nested_leaf( + nested_leaf_future("nested-transfer"), + ) + let stream = output.get().get() + read_one_leaf(stream, "nested-transfer-a") + read_one_leaf(stream, "nested-transfer-b") + guard stream.read(1) is None else { panic() } + stream.drop() + output.drop() + wait_for_leaf_drop_count(before + 2U) + } + { + let before = @leaf-interface.leaf_drop_count() + let output = @test-interface.relay_nested_leaf( + nested_leaf_future("nested-reject"), + ) + output.drop() + wait_for_leaf_drop_count(before + 2U) + } + { + let before = @leaf-interface.leaf_drop_count() + let input = @async-core.Stream::produce(async fn(sink) { + let values : FixedArray[@async-core.Future[@leaf-interface.LeafThing]] = [ + @async-core.Future::ready( + @leaf-interface.LeafThing::leaf_thing("future-stream-a"), + ), + @async-core.Future::ready( + @leaf-interface.LeafThing::leaf_thing("future-stream-b"), + ), + ] + let _ = sink.write_all(values[:]) + sink.close() + }) + let output = @test-interface.relay_leaf_futures(input) + guard output.read(1) is Some(values) && values.length() == 1 else { + panic() + } + let leaf = values[0].get() + guard leaf.get() == "future-stream-a" else { panic() } + leaf.drop() + output.drop() + wait_for_leaf_drop_count(before + 2U) + } + { + let (input, sink) = @async-core.Stream::new_with_cleanup( + fn(value : @leaf-interface.LeafThing) { value.drop() }, + capacity=3, + ) + let values : FixedArray[@leaf-interface.LeafThing] = [ + @leaf-interface.LeafThing::leaf_thing("a"), + @leaf-interface.LeafThing::leaf_thing("b"), + @leaf-interface.LeafThing::leaf_thing("c"), + ] + guard sink.write_all(values[:]) else { panic() } + sink.close() + + let stream = @test-interface.short_reads_leaf(input) + read_one_leaf(stream, "a") + read_one_leaf(stream, "b") + read_one_leaf(stream, "c") + stream.drop() + } + { + let before = @leaf-interface.leaf_drop_count() + let (input, sink) = @async-core.Stream::new_with_cleanup( + fn(value : @leaf-interface.LeafThing) { value.drop() }, + capacity=3, + ) + let values : FixedArray[@leaf-interface.LeafThing] = [ + @leaf-interface.LeafThing::leaf_thing("drop-a"), + @leaf-interface.LeafThing::leaf_thing("drop-b"), + @leaf-interface.LeafThing::leaf_thing("drop-c"), + ] + guard sink.write_all(values[:]) else { panic() } + sink.close() + + let stream = @test-interface.short_reads_leaf(input) + read_one_leaf(stream, "drop-a") + stream.drop() + wait_for_leaf_drop_count(before + 3U) + } + { + let (release_first, release_first_sink) = @async-core.Stream::new( + capacity=1, + ) + let first_payload = @async-core.Future::ready_with_cleanup( + @leaf-interface.LeafThing::leaf_thing("a"), + fn(value : @leaf-interface.LeafThing) { value.drop() }, + ) + let first = @async-core.Future::from(async fn() { + guard release_first.read(1) is Some(signal) && signal.length() == 1 else { + abort("future release signal closed early") + } + first_payload.get() + }) + let second = @async-core.Future::ready_with_cleanup( + @leaf-interface.LeafThing::leaf_thing("a"), + fn(value : @leaf-interface.LeafThing) { value.drop() }, + ) + let (out1, out2) = @test-interface.dropped_reader_leaf(first, second) + + out1.drop() + let thing = out2.get() + let value = thing.get() + thing.drop() + out2.drop() + guard value == "a" else { panic() } + + // `out2` is only produced after the peer has dropped `first`. + let signal : FixedArray[Unit] = [()] + guard release_first_sink.write_all(signal[:]) else { panic() } + release_first_sink.close() + assert_no_live_resources() + } + { + let before = @leaf-interface.leaf_drop_count() + let (release, release_sink) = @async-core.Stream::new(capacity=1) + let payload = @async-core.Future::ready_with_cleanup( + @leaf-interface.LeafThing::leaf_thing("cancelled-future-read"), + fn(value : @leaf-interface.LeafThing) { value.drop() }, + ) + let future = @async-core.Future::from(async fn() { + guard release.read(1) is Some(signal) && signal.length() == 1 else { + abort("future release signal closed early") + } + payload.get() + }) + guard @test-interface.cancel_future_read(future) else { panic() } + + let signal : FixedArray[Unit] = [()] + guard release_sink.write_all(signal[:]) else { panic() } + release_sink.close() + wait_for_leaf_drop_count(before + 1U) + } + { + let before = @leaf-interface.leaf_drop_count() + let (stream, sink) = @async-core.Stream::new_with_cleanup( + fn(value : @leaf-interface.LeafThing) { value.drop() }, + capacity=0, + ) + guard @test-interface.cancel_stream_read(stream) else { panic() } + + let values : FixedArray[@leaf-interface.LeafThing] = [ + @leaf-interface.LeafThing::leaf_thing("cancelled-stream-read"), + ] + let _ = sink.write_all(values[:]) + sink.close() + wait_for_leaf_drop_count(before + 1U) + } + { + let before = @leaf-interface.leaf_drop_count() + let stream = @test-interface.make_leaf_stream() + read_one_leaf(stream, "stream-a") + stream.drop() + wait_for_leaf_drop_count(before + 4U) + } + { + let stream = @test-interface.make_local_leaf_stream() + read_one_leaf(stream, "local-a") + read_one_leaf(stream, "local-b") + read_one_leaf(stream, "local-c") + match stream.read(1) { + None => () + Some(things) => { + for thing in things { + thing.drop() + } + panic() + } + } + stream.drop() + } + { + let response = @test-interface.make_response() + let (body, trailers) = response.collect() + response.drop() + guard body == "hello http p3" else { panic() } + guard trailers == Some("done") else { panic() } + } + { + let response = @test-interface.make_response_post_work() + let before = @test-interface.post_response_count() + guard before < 3U else { panic() } + + let (body, trailers) = response.collect() + response.drop() + guard body == "post work" else { panic() } + guard trailers == Some("after-body") else { panic() } + guard @test-interface.post_response_count() >= 3U else { panic() } + } + { + let response = @test-interface.make_response_background() + guard @test-interface.background_count() == 0U else { panic() } + + let (body, trailers) = response.collect() + response.drop() + guard body == "background" else { panic() } + guard trailers == Some("background-done") else { panic() } + guard @test-interface.background_count() == 1U else { panic() } + } + { + let future = @test-interface.delayed_leaf_future() + let thing = future.get() + let value = thing.get() + thing.drop() + future.drop() + guard value == "delayed" else { panic() } + } + + guard @test-interface.local_future_drop_cleanup() == 1U else { panic() } + guard @test-interface.local_future_drop_resource_cleanup() == 1U else { + panic() + } + guard @test-interface.local_lazy_future_drop_resource_cleanup() == 1U else { + panic() + } + guard @test-interface.local_stream_drop_resource_cleanup() == 4U else { + panic() + } + guard @test-interface.local_lazy_stream_drop_resource_cleanup() == 1U else { + panic() + } + guard @test-interface.local_lazy_stream_partial_drop_resource_cleanup() == 2U else { + panic() + } + guard @test-interface.local_stream_cancelled_write_resource_cleanup() == 3U else { + panic() + } + guard @test-interface.local_stream_rendezvous() else { panic() } + guard @test-interface.local_stream_bounded_backpressure() else { panic() } + guard @test-interface.local_stream_cancelled_waiters() else { panic() } + guard @test-interface.local_stream_produce_read() else { panic() } + assert_no_live_resources() + + // The export ABI supplies this group even when this runner has no detached work. + ignore(background_group) +} diff --git a/tests/runtime/moonbit/resource-payloads/test.mbt b/tests/runtime/moonbit/resource-payloads/test.mbt new file mode 100644 index 000000000..477223aae --- /dev/null +++ b/tests/runtime/moonbit/resource-payloads/test.mbt @@ -0,0 +1,714 @@ +//@ [lang] +//@ path = 'gen/interface/my/test/test-interface/stub.mbt' + +///| +let post_response_steps : Ref[UInt] = Ref(0) + +///| +fn record_post_response_step() -> Unit { + post_response_steps.val += 1 +} + +///| +let background_steps : Ref[UInt] = Ref(0) + +///| +fn reset_background_work() -> Unit { + background_steps.val = 0 +} + +///| +pub async fn settle(_background_group : @async-core.TaskGroup[Unit]) -> Unit { + +} + +///| +pub async fn short_reads_leaf( + s : @async-core.Stream[@leaf-interface.LeafThing], + _background_group : @async-core.TaskGroup[Unit], +) -> @async-core.Stream[@leaf-interface.LeafThing] { + @async-core.Stream::produce(async fn(sink) { + let things : Array[@leaf-interface.LeafThing] = [] + for ;; { + match s.read(1) { + Some(chunk) => + for i in 0.. break + } + } + let data = FixedArray::from_array(things) + let _ = sink.write_all(data[:]) + sink.close() + }) +} + +///| +pub async fn dropped_reader_leaf( + f1 : @async-core.Future[@leaf-interface.LeafThing], + f2 : @async-core.Future[@leaf-interface.LeafThing], + _background_group : @async-core.TaskGroup[Unit], +) -> ( + @async-core.Future[@leaf-interface.LeafThing], + @async-core.Future[@leaf-interface.LeafThing], +) { + let value : Ref[String?] = Ref(None) + let (value_ready, value_ready_sink) = @async-core.Stream::new(capacity=1) + let out1 = @async-core.Future::from(async fn() { + f1.drop() + let thing = f2.get() + value.val = Some(thing.get()) + let signal : FixedArray[Unit] = [()] + guard value_ready_sink.write_all(signal[:]) else { + abort("future value signal closed early") + } + value_ready_sink.close() + thing + }) + let out2 = @async-core.Future::from(async fn() { + guard value_ready.read(1) is Some(signal) && signal.length() == 1 else { + abort("future value signal closed early") + } + @leaf-interface.LeafThing::leaf_thing(value.val.unwrap()) + }) + (out1, out2) +} + +///| +pub async fn make_leaf_stream( + _background_group : @async-core.TaskGroup[Unit], +) -> @async-core.Stream[@leaf-interface.LeafThing] { + @async-core.Stream::produce(async fn(sink) { + let data : FixedArray[@leaf-interface.LeafThing] = [ + @leaf-interface.LeafThing::leaf_thing("stream-a"), + @leaf-interface.LeafThing::leaf_thing("stream-b"), + @leaf-interface.LeafThing::leaf_thing("stream-c"), + @leaf-interface.LeafThing::leaf_thing("stream-d"), + ] + let _ = sink.write_all(data[:]) + sink.close() + }) +} + +///| +pub async fn make_local_leaf_stream( + background_group : @async-core.TaskGroup[Unit], +) -> @async-core.Stream[@leaf-interface.LeafThing] { + let (stream, sink) = @async-core.Stream::new_with_cleanup( + fn(value : @leaf-interface.LeafThing) { value.drop() }, + capacity=1, + ) + background_group.spawn_bg(async fn() { + let data : FixedArray[@leaf-interface.LeafThing] = [ + @leaf-interface.LeafThing::leaf_thing("local-a"), + @leaf-interface.LeafThing::leaf_thing("local-b"), + @leaf-interface.LeafThing::leaf_thing("local-c"), + ] + let _ = sink.write_all(data[:]) + sink.close() + }) + stream +} + +///| +pub async fn make_response( + _background_group : @async-core.TaskGroup[Unit], +) -> @leaf-interface.Response { + let trailers = @leaf-interface.Fields::fields("done") + let body = @leaf-interface.Body::body( + @async-core.Stream::produce(async fn(sink) { + let _ = sink.write_all_bytes(b"hello "[:]) + let _ = sink.write_all_bytes(b"http p3"[:]) + sink.close() + }), + Some(@async-core.Future::ready(trailers)), + ) + @leaf-interface.Response::response(body) +} + +///| +pub async fn make_response_post_work( + _background_group : @async-core.TaskGroup[Unit], +) -> @leaf-interface.Response { + let (body_done, body_done_sink) = @async-core.Stream::new(capacity=1) + let trailers = @async-core.Future::from(async fn() { + guard body_done.read(1) is Some(signal) && signal.length() == 1 else { + abort("body completion signal closed early") + } + guard post_response_steps.val >= 3 else { + abort("body completion signal arrived too early") + } + @leaf-interface.Fields::fields("after-body") + }) + let body = @leaf-interface.Body::body( + @async-core.Stream::produce(async fn(sink) { + record_post_response_step() + let _ = sink.write_all_bytes(b"post "[:]) + record_post_response_step() + let _ = sink.write_all_bytes(b"work"[:]) + sink.close() + record_post_response_step() + let signal : FixedArray[Unit] = [()] + guard body_done_sink.write_all(signal[:]) else { + abort("body completion signal closed early") + } + body_done_sink.close() + }), + Some(trailers), + ) + @leaf-interface.Response::response(body) +} + +///| +pub fn post_response_count() -> UInt { + post_response_steps.val +} + +///| +pub async fn make_response_background( + background_group : @async-core.TaskGroup[Unit], +) -> @leaf-interface.Response { + reset_background_work() + let (body_done, body_done_sink) = @async-core.Stream::new(capacity=1) + let (background_done, background_done_sink) = @async-core.Stream::new( + capacity=1, + ) + background_group.spawn_bg(async fn() { + guard body_done.read(1) is Some(signal) && signal.length() == 1 else { + abort("body completion signal closed early") + } + background_steps.val += 1 + let signal : FixedArray[Unit] = [()] + guard background_done_sink.write_all(signal[:]) else { + abort("background completion signal closed early") + } + background_done_sink.close() + }) + let trailers = @async-core.Future::from(async fn() { + guard background_done.read(1) is Some(signal) && signal.length() == 1 else { + abort("background completion signal closed early") + } + @leaf-interface.Fields::fields("background-done") + }) + let body = @leaf-interface.Body::body( + @async-core.Stream::produce(async fn(sink) { + let _ = sink.write_all_bytes(b"background"[:]) + sink.close() + let signal : FixedArray[Unit] = [()] + guard body_done_sink.write_all(signal[:]) else { + abort("body completion signal closed early") + } + body_done_sink.close() + }), + Some(trailers), + ) + @leaf-interface.Response::response(body) +} + +///| +pub fn background_count() -> UInt { + background_steps.val +} + +///| +pub async fn delayed_leaf_future( + background_group : @async-core.TaskGroup[Unit], +) -> @async-core.Future[@leaf-interface.LeafThing] { + let (ready, ready_sink) = @async-core.Stream::new(capacity=0) + background_group.spawn_bg(async fn() { + let signal : FixedArray[Unit] = [()] + guard ready_sink.write_all(signal[:]) else { return } + ready_sink.close() + }) + @async-core.Future::from(async fn() { + guard ready.read(1) is Some(signal) && signal.length() == 1 else { + abort("future readiness signal closed early") + } + @leaf-interface.LeafThing::leaf_thing("delayed") + }) +} + +///| +pub async fn local_future_drop_cleanup( + _background_group : @async-core.TaskGroup[Unit], +) -> UInt { + let counts : Array[UInt] = [0] + let f = @async-core.Future::ready_with_cleanup("owned", fn(_value) { + counts[0] = counts[0] + 1 + }) + f.drop() + let unstarted_cleanup_ran = Ref(false) + let started = @async-core.Future::from(async fn() { 42 }, on_unstarted_drop=fn() { + unstarted_cleanup_ran.val = true + }) + guard started.get() == 42 else { + abort("future producer returned wrong value") + } + started.drop() + guard !unstarted_cleanup_ran.val else { + abort("started future ran its unstarted cleanup") + } + counts[0] +} + +///| +pub async fn local_future_drop_resource_cleanup( + _background_group : @async-core.TaskGroup[Unit], +) -> UInt { + let before = @leaf-interface.leaf_drop_count() + let thing = @leaf-interface.LeafThing::leaf_thing("cleanup") + let f = @async-core.Future::ready_with_cleanup(thing, fn(value) { + value.drop() + }) + f.drop() + @leaf-interface.leaf_drop_count() - before +} + +///| +pub async fn local_lazy_future_drop_resource_cleanup( + _background_group : @async-core.TaskGroup[Unit], +) -> UInt { + let before = @leaf-interface.leaf_drop_count() + let producer_ran = Ref(false) + let thing = @leaf-interface.LeafThing::leaf_thing("lazy-future-cleanup") + let future = @async-core.Future::from( + async fn() { + producer_ran.val = true + thing + }, + on_unstarted_drop=fn() { thing.drop() }, + ) + future.drop() + guard !producer_ran.val else { + abort("dropping an unstarted future ran its producer") + } + @leaf-interface.leaf_drop_count() - before +} + +///| +pub async fn local_stream_drop_resource_cleanup( + _background_group : @async-core.TaskGroup[Unit], +) -> UInt { + let before = @leaf-interface.leaf_drop_count() + let (stream, sink) = @async-core.Stream::new_with_cleanup( + fn(value : @leaf-interface.LeafThing) { value.drop() }, + capacity=4, + ) + let data : FixedArray[@leaf-interface.LeafThing] = [ + @leaf-interface.LeafThing::leaf_thing("buffered-a"), + @leaf-interface.LeafThing::leaf_thing("buffered-b"), + @leaf-interface.LeafThing::leaf_thing("buffered-c"), + @leaf-interface.LeafThing::leaf_thing("buffered-d"), + ] + let _ = sink.write_all(data[:]) + sink.close() + match stream.read(1) { + Some(chunk) => chunk[0].drop() + None => panic() + } + stream.drop() + @leaf-interface.leaf_drop_count() - before +} + +///| +pub async fn local_lazy_stream_drop_resource_cleanup( + _background_group : @async-core.TaskGroup[Unit], +) -> UInt { + let before = @leaf-interface.leaf_drop_count() + let producer_ran = Ref(false) + let thing = @leaf-interface.LeafThing::leaf_thing("lazy-stream-cleanup") + let stream : @async-core.Stream[@leaf-interface.LeafThing] = @async-core.Stream::produce( + async fn(sink) { + producer_ran.val = true + let values : FixedArray[@leaf-interface.LeafThing] = [thing] + guard sink.write_all(values[:]) else { return } + sink.close() + }, + on_unstarted_drop=fn() { thing.drop() }, + ) + stream.drop() + guard !producer_ran.val else { + abort("dropping an unstarted stream ran its producer") + } + @leaf-interface.leaf_drop_count() - before +} + +///| +pub async fn local_lazy_stream_partial_drop_resource_cleanup( + _background_group : @async-core.TaskGroup[Unit], +) -> UInt { + let before = @leaf-interface.leaf_drop_count() + let (producer_done, producer_done_promise) = @async-core.Future::new() + let stream : @async-core.Stream[@leaf-interface.LeafThing] = @async-core.Stream::produce( + async fn(sink) { + let values : FixedArray[@leaf-interface.LeafThing] = [ + @leaf-interface.LeafThing::leaf_thing("partial-a"), + @leaf-interface.LeafThing::leaf_thing("partial-b"), + ] + ignore(producer_done_promise.complete(sink.write_all(values[:]))) + }, + cleanup=fn(value) { value.drop() }, + ) + guard stream.read(1) is Some(values) && values.length() == 1 else { panic() } + values[0].drop() + stream.drop() + guard !producer_done.get() else { panic() } + @leaf-interface.leaf_drop_count() - before +} + +///| +pub async fn local_stream_cancelled_write_resource_cleanup( + background_group : @async-core.TaskGroup[Unit], +) -> UInt { + let before = @leaf-interface.leaf_drop_count() + let (stream, sink) = @async-core.Stream::new_with_cleanup( + fn(value : @leaf-interface.LeafThing) { value.drop() }, + capacity=1, + ) + let (write_blocked, write_blocked_sink) = @async-core.Stream::new(capacity=1) + let writer = background_group.spawn( + async fn() -> Unit { + let data : FixedArray[@leaf-interface.LeafThing] = [ + @leaf-interface.LeafThing::leaf_thing("cancelled-a"), + @leaf-interface.LeafThing::leaf_thing("cancelled-b"), + @leaf-interface.LeafThing::leaf_thing("cancelled-c"), + ] + let accepted = sink.write(data[:]) + guard accepted == 1 else { panic() } + let signal : FixedArray[Unit] = [()] + guard write_blocked_sink.write_all(signal[:]) else { panic() } + write_blocked_sink.close() + let _ = sink.write_all(data[accepted:]) + }, + allow_failure=true, + ) + guard write_blocked.read(1) is Some(signal) && signal.length() == 1 else { + panic() + } + writer.cancel() + writer.wait() catch { + _ => () + } + guard @leaf-interface.leaf_drop_count() - before == 2 else { panic() } + stream.drop() + @leaf-interface.leaf_drop_count() - before +} + +///| +pub async fn cancel_future_read( + future : @async-core.Future[@leaf-interface.LeafThing], + background_group : @async-core.TaskGroup[Unit], +) -> Bool { + let (read_started, read_started_sink) = @async-core.Stream::new(capacity=1) + let reader = background_group.spawn( + async fn() -> Bool { + let signal : FixedArray[Unit] = [()] + guard read_started_sink.write_all(signal[:]) else { panic() } + read_started_sink.close() + let value = future.get() + value.drop() + false + }, + allow_failure=true, + ) + guard read_started.read(1) is Some(signal) && signal.length() == 1 else { + panic() + } + reader.cancel() + try reader.wait() catch { + @async-core.Cancelled::Cancelled => true + _ => false + } noraise { + _ => false + } +} + +///| +pub async fn cancel_stream_read( + stream : @async-core.Stream[@leaf-interface.LeafThing], + background_group : @async-core.TaskGroup[Unit], +) -> Bool { + let (read_started, read_started_sink) = @async-core.Stream::new(capacity=1) + let reader = background_group.spawn( + async fn() -> Bool { + let signal : FixedArray[Unit] = [()] + guard read_started_sink.write_all(signal[:]) else { panic() } + read_started_sink.close() + match stream.read(1) { + Some(chunk) => + for value in chunk { + value.drop() + } + None => () + } + false + }, + allow_failure=true, + ) + guard read_started.read(1) is Some(signal) && signal.length() == 1 else { + panic() + } + reader.cancel() + try reader.wait() catch { + @async-core.Cancelled::Cancelled => true + _ => false + } noraise { + _ => false + } +} + +///| +pub async fn relay_nested_leaf( + value : @async-core.Future[ + @async-core.Future[@async-core.Stream[@leaf-interface.LeafThing]], + ], + _background_group : @async-core.TaskGroup[Unit], +) -> @async-core.Future[ + @async-core.Future[@async-core.Stream[@leaf-interface.LeafThing]], +] { + @async-core.Future::from(async fn() { + let inner = value.get() + @async-core.Future::from(async fn() { inner.get() }) + }) +} + +///| +pub async fn relay_leaf_futures( + value : @async-core.Stream[@async-core.Future[@leaf-interface.LeafThing]], + _background_group : @async-core.TaskGroup[Unit], +) -> @async-core.Stream[@async-core.Future[@leaf-interface.LeafThing]] { + value +} + +///| +pub async fn local_stream_rendezvous( + background_group : @async-core.TaskGroup[Unit], +) -> Bool { + let (stream, sink) = @async-core.Stream::new(capacity=0) + let phase = Ref(0) + let (writer_started, writer_started_sink) = @async-core.Stream::new( + capacity=1, + ) + let writer = background_group.spawn(async fn() -> Bool { + let data : FixedArray[Int] = [42] + phase.val = 1 + let signal : FixedArray[Unit] = [()] + guard writer_started_sink.write_all(signal[:]) else { return false } + writer_started_sink.close() + guard sink.write_all(data[:]) else { return false } + phase.val = 2 + sink.close() + true + }) + guard writer_started.read(1) is Some(signal) && signal.length() == 1 else { + return false + } + guard phase.val == 1 else { return false } + guard stream.read(1) is Some(chunk) && chunk.length() == 1 else { + return false + } + writer.wait() && phase.val == 2 && stream.read(1) is None +} + +///| +pub async fn local_stream_bounded_backpressure( + background_group : @async-core.TaskGroup[Unit], +) -> Bool { + let (stream, sink) = @async-core.Stream::new(capacity=2) + let phase = Ref(0) + let (write_blocked, write_blocked_sink) = @async-core.Stream::new(capacity=1) + let writer = background_group.spawn( + async fn() -> Bool { + let data : FixedArray[Int] = [1, 2, 3] + phase.val = 1 + let first = sink.write(data[:]) + guard first == 2 else { return false } + phase.val = 2 + let signal : FixedArray[Unit] = [()] + guard write_blocked_sink.write_all(signal[:]) else { return false } + write_blocked_sink.close() + let second = sink.write(data[first:]) + guard second == 1 else { return false } + phase.val = 3 + sink.close() + true + }, + allow_failure=true, + ) + guard write_blocked.read(1) is Some(signal) && signal.length() == 1 else { + return false + } + guard phase.val == 2 else { return false } + guard stream.read(1) is Some(first) else { return false } + guard first.length() == 1 && first[0] == 1 else { return false } + guard writer.wait() else { return false } + guard phase.val == 3 else { return false } + guard stream.read(2) is Some(second) else { return false } + guard second.length() == 1 && second[0] == 2 else { return false } + guard stream.read(2) is Some(third) else { return false } + guard third.length() == 1 && third[0] == 3 else { return false } + stream.read(1) is None +} + +///| +pub async fn local_stream_cancelled_waiters( + background_group : @async-core.TaskGroup[Unit], +) -> Bool { + let pre_start_cleanup = Ref(false) + let (never, never_sink) = @async-core.Stream::new(capacity=0) + let pre_start = background_group.spawn(async fn() -> Unit { + defer { + pre_start_cleanup.val = true + } + let _ = never.read(1) + }) + pre_start.cancel() + pre_start.wait() catch { + _ => () + } + ignore(never_sink) + guard pre_start_cleanup.val else { return false } + + let (stream, sink) = @async-core.Stream::new(capacity=0) + + let (writer_started, writer_started_sink) = @async-core.Stream::new( + capacity=1, + ) + let blocked_writer = background_group.spawn(async fn() -> Int { + let data : FixedArray[Int] = [1] + let signal : FixedArray[Unit] = [()] + guard writer_started_sink.write_all(signal[:]) else { return -1 } + writer_started_sink.close() + sink.write(data[:]) + }) + guard writer_started.read(1) is Some(signal) && signal.length() == 1 else { + return false + } + blocked_writer.cancel() + let blocked_writer_result = blocked_writer.wait() catch { _ => -1 } + guard blocked_writer_result == -1 else { return false } + + let (reader_started, reader_started_sink) = @async-core.Stream::new( + capacity=1, + ) + let blocked_reader = background_group.spawn( + async fn() -> Int { + let signal : FixedArray[Unit] = [()] + guard reader_started_sink.write_all(signal[:]) else { return -1 } + reader_started_sink.close() + match stream.read(1) { + Some(_) => 1 + None => 0 + } + }, + allow_failure=true, + ) + guard reader_started.read(1) is Some(signal) && signal.length() == 1 else { + return false + } + blocked_reader.cancel() + let blocked_reader_result = blocked_reader.wait() catch { _ => -1 } + guard blocked_reader_result == -1 else { return false } + + let writer = background_group.spawn( + async fn() -> Int { + let data : FixedArray[Int] = [7] + sink.write(data[:]) + }, + allow_failure=true, + ) + guard stream.read(1) is Some(chunk) else { return false } + guard chunk.length() == 1 && chunk[0] == 7 else { return false } + writer.cancel() + guard writer.wait() == 1 else { return false } + + let (finishing_reader_started, finishing_reader_started_sink) = @async-core.Stream::new( + capacity=1, + ) + let finishing_reader = background_group.spawn( + async fn() -> Int { + let signal : FixedArray[Unit] = [()] + guard finishing_reader_started_sink.write_all(signal[:]) else { + return -1 + } + finishing_reader_started_sink.close() + match stream.read(1) { + Some(data) if data.length() == 1 => data[0] + _ => -1 + } + }, + allow_failure=true, + ) + guard finishing_reader_started.read(1) is Some(signal) && signal.length() == 1 else { + return false + } + let data : FixedArray[Int] = [9] + guard sink.write(data[:]) == 1 else { return false } + finishing_reader.cancel() + guard finishing_reader.wait() == 9 else { return false } + + sink.close() + guard stream.read(1) is None else { return false } + + let (closed_stream, closed_sink) = @async-core.Stream::new(capacity=0) + let (close_writer_started, close_writer_started_sink) = @async-core.Stream::new( + capacity=1, + ) + let close_writer = background_group.spawn(async fn() -> Int { + let signal : FixedArray[Unit] = [()] + guard close_writer_started_sink.write_all(signal[:]) else { return -1 } + close_writer_started_sink.close() + let data : FixedArray[Int] = [11] + closed_sink.write(data[:]) + }) + guard close_writer_started.read(1) is Some(signal) && signal.length() == 1 else { + return false + } + closed_sink.close() + close_writer.cancel() + guard close_writer.wait() == 0 && !closed_sink.is_open() else { return false } + guard closed_stream.read(1) is None else { return false } + + let (dropped_stream, dropped_sink) = @async-core.Stream::new(capacity=0) + let (drop_writer_started, drop_writer_started_sink) = @async-core.Stream::new( + capacity=1, + ) + let drop_writer = background_group.spawn(async fn() -> Int { + let signal : FixedArray[Unit] = [()] + guard drop_writer_started_sink.write_all(signal[:]) else { return -1 } + drop_writer_started_sink.close() + let data : FixedArray[Int] = [13] + dropped_sink.write(data[:]) + }) + guard drop_writer_started.read(1) is Some(signal) && signal.length() == 1 else { + return false + } + dropped_stream.drop() + drop_writer.cancel() + drop_writer.wait() == 0 && !dropped_sink.is_open() +} + +///| +pub async fn local_stream_produce_read( + _background_group : @async-core.TaskGroup[Unit], +) -> Bool { + let unstarted_cleanup_ran = Ref(false) + let stream = @async-core.Stream::produce( + async fn(sink) { + let data : FixedArray[Int] = [1, 2, 3] + let _ = sink.write_all(data[:]) + sink.close() + }, + on_unstarted_drop=fn() { unstarted_cleanup_ran.val = true }, + ) + guard stream.read(2) is Some(first) else { return false } + guard first.length() == 2 && first[0] == 1 && first[1] == 2 else { + return false + } + guard stream.read(2) is Some(second) else { return false } + guard second.length() == 1 && second[0] == 3 else { return false } + guard stream.read(1) is None else { return false } + stream.drop() + !unstarted_cleanup_ran.val +} diff --git a/tests/runtime/moonbit/resource-payloads/test.wit b/tests/runtime/moonbit/resource-payloads/test.wit new file mode 100644 index 000000000..ea6ec9e2a --- /dev/null +++ b/tests/runtime/moonbit/resource-payloads/test.wit @@ -0,0 +1,75 @@ +//@ dependencies = ['test', 'leaf'] + +package my:test; + +interface leaf-interface { + resource leaf-thing { + constructor(s: string); + get: func() -> string; + } + leaf-drop-count: func() -> u32; + leaf-live-count: func() -> u32; + + resource fields { + constructor(s: string); + get: func() -> string; + } + + resource body { + constructor(contents: stream, trailers: option>); + collect: async func() -> tuple>; + } + + resource response { + constructor(body: body); + collect: async func() -> tuple>; + } + + fields-live-count: func() -> u32; + body-live-count: func() -> u32; + response-live-count: func() -> u32; +} + +interface test-interface { + use leaf-interface.{leaf-thing, response}; + + settle: async func(); + short-reads-leaf: async func(s: stream) -> stream; + dropped-reader-leaf: async func(f1: future, f2: future) -> tuple, future>; + make-leaf-stream: async func() -> stream; + make-local-leaf-stream: async func() -> stream; + make-response: async func() -> response; + make-response-post-work: async func() -> response; + post-response-count: func() -> u32; + make-response-background: async func() -> response; + background-count: func() -> u32; + delayed-leaf-future: async func() -> future; + local-future-drop-cleanup: async func() -> u32; + local-future-drop-resource-cleanup: async func() -> u32; + local-lazy-future-drop-resource-cleanup: async func() -> u32; + local-stream-drop-resource-cleanup: async func() -> u32; + local-lazy-stream-drop-resource-cleanup: async func() -> u32; + local-lazy-stream-partial-drop-resource-cleanup: async func() -> u32; + local-stream-cancelled-write-resource-cleanup: async func() -> u32; + cancel-future-read: async func(f: future) -> bool; + cancel-stream-read: async func(s: stream) -> bool; + relay-nested-leaf: async func(value: future>>) -> future>>; + relay-leaf-futures: async func(value: stream>) -> stream>; + local-stream-rendezvous: async func() -> bool; + local-stream-bounded-backpressure: async func() -> bool; + local-stream-cancelled-waiters: async func() -> bool; + local-stream-produce-read: async func() -> bool; +} + +world leaf { + export leaf-interface; +} + +world test { + export test-interface; +} + +world runner { + import test-interface; + export run: async func(); +} diff --git a/tests/runtime/moonbit/stream-write-cancel/holder-service.rs b/tests/runtime/moonbit/stream-write-cancel/holder-service.rs new file mode 100644 index 000000000..5d1057fb5 --- /dev/null +++ b/tests/runtime/moonbit/stream-write-cancel/holder-service.rs @@ -0,0 +1,90 @@ +include!(env!("BINDINGS")); + +use crate::exports::test::moonbit_stream_write_cancel::holder::{ + Guest, GuestLeaf, Leaf, +}; +use std::cell::RefCell; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use std::sync::Mutex; +use std::task::Waker; +use wit_bindgen::{StreamReader, StreamResult}; + +struct Component; + +export!(Component); + +static WRITE_STARTED: AtomicBool = AtomicBool::new(false); +static WRITE_STARTED_WAKER: Mutex> = Mutex::new(None); +static LEAF_LIVE_COUNT: AtomicU32 = AtomicU32::new(0); +static LEAF_DROP_COUNT: AtomicU32 = AtomicU32::new(0); + +thread_local! { + static HELD_STREAM: RefCell>> = const { RefCell::new(None) }; +} + +struct MyLeaf; + +impl Guest for Component { + type Leaf = MyLeaf; + + async fn hold(value: StreamReader) { + HELD_STREAM.with(|stream| assert!(stream.borrow_mut().replace(value).is_none())); + } + + fn mark_write_started() { + WRITE_STARTED.store(true, Ordering::SeqCst); + if let Some(waker) = WRITE_STARTED_WAKER.lock().unwrap().take() { + waker.wake(); + } + } + + fn write_started() -> bool { + WRITE_STARTED.load(Ordering::SeqCst) + } + + async fn wait_write_started() { + std::future::poll_fn(|cx| { + if WRITE_STARTED.load(Ordering::SeqCst) { + std::task::Poll::Ready(()) + } else { + *WRITE_STARTED_WAKER.lock().unwrap() = Some(cx.waker().clone()); + std::task::Poll::Pending + } + }) + .await + } + + async fn writable_dropped() -> bool { + let mut stream = HELD_STREAM.with(|stream| stream.borrow_mut().take().unwrap()); + let (result, values) = stream.read(Vec::with_capacity(1)).await; + result == StreamResult::Dropped && values.is_empty() + } + + async fn read_one_and_drop() -> bool { + let mut stream = HELD_STREAM.with(|stream| stream.borrow_mut().take().unwrap()); + let (result, values) = stream.read(Vec::with_capacity(1)).await; + result == StreamResult::Complete(1) && values.len() == 1 + } + + fn leaf_live_count() -> u32 { + LEAF_LIVE_COUNT.load(Ordering::SeqCst) + } + + fn leaf_drop_count() -> u32 { + LEAF_DROP_COUNT.load(Ordering::SeqCst) + } +} + +impl GuestLeaf for MyLeaf { + fn new() -> Self { + LEAF_LIVE_COUNT.fetch_add(1, Ordering::SeqCst); + Self + } +} + +impl Drop for MyLeaf { + fn drop(&mut self) { + LEAF_LIVE_COUNT.fetch_sub(1, Ordering::SeqCst); + LEAF_DROP_COUNT.fetch_add(1, Ordering::SeqCst); + } +} diff --git a/tests/runtime/moonbit/stream-write-cancel/runner.rs b/tests/runtime/moonbit/stream-write-cancel/runner.rs new file mode 100644 index 000000000..df0a16e7f --- /dev/null +++ b/tests/runtime/moonbit/stream-write-cancel/runner.rs @@ -0,0 +1,43 @@ +//@ wasmtime-flags = '-Wcomponent-model-async' + +include!(env!("BINDINGS")); + +use crate::test::moonbit_stream_write_cancel::controller::{ + cancellation_observed, producer_finished, run_until_cancelled, second_write_started, + start_peer_drop, wait_cancellation_observed, wait_producer_finished, +}; +use crate::test::moonbit_stream_write_cancel::holder; +use futures::task::noop_waker_ref; +use std::future::Future; +use std::task::Context; + +struct Component; + +export!(Component); + +impl Guest for Component { + async fn run() { + let mut task = Box::pin(run_until_cancelled()); + assert!(task + .as_mut() + .poll(&mut Context::from_waker(noop_waker_ref())) + .is_pending()); + holder::wait_write_started().await; + assert!(holder::write_started()); + + drop(task); + wait_cancellation_observed().await; + assert!(cancellation_observed()); + assert!(holder::writable_dropped().await); + assert_eq!(holder::leaf_live_count(), 0); + assert_eq!(holder::leaf_drop_count(), 1); + + start_peer_drop().await; + assert!(holder::read_one_and_drop().await); + wait_producer_finished().await; + assert!(second_write_started()); + assert!(producer_finished()); + assert_eq!(holder::leaf_live_count(), 0); + assert_eq!(holder::leaf_drop_count(), 5); + } +} diff --git a/tests/runtime/moonbit/stream-write-cancel/test.mbt b/tests/runtime/moonbit/stream-write-cancel/test.mbt new file mode 100644 index 000000000..18c01d6ee --- /dev/null +++ b/tests/runtime/moonbit/stream-write-cancel/test.mbt @@ -0,0 +1,96 @@ +//@ [lang] +//@ path = 'gen/interface/test/moonbit-stream-write-cancel/controller/stub.mbt' +//@ pkg_config = """{ "import": [{ "path": "test/moonbit-stream-write-cancel/async-core", "alias": "async-core" }, { "path": "test/moonbit-stream-write-cancel/interface/test/moonbit-stream-write-cancel/holder", "alias": "holder" }] }""" + +///| +let never : (@async-core.Future[Unit], @async-core.Promise[Unit]) = + @async-core.Future::new() + +///| +let cancellation_seen : Ref[Bool] = { val: false } + +///| +let cancellation_done : (@async-core.Future[Unit], @async-core.Promise[Unit]) = + @async-core.Future::new() + +///| +let second_write_seen : Ref[Bool] = { val: false } + +///| +let peer_drop_producer_finished : Ref[Bool] = { val: false } + +///| +let producer_done : (@async-core.Future[Unit], @async-core.Promise[Unit]) = + @async-core.Future::new() + +///| +pub async fn run_until_cancelled( + _background_group : @async-core.TaskGroup[Unit] +) -> Unit { + let stream = @async-core.Stream::produce(async fn(sink) { + defer { + cancellation_seen.val = true + ignore(cancellation_done.1.complete(())) + } + @holder.mark_write_started() + let data : FixedArray[@holder.Leaf] = [@holder.Leaf::leaf()] + let _ = sink.write_all(data[:]) + }) + @holder.hold(stream) + never.0.get() +} + +///| +pub fn cancellation_observed() -> Bool { + cancellation_seen.val +} + +///| +pub async fn wait_cancellation_observed( + _background_group : @async-core.TaskGroup[Unit] +) -> Unit { + cancellation_done.0.get() +} + +///| +pub async fn start_peer_drop( + _background_group : @async-core.TaskGroup[Unit] +) -> Unit { + let stream = @async-core.Stream::produce(async fn(sink) { + defer { + peer_drop_producer_finished.val = true + ignore(producer_done.1.complete(())) + } + let first : FixedArray[@holder.Leaf] = [@holder.Leaf::leaf()] + guard sink.write_all(first[:]) else { panic() } + let second : FixedArray[@holder.Leaf] = [@holder.Leaf::leaf()] + guard sink.write_all(second[:]) else { panic() } + second_write_seen.val = true + let third : FixedArray[@holder.Leaf] = [@holder.Leaf::leaf()] + guard !sink.write_all(third[:]) else { panic() } + let fourth : FixedArray[@holder.Leaf] = [@holder.Leaf::leaf()] + guard !sink.write_all(fourth[:]) else { panic() } + }, cleanup=fn(value) { value.drop() }) + guard stream.read(1) is Some(priming) && priming.length() == 1 else { + panic() + } + priming[0].drop() + @holder.hold(stream) +} + +///| +pub fn second_write_started() -> Bool { + second_write_seen.val +} + +///| +pub fn producer_finished() -> Bool { + peer_drop_producer_finished.val +} + +///| +pub async fn wait_producer_finished( + _background_group : @async-core.TaskGroup[Unit] +) -> Unit { + producer_done.0.get() +} diff --git a/tests/runtime/moonbit/stream-write-cancel/test.wit b/tests/runtime/moonbit/stream-write-cancel/test.wit new file mode 100644 index 000000000..cf084aafa --- /dev/null +++ b/tests/runtime/moonbit/stream-write-cancel/test.wit @@ -0,0 +1,44 @@ +//@ async = true +//@ dependencies = ['test', 'holder-service'] + +package test:moonbit-stream-write-cancel; + +interface holder { + resource leaf { + constructor(); + } + + hold: async func(value: stream); + mark-write-started: func(); + write-started: func() -> bool; + wait-write-started: async func(); + writable-dropped: async func() -> bool; + read-one-and-drop: async func() -> bool; + leaf-live-count: func() -> u32; + leaf-drop-count: func() -> u32; +} + +interface controller { + run-until-cancelled: async func(); + cancellation-observed: func() -> bool; + wait-cancellation-observed: async func(); + start-peer-drop: async func(); + second-write-started: func() -> bool; + producer-finished: func() -> bool; + wait-producer-finished: async func(); +} + +world test { + import holder; + export controller; +} + +world holder-service { + export holder; +} + +world runner { + import controller; + import holder; + export run: async func(); +} diff --git a/tests/runtime/moonbit/wasi-cli-p3-stdout/deps/cli.wit b/tests/runtime/moonbit/wasi-cli-p3-stdout/deps/cli.wit new file mode 100644 index 000000000..2eb2bc408 --- /dev/null +++ b/tests/runtime/moonbit/wasi-cli-p3-stdout/deps/cli.wit @@ -0,0 +1,26 @@ +package wasi:cli@0.3.0; + +interface types { + enum error-code { + io, + illegal-byte-sequence, + pipe, + } +} + +interface stdout { + use types.{error-code}; + + write-via-stream: func(data: stream) -> future>; +} + +interface run { + run: async func() -> result; +} + +world command { + import types; + import stdout; + + export run; +} diff --git a/tests/runtime/moonbit/wasi-cli-p3-stdout/runner.mbt b/tests/runtime/moonbit/wasi-cli-p3-stdout/runner.mbt new file mode 100644 index 000000000..86d82c972 --- /dev/null +++ b/tests/runtime/moonbit/wasi-cli-p3-stdout/runner.mbt @@ -0,0 +1,9 @@ +//@ wasmtime-flags = '-Wcomponent-model-async -S p3' +//@ [lang] +//@ path = 'gen/world/runner/stub.mbt' +//@ pkg_config = """{ "warn-list": "-44", "import": [{ "path": "my/test/async-core", "alias": "async-core" }, { "path": "my/test/interface/my/test/i", "alias": "i" }] }""" + +///| +pub async fn run(_background_group : @async-core.TaskGroup[Unit]) -> Unit { + guard @i.write_stdout() is Ok(_) else { panic() } +} diff --git a/tests/runtime/moonbit/wasi-cli-p3-stdout/test.mbt b/tests/runtime/moonbit/wasi-cli-p3-stdout/test.mbt new file mode 100644 index 000000000..753229c10 --- /dev/null +++ b/tests/runtime/moonbit/wasi-cli-p3-stdout/test.mbt @@ -0,0 +1,28 @@ +//@ [lang] +//@ path = 'gen/interface/my/test/i/stub.mbt' +//@ pkg_config = """{ +//@ "import": [ +//@ { "path": "my/test/async-core", "alias": "async-core" }, +//@ { "path": "my/test/interface/wasi/cli/stdout", "alias": "stdout" } +//@ ] +//@ }""" + +///| +pub async fn write_stdout( + _background_group : @async-core.TaskGroup[Unit], +) -> Result[Unit, Unit] { + let data : FixedArray[Byte] = [ + b'h', b'e', b'l', b'l', b'o', b' ', b'p', b'3', b'\n', + ] + let done = @stdout.write_via_stream( + @async-core.Stream::produce(async fn(sink) { + let _ = sink.write_all(data[:]) + sink.close() + }), + ) + let result = done.get() catch { _ => return Err(()) } + match result { + Ok(_) => Ok(()) + Err(_) => Err(()) + } +} diff --git a/tests/runtime/moonbit/wasi-cli-p3-stdout/test.wit b/tests/runtime/moonbit/wasi-cli-p3-stdout/test.wit new file mode 100644 index 000000000..39cad09e4 --- /dev/null +++ b/tests/runtime/moonbit/wasi-cli-p3-stdout/test.wit @@ -0,0 +1,17 @@ +package my:test; + +interface i { + write-stdout: async func() -> result; +} + +world test { + import wasi:cli/stdout@0.3.0; + + export i; +} + +world runner { + import i; + + export run: async func(); +} diff --git a/tests/runtime/simple-future/test.mbt b/tests/runtime/simple-future/test.mbt index d4fc4b7a0..a30e62511 100644 --- a/tests/runtime/simple-future/test.mbt +++ b/tests/runtime/simple-future/test.mbt @@ -3,19 +3,16 @@ ///| pub async fn read_future( - x : @ffi.FutureReader[Unit], -) -> Unit noraise { - let task = @ffi.current_task() - task.spawn(fn() { - let _ = x.read() catch { _ => raise @ffi.Cancelled::Cancelled } - - }) + x : @async-core.Future[Unit], + _background_group : @async-core.TaskGroup[Unit], +) -> Unit { + let _ = x.get() catch { _ => () } } ///| pub async fn drop_future( - x : @ffi.FutureReader[Unit], -) -> Unit noraise { - let task = @ffi.current_task() - let _ = x.drop() + x : @async-core.Future[Unit], + _background_group : @async-core.TaskGroup[Unit], +) -> Unit { + x.drop() } diff --git a/tests/runtime/simple-import-params-results/test.mbt b/tests/runtime/simple-import-params-results/test.mbt index ddaaac29a..2661cbc0f 100644 --- a/tests/runtime/simple-import-params-results/test.mbt +++ b/tests/runtime/simple-import-params-results/test.mbt @@ -2,29 +2,43 @@ //@ path = 'gen/interface/a/b/i/stub.mbt' ///| -pub async fn one_argument(x : UInt) -> Unit { +pub async fn one_argument( + x : UInt, + _background_group : @async-core.TaskGroup[Unit], +) -> Unit { assert_eq(x, 1) } ///| -pub async fn one_result() -> UInt noraise { +pub async fn one_result(_background_group : @async-core.TaskGroup[Unit]) -> UInt { 2 } ///| -pub async fn one_argument_and_result(x : UInt) -> UInt { +pub async fn one_argument_and_result( + x : UInt, + _background_group : @async-core.TaskGroup[Unit], +) -> UInt { assert_eq(x, 3) 4 } ///| -pub async fn two_arguments(x : UInt, y : UInt) -> Unit { +pub async fn two_arguments( + x : UInt, + y : UInt, + _background_group : @async-core.TaskGroup[Unit], +) -> Unit { assert_eq(x, 5) assert_eq(y, 6) } ///| -pub async fn two_arguments_and_result(x : UInt, y : UInt) -> UInt { +pub async fn two_arguments_and_result( + x : UInt, + y : UInt, + _background_group : @async-core.TaskGroup[Unit], +) -> UInt { assert_eq(x, 7) assert_eq(y, 8) 9 diff --git a/tests/runtime/simple-stream-payload/test.mbt b/tests/runtime/simple-stream-payload/test.mbt index ef38dd6a6..48a18ef5d 100644 --- a/tests/runtime/simple-stream-payload/test.mbt +++ b/tests/runtime/simple-stream-payload/test.mbt @@ -1,20 +1,26 @@ //@ [lang] //@ path = 'gen/interface/my/test/i/stub.mbt' -pub async fn read_stream(x : @ffi.StreamReader[Byte]) -> Unit raise { - let task = @ffi.current_task() - let buffer = FixedArray::make(10, Byte::default()); +pub async fn read_stream( + x : @async-core.Stream[Byte], + _background_group : @async-core.TaskGroup[Unit], +) -> Unit raise { + let chunk0 = x.read(1).unwrap() + assert_eq(chunk0.length(), 1) + assert_eq(chunk0[0], b'\x00') - task.wait(fn(){ - let _ = x.read(buffer, 1) catch { _ => raise @ffi.Cancelled::Cancelled } - }) - task.wait(fn(){ - let _ = x.read(buffer, 2, offset=1) catch { _ => raise @ffi.Cancelled::Cancelled } - }) - task.wait(fn(){ - let _ = x.read(buffer, 1, offset=3) catch { _ => raise @ffi.Cancelled::Cancelled } - }) - task.wait(fn(){ - let _ = x.read(buffer, 1, offset=4) catch { _ => raise @ffi.Cancelled::Cancelled } - }) + let chunk1 = x.read(2).unwrap() + assert_eq(chunk1.length(), 2) + assert_eq(chunk1[0], b'\x01') + assert_eq(chunk1[1], b'\x02') + + let chunk2 = x.read(1).unwrap() + assert_eq(chunk2.length(), 1) + assert_eq(chunk2[0], b'\x03') + + let chunk3 = x.read(1).unwrap() + assert_eq(chunk3.length(), 1) + assert_eq(chunk3[0], b'\x04') + + x.drop() } diff --git a/tests/runtime/simple-stream/test.mbt b/tests/runtime/simple-stream/test.mbt index c2f38c7a6..2a10b58f4 100644 --- a/tests/runtime/simple-stream/test.mbt +++ b/tests/runtime/simple-stream/test.mbt @@ -1,14 +1,11 @@ //@ [lang] //@ path = 'gen/interface/my/test/i/stub.mbt' -pub async fn read_stream(x : @ffi.StreamReader[Unit]) -> Unit noraise { - let task = @ffi.current_task() - let buffer = FixedArray::make(10, Unit::default()); - - task.spawn(fn(){ - let _ = x.read(buffer, 1) catch { _ => raise @ffi.Cancelled::Cancelled } - }) - task.spawn(fn(){ - let _ = x.read(buffer, 2) catch { _ => raise @ffi.Cancelled::Cancelled } - }) +pub async fn read_stream( + x : @async-core.Stream[Unit], + _background_group : @async-core.TaskGroup[Unit], +) -> Unit { + let _ = x.read(1) catch { _ => None } + let _ = x.read(2) catch { _ => None } + let _ = x.drop() catch { _ => () } } From 09193bc1329b94eb6371fefe51b0c0d65adc5440 Mon Sep 17 00:00:00 2001 From: zihang Date: Mon, 20 Jul 2026 10:48:52 +0800 Subject: [PATCH 14/23] test(moonbit): add Rust async fixture pairings --- .../moonbit/nested-future-stream/test.rs | 150 ++++++++++++++++++ .../moonbit/stream-write-cancel/test.rs | 113 +++++++++++++ 2 files changed, 263 insertions(+) create mode 100644 tests/runtime/moonbit/nested-future-stream/test.rs create mode 100644 tests/runtime/moonbit/stream-write-cancel/test.rs diff --git a/tests/runtime/moonbit/nested-future-stream/test.rs b/tests/runtime/moonbit/nested-future-stream/test.rs new file mode 100644 index 000000000..7dec15953 --- /dev/null +++ b/tests/runtime/moonbit/nested-future-stream/test.rs @@ -0,0 +1,150 @@ +include!(env!("BINDINGS")); + +use crate::exports::test::moonbit_nested_future_stream::nested::Guest; +use std::sync::Mutex; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::task::{Poll, Waker}; +use wit_bindgen::{FutureReader, StreamReader, StreamResult}; + +struct Component; + +export!(Component); + +static SHARED: Mutex> = Mutex::new(None); +static SHARED_WAKER: Mutex> = Mutex::new(None); +static CANCELLATION_OBSERVED: AtomicBool = AtomicBool::new(false); +static CANCELLATION_WAKER: Mutex> = Mutex::new(None); + +struct CancellationGuard; + +impl Drop for CancellationGuard { + fn drop(&mut self) { + CANCELLATION_OBSERVED.store(true, Ordering::SeqCst); + if let Some(waker) = CANCELLATION_WAKER.lock().unwrap().take() { + waker.wake(); + } + } +} + +impl Guest for Component { + async fn relay( + value: FutureReader>>, + ) -> FutureReader>> { + let (outer_writer, outer_reader) = wit_future::new(|| unreachable!()); + wit_bindgen::spawn_local(async move { + let input_inner = value.await; + let (inner_writer, inner_reader) = wit_future::new(|| unreachable!()); + let outer_open = outer_writer.write(inner_reader).await.is_ok(); + + // Continue consuming the input chain after an outer rejection so + // that its producer observes the rejection at the innermost stream. + let mut input_stream = input_inner.await; + let (mut output_writer, output_reader) = wit_stream::new(); + let inner_open = inner_writer.write(output_reader).await.is_ok(); + if !outer_open || !inner_open { + return; + } + + loop { + let (result, values) = input_stream.read(Vec::with_capacity(16)).await; + if !values.is_empty() && !output_writer.write_all(values).await.is_empty() { + return; + } + match result { + StreamResult::Complete(_) => {} + StreamResult::Dropped => return, + StreamResult::Cancelled => unreachable!(), + } + } + }); + outer_reader + } + + async fn relay_stream(value: StreamReader>) -> StreamReader> { + let (mut output_writer, output_reader) = wit_stream::new(); + wit_bindgen::spawn_local(async move { + let mut input = value; + loop { + let (result, values) = input.read(Vec::with_capacity(1)).await; + for input_value in values { + let (value_writer, value_reader) = wit_future::new(|| unreachable!()); + if output_writer.write_one(value_reader).await.is_some() { + let _ = input_value.await; + return; + } + let value = input_value.await; + let _ = value_writer.write(value).await; + } + match result { + StreamResult::Complete(_) => {} + StreamResult::Dropped => return, + StreamResult::Cancelled => unreachable!(), + } + } + }); + output_reader + } + + async fn concurrent_writes() -> StreamReader { + let (mut writer, reader) = wit_stream::new(); + wit_bindgen::spawn_local(async move { + assert!(writer.write_all(vec![1, 2]).await.is_empty()); + }); + reader + } + + async fn post_return_lazy() -> StreamReader { + let (mut writer, reader) = wit_stream::new(); + wit_bindgen::spawn_local(async move { + assert!(writer.write_one(42).await.is_none()); + }); + reader + } + + async fn wait_shared() -> u32 { + std::future::poll_fn(|cx| { + if let Some(value) = SHARED.lock().unwrap().take() { + return Poll::Ready(value); + } + + *SHARED_WAKER.lock().unwrap() = Some(cx.waker().clone()); + match SHARED.lock().unwrap().take() { + Some(value) => Poll::Ready(value), + None => Poll::Pending, + } + }) + .await + } + + async fn resolve_shared(value: u32) { + assert!(SHARED.lock().unwrap().replace(value).is_none()); + if let Some(waker) = SHARED_WAKER.lock().unwrap().take() { + waker.wake(); + } + } + + async fn wait_cancelled() { + let _guard = CancellationGuard; + std::future::pending::<()>().await; + } + + async fn wait_cancellation_observed() { + std::future::poll_fn(|cx| { + if CANCELLATION_OBSERVED.load(Ordering::SeqCst) { + return Poll::Ready(()); + } + + *CANCELLATION_WAKER.lock().unwrap() = Some(cx.waker().clone()); + if CANCELLATION_OBSERVED.load(Ordering::SeqCst) { + Poll::Ready(()) + } else { + Poll::Pending + } + }) + .await + } + + fn cancellation_observed() -> bool { + CANCELLATION_OBSERVED.load(Ordering::SeqCst) + } +} diff --git a/tests/runtime/moonbit/stream-write-cancel/test.rs b/tests/runtime/moonbit/stream-write-cancel/test.rs new file mode 100644 index 000000000..3529557a0 --- /dev/null +++ b/tests/runtime/moonbit/stream-write-cancel/test.rs @@ -0,0 +1,113 @@ +include!(env!("BINDINGS")); + +use crate::exports::test::moonbit_stream_write_cancel::controller::Guest; +use crate::test::moonbit_stream_write_cancel::holder; +use std::sync::Mutex; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::task::{Poll, Waker}; + +struct Component; + +export!(Component); + +static CANCELLATION_OBSERVED: AtomicBool = AtomicBool::new(false); +static CANCELLATION_WAKER: Mutex> = Mutex::new(None); +static SECOND_WRITE_STARTED: AtomicBool = AtomicBool::new(false); +static PRODUCER_FINISHED: AtomicBool = AtomicBool::new(false); +static PRODUCER_WAKER: Mutex> = Mutex::new(None); + +struct CancellationGuard; + +impl Drop for CancellationGuard { + fn drop(&mut self) { + CANCELLATION_OBSERVED.store(true, Ordering::SeqCst); + if let Some(waker) = CANCELLATION_WAKER.lock().unwrap().take() { + waker.wake(); + } + } +} + +struct ProducerGuard; + +impl Drop for ProducerGuard { + fn drop(&mut self) { + PRODUCER_FINISHED.store(true, Ordering::SeqCst); + if let Some(waker) = PRODUCER_WAKER.lock().unwrap().take() { + waker.wake(); + } + } +} + +impl Guest for Component { + async fn run_until_cancelled() { + let _guard = CancellationGuard; + let (mut writer, reader) = wit_stream::new::(); + holder::hold(reader).await; + holder::mark_write_started(); + let _ = writer.write_one(holder::Leaf::new()).await; + std::future::pending::<()>().await; + } + + fn cancellation_observed() -> bool { + CANCELLATION_OBSERVED.load(Ordering::SeqCst) + } + + async fn wait_cancellation_observed() { + std::future::poll_fn(|cx| { + if CANCELLATION_OBSERVED.load(Ordering::SeqCst) { + return Poll::Ready(()); + } + + *CANCELLATION_WAKER.lock().unwrap() = Some(cx.waker().clone()); + if CANCELLATION_OBSERVED.load(Ordering::SeqCst) { + Poll::Ready(()) + } else { + Poll::Pending + } + }) + .await + } + + async fn start_peer_drop() { + // The MoonBit implementation primes its language-level queue locally. + // A CM stream cannot be read and written by the same component when + // its payload contains resources, so account for that local item here. + drop(holder::Leaf::new()); + + let (mut writer, reader) = wit_stream::new::(); + wit_bindgen::spawn_local(async move { + let _guard = ProducerGuard; + + assert!(writer.write_one(holder::Leaf::new()).await.is_none()); + SECOND_WRITE_STARTED.store(true, Ordering::SeqCst); + assert!(writer.write_one(holder::Leaf::new()).await.is_some()); + assert!(writer.write_one(holder::Leaf::new()).await.is_some()); + }); + + holder::hold(reader).await; + } + + fn second_write_started() -> bool { + SECOND_WRITE_STARTED.load(Ordering::SeqCst) + } + + fn producer_finished() -> bool { + PRODUCER_FINISHED.load(Ordering::SeqCst) + } + + async fn wait_producer_finished() { + std::future::poll_fn(|cx| { + if PRODUCER_FINISHED.load(Ordering::SeqCst) { + return Poll::Ready(()); + } + + *PRODUCER_WAKER.lock().unwrap() = Some(cx.waker().clone()); + if PRODUCER_FINISHED.load(Ordering::SeqCst) { + Poll::Ready(()) + } else { + Poll::Pending + } + }) + .await + } +} From 0eb0d53ae7ae83a40e01d64b50cebccb33568a12 Mon Sep 17 00:00:00 2001 From: zihang Date: Mon, 20 Jul 2026 15:18:51 +0800 Subject: [PATCH 15/23] refactor(moonbit): remove async duplication --- crates/moonbit/CONTEXT.md | 40 +- .../adr/0001-async-ffi-boundary-conversion.md | 172 ++--- crates/moonbit/docs/async-glossary.md | 626 ++++-------------- crates/moonbit/src/async/async_abi.mbt | 11 - crates/moonbit/src/async/ev.mbt | 11 - crates/moonbit/src/lib.rs | 143 +--- .../moonbit/async-import-cancel/runner.mbt | 76 +-- .../http-background-hook/runner-invalid.mbt | 51 -- .../moonbit/http-background-hook/runner.mbt | 49 +- .../moonbit/http-background-hook/test.mbt | 22 +- .../moonbit/http-background-hook/wkg.lock | 12 - .../http-body-trailers/deps/clocks.wit | 7 - .../moonbit/http-body-trailers/deps/http.wit | 460 ------------- .../moonbit/http-body-trailers/runner.mbt | 47 -- .../moonbit/http-body-trailers/test.mbt | 66 -- .../moonbit/http-body-trailers/test.wit | 14 - 16 files changed, 286 insertions(+), 1521 deletions(-) delete mode 100644 tests/runtime/moonbit/http-background-hook/runner-invalid.mbt delete mode 100644 tests/runtime/moonbit/http-background-hook/wkg.lock delete mode 100644 tests/runtime/moonbit/http-body-trailers/deps/clocks.wit delete mode 100644 tests/runtime/moonbit/http-body-trailers/deps/http.wit delete mode 100644 tests/runtime/moonbit/http-body-trailers/runner.mbt delete mode 100644 tests/runtime/moonbit/http-body-trailers/test.mbt delete mode 100644 tests/runtime/moonbit/http-body-trailers/test.wit diff --git a/crates/moonbit/CONTEXT.md b/crates/moonbit/CONTEXT.md index 8add75ee5..1d55a4488 100644 --- a/crates/moonbit/CONTEXT.md +++ b/crates/moonbit/CONTEXT.md @@ -1,34 +1,16 @@ # MoonBit Bindings Context -This context records shared language for the MoonBit binding generator. Async -future and stream vocabulary is split into [Async Glossary](docs/async-glossary.md). +MoonBit emits core wasm and `wasm-tools` adapts it into a component. Generated +bindings expose WIT imports and adapt MoonBit exports to the component ABI. -## Language +Async design references: -**MoonBit binding**: -Generated MoonBit source that exposes WIT imports to MoonBit code or adapts -MoonBit exports to the component ABI. +- [Design contract](docs/async-design.md) +- [Terminology](docs/async-glossary.md) +- [FFI-boundary conversion decision](docs/adr/0001-async-ffi-boundary-conversion.md) +- [Local Future/Promise decision](docs/adr/0002-local-future-promise.md) -**Component adapter path**: -The current implementation path where MoonBit emits core wasm and `wasm-tools` -converts it into a component using adapter imports and exports. -_Avoid_: direct component generation - -## Async Design - -- [Async design contract](docs/async-design.md) -- [Async glossary](docs/async-glossary.md) -- [ADR 0001: FFI-boundary conversion](docs/adr/0001-async-ffi-boundary-conversion.md) -- [ADR 0002: local Future/Promise pair](docs/adr/0002-local-future-promise.md) - -The implementation targets the official upstream generator architecture. WIT -`future` and `stream` remain distinct from local MoonBit `Future` and `Stream`; -generated code converts them only at concrete FFI positions whose intrinsic -names are supplied by `wit-parser`. Local `Future::new()` returns a -MoonBit-only Future/Promise pair, and local `Semaphore` coordinates coroutines; -neither can select or own a component endpoint from `T`. Async support is always -available in the MoonBit generator, but endpoint-free synchronous worlds do not -emit its runtime or wrappers. Component endpoint wrappers are -generated-code-only, enforce a single in-flight operation, and retain operation -buffers until cancellation or completion is observed. Each top-level component -task has its own waitable set and scheduler state. +Lowercase `future` and `stream` refer to Component Model types. Uppercase +`Future` and `Stream` refer to local MoonBit types. Generated code converts +between them only at concrete WIT positions whose intrinsic names come from +`wit-parser`. diff --git a/crates/moonbit/docs/adr/0001-async-ffi-boundary-conversion.md b/crates/moonbit/docs/adr/0001-async-ffi-boundary-conversion.md index 1cc18ec12..161db8c45 100644 --- a/crates/moonbit/docs/adr/0001-async-ffi-boundary-conversion.md +++ b/crates/moonbit/docs/adr/0001-async-ffi-boundary-conversion.md @@ -1,141 +1,43 @@ -# Keep MoonBit async values detached from component futures and streams +# Keep local async values separate from component endpoints -Status: accepted; implemented in the current branch +Status: accepted; implemented -MoonBit `Future[T]` handles and local `Stream[T]` values are over arbitrary -MoonBit types, while component `future` and `stream` are ABI values over -WIT-representable payloads. MoonBit async bindings will keep those concepts -separate: generated FFI-boundary code converts between local async values and -component endpoints only at concrete WIT positions where the endpoint operations -and payload lift/lower code are known. +## Context -For MVP, component `future` maps to a one-shot `Future[T]` handle. The handle -represents a ready value or an owned local source computation with -value-discarding drop cleanup. For an incoming component future, generated code -supplies a source closure that captures the raw readable handle and directly -binds the concrete WIT position's read/cancel/drop intrinsics. The generic local -type has no CM-specific variant or operation table. A public local `Promise[T]` -may settle the same Future state, but it is not a component writable endpoint. +MoonBit `Future[T]` and `Stream[T]` must support arbitrary MoonBit values. A +Component Model `future` or `stream` is instead a transferable readable +endpoint whose operations and payload representation belong to a concrete WIT +function position. + +The previous implementation exposed generic component endpoint wrappers backed +by operation vtables. That tied local construction and recursive conversion to +position-specific ABI machinery. + +## Decision + +`Future[T]`, `Promise[T]`, `Stream[T]`, and `Sink[T]` are local coordination +types and never contain component handles or operation tables. + +For each endpoint occurrence, generated FFI code obtains intrinsic names from +`wit-parser`, owns the raw endpoint, and recursively lifts or lowers the payload. +Nested endpoints are converted one layer at a time when their containing value +crosses or is read at the boundary. ## Consequences -- `Stream::new()` creates local MoonBit values only. -- `Future::new()` creates a local `Future[T]` / `Promise[T]` pair only. It does - not select or invoke a component `future.new` intrinsic. -- Endpoint operation tables are not part of the target design. Generated - position-specific helpers call canonical intrinsics directly and keep raw - handle state inside generated source closures or producer tasks. -- Component intrinsic module and field names are generated through - `wit-parser`'s `WasmImport::{FutureIntrinsic, StreamIntrinsic}` API. The - MoonBit generator does not reproduce position indices, export prefixes, - unit-payload names, or async-lower prefixes by string formatting. -- The Rust generator boundary owns a recursive conversion plan for each - function. Ordinary lift/lower code requests position-specific helpers from - that plan instead of naming runtime endpoint types or type-shaped tables. -- The existing async generator boundary is retained. The static recursive - rewrite happens behind it without changing ordinary lift/lower ownership or - the endpoint-free sync path. -- User-facing bindings expose local `Future[T]` handles and local `Stream[T]` - for ordinary async composition. -- A nested WIT shape such as `future>>` maps to - `Future[Future[Stream[T]]]`. Only the current layer's readable handle appears - at each canonical payload stage. Generated recursive lift/lower functions bind - each layer to its own function-position intrinsic and apply generated - commit/reject dispositions at every transfer boundary. -- Recursive lower retains its canonical buffer and prepared producer state until - the transfer reports a disposition. Commit starts producer work only after - ownership transfers. Stream progress commits the accepted prefix and retries - the same lowered suffix; an abandoned suffix is rejected exactly once. -- A freshly-created component future is not fully rollbackable. Canonical ABI - permits its writable end to be dropped only after a write succeeds or a write - reports that the reader was dropped. If an outer transfer rejects a nested - future readable, generated code drops that readable but must still drive the - paired writer with the local future value until its write observes `dropped`. - A cancelled write is not settlement and is retried while the component task - remains alive. -- Stream batches whose element type recursively contains a future use a - one-element staging window for MVP. This bounds settlement obligations created - before downstream acceptance without changing the public stream API. -- Generated stream producers remain prepared when their component pair is - created. Commit starts normal pumping. Parent rejection drops both - untransferred component ends and performs state-aware local rejection: incoming - component sources close immediately, buffered values use configured cleanup or - generated cleanup as fallback, and an unstarted local producer is discarded - without executing user code. `Stream::produce` accepts an optional - `on_unstarted_drop` callback for resources captured by that branch and a - separate per-element cleanup for values written after it starts. -- Canonical `backpressure.inc/dec` controls admission of new async component - tasks. It is not tied to future/stream bridge lifetime and is not called - implicitly by the MoonBit runtime. Endpoint read/write suspension provides - data-flow backpressure independently. -- Async import argument settlement distinguishes `cancelled-before-started` - from every state in which the callee may have started. The former recursively - rejects owned resources and endpoints; the latter only reclaims guest-owned - canonical list allocations. -- Local future MVP has consuming `Future::get()` and value-discarding async - `Future::drop()`. Strong cancellation belongs to `Task` and `TaskGroup`, not - to a future-specific result type. `Future::drop()` is explicit cleanup, not a - direct alias for component `future.drop-*`, and futures that may discard - completed WIT payloads carry generated payload cleanup logic. -- Outgoing component `future` producer tasks must settle their raw writable - handle by writing a real value or by attempting the write and observing reader - drop. The MVP does not fabricate default values to satisfy unwritten futures. - If user code produces a ready `T`, generated code may write immediately; if - user code returns a pending `Future[T]`, the bridge exists before the CM - `future.write` operation starts because the component boundary already needs a - readable end to return. -- After an outgoing component `future` readable end is committed, or after a - parent transfer rejects and locally drops it, the bridge shields producer work - from ordinary task/subtask cancellation. Component-task cancellation is - cooperative: it may resolve the cancelled call with `task.cancel`, but it does - not forcibly destroy shielded settlement work. Only instance teardown or a - trap can abandon the writer without settlement. Peer reader drop is - loss-of-interest, not task cancellation; before `future.write` has started, - the bridge does nothing special for it. If a later write reports `dropped`, - the bridge cleans the value and settles the writer. -- If that local future never produces `T`, the Component Model provides no - generic close-without-value operation. This is an explicit liveness limit; the - binding does not fabricate a default value or pretend an idle writer can be - dropped safely. The same limit applies when the paired readable was created - for a nested payload but the outer transfer rejected it before ownership - crossed the boundary. -- Local stream MVP has `Sink::close()` for graceful producer close and async - `Stream::drop()` or `Stream[T]` drop for consumer loss-of-interest. It does - not expose public `Sink::cancel()` because component `stream` has no distinct - generic producer-failure signal to preserve across the boundary. -- Local stream state keeps producer close separate from reader drop. Producer - close preserves buffered `FixedArray[T]` chunks for draining; reader drop - discards unread chunks and uses an explicit `Stream::new_with_cleanup` or - `Stream::produce(cleanup=...)` callback for payloads that need resource - cleanup. -- Local stream capacity is measured in elements. Zero is strict rendezvous and - a positive value is a hard bound on accepted unread values; local streams are - never implicitly unbounded. Waiting readers and writers use direct FIFO - handoff with completion-versus-cancellation race handling. -- An incoming component stream uses a generated demand-driven source whose - reads directly call its concrete site intrinsics. It does not start an eager - pump into a local stream pipe. -- Forwarding component endpoints without reading them is not the default path and - needs an explicit advanced API if we decide to support it. -- Runtime validation includes real `wasi:cli@0.3.0` stream output and a real - `wasi:http@0.3.0` handler response whose body stream, trailers future, and - transmission completion continue after the handler returns the response. -- Local stream validation covers strict rendezvous, bounded buffering, cancelled - waiter removal, completion-versus-cancellation ownership races, local - producer reads, and unread resource cleanup. -- Async export stubs intentionally expose - `background_group : @async-core.TaskGroup[Unit]`. It adapts the mismatch - between MoonBit structured completion and component task return. Generated code - publishes component task return when the user function produces its result, - then keeps the underlying MoonBit task group alive for hook-style post-return - work. That work remains structurally owned and bounded by the component task - or instance lifetime, but cannot change the already-published export result. -- Lowering local `Future[T]` and `Stream[T]` values requires an active component - async task scope, including recursive occurrences inside structured payloads. - A sync WIT import called in that scope prepares its endpoint arguments and - commits those same handles immediately after the core call returns. Sync export - results, and sync imports called without an active scope, remain unsupported. - Incoming lift remains lazy and does not require a producer task until user code - later reads in an async scope. Scope-free sync lowering requires cooperative - component-thread support and must not be faked by using the stackless callback - ABI for a non-async WIT function. +- `Future::new()` creates a local Future/Promise pair; `Stream::new()` creates a + local Stream/Sink pair. +- Incoming endpoints become lazy generated sources. Outgoing local values are + bridged through newly created component endpoint pairs. +- Lowering uses explicit prepare, commit, and reject paths so resources and + partial stream prefixes transfer or clean exactly once. +- Once a component future readable end is exposed, its writer must eventually + write a real value or observe that the reader was dropped. The binding cannot + fabricate a default value or close it without a value. +- Position-specific endpoint traversal remains generator data, not a MoonBit + runtime vtable. +- Producing a component future or stream requires an active component async task + scope. Scope-free synchronous lowering remains unsupported. + +Detailed API and lifecycle invariants live in the +[async design contract](../async-design.md). diff --git a/crates/moonbit/docs/async-glossary.md b/crates/moonbit/docs/async-glossary.md index 3807c883f..14fedcd8e 100644 --- a/crates/moonbit/docs/async-glossary.md +++ b/crates/moonbit/docs/async-glossary.md @@ -1,488 +1,138 @@ -# MoonBit Async Glossary - -This glossary records the vocabulary used by the MoonBit async future and stream -design notes. The naming rule is: - -- lowercase code-form names, `future` and `stream`, mean component-model WIT - types and operations; -- uppercase code-form names, `Future` and `Stream`, mean MoonBit-owned local - async types; -- `CM` describes component-model concepts in prose. The target runtime does not - expose reusable generic `CMFuture*` or `CMStream*` wrapper types; raw endpoint - handles and their direct operations stay inside generated site helpers. - -## Component Model Language - -**Component `future`**: -The component-model `future` WIT type. In a WIT signature it transfers -ownership of a readable end, not a MoonBit-local computation. -_Avoid_: `Future`, local Future - -**Component `stream`**: -The component-model `stream` WIT type. In a WIT signature it transfers -ownership of a readable end, not a MoonBit-owned `Stream` buffer. -_Avoid_: `Stream`, local Stream - -**Readable end**: -The endpoint of a component `future` or component `stream` that can be read and -transferred through WIT. - -**Writable end**: -The endpoint created with `future.new` or `stream.new` that stays in the -producing component and writes values to the paired readable end. - -**Async task scope**: -The component-model task context that lets stackless MoonBit async export code -join waitables, receive callback events, and resume suspended bridge -continuations. -_Avoid_: global event loop - -**Background group**: -The `background_group : @async-core.TaskGroup[Unit]` parameter intentionally -exposed by an async MoonBit export. The generated adapter publishes component -task return when the user function produces its result, then keeps the -underlying MoonBit task group alive for this hook-style work. The work remains -structurally owned inside MoonBit and bounded by the component task or instance -lifetime, but its later outcome cannot change the already-published export -result. -_Avoid_: leaked task group, detached global task - -**Async core package**: -The generated `@async-core` MoonBit package. It exposes the local `Future[T]`, -`Promise[T]`, `Stream[T]`, `Sink[T]`, `Task[T]`, `TaskGroup[T]`, `Semaphore`, -`Mutex`, and `CondVar` API while keeping component-model endpoint handles and -scheduler machinery private. -Interface-specific `ffi.mbt` files remain generated boundary implementations -and are not this package. - -**Sync lower producer gap**: -The unsupported case where boundary code without an active component async task -scope must lower a local Future or Stream, including recursive occurrences in a -sync import parameter or sync export result. The producer must continue after -the call returns, but the stackless callback ABI cannot resume it. This requires -cooperative component-thread support. Incoming lift is not this case because it -can remain lazy until a later async read. -_Avoid_: hidden async export - -## MoonBit Binding Language - -**CM future endpoint**: -A conceptual component `future` readable or writable handle owned by generated -boundary code. Its operations are statically bound to a concrete function site, -not stored in a generic MoonBit operation table. -_Avoid_: `Future`, local Future - -**CM stream endpoint**: -A conceptual component `stream` readable or writable handle owned by generated -boundary code. Its operations are statically bound to a concrete function site, -not stored in a generic MoonBit operation table. -_Avoid_: `Stream`, local Stream - -**Endpoint site**: -One component `future` or component `stream` node in a concrete WIT function's -canonical type traversal. A site binds its payload plan and intrinsic names to -that function context. It is generator metadata, not a runtime value. -_Avoid_: payload type alone - -**Recursive boundary plan**: -The generator-owned tree that connects an endpoint site to the endpoint sites -inside its payload. It drives direct lift/lower code and recursive cleanup for -records, variants, collections, nested futures, and nested streams. -_Avoid_: endpoint vtable, flat type lookup - -**Generated readable source**: -A private source closure produced when lifting an incoming component endpoint. -It exclusively owns one raw readable handle and directly calls that site's -read, cancel-read, and drop-readable intrinsics. A future source plugs into local -`Future[T]`; a stream source implements demand-driven pulls for local -`Stream[T]`. -_Avoid_: `CMFutureReader[T]`, `CMStreamReader[T]` - -**Generated writable state**: -A private prepared producer state created when lowering a local Future or Stream. -It exclusively owns one raw writable handle and binds directly to that site's -write/drop intrinsics. Lowering does not start user computation. Commit starts a -normal producer task; rejection starts future writer settlement when required or -discards a prepared stream without transferring its readable end. A future state -carries a settlement obligation from the moment `future.new` succeeds. -_Avoid_: `CMFutureWriter[T]`, `CMStreamWriter[T]` - -**Staged nested endpoint payload**: -The rule that only the current endpoint layer's readable handle appears in its -canonical payload. For `future>>`, the function boundary carries -the outer future handle, reading it carries the inner future handle, and reading -that carries the stream handle. -_Avoid_: flattened endpoint graph - -**Transfer commit/reject**: -Generated ownership handling after lowering a payload containing resources or -nested endpoints. A successful transfer commits the transferred values to the -peer. A rejected ordinary resource is cleaned. A prepared stream drops its -untransferred component pair and rejects guest-owned local state without -pretending the peer accepted values. A rejected future drops its readable end, -but its paired writer and local source remain owned until a later write reaches -settlement. A partial stream transfer commits the accepted prefix. Generated -code retries the remainder of the current staging window while the peer remains -open; peer drop or write cancellation rejects that staged remainder exactly -once. Values beyond the staging window remain caller-owned. -_Avoid_: unconditional cleanup - -**Future writer settlement obligation**: -The non-rollbackable obligation created by `future.new`. The writable end can be -dropped only after a successful write or after a write reports that the readable -end was dropped. Dropping a still-guest-owned readable end does not settle its -paired writer; the generated producer must still obtain a real payload and -attempt the write. A `cancelled` write has returned its ABI buffer but has not -settled the writer. -_Avoid_: future rollback - -**Prepared lower payload**: -The private combination of a canonical ABI value or buffer, unstarted generated -producer state, and recursive `commit`/`reject` paths. Commit starts producer work -only after ownership transfers. The same lowered buffer is retained across -blocked or partial writes. For a staging window, the accepted prefix is -committed and the staged suffix is retried from the same buffer or rejected. -Both portions are consumed from the public `Sink` input; only the unstaged tail -remains caller-owned. It is not a user-facing MoonBit type. -_Avoid_: lowered value alone - -**Local Future**: -A MoonBit-only one-shot handle, expected to be spelled `Future[T]`, that wraps an -owned source computation plus cancellation/drop state. Its generic state has no -CM endpoint variant and does not require `T` to be representable in WIT. A -generated readable source may capture a raw handle inside its private closure, -but the local runtime cannot inspect or forward it. -_Avoid_: component `future`, future endpoint - -**One-shot async thunk**: -A MoonBit `async () -> T` value wrapped by local `Future[T]` or generated bridge -code to read or produce exactly one value. It is the computation inside the -handle, not the public component `future` representation by itself. -_Avoid_: future handle - -**Future state cell**: -The local mutable runtime state owned by a `Future[T]` handle. Depending on the -constructor, it records a ready value, an owned source, or a shared local -Future/Promise settlement cell. It does not contain CM-specific states or own -writable ends. -_Avoid_: endpoint table - -**Owned Future source**: -The generic pending source stored by local `Future[T]`. It has a one-shot async -`run` operation and may have an explicit unstarted cleanup action. Ordinary -MoonBit futures use a local thunk; generated incoming futures use a readable -source whose closure owns the endpoint. -_Avoid_: pending CM state - -**Unstarted producer cleanup**: -The optional `on_unstarted_drop : () -> Unit` action attached to a lazy local -`Future::from` or `Stream::produce`. It owns resources captured by the producer -only until the producer starts. Drop or prepared-transfer rejection invokes it -without executing the producer; get, read, or commit clears it before producer -execution. It is not a cancellation handler for an already-running task. -_Avoid_: producer finalizer, implicit cancellation - -**Bridge-owned future write**: -The separate generated task state that drives a local `Future[T]` computation -into an outgoing component `future`. This state owns the raw writable handle; -the local `Future[T]` does not. -_Avoid_: local Promise, component future writer - -**Future writer settlement obligation**: -The rule that once an outgoing component future pair has exposed its readable -end, the generated task owning the writable handle must eventually write one -real value or attempt the write and observe that the reader was dropped before -the writable end can be dropped. It cannot be satisfied by silently dropping -the writer. -_Avoid_: writer close - -**In-flight future write**: -A generated writable operation that has already called component `future.write` -and is waiting for the `FUTURE_WRITE` waitable event. If the peer -drops the readable end while this operation is in flight, the event reports -`dropped` and returns ownership of the lowered payload buffer to the writer. -_Avoid_: bridge task in flight - -**Pre-write future writer**: -A generated task that owns the writable end but has not started component -`future.write` because the payload value is not available yet. The -bridge task may be running or suspended on local work in this phase, but there -is no CM write operation waiting in a waitable set. Reader drop is observed when -the later `future.write` attempt returns or reports `dropped`. -_Avoid_: idle writer, in-flight future write - -**Bridge-shielded future task**: -An outgoing future bridge task that is protected from ordinary task -cancellation after its readable end has crossed the component boundary. Peer -loss-of-interest does not cancel it; the bridge keeps running until it satisfies -the future writer settlement obligation, unless the whole component instance is -trapping or being torn down. -_Avoid_: cancellable bridge - -**Generated future site helper**: -A generated function that directly calls `future.new` and the other future -intrinsics for one concrete WIT function site. It returns or consumes raw handles -only within generated boundary code. -_Avoid_: local `Future::new()`, generic component endpoint factory - -**Local Promise**: -A MoonBit-only producer side paired with a local `Future[T]` by -`Future::new()` or `Future::new_with_cleanup()`. `complete` transfers a value -only if the reader still exists; `fail` and `close` settle local waiters without -a value. It never creates or owns a component `future` writable end. If its -paired Future later crosses an FFI boundary, generated code creates and owns the -separate component endpoint pair. -_Avoid_: future writer - -**Local Semaphore**: -A MoonBit-only counting semaphore used for coroutine coordination. Waiters are -FIFO. If a release assigns a permit before cancellation resumes the waiter, the -permit wins that race and acquire succeeds so the permit is not lost. It is not -a component-model waitable or backpressure counter. -_Avoid_: waitable, `backpressure.inc` - -**Local Mutex**: -A MoonBit-only FIFO mutex implemented by a one-permit Local Semaphore. Its -acquire operation has the same cancellation race semantics as the semaphore. -Generated stream writers use it to serialize writes and close without polling -the scheduler. -_Avoid_: component stream lock - -**Local Condition Variable**: -A MoonBit-only `CondVar` used for direct task notification. If a signal has -already been assigned when cancellation resumes a waiter, the signal wins so -it cannot be lost. Generated future and stream sources use it to wait for -in-flight component read cancellation to return the operation buffer. -_Avoid_: waitable-set event, predicate polling - -**Local Stream**: -A MoonBit-only stream type, expected to be spelled `Stream[T]`, that coordinates -MoonBit coroutines or wraps a generic demand-driven source without a CM-specific -state variant. A generated readable source may capture a raw component handle, -but the local runtime cannot inspect or forward it, and `T` need not be -representable in WIT. -_Avoid_: component `stream`, stream endpoint - -**Local Sink**: -The producer side of a local Stream. It is not a component `stream` writable end. -_Avoid_: stream writer - -**Producer close**: -The graceful local-stream terminal action performed by `Sink::close()`. It means -no more values will be written, while already-buffered values remain readable. -_Avoid_: producer cancel - -**Consumer stream drop**: -The local-stream terminal action performed when the `Stream[T]` side is dropped -or explicitly consumed by async `Stream::drop()`. It means the consumer has lost -interest, so buffered values still owned by the local stream are cleaned and -producer-side waiters are woken. Bridge internals may cancel in-flight endpoint -copy operations to recover buffers, but the user-facing operation is not hard -cancellation of a producer. -_Avoid_: `Stream::cancel`, stream close - -**Producer failure**: -A possible future local-stream operation where the producer ends the stream with -a failure state instead of graceful EOF. It is not `unreachable`, a wasm trap, or -process abort. It is not part of the MVP public `Sink[T]` interface because -component `stream` does not carry a distinct generic producer-failure signal. -_Avoid_: producer abort, sink close, trap - -**Stream pipe**: -The shared runtime state behind a local `Stream[T]` and `Sink[T]` pair. It owns -bounded local chunk storage, FIFO waiting readers and writers, and separate -writer-close and reader-drop state. Capacity zero means strict rendezvous; -positive capacity is the maximum number of accepted unread elements. It does -not own CM stream endpoints. -_Avoid_: stream endpoint - -**Owned stream chunk**: -An owned, exact-length batch of stream items returned by local `Stream[T]` -reads. The expected representation is `FixedArray[T]`; local stream -implementation should pass chunks by ownership instead of assembling them in a -growable `Array[T]`. -_Avoid_: ABI buffer - -**Borrowed stream chunk**: -A temporary view of stream items, such as `ArrayView[T]`, accepted by the MVP -local `Sink[T]` write API. Before a write operation suspends, the stream runtime -must materialize the relevant view window into owned stream storage or an owned -canonical ABI buffer. -_Avoid_: stream storage - -**Staged write window**: -The bounded part of a borrowed stream chunk that one awaited write operation -materializes into owned storage. MVP `Sink[T]` writes stage at most one window -per awaited operation; `write_all` loops over windows. -_Avoid_: whole input buffer - -**CM operation buffer**: -The canonical ABI buffer supplied to an in-flight component `future` or -component `stream` read/write operation. The buffer is not reusable by MoonBit -until the operation completes or cancellation/drop is observed through the -terminal waitable event. -_Avoid_: local stream buffer - -**Accepted stream prefix**: -The prefix length reported by a completed or terminal stream copy event. Values -inside this prefix have semantically transferred across the stream operation. -For runtime-owned buffers, the source side must not clean them again. For -caller-owned MoonBit values, this is a best-effort API contract rather than a -type-system-enforced lifetime. Values outside the prefix remain owned by the -side that staged the buffer. -_Avoid_: whole chunk - -**Stream cleanup invariant**: -After every stream read, write, close, cancel, peer drop, or partial transfer, -each value is caller-owned, stream-owned, bridge-operation-owned, transferred to -the component peer, or cleaned exactly once. -_Avoid_: best-effort cleanup - -**Local producer close**: -`Sink::close()` stops local writes and marks graceful EOF. Buffered chunks remain -stream-owned and can still be drained before `Stream::read` returns `None`. -_Avoid_: reader drop, cancellation - -**Local reader drop**: -`Stream::drop()` records local consumer loss-of-interest. It wakes blocked -writers, discards unread chunks, and invokes the cleanup operation supplied by -`Stream::new_with_cleanup` or `Stream::produce(cleanup=...)` for each unread -owned value. A suspended -`Sink::write` owns one bounded staged `FixedArray`; `write_all` uses the same -cleanup for any remaining suffix that never entered the stream buffer. -_Avoid_: producer close, hard cancellation - -**Generated stream adapter**: -Boundary-generated state connecting local and component stream semantics. An -incoming adapter is a lazy readable source and starts no pump task. An outgoing -adapter is a producer task that owns the raw writable handle and pulls from a -local `Stream[T]`. -_Avoid_: local stream state - -**Incoming stream demand policy**: -The rule that a generated incoming stream source issues one component read only -for a corresponding local read demand. It does not prefetch into a local pipe. -_Avoid_: unbounded prefetch - -**Source cleanup hook**: -An owned cleanup action stored with a generic local source. Explicit local -cleanup, such as async `Future::drop` or `Stream::drop`, runs the generated hook -so an idle endpoint is dropped directly or an in-flight copy is cancelled and -observed before buffers and handles are released. -_Avoid_: destructor - -**BYOB stream read**: -A "bring your own buffer" read shape where the caller supplies storage to fill. -This is useful for component ABI buffers and specialized fast paths, but should -not be part of the MVP local `Stream[T]` interface for arbitrary `T`. - -**Local-component bridge**: -Generated FFI-boundary code that adapts between a one-shot async thunk or local -Stream and component future/stream endpoints. It exists only for a concrete WIT -site whose payload has generated recursive lift/lower operations. - -**Async generator boundary**: -The Rust generator layer that decides which MoonBit async runtime package, -endpoint bridge helpers, and public async type names to emit. This boundary -lets the old async runtime be peeled out and the replacement runtime added back -without scattering runtime-specific decisions across ordinary lift/lower code. -_Avoid_: inline async codegen - -**Mergeable prototype**: -A production-intended implementation slice built after the async generator -boundary exists. It is allowed to be experimental in scope, but it must use the -real generator boundary, carry tests, and be suitable to harden in place if the -shape proves correct. It is not throwaway code. -_Avoid_: throwaway prototype - -**ABI-compatible payload**: -A MoonBit type that is the generated representation of a WIT type and has the -payload lift/lower operations needed at a specific FFI boundary. -_Avoid_: arbitrary `T` - -**Bridge task**: -Runtime-owned async work that drives a local-component bridge after a readable -end has been returned or passed to another component. - -**Bridge continuation**: -The suspended MoonBit coroutine captured while a local-component bridge is -waiting on a component-model waitable operation. Reader drop, writer drop, and -task cancellation must be routed through this continuation rather than modeled -only as ordinary value destruction. -_Avoid_: destructor - -**Endpoint copy cancellation**: -Cancellation of one in-flight component `future` or component `stream` read or -write operation. This returns ownership of the operation's buffer when the -cancelled event is observed. It is not the same thing as cancelling the MoonBit -coroutine that requested the operation. -_Avoid_: task cancellation - -**Hard cancellation**: -A MoonBit-side request to stop local async work with strong semantics. Examples -include task/subtask cancellation and component teardown or failure. Hard -cancellation may cancel a producer coroutine or cancel and observe an in-flight -endpoint copy operation. It is not the same as the peer dropping a component -endpoint. -_Avoid_: peer drop, consumer loss-of-interest - -**Peer loss-of-interest**: -The opposite component endpoint was dropped. When an endpoint operation observes -this, the component result is `dropped`, not `cancelled`. If there is no endpoint -operation that can observe it yet, such as a pre-write outgoing future writer, -the bridge must not infer hard cancellation from it. -_Avoid_: task cancellation - -**Bridge cancellation**: -A hard cancellation of the MoonBit bridge continuation that is driving -conversion between local async work and a component endpoint. For streams and -in-flight future operations, this may cancel endpoint copy work. For a pre-write -outgoing component future writer, peer readable-end drop is not bridge -cancellation; it is observed only if a later `future.write` reports `dropped`. -_Avoid_: endpoint copy cancellation - -**Future drop**: -The async user-facing operation that consumes a local `Future[T]` handle and -runs its explicit cleanup protocol. If the handle is backed by an unread component -readable end and no read is in flight, this cleanup may call -`future.drop-readable`; otherwise it may need to cancel and observe cleanup -first. If cancellation races with completion, `Future::drop` cleans the produced -value using the future's payload cleanup operation. It is not a direct alias for -component `future.drop-*`, and it is not an automatic destructor. -_Avoid_: read cancellation - -**Payload cleanup operation**: -A generated or stored function that cleans a produced payload value when a -future or stream operation owns that value but will not return it to user code. -For WIT payloads, this operation is generated from lift/lower cleanup logic. A -generic local `Future[T]` or `Stream[T]` cannot invent this operation for -arbitrary `T`; local constructors therefore accept it explicitly through -`Future::ready_with_cleanup`, `Stream::new_with_cleanup`, and -`Stream::produce(cleanup=...)` when needed. -_Avoid_: generic destructor - -## Adapter Generation Language - -**Adapter-generated intrinsic**: -A canonical ABI import generated by the core-wasm-to-component adapter path. For -component `future` and component `stream`, these intrinsics are identified by -the containing function and a discovered component `future` or component -`stream` position. - -**Function-position index**: -The enumeration index assigned to a component `future` or component `stream` -discovered in one concrete WIT function. It is not derivable from the MoonBit -payload type alone. -_Avoid_: parameter index, type index - -**Endpoint operation table**: -The rejected design where adapter intrinsics and payload callbacks are stored in -a runtime record selected for a generic endpoint wrapper. The target design emits -direct site-specific calls instead. -_Avoid_: vtable - -**Endpoint factory**: -A generated site helper that directly creates a component future or stream pair -because it is tied to a concrete WIT function position and therefore knows the -adapter-generated intrinsic names. -_Avoid_: local `Future::new()`, local `Stream::new()`, generic endpoint factory +# MoonBit Async Terminology + +Use lowercase `future` and `stream` for Component Model types. Use uppercase +`Future` and `Stream` for local MoonBit types. `CM` may qualify a Component +Model concept in prose, but there are no public generic `CMFuture` or `CMStream` +wrapper types. + +## Component Model + +**Component `future`** +: A WIT `future`. Its canonical value transfers a readable endpoint, not a + MoonBit computation. + +**Component `stream`** +: A WIT `stream`. Its canonical value transfers a readable endpoint, not a + local MoonBit buffer. + +**Readable end / writable end** +: The receiving and producing halves of a component future or stream. Generated + `future.new` and `stream.new` calls create the pair; WIT values carry the + readable end. + +**Endpoint site** +: One future or stream occurrence in the canonical traversal of a concrete WIT + function. A site binds payload conversion to intrinsic names supplied by + `wit-parser`. + +**Async task scope** +: The component task context that owns a waitable set and can suspend generated + bridge work. It is not a global event loop. + +## Local MoonBit + +**Local `Future[T]`** +: A consuming one-shot local value. It may hold a ready value or a lazy source, + but its generic state contains no component endpoint or intrinsic table. + +**Local `Promise[T]`** +: The producer paired with a local Future. It can complete, fail, or close local + waiters. It is not a component future writer. + +**Local `Stream[T]` / `Sink[T]`** +: A bounded local pipe. Consumers pull owned `FixedArray[T]` chunks; producers + push immutable `ArrayView[T]` values. Capacity zero is strict rendezvous. + +**Producer close** +: `Sink::close()` marks graceful EOF while preserving buffered values for + readers. + +**Consumer drop** +: `Stream::drop()` records loss of interest, wakes writers, and cleans unread + values through the configured payload cleanup operation. + +**Background group** +: The generated `background_group : @async-core.TaskGroup[Unit]` export + parameter. Work spawned into it may continue after the component result is + published, but cannot alter that result. + +**Unstarted producer cleanup** +: An optional callback owned by a lazy Future or Stream until its producer + starts. It releases explicitly captured resources when the unstarted value is + rejected or dropped. + +## Boundary Conversion + +**Recursive boundary plan** +: Generator metadata connecting every endpoint site to endpoint sites nested in + its payload. It drives position-specific lift, lower, commit, reject, and + cleanup code. + +**Generated readable source** +: A private lazy source that owns an incoming raw readable handle and calls that + site's read, cancel-read, and drop-readable intrinsics. + +**Generated writable state** +: Private producer state that owns an outgoing raw writable handle. It starts + bridge work only after the paired readable end is committed to the peer. + +**Staged nested endpoint** +: An endpoint revealed only at its containing transfer layer. For + `future>>`, reading each layer reveals the next handle; the + graph is not flattened at the function boundary. + +**Prepared lower payload** +: A canonical value or buffer together with unstarted producer state and + recursive commit/reject actions. It stays owned until the transfer outcome is + known. + +**Commit / reject** +: Commit transfers the accepted payload and starts its producer work. Reject + cleans values and untransferred streams. A rejected future still drives its + paired writer until a write observes reader drop. + +**Future writer settlement obligation** +: After `future.new`, the writable end may be dropped only after a successful + write or a write that reports the readable end was dropped. Cancellation does + not settle it. + +**Staged write window** +: The bounded portion of a local stream chunk lowered for one component write. + Nested future payloads use a one-element window in the MVP. + +**Accepted stream prefix** +: The count reported by a component stream operation. Accepted values belong to + the peer; the rejected staged suffix is cleaned exactly once; unstaged values + remain with the local stream. + +**Operation buffer** +: Canonical ABI memory borrowed by an in-flight endpoint operation. It remains + live until completion or a terminal cancellation event returns ownership. + +## Cancellation And Limits + +**Endpoint copy cancellation** +: Cancellation of one component read or write so its operation buffer can be + recovered. It is separate from cancelling the MoonBit coroutine. + +**Hard cancellation** +: A request to stop local task work, such as `Task::cancel` or component task + cancellation. + +**Peer loss of interest** +: The opposite endpoint was dropped. Operations report `dropped`; this does not + imply hard cancellation of the producing coroutine. + +**Sync lower producer gap** +: Lowering a newly produced Future or Stream without an active component async + scope. The stackless callback ABI cannot resume such work after a synchronous + call returns, so the MVP does not support it. + +**BYOB / `read_into`** +: A deferred read API where callers provide destination storage to reduce + copying. It is not part of the MVP local Stream interface. + +**Endpoint operation table** +: The rejected vtable design that selected position-specific intrinsics and + payload callbacks at runtime. The implemented design emits direct site helpers + instead. diff --git a/crates/moonbit/src/async/async_abi.mbt b/crates/moonbit/src/async/async_abi.mbt index e15b80413..373cded54 100644 --- a/crates/moonbit/src/async/async_abi.mbt +++ b/crates/moonbit/src/async/async_abi.mbt @@ -212,17 +212,6 @@ fn CallbackCode::encode(self : Self) -> Int { } } -///| -fn CallbackCode::_decode(int : Int) -> CallbackCode { - let id = int >> 4 - match int & 0xf { - 0 => Completed - 1 => Yield - 2 => Wait(id) - _ => panic() - } -} - // #endregion // #region Component async primitives diff --git a/crates/moonbit/src/async/ev.mbt b/crates/moonbit/src/async/ev.mbt index e08383271..c1b669ca7 100644 --- a/crates/moonbit/src/async/ev.mbt +++ b/crates/moonbit/src/async/ev.mbt @@ -663,16 +663,6 @@ pub async fn suspend_for_future_write_terminal(idx : Int, val : Int) -> Bool? { } } -///| -#internal(wit_bindgen, "generated binding code only") -#doc(hidden) -pub async fn suspend_for_future_write(idx : Int, val : Int) -> Bool { - match suspend_for_future_write_terminal(idx, val) { - Some(transferred) => transferred - None => raise FutureWriteCancelled - } -} - ///| #internal(wit_bindgen, "generated binding code only") #doc(hidden) @@ -718,7 +708,6 @@ pub async fn suspend_for_stream_write(idx : Int, val : Int) -> (Int, Bool) { pub suberror OpCancelled { SubTaskCancelled(before_started~ : Bool) StreamReadCancelled - FutureWriteCancelled } ///| diff --git a/crates/moonbit/src/lib.rs b/crates/moonbit/src/lib.rs index 99189d9f8..8810db305 100644 --- a/crates/moonbit/src/lib.rs +++ b/crates/moonbit/src/lib.rs @@ -14,14 +14,12 @@ use wit_bindgen_core::{ uwrite, uwriteln, wit_parser::{ Alignment, ArchitectureSize, Docs, Enum, Flags, FlagsRepr, Function, Int, InterfaceId, - LiftLowerAbi, ManglingAndAbi, Param, Record, Resolve, ResourceIntrinsic, Result_, - SizeAlign, Tuple, Type, TypeDefKind, TypeId, Variant, WasmExport, WasmExportKind, + LiftLowerAbi, LiveTypes, ManglingAndAbi, Param, Record, Resolve, ResourceIntrinsic, + Result_, SizeAlign, Tuple, Type, TypeDefKind, TypeId, Variant, WasmExport, WasmExportKind, WasmImport, WorldId, WorldKey, }, }; -use wit_bindgen_core::wit_parser::WorldItem; - use crate::async_support::{AsyncFunctionState, AsyncSupport}; use crate::pkg::{Imports, MoonbitSignature, PkgResolver, ToMoonBitIdent, ToMoonBitTypeIdent}; @@ -2955,44 +2953,14 @@ fn flags_repr(flags: &Flags) -> Int { } fn type_contains_future_or_stream(resolve: &Resolve, ty: &Type) -> bool { - let Type::Id(id) = ty else { - return false; - }; - - match &resolve.types[*id].kind { - TypeDefKind::Future(_) | TypeDefKind::Stream(_) => true, - TypeDefKind::Record(record) => record - .fields - .iter() - .any(|field| type_contains_future_or_stream(resolve, &field.ty)), - TypeDefKind::Tuple(tuple) => tuple - .types - .iter() - .any(|ty| type_contains_future_or_stream(resolve, ty)), - TypeDefKind::Variant(variant) => variant - .cases - .iter() - .filter_map(|case| case.ty.as_ref()) - .any(|ty| type_contains_future_or_stream(resolve, ty)), - TypeDefKind::Option(ty) - | TypeDefKind::List(ty) - | TypeDefKind::FixedLengthList(ty, _) - | TypeDefKind::Type(ty) => type_contains_future_or_stream(resolve, ty), - TypeDefKind::Map(key, value) => { - type_contains_future_or_stream(resolve, key) - || type_contains_future_or_stream(resolve, value) - } - TypeDefKind::Result(result) => result - .ok - .iter() - .chain(result.err.iter()) - .any(|ty| type_contains_future_or_stream(resolve, ty)), - TypeDefKind::Resource - | TypeDefKind::Handle(_) - | TypeDefKind::Flags(_) - | TypeDefKind::Enum(_) => false, - TypeDefKind::Unknown => unreachable!(), - } + let mut live = LiveTypes::default(); + live.add_type(resolve, ty); + live.iter().any(|id| { + matches!( + resolve.types[id].kind, + TypeDefKind::Future(_) | TypeDefKind::Stream(_) + ) + }) } fn type_contains_endpoint_fixed_length_list_combination( @@ -3085,85 +3053,22 @@ fn world_contains_endpoint_fixed_length_list_combination( resolve: &Resolve, world: WorldId, ) -> bool { - let item_contains_endpoint = |item: &WorldItem| match item { - WorldItem::Function(func) => { - func.params.iter().any(|param| { - type_contains_endpoint_fixed_length_list_combination( - resolve, ¶m.ty, false, false, - ) - }) || func.result.as_ref().is_some_and(|ty| { - type_contains_endpoint_fixed_length_list_combination(resolve, ty, false, false) - }) - } - WorldItem::Interface { id, .. } => { - let interface = &resolve.interfaces[*id]; - interface.types.values().any(|id| { - type_contains_endpoint_fixed_length_list_combination( - resolve, - &Type::Id(*id), - false, - false, - ) - }) || interface.functions.values().any(|func| { - func.params.iter().any(|param| { - type_contains_endpoint_fixed_length_list_combination( - resolve, ¶m.ty, false, false, - ) - }) || func.result.as_ref().is_some_and(|ty| { - type_contains_endpoint_fixed_length_list_combination(resolve, ty, false, false) - }) - }) - } - WorldItem::Type { id, .. } => type_contains_endpoint_fixed_length_list_combination( - resolve, - &Type::Id(*id), - false, - false, - ), - }; - let world = &resolve.worlds[world]; - world - .imports - .values() - .chain(world.exports.values()) - .any(item_contains_endpoint) + let mut live = LiveTypes::default(); + live.add_world(resolve, world); + live.iter().any(|id| { + type_contains_endpoint_fixed_length_list_combination(resolve, &Type::Id(id), false, false) + }) } fn world_contains_future_or_stream(resolve: &Resolve, world: WorldId) -> bool { - let item_contains_endpoint = |item: &WorldItem| match item { - WorldItem::Function(func) => { - func.params - .iter() - .any(|param| type_contains_future_or_stream(resolve, ¶m.ty)) - || func - .result - .as_ref() - .is_some_and(|ty| type_contains_future_or_stream(resolve, ty)) - } - WorldItem::Interface { id, .. } => { - let interface = &resolve.interfaces[*id]; - interface - .types - .values() - .any(|id| type_contains_future_or_stream(resolve, &Type::Id(*id))) - || interface.functions.values().any(|func| { - func.params - .iter() - .any(|param| type_contains_future_or_stream(resolve, ¶m.ty)) - || func - .result - .as_ref() - .is_some_and(|ty| type_contains_future_or_stream(resolve, ty)) - }) - } - WorldItem::Type { id, .. } => type_contains_future_or_stream(resolve, &Type::Id(*id)), - }; - let world = &resolve.worlds[world]; - world - .imports - .values() - .chain(world.exports.values()) - .any(item_contains_endpoint) + let mut live = LiveTypes::default(); + live.add_world(resolve, world); + live.iter().any(|id| { + matches!( + resolve.types[id].kind, + TypeDefKind::Future(_) | TypeDefKind::Stream(_) + ) + }) } fn indent(code: &str) -> Source { @@ -3609,7 +3514,7 @@ mod tests { "#doc(hidden)\npub fn has_component_task_scope", "#doc(hidden)\npub async fn suspend_for_subtask", "#doc(hidden)\npub async fn suspend_for_future_read", - "#doc(hidden)\npub async fn suspend_for_future_write", + "#doc(hidden)\npub async fn suspend_for_future_write_terminal", "#doc(hidden)\npub async fn suspend_for_stream_read", "#doc(hidden)\npub async fn suspend_for_stream_write", ] { diff --git a/tests/runtime/moonbit/async-import-cancel/runner.mbt b/tests/runtime/moonbit/async-import-cancel/runner.mbt index f7e76b5c8..03d87d85d 100644 --- a/tests/runtime/moonbit/async-import-cancel/runner.mbt +++ b/tests/runtime/moonbit/async-import-cancel/runner.mbt @@ -3,6 +3,19 @@ //@ path = 'gen/world/runner/stub.mbt' //@ pkg_config = """{ "import": [{ "path": "test/async-import-cancel/async-core", "alias": "async-core" }, { "path": "test/async-import-cancel/interface/test/async-import-cancel/pending", "alias": "pending" }, { "path": "test/async-import-cancel/interface/test/async-import-cancel/gate-control", "alias": "gate-control" }] }""" +///| +async fn settle_leaf_counts(expected_drops : UInt) -> Unit { + for _ in 0..<32 { + if @pending.leaf_live_count() == 0U && + @pending.leaf_drop_count() == expected_drops { + break + } + @pending.settle() + } + guard @pending.leaf_live_count() == 0U else { panic() } + guard @pending.leaf_drop_count() == expected_drops else { panic() } +} + ///| pub async fn run(background_group : @async-core.TaskGroup[Unit]) -> Unit { guard @pending.leaf_live_count() == 0U else { panic() } @@ -30,14 +43,7 @@ pub async fn run(background_group : @async-core.TaskGroup[Unit]) -> Unit { @pending.settle() guard !@pending.pending_started() else { panic() } - for _ in 0..<32 { - if @pending.leaf_live_count() == 0U && @pending.leaf_drop_count() == 1U { - break - } - @pending.settle() - } - guard @pending.leaf_live_count() == 0U else { panic() } - guard @pending.leaf_drop_count() == 1U else { panic() } + settle_leaf_counts(1U) @pending.backpressure_set(true) let stream_leaf = @pending.Leaf::leaf() @@ -71,14 +77,7 @@ pub async fn run(background_group : @async-core.TaskGroup[Unit]) -> Unit { guard !@pending.pending_stream_started() else { panic() } guard !stream_producer_ran.val else { panic() } - for _ in 0..<32 { - if @pending.leaf_live_count() == 0U && @pending.leaf_drop_count() == 2U { - break - } - @pending.settle() - } - guard @pending.leaf_live_count() == 0U else { panic() } - guard @pending.leaf_drop_count() == 2U else { panic() } + settle_leaf_counts(2U) let returned_leaf = @pending.Leaf::leaf() let (return_started, return_started_sink) = @async-core.Stream::new( @@ -98,14 +97,7 @@ pub async fn run(background_group : @async-core.TaskGroup[Unit]) -> Unit { returned_task.cancel() @gate-control.release() - for _ in 0..<32 { - if @pending.leaf_live_count() == 0U && @pending.leaf_drop_count() == 5U { - break - } - @pending.settle() - } - guard @pending.leaf_live_count() == 0U else { panic() } - guard @pending.leaf_drop_count() == 5U else { panic() } + settle_leaf_counts(5U) let cleanup_leaf = fn(value : @pending.Leaf) { value.drop() } let stream_values : FixedArray[@async-core.Future[@pending.Leaf]] = [ @@ -120,14 +112,7 @@ pub async fn run(background_group : @async-core.TaskGroup[Unit]) -> Unit { sink.close() }) @pending.drop_future_stream(cleanup_stream) - for _ in 0..<32 { - if @pending.leaf_live_count() == 0U && @pending.leaf_drop_count() == 8U { - break - } - @pending.settle() - } - guard @pending.leaf_live_count() == 0U else { panic() } - guard @pending.leaf_drop_count() == 8U else { panic() } + settle_leaf_counts(8U) let after_start_future = @async-core.Future::ready_with_cleanup( @pending.Leaf::leaf(), @@ -158,14 +143,7 @@ pub async fn run(background_group : @async-core.TaskGroup[Unit]) -> Unit { after_start_task.wait() catch { _ => () } - for _ in 0..<32 { - if @pending.leaf_live_count() == 0U && @pending.leaf_drop_count() == 11U { - break - } - @pending.settle() - } - guard @pending.leaf_live_count() == 0U else { panic() } - guard @pending.leaf_drop_count() == 11U else { panic() } + settle_leaf_counts(11U) let race_future = @pending.open_race_future() let (race_read_started, race_read_started_sink) = @async-core.Stream::new( @@ -185,14 +163,7 @@ pub async fn run(background_group : @async-core.TaskGroup[Unit]) -> Unit { @pending.complete_race_future() race_read.cancel() guard race_read.wait() else { panic() } - for _ in 0..<32 { - if @pending.leaf_live_count() == 0U && @pending.leaf_drop_count() == 12U { - break - } - @pending.settle() - } - guard @pending.leaf_live_count() == 0U else { panic() } - guard @pending.leaf_drop_count() == 12U else { panic() } + settle_leaf_counts(12U) let race_stream = @pending.open_race_stream() let (race_stream_started, race_stream_started_sink) = @async-core.Stream::new( @@ -214,14 +185,7 @@ pub async fn run(background_group : @async-core.TaskGroup[Unit]) -> Unit { @pending.complete_race_stream() race_stream_read.cancel() guard race_stream_read.wait() else { panic() } - for _ in 0..<32 { - if @pending.leaf_live_count() == 0U && @pending.leaf_drop_count() == 13U { - break - } - @pending.settle() - } - guard @pending.leaf_live_count() == 0U else { panic() } - guard @pending.leaf_drop_count() == 13U else { panic() } + settle_leaf_counts(13U) let pending_future = @pending.open_future() let (future_read_started, future_read_started_sink) = @async-core.Stream::new( diff --git a/tests/runtime/moonbit/http-background-hook/runner-invalid.mbt b/tests/runtime/moonbit/http-background-hook/runner-invalid.mbt deleted file mode 100644 index f9c414822..000000000 --- a/tests/runtime/moonbit/http-background-hook/runner-invalid.mbt +++ /dev/null @@ -1,51 +0,0 @@ -//@ wasmtime-flags = '-Wcomponent-model-async -S p3 -S http' -//@ [lang] -//@ path = 'gen/world/runner/stub.mbt' -//@ pkg_config = """{ "warn-list": "-44", "import": [{ "path": "example/background-hook/async-core", "alias": "async-core" }, { "path": "example/background-hook/interface/wasi/http/handler", "alias": "handler" }, { "path": "example/background-hook/interface/wasi/http/types", "alias": "types" }] }""" - -///| -pub async fn run(_background_group : @async-core.TaskGroup[Unit]) -> Unit { - let request_body = @async-core.Stream::produce(async fn(sink) { - guard sink.write_all_bytes(b"build=debug"[:]) else { - abort("request body closed early") - } - sink.close() - }) - let (request, request_transmitted) = @types.Request::new( - @types.Fields::fields(), - Some(request_body), - @async-core.Future::ready(Ok(None)), - None, - ) - guard request.set_method(@types.Method::Post) is Ok(_) else { panic() } - guard request.set_path_with_query(Some("/hooks/build")) is Ok(_) else { - panic() - } - - let response = match @handler.handle(request) { - Ok(response) => response - Err(_) => panic() - } - guard response.get_status_code() == 202U else { panic() } - - let (response_body, response_trailers) = response.consume_body( - @async-core.Future::ready(Ok(())), - ) - guard response_body.read(1) is None else { panic() } - response_body.drop() - match response_trailers.get() { - Ok(None) => () - Ok(Some(fields)) => { - fields.drop() - panic() - } - Err(_) => panic() - } - - match request_transmitted.get() { - Err(@types.ErrorCode::InternalError(Some(message))) => { - guard message == "unexpected hook payload" else { panic() } - } - _ => panic() - } -} diff --git a/tests/runtime/moonbit/http-background-hook/runner.mbt b/tests/runtime/moonbit/http-background-hook/runner.mbt index 71845d74b..d01bff8bc 100644 --- a/tests/runtime/moonbit/http-background-hook/runner.mbt +++ b/tests/runtime/moonbit/http-background-hook/runner.mbt @@ -4,9 +4,9 @@ //@ pkg_config = """{ "warn-list": "-44", "import": [{ "path": "example/background-hook/async-core", "alias": "async-core" }, { "path": "example/background-hook/interface/wasi/http/handler", "alias": "handler" }, { "path": "example/background-hook/interface/wasi/http/types", "alias": "types" }] }""" ///| -pub async fn run(_background_group : @async-core.TaskGroup[Unit]) -> Unit { +async fn run_hook(payload : Bytes, expected_error : String?) -> Unit { let request_body = @async-core.Stream::produce(async fn(sink) { - guard sink.write_all_bytes(b"build=release"[:]) else { + guard sink.write_all_bytes(payload[:]) else { abort("request body closed early") } sink.close() @@ -31,19 +31,44 @@ pub async fn run(_background_group : @async-core.TaskGroup[Unit]) -> Unit { let (response_body, response_trailers) = response.consume_body( @async-core.Future::ready(Ok(())), ) - guard response_body.read(1) is None else { panic() } + let expected_response = b"accepted" + let received = Ref(0) + for ;; { + match response_body.read(4) { + Some(chunk) => + for byte in chunk { + guard received.val < expected_response.length() else { panic() } + guard byte == expected_response[received.val] else { panic() } + received.val += 1 + } + None => break + } + } + guard received.val == expected_response.length() else { panic() } response_body.drop() match response_trailers.get() { - Ok(None) => () - Ok(Some(fields)) => { - fields.drop() - panic() - } - Err(_) => panic() + Ok(Some(fields)) => fields.drop() + Ok(None) | Err(_) => panic() } - match request_transmitted.get() { - Ok(_) => () - Err(_) => panic() + match expected_error { + None => + match request_transmitted.get() { + Ok(_) => () + Err(_) => panic() + } + Some(expected) => + match request_transmitted.get() { + Err(@types.ErrorCode::InternalError(Some(message))) => { + guard message == expected else { panic() } + } + _ => panic() + } } } + +///| +pub async fn run(_background_group : @async-core.TaskGroup[Unit]) -> Unit { + run_hook(b"build=release", None) + run_hook(b"build=debug", Some("unexpected hook payload")) +} diff --git a/tests/runtime/moonbit/http-background-hook/test.mbt b/tests/runtime/moonbit/http-background-hook/test.mbt index b979824bb..ed1114014 100644 --- a/tests/runtime/moonbit/http-background-hook/test.mbt +++ b/tests/runtime/moonbit/http-background-hook/test.mbt @@ -37,10 +37,24 @@ pub async fn handle( } let payload = payload.to_bytes() + let (body_done, body_done_promise) = @async-core.Future::new() + let response_body = @async-core.Stream::produce(async fn(sink) { + guard sink.write_all_bytes(b"accepted"[:]) else { + abort("response body closed early") + } + sink.close() + let _ = body_done_promise.complete(()) + }) + let (transmitted_done, transmitted_done_promise) = @async-core.Future::new() + let response_trailers = @async-core.Future::from(async fn() { + body_done.get() + transmitted_done.get() + Ok(Some(@types.Fields::fields())) + }) let (response, response_transmitted) = @types.Response::new( @types.Fields::fields(), - None, - @async-core.Future::ready(Ok(None)), + Some(response_body), + response_trailers, ) guard response.set_status_code(202U) is Ok(_) else { let error = @types.ErrorCode::InternalError( @@ -53,7 +67,9 @@ pub async fn handle( background_group.spawn_bg(async fn() { let result = (async fn() -> Result[Unit, @types.ErrorCode] { - match response_transmitted.get() { + let transmission_result = response_transmitted.get() + let _ = transmitted_done_promise.complete(()) + match transmission_result { Err(error) => return Err(error) Ok(_) => () } diff --git a/tests/runtime/moonbit/http-background-hook/wkg.lock b/tests/runtime/moonbit/http-background-hook/wkg.lock deleted file mode 100644 index 1c6187c52..000000000 --- a/tests/runtime/moonbit/http-background-hook/wkg.lock +++ /dev/null @@ -1,12 +0,0 @@ -# This file is automatically generated. -# It is not intended for manual editing. -version = 1 - -[[packages]] -name = "wasi:http" -registry = "wasi.dev" - -[[packages.versions]] -requirement = "=0.3.0" -version = "0.3.0" -digest = "sha256:92cd8f3730c00226dc15626a2e7b21834dd187fc221f09818720d228585bbbf7" diff --git a/tests/runtime/moonbit/http-body-trailers/deps/clocks.wit b/tests/runtime/moonbit/http-body-trailers/deps/clocks.wit deleted file mode 100644 index 6522c9fdc..000000000 --- a/tests/runtime/moonbit/http-body-trailers/deps/clocks.wit +++ /dev/null @@ -1,7 +0,0 @@ -package wasi:clocks@0.3.0; - -@since(version = 0.3.0) -interface types { - @since(version = 0.3.0) - type duration = u64; -} diff --git a/tests/runtime/moonbit/http-body-trailers/deps/http.wit b/tests/runtime/moonbit/http-body-trailers/deps/http.wit deleted file mode 100644 index e453ace5a..000000000 --- a/tests/runtime/moonbit/http-body-trailers/deps/http.wit +++ /dev/null @@ -1,460 +0,0 @@ -package wasi:http@0.3.0; - -/// This interface defines all of the types and methods for implementing HTTP -/// Requests and Responses, as well as their headers, trailers, and bodies. -@since(version = 0.3.0) -interface types { - use wasi:clocks/types@0.3.0.{duration}; - - /// This type corresponds to HTTP standard Methods. - @since(version = 0.3.0) - variant method { - get, - head, - post, - put, - delete, - connect, - options, - trace, - patch, - other(string), - } - - /// This type corresponds to HTTP standard Related Schemes. - @since(version = 0.3.0) - variant scheme { - HTTP, - HTTPS, - other(string), - } - - /// Defines the case payload type for `DNS-error` above: - @since(version = 0.3.0) - record DNS-error-payload { - rcode: option, - info-code: option, - } - - /// Defines the case payload type for `TLS-alert-received` above: - @since(version = 0.3.0) - record TLS-alert-received-payload { - alert-id: option, - alert-message: option, - } - - /// Defines the case payload type for `HTTP-response-{header,trailer}-size` above: - @since(version = 0.3.0) - record field-size-payload { - field-name: option, - field-size: option, - } - - /// These cases are inspired by the IANA HTTP Proxy Error Types: - /// - @since(version = 0.3.0) - variant error-code { - DNS-timeout, - DNS-error(DNS-error-payload), - destination-not-found, - destination-unavailable, - destination-IP-prohibited, - destination-IP-unroutable, - connection-refused, - connection-terminated, - connection-timeout, - connection-read-timeout, - connection-write-timeout, - connection-limit-reached, - TLS-protocol-error, - TLS-certificate-error, - TLS-alert-received(TLS-alert-received-payload), - HTTP-request-denied, - HTTP-request-length-required, - HTTP-request-body-size(option), - HTTP-request-method-invalid, - HTTP-request-URI-invalid, - HTTP-request-URI-too-long, - HTTP-request-header-section-size(option), - HTTP-request-header-size(option), - HTTP-request-trailer-section-size(option), - HTTP-request-trailer-size(field-size-payload), - HTTP-response-incomplete, - HTTP-response-header-section-size(option), - HTTP-response-header-size(field-size-payload), - HTTP-response-body-size(option), - HTTP-response-trailer-section-size(option), - HTTP-response-trailer-size(field-size-payload), - HTTP-response-transfer-coding(option), - HTTP-response-content-coding(option), - HTTP-response-timeout, - HTTP-upgrade-failed, - HTTP-protocol-error, - loop-detected, - configuration-error, - /// This is a catch-all error for anything that doesn't fit cleanly into a - /// more specific case. It also includes an optional string for an - /// unstructured description of the error. Users should not depend on the - /// string for diagnosing errors, as it's not required to be consistent - /// between implementations. - internal-error(option), - } - - /// This type enumerates the different kinds of errors that may occur when - /// setting or appending to a `fields` resource. - @since(version = 0.3.0) - variant header-error { - /// This error indicates that a `field-name` or `field-value` was - /// syntactically invalid when used with an operation that sets headers in a - /// `fields`. - invalid-syntax, - /// This error indicates that a forbidden `field-name` was used when trying - /// to set a header in a `fields`. - forbidden, - /// This error indicates that the operation on the `fields` was not - /// permitted because the fields are immutable. - immutable, - /// This error indicates that the operation would exceed an - /// implementation-defined limit on field sizes. This may apply to - /// an individual `field-value`, a single `field-name` plus all its - /// values, or the total aggregate size of all fields. - size-exceeded, - /// This is a catch-all error for anything that doesn't fit cleanly into a - /// more specific case. Implementations can use this to extend the error - /// type without breaking existing code. It also includes an optional - /// string for an unstructured description of the error. Users should not - /// depend on the string for diagnosing errors, as it's not required to be - /// consistent between implementations. - other(option), - } - - /// This type enumerates the different kinds of errors that may occur when - /// setting fields of a `request-options` resource. - @since(version = 0.3.0) - variant request-options-error { - /// Indicates the specified field is not supported by this implementation. - not-supported, - /// Indicates that the operation on the `request-options` was not permitted - /// because it is immutable. - immutable, - /// This is a catch-all error for anything that doesn't fit cleanly into a - /// more specific case. Implementations can use this to extend the error - /// type without breaking existing code. It also includes an optional - /// string for an unstructured description of the error. Users should not - /// depend on the string for diagnosing errors, as it's not required to be - /// consistent between implementations. - other(option), - } - - /// Field names are always strings. - /// - /// Field names should always be treated as case insensitive by the `fields` - /// resource for the purposes of equality checking. - @since(version = 0.3.0) - type field-name = string; - - /// Field values should always be ASCII strings. However, in - /// reality, HTTP implementations often have to interpret malformed values, - /// so they are provided as a list of bytes. - @since(version = 0.3.0) - type field-value = list; - - /// This following block defines the `fields` resource which corresponds to - /// HTTP standard Fields. Fields are a common representation used for both - /// Headers and Trailers. - /// - /// A `fields` may be mutable or immutable. A `fields` created using the - /// constructor, `from-list`, or `clone` will be mutable, but a `fields` - /// resource given by other means (including, but not limited to, - /// `request.headers`) might be be immutable. In an immutable fields, the - /// `set`, `append`, and `delete` operations will fail with - /// `header-error.immutable`. - /// - /// A `fields` resource should store `field-name`s and `field-value`s in their - /// original casing used to construct or mutate the `fields` resource. The `fields` - /// resource should use that original casing when serializing the fields for - /// transport or when returning them from a method. - /// - /// Implementations may impose limits on individual field values and on total - /// aggregate field section size. Operations that would exceed these limits - /// fail with `header-error.size-exceeded` - @since(version = 0.3.0) - resource fields { - /// Construct an empty HTTP Fields. - /// - /// The resulting `fields` is mutable. - constructor(); - /// Construct an HTTP Fields. - /// - /// The resulting `fields` is mutable. - /// - /// The list represents each name-value pair in the Fields. Names - /// which have multiple values are represented by multiple entries in this - /// list with the same name. - /// - /// The tuple is a pair of the field name, represented as a string, and - /// Value, represented as a list of bytes. In a valid Fields, all names - /// and values are valid UTF-8 strings. However, values are not always - /// well-formed, so they are represented as a raw list of bytes. - /// - /// An error result will be returned if any header or value was - /// syntactically invalid, if a header was forbidden, or if the - /// entries would exceed an implementation size limit. - from-list: static func(entries: list>) -> result; - /// Get all of the values corresponding to a name. If the name is not present - /// in this `fields`, an empty list is returned. However, if the name is - /// present but empty, this is represented by a list with one or more - /// empty field-values present. - get: func(name: field-name) -> list; - /// Returns `true` when the name is present in this `fields`. If the name is - /// syntactically invalid, `false` is returned. - has: func(name: field-name) -> bool; - /// Set all of the values for a name. Clears any existing values for that - /// name, if they have been set. - /// - /// Fails with `header-error.immutable` if the `fields` are immutable. - /// - /// Fails with `header-error.size-exceeded` if the name or values would - /// exceed an implementation-defined size limit. - set: func(name: field-name, value: list) -> result<_, header-error>; - /// Delete all values for a name. Does nothing if no values for the name - /// exist. - /// - /// Fails with `header-error.immutable` if the `fields` are immutable. - delete: func(name: field-name) -> result<_, header-error>; - /// Delete all values for a name. Does nothing if no values for the name - /// exist. - /// - /// Returns all values previously corresponding to the name, if any. - /// - /// Fails with `header-error.immutable` if the `fields` are immutable. - get-and-delete: func(name: field-name) -> result, header-error>; - /// Append a value for a name. Does not change or delete any existing - /// values for that name. - /// - /// Fails with `header-error.immutable` if the `fields` are immutable. - /// - /// Fails with `header-error.size-exceeded` if the value would exceed - /// an implementation-defined size limit. - append: func(name: field-name, value: field-value) -> result<_, header-error>; - /// Retrieve the full set of names and values in the Fields. Like the - /// constructor, the list represents each name-value pair. - /// - /// The outer list represents each name-value pair in the Fields. Names - /// which have multiple values are represented by multiple entries in this - /// list with the same name. - /// - /// The names and values are always returned in the original casing and in - /// the order in which they will be serialized for transport. - copy-all: func() -> list>; - /// Make a deep copy of the Fields. Equivalent in behavior to calling the - /// `fields` constructor on the return value of `copy-all`. The resulting - /// `fields` is mutable. - clone: func() -> fields; - } - - /// Headers is an alias for Fields. - @since(version = 0.3.0) - type headers = fields; - - /// Trailers is an alias for Fields. - @since(version = 0.3.0) - type trailers = fields; - - /// Represents an HTTP Request. - @since(version = 0.3.0) - resource request { - /// Construct a new `request` with a default `method` of `GET`, and - /// `none` values for `path-with-query`, `scheme`, and `authority`. - /// - /// `headers` is the HTTP Headers for the Request. - /// - /// `contents` is the optional body content stream with `none` - /// representing a zero-length content stream. - /// Once it is closed, `trailers` future must resolve to a result. - /// If `trailers` resolves to an error, underlying connection - /// will be closed immediately. - /// - /// `options` is optional `request-options` resource to be used - /// if the request is sent over a network connection. - /// - /// It is possible to construct, or manipulate with the accessor functions - /// below, a `request` with an invalid combination of `scheme` - /// and `authority`, or `headers` which are not permitted to be sent. - /// It is the obligation of the `handler.handle` implementation - /// to reject invalid constructions of `request`. - /// - /// The returned future resolves to result of transmission of this request. - new: static func(headers: headers, contents: option>, trailers: future, error-code>>, options: option) -> tuple>>; - /// Get the Method for the Request. - get-method: func() -> method; - /// Set the Method for the Request. Fails if the string present in a - /// `method.other` argument is not a syntactically valid method. - set-method: func(method: method) -> result; - /// Get the combination of the HTTP Path and Query for the Request. When - /// `none`, this represents an empty Path and empty Query. - get-path-with-query: func() -> option; - /// Set the combination of the HTTP Path and Query for the Request. When - /// `none`, this represents an empty Path and empty Query. Fails is the - /// string given is not a syntactically valid path and query uri component. - set-path-with-query: func(path-with-query: option) -> result; - /// Get the HTTP Related Scheme for the Request. When `none`, the - /// implementation may choose an appropriate default scheme. - get-scheme: func() -> option; - /// Set the HTTP Related Scheme for the Request. When `none`, the - /// implementation may choose an appropriate default scheme. Fails if the - /// string given is not a syntactically valid uri scheme. - set-scheme: func(scheme: option) -> result; - /// Get the authority of the Request's target URI. A value of `none` may be used - /// with Related Schemes which do not require an authority. The HTTP and - /// HTTPS schemes always require an authority. - get-authority: func() -> option; - /// Set the authority of the Request's target URI. A value of `none` may be used - /// with Related Schemes which do not require an authority. The HTTP and - /// HTTPS schemes always require an authority. Fails if the string given is - /// not a syntactically valid URI authority. - set-authority: func(authority: option) -> result; - /// Get the `request-options` to be associated with this request - /// - /// The returned `request-options` resource is immutable: `set-*` operations - /// will fail if invoked. - /// - /// This `request-options` resource is a child: it must be dropped before - /// the parent `request` is dropped, or its ownership is transferred to - /// another component by e.g. `handler.handle`. - get-options: func() -> option; - /// Get the headers associated with the Request. - /// - /// The returned `headers` resource is immutable: `set`, `append`, and - /// `delete` operations will fail with `header-error.immutable`. - get-headers: func() -> headers; - /// Get body of the Request. - /// - /// Stream returned by this method represents the contents of the body. - /// Once the stream is reported as closed, callers should await the returned - /// future to determine whether the body was received successfully. - /// The future will only resolve after the stream is reported as closed. - /// - /// This function takes a `res` future as a parameter, which can be used to - /// communicate an error in handling of the request. - /// - /// Note that function will move the `request`, but references to headers or - /// request options acquired from it previously will remain valid. - consume-body: static func(this: request, res: future>) -> tuple, future, error-code>>>; - } - - /// Parameters for making an HTTP Request. Each of these parameters is - /// currently an optional timeout applicable to the transport layer of the - /// HTTP protocol. - /// - /// These timeouts are separate from any the user may use to bound an - /// asynchronous call. - @since(version = 0.3.0) - resource request-options { - /// Construct a default `request-options` value. - constructor(); - /// The timeout for the initial connect to the HTTP Server. - get-connect-timeout: func() -> option; - /// Set the timeout for the initial connect to the HTTP Server. An error - /// return value indicates that this timeout is not supported or that this - /// handle is immutable. - set-connect-timeout: func(duration: option) -> result<_, request-options-error>; - /// The timeout for receiving the first byte of the Response body. - get-first-byte-timeout: func() -> option; - /// Set the timeout for receiving the first byte of the Response body. An - /// error return value indicates that this timeout is not supported or that - /// this handle is immutable. - set-first-byte-timeout: func(duration: option) -> result<_, request-options-error>; - /// The timeout for receiving subsequent chunks of bytes in the Response - /// body stream. - get-between-bytes-timeout: func() -> option; - /// Set the timeout for receiving subsequent chunks of bytes in the Response - /// body stream. An error return value indicates that this timeout is not - /// supported or that this handle is immutable. - set-between-bytes-timeout: func(duration: option) -> result<_, request-options-error>; - /// Make a deep copy of the `request-options`. - /// The resulting `request-options` is mutable. - clone: func() -> request-options; - } - - /// This type corresponds to the HTTP standard Status Code. - @since(version = 0.3.0) - type status-code = u16; - - /// Represents an HTTP Response. - @since(version = 0.3.0) - resource response { - /// Construct a new `response`, with a default `status-code` of `200`. - /// If a different `status-code` is needed, it must be set via the - /// `set-status-code` method. - /// - /// `headers` is the HTTP Headers for the Response. - /// - /// `contents` is the optional body content stream with `none` - /// representing a zero-length content stream. - /// Once it is closed, `trailers` future must resolve to a result. - /// If `trailers` resolves to an error, underlying connection - /// will be closed immediately. - /// - /// The returned future resolves to result of transmission of this response. - new: static func(headers: headers, contents: option>, trailers: future, error-code>>) -> tuple>>; - /// Get the HTTP Status Code for the Response. - get-status-code: func() -> status-code; - /// Set the HTTP Status Code for the Response. Fails if the status-code - /// given is not a valid http status code. - set-status-code: func(status-code: status-code) -> result; - /// Get the headers associated with the Response. - /// - /// The returned `headers` resource is immutable: `set`, `append`, and - /// `delete` operations will fail with `header-error.immutable`. - get-headers: func() -> headers; - /// Get body of the Response. - /// - /// Stream returned by this method represents the contents of the body. - /// Once the stream is reported as closed, callers should await the returned - /// future to determine whether the body was received successfully. - /// The future will only resolve after the stream is reported as closed. - /// - /// This function takes a `res` future as a parameter, which can be used to - /// communicate an error in handling of the response. - /// - /// Note that function will move the `response`, but references to headers - /// acquired from it previously will remain valid. - consume-body: static func(this: response, res: future>) -> tuple, future, error-code>>>; - } -} - -/// This interface defines a handler of HTTP Requests. -/// -/// In a `wasi:http/service` this interface is exported to respond to an -/// incoming HTTP Request with a Response. -/// -/// In `wasi:http/middleware` this interface is both exported and imported as -/// the "downstream" and "upstream" directions of the middleware chain. -@since(version = 0.3.0) -interface handler { - use types.{request, response, error-code}; - - /// This function may be called with either an incoming request read from the - /// network or a request synthesized or forwarded by another component. - handle: async func(request: request) -> result; -} - -/// This interface defines an HTTP client for sending "outgoing" requests. -/// -/// Most components are expected to import this interface to provide the -/// capability to send HTTP requests to arbitrary destinations on a network. -/// -/// The type signature of `client.send` is the same as `handler.handle`. This -/// duplication is currently necessary because some Component Model tooling -/// (including WIT itself) is unable to represent a component importing two -/// instances of the same interface. A `client.send` import may be linked -/// directly to a `handler.handle` export to bypass the network. -@since(version = 0.3.0) -interface client { - use types.{request, response, error-code}; - - /// This function may be used to either send an outgoing request over the - /// network or to forward it to another component. - send: async func(request: request) -> result; -} diff --git a/tests/runtime/moonbit/http-body-trailers/runner.mbt b/tests/runtime/moonbit/http-body-trailers/runner.mbt deleted file mode 100644 index e18c150a5..000000000 --- a/tests/runtime/moonbit/http-body-trailers/runner.mbt +++ /dev/null @@ -1,47 +0,0 @@ -//@ wasmtime-flags = '-Wcomponent-model-async -S p3 -S http' -//@ [lang] -//@ path = 'gen/world/runner/stub.mbt' -//@ pkg_config = """{ "warn-list": "-44", "import": [{ "path": "my/test/async-core", "alias": "async-core" }, { "path": "my/test/interface/wasi/http/handler", "alias": "handler" }, { "path": "my/test/interface/wasi/http/types", "alias": "types" }] }""" - -///| -pub async fn run(_background_group : @async-core.TaskGroup[Unit]) -> Unit { - let (request, request_transmitted) = @types.Request::new( - @types.Fields::fields(), - None, - @async-core.Future::ready(Ok(None)), - None, - ) - let response = match @handler.handle(request) { - Ok(response) => response - Err(_) => panic() - } - - // Completing this future tells the handler that the response was transmitted. - let (body, trailers) = @types.Response::consume_body( - response, - @async-core.Future::ready(Ok(())), - ) - let bytes : Array[Byte] = [] - for ;; { - match body.read(4) { - Some(chunk) => - for i = 0; i < chunk.length(); i = i + 1 { - bytes.push(chunk[i]) - } - None => break - } - } - - let expected = b"hello http p3" - guard bytes.length() == expected.length() else { panic() } - for i = 0; i < expected.length(); i = i + 1 { - guard bytes[i] == expected[i] else { panic() } - } - - match trailers.get() { - Ok(Some(fields)) => fields.drop() - Ok(None) | Err(_) => panic() - } - body.drop() - request_transmitted.drop() -} diff --git a/tests/runtime/moonbit/http-body-trailers/test.mbt b/tests/runtime/moonbit/http-body-trailers/test.mbt deleted file mode 100644 index 6748a3c5b..000000000 --- a/tests/runtime/moonbit/http-body-trailers/test.mbt +++ /dev/null @@ -1,66 +0,0 @@ -//@ [lang] -//@ path = 'gen/interface/wasi/http/handler/stub.mbt' -//@ pkg_config = """{ -//@ "import": [ -//@ { "path": "my/test/async-core", "alias": "async-core" }, -//@ { "path": "my/test/interface/wasi/http/types", "alias": "types" } -//@ ] -//@ }""" - -///| -let body_progress : Ref[UInt] = Ref(0) - -///| -pub async fn handle( - request : @types.Request, - background_group : @async-core.TaskGroup[Unit], -) -> Result[@types.Response, @types.ErrorCode] { - request.drop() - body_progress.val = 0 - let transmitted_done = Ref(false) - let (completed, completed_sink) = @async-core.Stream::new(capacity=2) - - let trailers = @async-core.Future::from(async fn() { - for _ in 0..<2 { - guard completed.read(1) is Some(signal) && signal.length() == 1 else { - abort("response completion signal closed early") - } - } - guard body_progress.val >= 3 && transmitted_done.val else { - abort("response completion signal arrived too early") - } - Ok(Some(@types.Fields::fields())) - }) - - let body = @async-core.Stream::produce(async fn(sink) { - body_progress.val = 1 - let _ = sink.write_all_bytes(b"hello "[:]) - body_progress.val = 2 - let _ = sink.write_all_bytes(b"http p3"[:]) - sink.close() - body_progress.val = 3 - let signal : FixedArray[Unit] = [()] - guard completed_sink.write_all(signal[:]) else { - abort("response completion signal closed early") - } - }) - - let (response, transmitted) = @types.Response::new( - @types.Fields::fields(), - Some(body), - trailers, - ) - background_group.spawn_bg(async fn() { - match transmitted.get() { - Ok(_) => { - transmitted_done.val = true - let signal : FixedArray[Unit] = [()] - guard completed_sink.write_all(signal[:]) else { - abort("response completion signal closed early") - } - } - Err(_) => abort("response transmission failed") - } - }) - Ok(response) -} diff --git a/tests/runtime/moonbit/http-body-trailers/test.wit b/tests/runtime/moonbit/http-body-trailers/test.wit deleted file mode 100644 index 10365d6a1..000000000 --- a/tests/runtime/moonbit/http-body-trailers/test.wit +++ /dev/null @@ -1,14 +0,0 @@ -package my:test; - -world test { - import wasi:http/types@0.3.0; - - export wasi:http/handler@0.3.0; -} - -world runner { - import wasi:http/types@0.3.0; - import wasi:http/handler@0.3.0; - - export run: async func(); -} From 81fcf84f6aee56dc4fbb5d122720bfd961966e89 Mon Sep 17 00:00:00 2001 From: zihang Date: Tue, 21 Jul 2026 10:32:51 +0800 Subject: [PATCH 16/23] fix(moonbit): serialize stream writer teardown --- crates/moonbit/src/async_support.rs | 19 ++++++++---- crates/moonbit/src/lib.rs | 5 ++++ .../stream-write-cancel/holder-service.rs | 7 +++++ .../moonbit/stream-write-cancel/runner.rs | 5 ++-- .../moonbit/stream-write-cancel/test.mbt | 29 +++++++++++++------ .../moonbit/stream-write-cancel/test.rs | 4 ++- .../moonbit/stream-write-cancel/test.wit | 1 + 7 files changed, 52 insertions(+), 18 deletions(-) diff --git a/crates/moonbit/src/async_support.rs b/crates/moonbit/src/async_support.rs index 128d41afd..61c2976fa 100644 --- a/crates/moonbit/src/async_support.rs +++ b/crates/moonbit/src/async_support.rs @@ -1984,7 +1984,11 @@ fn wasm{symbol_name}StreamCommit(handle: Int) -> Unit {{ wasmImport{symbol_name}DropWritable(writer) }} }} - defer close_writer() + let close_writer_serialized = async fn() -> Unit {{ + writer_lock.acquire() + defer writer_lock.release() + close_writer() + }} let cleanup_value = fn(value : {result}) -> Unit {{ let ptr = wasm{symbol_name}Malloc(1) wasm{symbol_name}Lower(value, ptr) @@ -2054,11 +2058,7 @@ fn wasm{symbol_name}StreamCommit(handle: Int) -> Unit {{ // consumed the rest of this staging window. data_len }}, - () => {{ - writer_lock.acquire() - defer writer_lock.release() - close_writer() - }}, + () => close_writer_serialized(), () => !writer_closed.val, Some(cleanup_value), ) @@ -2096,6 +2096,13 @@ fn wasm{symbol_name}StreamCommit(handle: Int) -> Unit {{ }} }} }} + // A retained Sink may still own an in-flight canonical write after its + // producer returns. Do not drop the writable endpoint until that write + // has reached a terminal event and released its staging buffer. + {ffi}protect_from_cancel( + () => close_writer_serialized(), + resume_on_cancel=true, + ) }}) }} diff --git a/crates/moonbit/src/lib.rs b/crates/moonbit/src/lib.rs index 8810db305..d4013efb7 100644 --- a/crates/moonbit/src/lib.rs +++ b/crates/moonbit/src/lib.rs @@ -3585,8 +3585,13 @@ mod tests { assert!(ffi.contains("suspend_for_future_write_terminal")); assert!(ffi.contains("let data_len = if data.length() < 1")); assert!(ffi.contains("let writer_lock = @async-core.Mutex()")); + assert!(ffi.contains("let close_writer_serialized = async fn()")); assert!(ffi.contains("writer_lock.acquire()")); assert!(ffi.contains("defer writer_lock.release()")); + assert!( + ffi.contains("() => close_writer_serialized(),\n resume_on_cancel=true,") + ); + assert!(!ffi.contains("defer close_writer()")); assert!(ffi.contains("read_cleanup : @async-core.CondVar")); assert!(ffi.contains("self.read_cleanup.broadcast()")); assert!(ffi.contains("@async-core.cancel_future_read(")); diff --git a/tests/runtime/moonbit/stream-write-cancel/holder-service.rs b/tests/runtime/moonbit/stream-write-cancel/holder-service.rs index 5d1057fb5..31fb6668c 100644 --- a/tests/runtime/moonbit/stream-write-cancel/holder-service.rs +++ b/tests/runtime/moonbit/stream-write-cancel/holder-service.rs @@ -60,6 +60,13 @@ impl Guest for Component { result == StreamResult::Dropped && values.is_empty() } + async fn read_one_and_keep() -> bool { + let mut stream = HELD_STREAM.with(|stream| stream.borrow_mut().take().unwrap()); + let (result, values) = stream.read(Vec::with_capacity(1)).await; + HELD_STREAM.with(|held| assert!(held.borrow_mut().replace(stream).is_none())); + result == StreamResult::Complete(1) && values.len() == 1 + } + async fn read_one_and_drop() -> bool { let mut stream = HELD_STREAM.with(|stream| stream.borrow_mut().take().unwrap()); let (result, values) = stream.read(Vec::with_capacity(1)).await; diff --git a/tests/runtime/moonbit/stream-write-cancel/runner.rs b/tests/runtime/moonbit/stream-write-cancel/runner.rs index df0a16e7f..5a09a586a 100644 --- a/tests/runtime/moonbit/stream-write-cancel/runner.rs +++ b/tests/runtime/moonbit/stream-write-cancel/runner.rs @@ -24,13 +24,14 @@ impl Guest for Component { .is_pending()); holder::wait_write_started().await; assert!(holder::write_started()); + assert!(holder::read_one_and_keep().await); drop(task); wait_cancellation_observed().await; assert!(cancellation_observed()); assert!(holder::writable_dropped().await); assert_eq!(holder::leaf_live_count(), 0); - assert_eq!(holder::leaf_drop_count(), 1); + assert_eq!(holder::leaf_drop_count(), 2); start_peer_drop().await; assert!(holder::read_one_and_drop().await); @@ -38,6 +39,6 @@ impl Guest for Component { assert!(second_write_started()); assert!(producer_finished()); assert_eq!(holder::leaf_live_count(), 0); - assert_eq!(holder::leaf_drop_count(), 5); + assert_eq!(holder::leaf_drop_count(), 6); } } diff --git a/tests/runtime/moonbit/stream-write-cancel/test.mbt b/tests/runtime/moonbit/stream-write-cancel/test.mbt index 18c01d6ee..630427372 100644 --- a/tests/runtime/moonbit/stream-write-cancel/test.mbt +++ b/tests/runtime/moonbit/stream-write-cancel/test.mbt @@ -25,17 +25,28 @@ let producer_done : (@async-core.Future[Unit], @async-core.Promise[Unit]) = ///| pub async fn run_until_cancelled( - _background_group : @async-core.TaskGroup[Unit] + background_group : @async-core.TaskGroup[Unit] ) -> Unit { + let writer_started : (@async-core.Future[Unit], @async-core.Promise[Unit]) = + @async-core.Future::new() let stream = @async-core.Stream::produce(async fn(sink) { - defer { - cancellation_seen.val = true - ignore(cancellation_done.1.complete(())) - } - @holder.mark_write_started() - let data : FixedArray[@holder.Leaf] = [@holder.Leaf::leaf()] - let _ = sink.write_all(data[:]) - }) + // Return while a child owns a partially accepted write, forcing generated + // finalization to wait for the child's canonical buffer cleanup. + background_group.spawn_bg(async fn() { + defer { + cancellation_seen.val = true + ignore(cancellation_done.1.complete(())) + } + @holder.mark_write_started() + ignore(writer_started.1.complete(())) + let data : FixedArray[@holder.Leaf] = [ + @holder.Leaf::leaf(), + @holder.Leaf::leaf(), + ] + let _ = sink.write_all(data[:]) + }) + writer_started.0.get() + }, cleanup=fn(value) { value.drop() }) @holder.hold(stream) never.0.get() } diff --git a/tests/runtime/moonbit/stream-write-cancel/test.rs b/tests/runtime/moonbit/stream-write-cancel/test.rs index 3529557a0..53a6ea7cd 100644 --- a/tests/runtime/moonbit/stream-write-cancel/test.rs +++ b/tests/runtime/moonbit/stream-write-cancel/test.rs @@ -44,7 +44,9 @@ impl Guest for Component { let (mut writer, reader) = wit_stream::new::(); holder::hold(reader).await; holder::mark_write_started(); - let _ = writer.write_one(holder::Leaf::new()).await; + let _ = writer + .write(vec![holder::Leaf::new(), holder::Leaf::new()]) + .await; std::future::pending::<()>().await; } diff --git a/tests/runtime/moonbit/stream-write-cancel/test.wit b/tests/runtime/moonbit/stream-write-cancel/test.wit index cf084aafa..38644f172 100644 --- a/tests/runtime/moonbit/stream-write-cancel/test.wit +++ b/tests/runtime/moonbit/stream-write-cancel/test.wit @@ -13,6 +13,7 @@ interface holder { write-started: func() -> bool; wait-write-started: async func(); writable-dropped: async func() -> bool; + read-one-and-keep: async func() -> bool; read-one-and-drop: async func() -> bool; leaf-live-count: func() -> u32; leaf-drop-count: func() -> u32; From 20d591e2ba160b7a991ca095d1b0cf44d2e56da6 Mon Sep 17 00:00:00 2001 From: zihang Date: Tue, 21 Jul 2026 11:01:29 +0800 Subject: [PATCH 17/23] fix(moonbit): close stream test wakeup race --- .../moonbit/stream-write-cancel/holder-service.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/runtime/moonbit/stream-write-cancel/holder-service.rs b/tests/runtime/moonbit/stream-write-cancel/holder-service.rs index 31fb6668c..1fc2aadfc 100644 --- a/tests/runtime/moonbit/stream-write-cancel/holder-service.rs +++ b/tests/runtime/moonbit/stream-write-cancel/holder-service.rs @@ -47,8 +47,14 @@ impl Guest for Component { if WRITE_STARTED.load(Ordering::SeqCst) { std::task::Poll::Ready(()) } else { - *WRITE_STARTED_WAKER.lock().unwrap() = Some(cx.waker().clone()); - std::task::Poll::Pending + let mut waker = WRITE_STARTED_WAKER.lock().unwrap(); + *waker = Some(cx.waker().clone()); + if WRITE_STARTED.load(Ordering::SeqCst) { + waker.take(); + std::task::Poll::Ready(()) + } else { + std::task::Poll::Pending + } } }) .await From 8162df06874a87f972f0db86c6cffb895e932be1 Mon Sep 17 00:00:00 2001 From: zihang Date: Tue, 21 Jul 2026 11:01:57 +0800 Subject: [PATCH 18/23] refactor(moonbit): clean up async binding code --- crates/moonbit/src/async/async_abi.mbt | 14 +- crates/moonbit/src/async/ev.mbt | 4 +- crates/moonbit/src/async/promise.mbt | 2 +- crates/moonbit/src/async/trait.mbt | 50 ++- crates/moonbit/src/async_support.rs | 360 +++++++++--------- crates/moonbit/src/lib.rs | 32 +- .../moonbit/async-import-cancel/runner.mbt | 2 +- .../concurrent-export-isolation/runner.mbt | 2 +- .../moonbit/http-background-hook/runner.mbt | 2 +- .../moonbit/http-background-hook/test.mbt | 2 +- .../moonbit/local-async-primitives/runner.mbt | 2 +- .../moonbit/resource-payloads/leaf.mbt | 2 +- .../moonbit/resource-payloads/runner.mbt | 2 +- .../moonbit/stream-write-cancel/test.mbt | 2 +- .../moonbit/wasi-cli-p3-stdout/runner.mbt | 2 +- .../moonbit/wasi-cli-p3-stdout/test.mbt | 1 + 16 files changed, 268 insertions(+), 213 deletions(-) diff --git a/crates/moonbit/src/async/async_abi.mbt b/crates/moonbit/src/async/async_abi.mbt index 373cded54..c13492612 100644 --- a/crates/moonbit/src/async/async_abi.mbt +++ b/crates/moonbit/src/async/async_abi.mbt @@ -57,13 +57,13 @@ priv enum EventCode { ///| fn EventCode::from(int : Int) -> EventCode { match int { - 0 => EventCode::None - 1 => EventCode::SubTask - 2 => EventCode::StreamRead - 3 => EventCode::StreamWrite - 4 => EventCode::FutureRead - 5 => EventCode::FutureWrite - 6 => EventCode::TaskCancelled + 0 => None + 1 => SubTask + 2 => StreamRead + 3 => StreamWrite + 4 => FutureRead + 5 => FutureWrite + 6 => TaskCancelled _ => panic() } } diff --git a/crates/moonbit/src/async/ev.mbt b/crates/moonbit/src/async/ev.mbt index c1b669ca7..d5ea97263 100644 --- a/crates/moonbit/src/async/ev.mbt +++ b/crates/moonbit/src/async/ev.mbt @@ -354,7 +354,7 @@ fn arm_task_wakeup(waitable_set : WaitableSet) -> Unit { guard result == -1 let subscribers = subscribers_for(waitable_set) guard subscribers.get(wakeup.reader) is None - subscribers.set(wakeup.reader, { event: None, coro: @set.Set([]) }) + subscribers.set(wakeup.reader, { event: None, coro: Set([]) }) waitable_join(wakeup.reader, waitable_set.0) wakeup.reading = true wakeup.signaled = false @@ -447,7 +447,7 @@ async fn waitable_event(waitable_id : Int, immediate : Events?) -> Events { let subscriber = if subscribers.get(waitable_id) is Some(subscriber) { subscriber } else { - let subscriber = { event: None, coro: @set.Set([]) } + let subscriber = { event: None, coro: Set([]) } subscribers.set(waitable_id, subscriber) subscriber } diff --git a/crates/moonbit/src/async/promise.mbt b/crates/moonbit/src/async/promise.mbt index ec72b21b7..cce7b5cc2 100644 --- a/crates/moonbit/src/async/promise.mbt +++ b/crates/moonbit/src/async/promise.mbt @@ -52,7 +52,7 @@ fn[X] new_future_promise(cleanup : ((X) -> Unit)?) -> (Future[X], Promise[X]) { reading: false, }, } - (Future::{ inner: Promised(cell) }, Promise::{ cell, }) + ({ inner: Promised(cell) }, { cell, }) } ///| diff --git a/crates/moonbit/src/async/trait.mbt b/crates/moonbit/src/async/trait.mbt index 1413502d0..516a3a0e1 100644 --- a/crates/moonbit/src/async/trait.mbt +++ b/crates/moonbit/src/async/trait.mbt @@ -14,6 +14,7 @@ let sink_write_window_size : Int = 64 /// The write callback must either raise before consuming any value or return /// the number consumed. It must not raise after partial consumption because /// `ArrayView` cannot carry an ownership offset through an exception. +/// The close callback must be idempotent. #internal(wit_bindgen, "generated binding code only") pub fn[X] Sink::from_callbacks( write : async (ArrayView[X]) -> Int, @@ -162,6 +163,10 @@ pub async fn Sink::write_all_bytes(self : Sink[Byte], data : BytesView) -> Bool } ///| +/// Closes the writable end of the stream. +/// +/// Closing is idempotent. A peer waiting for values observes the end of the +/// stream after any values already accepted by the sink have been read. pub async fn[X] Sink::close(self : Sink[X]) -> Unit { (self.close)() } @@ -540,7 +545,7 @@ pub fn[X] Future::ready_with_cleanup( let state : Ref[LocalFutureState[X]] = { val: { value: Some(value), closed: false, cleanup: Some(cleanup) }, } - Future::{ inner: Local(state) } + { inner: Local(state) } } ///| @@ -548,7 +553,7 @@ pub fn[X] Future::ready(value : X) -> Future[X] { let state : Ref[LocalFutureState[X]] = { val: { value: Some(value), closed: false, cleanup: None }, } - Future::{ inner: Local(state) } + { inner: Local(state) } } ///| @@ -561,9 +566,7 @@ pub fn[X] Future::from( producer : async () -> X, on_unstarted_drop? : () -> Unit, ) -> Future[X] { - Future::{ - inner: Pending({ val: { producer: Some(producer), on_unstarted_drop } }), - } + { inner: Pending({ val: { producer: Some(producer), on_unstarted_drop } }) } } ///| @@ -573,10 +576,14 @@ pub fn[X] Future::from_source( close : async () -> Unit, close_sync : () -> Unit, ) -> Future[X] { - Future::{ inner: Source({ get, close, close_sync }) } + { inner: Source({ get, close, close_sync }) } } ///| +/// Waits for and consumes the future's single value. +/// +/// A `Future` is a one-shot value. Calling `get` again, or calling it after the +/// readable end has been dropped, fails instead of replaying the value. pub async fn[X] Future::get(self : Future[X]) -> X { match self.inner { Source(source) => (source.get)() @@ -595,6 +602,11 @@ pub async fn[X] Future::get(self : Future[X]) -> X { } ///| +/// Releases the readable end without consuming its value. +/// +/// If the value is already locally available, its registered cleanup callback +/// runs. An unstarted lazy producer is discarded and runs +/// `on_unstarted_drop`, when provided. pub async fn[X] Future::drop(self : Future[X]) -> Unit noraise { match self.inner { Source(source) => (source.close)() catch { _ => () } @@ -669,22 +681,22 @@ fn[X] new_stream_pipe( { val: { capacity, - chunks: @deque.Deque([]), + chunks: Deque([]), buffered: 0, writer_closed: false, reader_dropped: false, cleanup, head: None, head_pos: 0, - readers: @deque.Deque([]), - writers: @deque.Deque([]), + readers: Deque([]), + writers: Deque([]), }, } } ///| fn[X] stream_pipe_sink(pipe : Ref[StreamPipe[X]]) -> Sink[X] { - Sink::{ + { write: (data : ArrayView[X]) => stream_pipe_write(pipe, data), close: () => stream_pipe_close_writer(pipe), is_open: () => !pipe.val.writer_closed && !pipe.val.reader_dropped, @@ -714,9 +726,7 @@ fn spawn_stream_producer(task : async () -> Unit) -> Unit { let current = current_coroutine() match current.spawner { Some(spawner) => spawner(task) - None => { - let _ = spawn(task) - } + None => ignore(spawn(task)) } } @@ -785,7 +795,7 @@ pub fn[X] Stream::produce( cleanup? : (X) -> Unit, on_unstarted_drop? : () -> Unit, ) -> Stream[X] { - Stream::{ + { inner: Outgoing({ val: { producer: Some(producer), cleanup, on_unstarted_drop, pipe: None }, }), @@ -799,10 +809,16 @@ pub fn[X] Stream::from_source( close : async () -> Unit, close_sync : () -> Unit, ) -> Stream[X] { - Stream::{ inner: Source({ read, close, close_sync }) } + { inner: Source({ read, close, close_sync }) } } ///| +/// Reads at most `count` values from the stream. +/// +/// `Some(values)` contains the next owned chunk and may be empty when the peer +/// reports no progress. `None` means the writable end has closed and no +/// buffered values remain. A non-positive count returns an empty chunk without +/// waiting. pub async fn[X] Stream::read(self : Stream[X], count : Int) -> FixedArray[X]? { match self.inner { Source(source) => (source.read)(count) @@ -813,6 +829,10 @@ pub async fn[X] Stream::read(self : Stream[X], count : Int) -> FixedArray[X]? { } ///| +/// Releases the readable end and discards unread values. +/// +/// Registered cleanup callbacks run for locally buffered values. An unstarted +/// lazy producer is discarded and runs `on_unstarted_drop`, when provided. pub async fn[X] Stream::drop(self : Stream[X]) -> Unit noraise { match self.inner { Source(source) => (source.close)() catch { _ => () } diff --git a/crates/moonbit/src/async_support.rs b/crates/moonbit/src/async_support.rs index 61c2976fa..52ce72a12 100644 --- a/crates/moonbit/src/async_support.rs +++ b/crates/moonbit/src/async_support.rs @@ -34,6 +34,13 @@ impl PayloadFor { PayloadFor::Stream => "stream", } } + + fn type_name(self) -> &'static str { + match self { + PayloadFor::Future => "Future", + PayloadFor::Stream => "Stream", + } + } } #[derive(Clone, Debug, Default)] @@ -110,6 +117,17 @@ struct AsyncEndpointIntrinsics { drop_writable: AsyncIntrinsicName, } +struct EndpointPayloadFragments { + lift: String, + lift_result: String, + lower: String, + malloc: String, + lift_list: String, + commit: String, + reject: String, + free_outer: String, +} + #[derive(Clone, Debug)] pub(crate) struct AsyncFunctionPlan { endpoint_sites: Vec, @@ -355,6 +373,10 @@ impl AsyncEndpointNames { } impl AsyncSupport { + pub(crate) fn is_required(&self) -> bool { + self.runtime_required || !self.endpoints.is_empty() + } + pub(crate) fn import_plan( &mut self, async_filter: &mut AsyncFilterSet, @@ -401,7 +423,7 @@ impl AsyncSupport { } pub(crate) fn emit_runtime_files(&self, files: &mut Files, version: &str) { - if !self.runtime_required && self.endpoints.is_empty() { + if !self.is_required() { return; } @@ -710,7 +732,7 @@ impl<'a> InterfaceGenerator<'a> { r#" #doc(hidden) pub fn {func_name}({params}) -> {result_type} {{ - return {ffi}with_waitableset(async fn() {{ + {ffi}with_waitableset(async fn() {{ {ffi}with_task_group(async fn({background_group}) {{ {cleanup_list} {body} @@ -909,7 +931,7 @@ impl<'a> InterfaceGenerator<'a> { self.ffi, r#" #doc(hidden) - pub fn {export_func_name}(event_raw: Int, waitable: Int, code: Int) -> Int {{ + pub fn {export_func_name}(event_raw : Int, waitable : Int, code : Int) -> Int {{ {ffi}cb(event_raw, waitable, code) }} "# @@ -923,7 +945,7 @@ impl<'a> InterfaceGenerator<'a> { let export = format!( r#" #doc(hidden) - pub fn {export_func_name}(event_raw: Int, waitable: Int, code: Int) -> Int {{ + pub fn {export_func_name}(event_raw : Int, waitable : Int, code : Int) -> Int {{ {package}{export_func_name}(event_raw, waitable, code) }} "#, @@ -949,6 +971,10 @@ impl<'a> InterfaceGenerator<'a> { let mut lower_results = Vec::new(); let mut async_state = endpoint_plan.state(); let mut needs_cleanup_list = false; + let mut local_names = Ns::default(); + for (name, _) in &mbt_sig.params { + local_names.tmp(name); + } self.ffi_imports.insert(ffi::FREE); let ffi = self .world_gen @@ -956,19 +982,20 @@ impl<'a> InterfaceGenerator<'a> { .qualify_package(self.name, ASYNC_CORE_DIR); if sig.indirect_params { + let lower_ptr = local_names.tmp("lower_ptr"); match &func.params[..] { [] => {} [Param { name, ty, .. }] => { - body.push_str(&self.malloc_memory("_lower_ptr", "1", ty, self.name)); - body.push_str("\ndefer mbt_ffi_free(_lower_ptr)\n"); + body.push_str(&self.malloc_memory(&lower_ptr, "1", ty)); + body.push_str(&format!("\ndefer mbt_ffi_free({lower_ptr})\n")); body.push_str(&self.lower_to_memory( - "_lower_ptr", + &lower_ptr, &name.to_moonbit_ident(), ty, self.name, &mut async_state, )); - lower_params.push("_lower_ptr".into()); + lower_params.push(lower_ptr); } multiple_params => { let params = multiple_params.iter().map(|Param { ty, .. }| ty); @@ -977,8 +1004,8 @@ impl<'a> InterfaceGenerator<'a> { self.ffi_imports.insert(ffi::MALLOC); body.push_str(&format!( r#" - let _lower_ptr : Int = mbt_ffi_malloc({}) - defer mbt_ffi_free(_lower_ptr) + let {lower_ptr} : Int = mbt_ffi_malloc({}) + defer mbt_ffi_free({lower_ptr}) "#, elem_info.size.size_wasm32(), )); @@ -989,7 +1016,7 @@ impl<'a> InterfaceGenerator<'a> { .map(|Param { name, .. }| name.to_moonbit_ident()), ) { let result = self.lower_to_memory( - &format!("_lower_ptr + {}", offset.size_wasm32()), + &format!("{lower_ptr} + {}", offset.size_wasm32()), &name, ty, self.name, @@ -998,7 +1025,7 @@ impl<'a> InterfaceGenerator<'a> { body.push_str(&result); } - lower_params.push("_lower_ptr".into()); + lower_params.push(lower_ptr); } } } else { @@ -1022,9 +1049,8 @@ impl<'a> InterfaceGenerator<'a> { let mut stable_params = Vec::with_capacity(lower_params.len()); let bindings = lower_params .iter() - .enumerate() - .map(|(index, value)| { - let name = format!("_lower_arg{index}"); + .map(|value| { + let name = local_names.tmp("lower_arg"); stable_params.push(name.clone()); format!("let {name} = {value}") }) @@ -1093,12 +1119,13 @@ impl<'a> InterfaceGenerator<'a> { ); let func_name = func.name.to_upper_camel_case(); - let call_import = |params: &Vec, drop_returned_result: &str| { + let subtask_code = local_names.tmp("subtask_code"); + let call_import = |params: &[String], drop_returned_result: &str| { format!( r#" - let _subtask_code = wasmImport{func_name}({}) + let {subtask_code} = wasmImport{func_name}({}) {ffi}suspend_for_subtask( - _subtask_code, + {subtask_code}, {settle_arguments}, fn() {{ {drop_returned_result} @@ -1111,11 +1138,12 @@ impl<'a> InterfaceGenerator<'a> { }; match &func.result { Some(ty) => { - lower_params.push("_result_ptr".into()); + let result_ptr = local_names.tmp("result_ptr"); + lower_params.push(result_ptr.clone()); let (drop_returned_result, drop_result_state) = self .deallocate_lists_and_own_with_state( std::slice::from_ref(ty), - &["_result_ptr".into()], + std::slice::from_ref(&result_ptr), true, self.name, endpoint_plan.endpoint_sites.clone(), @@ -1130,18 +1158,18 @@ impl<'a> InterfaceGenerator<'a> { } let call_import = call_import(&lower_params, &drop_returned_result); let (lift, lift_result) = - &self.lift_from_memory("_result_ptr", ty, self.name, &mut async_state); + &self.lift_from_memory(&result_ptr, ty, self.name, &mut async_state); body.push_str(&format!( r#" {} {} - defer mbt_ffi_free(_result_ptr) + defer mbt_ffi_free({result_ptr}) {call_import} {lift} {lift_result} "#, lower_results.join("\n"), - &self.malloc_memory("_result_ptr", "1", ty, self.name) + &self.malloc_memory(&result_ptr, "1", ty) )); } None => { @@ -1180,7 +1208,7 @@ impl<'a> InterfaceGenerator<'a> { let symbol_name = &site.symbol_name; let payload_len_arg = match site.kind { PayloadFor::Future => "", - PayloadFor::Stream => " ,length : Int", + PayloadFor::Stream => ", length : Int", }; let new_module = &site.intrinsics.new.module; let new_field = &site.intrinsics.new.field; @@ -1204,75 +1232,81 @@ impl<'a> InterfaceGenerator<'a> { let elem_size = result_type .map(|ty| self.world_gen.sizes.size(ty).size_wasm32()) .unwrap_or(0); - let read_chunk_owns_buffer = - result_type.is_some_and(|ty| self.is_list_canonical(self.resolve, ty)); + let read_chunk_owns_buffer = result_type.is_some_and(|ty| self.is_list_canonical(ty)); let staging_window = if payload_sites.is_empty() { 64 } else { 1 }; - let (lift, lift_result, lower, malloc, lift_list, commit, reject, free_outer) = - if let Some(result_type) = result_type { - let mut payload_state = AsyncFunctionState::from_sites(payload_sites.clone()); - let (lift, lift_result) = - self.lift_from_memory("ptr", result_type, &helper_package, &mut payload_state); - let mut payload_state = AsyncFunctionState::from_sites(payload_sites.clone()); - let lower = self.lower_to_memory( - "ptr", - "value", - result_type, - &helper_package, - &mut payload_state, - ); - let (commit, _) = self.commit_lists_and_endpoints_with_state( - std::slice::from_ref(result_type), - &[String::from("elem_ptr")], - true, - &helper_package, - payload_sites.clone(), - ); - let reject = self.deallocate_lists_and_own( - std::slice::from_ref(result_type), - &[String::from("elem_ptr")], - true, - &helper_package, - payload_sites.clone(), - ); - let lift_list = self.list_lift_from_memory( - "ptr", - "length", - &format!("wasm{symbol_name}Lift"), - result_type, - &helper_package, - ); - let malloc = self.malloc_memory("ptr", "length", result_type, &helper_package); - self.ffi_imports.insert(ffi::FREE); - ( - lift, - lift_result, - lower, - malloc, - lift_list, - commit, - reject, - "mbt_ffi_free(ptr)".to_string(), - ) - } else { - ( - "let _ = ptr".into(), - "".into(), - "let _ = (ptr, value)".into(), - "let ptr = 0".into(), - "FixedArray::make(length, Unit::default())".into(), - String::new(), - String::new(), - "let _ = ptr".into(), - ) - }; + let EndpointPayloadFragments { + lift, + lift_result, + lower, + malloc, + lift_list, + commit, + reject, + free_outer, + } = if let Some(result_type) = result_type { + let mut payload_state = AsyncFunctionState::from_sites(payload_sites.clone()); + let (lift, lift_result) = + self.lift_from_memory("ptr", result_type, &helper_package, &mut payload_state); + let mut payload_state = AsyncFunctionState::from_sites(payload_sites.clone()); + let lower = self.lower_to_memory( + "ptr", + "value", + result_type, + &helper_package, + &mut payload_state, + ); + let (commit, _) = self.commit_lists_and_endpoints_with_state( + std::slice::from_ref(result_type), + &[String::from("elem_ptr")], + true, + &helper_package, + payload_sites.clone(), + ); + let reject = self.deallocate_lists_and_own( + std::slice::from_ref(result_type), + &[String::from("elem_ptr")], + true, + &helper_package, + payload_sites.clone(), + ); + let lift_list = self.list_lift_from_memory( + "ptr", + "length", + &format!("wasm{symbol_name}Lift"), + result_type, + ); + let malloc = self.malloc_memory("ptr", "length", result_type); + self.ffi_imports.insert(ffi::FREE); + EndpointPayloadFragments { + lift, + lift_result, + lower, + malloc, + lift_list, + commit, + reject, + free_outer: "mbt_ffi_free(ptr)".to_string(), + } + } else { + EndpointPayloadFragments { + lift: "ignore(ptr)".into(), + lift_result: String::new(), + lower: "ignore((ptr, value))".into(), + malloc: "let ptr = 0".into(), + lift_list: "FixedArray::make(length, Unit::default())".into(), + commit: String::new(), + reject: String::new(), + free_outer: "ignore(ptr)".into(), + } + }; let commit = if commit.trim().is_empty() { - "let _ = (ptr, start, length)".to_string() + "ignore((ptr, start, length))".to_string() } else { format!( r#" - for i = start; i < start + length; i = i + 1 {{ + for i in start..<(start + length) {{ let elem_ptr = ptr + i * {elem_size} {commit} }} @@ -1280,11 +1314,11 @@ impl<'a> InterfaceGenerator<'a> { ) }; let reject = if reject.trim().is_empty() { - "let _ = (ptr, start, length)".to_string() + "ignore((ptr, start, length))".to_string() } else { format!( r#" - for i = start; i < start + length; i = i + 1 {{ + for i in start..<(start + length) {{ let elem_ptr = ptr + i * {elem_size} {reject} }} @@ -1297,7 +1331,7 @@ impl<'a> InterfaceGenerator<'a> { .then(|| { format!( r#" - fn wasm{symbol_name}Lift(ptr: Int) -> {result} {{ + fn wasm{symbol_name}Lift(ptr : Int) -> {result} {{ {lift} {lift_result} }} @@ -1310,7 +1344,7 @@ impl<'a> InterfaceGenerator<'a> { .then(|| { format!( r#" - fn wasm{symbol_name}Lower(value: {result}, ptr: Int) -> Unit {{ + fn wasm{symbol_name}Lower(value : {result}, ptr : Int) -> Unit {{ {lower} }} "# @@ -1322,8 +1356,8 @@ impl<'a> InterfaceGenerator<'a> { format!( r#" fn wasm{symbol_name}ListLift( - ptr: Int, - length: Int, + ptr : Int, + length : Int, ) -> FixedArray[{result}] {{ {lift_list} }} @@ -1382,9 +1416,9 @@ impl<'a> InterfaceGenerator<'a> { format!( r#" fn wasm{symbol_name}Commit( - ptr: Int, - start: Int, - length: Int, + ptr : Int, + start : Int, + length : Int, ) -> Unit {{ {commit} }} @@ -1413,7 +1447,7 @@ impl<'a> InterfaceGenerator<'a> { .then(|| { format!( r#" -fn wasm{symbol_name}FutureRejectPrepared(handle: Int) -> Bool {{ +fn wasm{symbol_name}FutureRejectPrepared(handle : Int) -> Bool {{ guard wasm{symbol_name}FutureProducers.get(handle) is Some(producer) else {{ return false }} @@ -1601,7 +1635,7 @@ async fn Wasm{symbol_name}FutureSource::read( value }} -fn wasm{symbol_name}FutureLift(handle: Int) -> {ffi}Future[{result}] {{ +fn wasm{symbol_name}FutureLift(handle : Int) -> {ffi}Future[{result}] {{ let source = Wasm{symbol_name}FutureSource::{{ handle, closed: false, @@ -1610,7 +1644,7 @@ fn wasm{symbol_name}FutureLift(handle: Int) -> {ffi}Future[{result}] {{ read_buffer: 0, read_discarding: false, read_cleanup_done: false, - read_cleanup: {ffi}CondVar(), + read_cleanup: CondVar(), }} {ffi}Future::from_source( () => source.read(), @@ -1627,7 +1661,7 @@ fn wasm{symbol_name}FutureLift(handle: Int) -> {ffi}Future[{result}] {{ .then(|| { format!( r#" -fn wasm{symbol_name}FutureLowerCommitted(future: {ffi}Future[{result}]) -> Int {{ +fn wasm{symbol_name}FutureLowerCommitted(future : {ffi}Future[{result}]) -> Int {{ let reader = wasm{symbol_name}FutureLower(future) wasm{symbol_name}FutureCommit(reader) reader @@ -1648,7 +1682,7 @@ let wasm{symbol_name}FutureProducers : Map[Int, Wasm{symbol_name}FutureProducer] {reject_prepared_func} -fn wasm{symbol_name}FutureCommit(handle: Int) -> Unit {{ +fn wasm{symbol_name}FutureCommit(handle : Int) -> Unit {{ guard wasm{symbol_name}FutureProducers.get(handle) is Some(producer) else {{ return }} @@ -1686,7 +1720,7 @@ fn wasm{symbol_name}FutureCommit(handle: Int) -> Unit {{ }}) }} -fn wasm{symbol_name}FutureLower(future: {ffi}Future[{result}]) -> Int {{ +fn wasm{symbol_name}FutureLower(future : {ffi}Future[{result}]) -> Int {{ if !{ffi}has_component_task_scope() {{ abort("component future producer requires an async task scope") }} @@ -1722,7 +1756,7 @@ fn wasm{symbol_name}FutureLower(future: {ffi}Future[{result}]) -> Int {{ .then(|| { format!( r#" -fn wasm{symbol_name}StreamRejectPrepared(handle: Int) -> Bool {{ +fn wasm{symbol_name}StreamRejectPrepared(handle : Int) -> Bool {{ guard wasm{symbol_name}StreamProducers.get(handle) is Some(prepared) else {{ return false }} @@ -1917,7 +1951,7 @@ async fn Wasm{symbol_name}StreamSource::read( Some(values) }} -fn wasm{symbol_name}StreamLift(handle: Int) -> {ffi}Stream[{result}] {{ +fn wasm{symbol_name}StreamLift(handle : Int) -> {ffi}Stream[{result}] {{ let source = Wasm{symbol_name}StreamSource::{{ handle, closed: false, @@ -1926,7 +1960,7 @@ fn wasm{symbol_name}StreamLift(handle: Int) -> {ffi}Stream[{result}] {{ read_buffer: 0, read_discarding: false, read_cleanup_done: false, - read_cleanup: {ffi}CondVar(), + read_cleanup: CondVar(), }} {ffi}Stream::from_source( (count) => source.read(count), @@ -1943,7 +1977,7 @@ fn wasm{symbol_name}StreamLift(handle: Int) -> {ffi}Stream[{result}] {{ .then(|| { format!( r#" -fn wasm{symbol_name}StreamLowerCommitted(stream: {ffi}Stream[{result}]) -> Int {{ +fn wasm{symbol_name}StreamLowerCommitted(stream : {ffi}Stream[{result}]) -> Int {{ let reader = wasm{symbol_name}StreamLower(stream) wasm{symbol_name}StreamCommit(reader) reader @@ -1964,7 +1998,7 @@ let wasm{symbol_name}StreamProducers : Map[Int, Wasm{symbol_name}StreamProducer] {reject_prepared_func} -fn wasm{symbol_name}StreamCommit(handle: Int) -> Unit {{ +fn wasm{symbol_name}StreamCommit(handle : Int) -> Unit {{ guard wasm{symbol_name}StreamProducers.get(handle) is Some(prepared) else {{ return }} @@ -2008,11 +2042,11 @@ fn wasm{symbol_name}StreamCommit(handle: Int) -> Unit {{ {staging_window} }} let ptr = wasm{symbol_name}Malloc(data_len) - for i = 0; i < data_len; i = i + 1 {{ + for i in 0.. Unit {{ + let settle_staging = fn(transferred : Int) -> Unit {{ wasm{symbol_name}Commit(ptr, 0, transferred) if transferred < data_len {{ wasm{symbol_name}Reject( @@ -2106,7 +2140,7 @@ fn wasm{symbol_name}StreamCommit(handle: Int) -> Unit {{ }}) }} -fn wasm{symbol_name}StreamLower(stream: {ffi}Stream[{result}]) -> Int {{ +fn wasm{symbol_name}StreamLower(stream : {ffi}Stream[{result}]) -> Int {{ if !{ffi}has_component_task_scope() {{ abort("component stream producer requires an async task scope") }} @@ -2134,7 +2168,7 @@ fn wasm{symbol_name}StreamLower(stream: {ffi}Stream[{result}]) -> Int {{ {drop_readable_intrinsic} {lower_intrinsics} -fn wasm{symbol_name}Malloc(length: Int) -> Int {{ +fn wasm{symbol_name}Malloc(length : Int) -> Int {{ {malloc} ptr }} @@ -2142,9 +2176,9 @@ fn wasm{symbol_name}Malloc(length: Int) -> Int {{ {commit_func} fn wasm{symbol_name}Reject( - ptr: Int, - start: Int, - length: Int, + ptr : Int, + start : Int, + length : Int, ) -> Unit {{ {reject} }} @@ -2252,14 +2286,13 @@ fn wasm{symbol_name}Reject( f.src } - fn malloc_memory(&mut self, address: &str, length: &str, ty: &Type, package: &str) -> String { + fn malloc_memory(&mut self, address: &str, length: &str, ty: &Type) -> String { let size = self.world_gen.sizes.size(ty).size_wasm32(); - let _ = package; self.ffi_imports.insert(ffi::MALLOC); - format!("let {address} = mbt_ffi_malloc({size} * {length});") + format!("let {address} = mbt_ffi_malloc({size} * {length})") } - fn is_list_canonical(&self, _resolve: &Resolve, element: &Type) -> bool { + fn is_list_canonical(&self, element: &Type) -> bool { matches!( element, Type::U8 | Type::U32 | Type::U64 | Type::S32 | Type::S64 | Type::F32 | Type::F64 @@ -2272,10 +2305,8 @@ fn wasm{symbol_name}Reject( length: &str, lift_func: &str, ty: &Type, - package: &str, ) -> String { - let _ = package; - if self.is_list_canonical(self.resolve, ty) { + if self.is_list_canonical(ty) { if ty == &Type::U8 { self.ffi_imports.insert(ffi::PTR2BYTES); return format!("mbt_ffi_ptr2bytes({address}, {length})"); @@ -2299,7 +2330,7 @@ fn wasm{symbol_name}Reject( FixedArray::makei( {length}, (index) => {{ - let ptr = ({address}) + (index * {size}); + let ptr = ({address}) + (index * {size}) {lift_func}(ptr) }} ) @@ -2447,8 +2478,9 @@ impl<'a, 'b> FunctionBindgen<'a, 'b> { }; } - pub(super) fn emit_future_lift( + fn emit_endpoint_lift( &mut self, + kind: PayloadFor, ty: TypeId, operands: &[String], results: &mut Vec, @@ -2457,23 +2489,25 @@ impl<'a, 'b> FunctionBindgen<'a, 'b> { let op = &operands[0]; let site = self .async_state - .next_site(PayloadFor::Future, ty, EndpointOperation::Lift); - let lift_name = format!("wasm{}FutureLift", site.symbol_name); + .next_site(kind, ty, EndpointOperation::Lift); + let type_name = kind.type_name(); + let lift_name = format!("wasm{}{type_name}Lift", site.symbol_name); if self.commit_endpoints { - let commit_name = format!("wasm{}FutureCommit", site.symbol_name); - uwriteln!(self.src, r#"{commit_name}({op});"#); + let commit_name = format!("wasm{}{type_name}Commit", site.symbol_name); + uwriteln!(self.src, r#"{commit_name}({op})"#); results.push("()".into()); return; } - uwriteln!(self.src, r#"let {result} = {lift_name}({op});"#,); + uwriteln!(self.src, r#"let {result} = {lift_name}({op})"#,); results.push(result); } - pub(super) fn emit_future_lower( + fn emit_endpoint_lower( &mut self, + kind: PayloadFor, ty: TypeId, operands: &[String], results: &mut Vec, @@ -2487,19 +2521,32 @@ impl<'a, 'b> FunctionBindgen<'a, 'b> { } else { EndpointOperation::Lower }; - let site = self - .async_state - .next_site(PayloadFor::Future, ty, operation); - let suffix = if committed { - "FutureLowerCommitted" - } else { - "FutureLower" - }; - let lower_name = format!("wasm{}{suffix}", site.symbol_name); + let site = self.async_state.next_site(kind, ty, operation); + let type_name = kind.type_name(); + let suffix = if committed { "LowerCommitted" } else { "Lower" }; + let lower_name = format!("wasm{}{type_name}{suffix}", site.symbol_name); let op = &operands[0]; results.push(format!("{lower_name}({op})")); } + pub(super) fn emit_future_lift( + &mut self, + ty: TypeId, + operands: &[String], + results: &mut Vec, + ) { + self.emit_endpoint_lift(PayloadFor::Future, ty, operands, results); + } + + pub(super) fn emit_future_lower( + &mut self, + ty: TypeId, + operands: &[String], + results: &mut Vec, + ) { + self.emit_endpoint_lower(PayloadFor::Future, ty, operands, results); + } + pub(super) fn capture_task_return(&mut self, params: &[WasmType], operands: &[String]) { let (body, needs_cleanup_list, return_param, return_value) = match &mut self.async_state.task_return { @@ -2541,26 +2588,7 @@ impl<'a, 'b> FunctionBindgen<'a, 'b> { operands: &[String], results: &mut Vec, ) { - let committed = matches!( - self.async_state.task_return, - AsyncTaskReturnState::Generating { .. } - ); - let operation = if committed { - EndpointOperation::LowerCommitted - } else { - EndpointOperation::Lower - }; - let site = self - .async_state - .next_site(PayloadFor::Stream, ty, operation); - let suffix = if committed { - "StreamLowerCommitted" - } else { - "StreamLower" - }; - let lower_name = format!("wasm{}{suffix}", site.symbol_name); - let op = &operands[0]; - results.push(format!("{lower_name}({op})")); + self.emit_endpoint_lower(PayloadFor::Stream, ty, operands, results); } pub(super) fn emit_stream_lift( @@ -2569,23 +2597,7 @@ impl<'a, 'b> FunctionBindgen<'a, 'b> { operands: &[String], results: &mut Vec, ) { - let result = self.locals.tmp("result"); - let op = &operands[0]; - let site = self - .async_state - .next_site(PayloadFor::Stream, ty, EndpointOperation::Lift); - let lift_name = format!("wasm{}StreamLift", site.symbol_name); - - if self.commit_endpoints { - let commit_name = format!("wasm{}StreamCommit", site.symbol_name); - uwriteln!(self.src, r#"{commit_name}({op});"#); - results.push("()".into()); - return; - } - - uwriteln!(self.src, r#"let {result} = {lift_name}({op});"#,); - - results.push(result); + self.emit_endpoint_lift(PayloadFor::Stream, ty, operands, results); } } diff --git a/crates/moonbit/src/lib.rs b/crates/moonbit/src/lib.rs index d4013efb7..bd377de42 100644 --- a/crates/moonbit/src/lib.rs +++ b/crates/moonbit/src/lib.rs @@ -21,7 +21,9 @@ use wit_bindgen_core::{ }; use crate::async_support::{AsyncFunctionState, AsyncSupport}; -use crate::pkg::{Imports, MoonbitSignature, PkgResolver, ToMoonBitIdent, ToMoonBitTypeIdent}; +use crate::pkg::{ + ASYNC_CORE_DIR, Imports, MoonbitSignature, PkgResolver, ToMoonBitIdent, ToMoonBitTypeIdent, +}; mod async_support; mod ffi; @@ -177,6 +179,11 @@ impl MoonBit { moon_pkg.deindent(1); moon_pkg.push_str("\n]"); } + let imports_async_core = + imports.is_some_and(|imports| imports.packages.contains_key(ASYNC_CORE_DIR)); + if imports_async_core || (link && self.async_support.is_required()) { + moon_pkg.push_str(",\n\"supported-targets\": \"+wasm\""); + } // Link target if link { let memory_name = self.pkg_resolver.resolve.wasm_export_name( @@ -3236,6 +3243,8 @@ mod tests { .iter() .all(|(name, _)| !name.starts_with("async-core/async_")) ); + let package = file(&files, "gen/moon.pkg.json"); + assert!(!package.contains("supported-targets"), "{package}"); } #[test] @@ -3307,6 +3316,16 @@ mod tests { let source = file(&files, "interface/a/b/types/top.mbt"); assert!(source.contains("@async-core.Future[UInt]"), "{source}"); + let package = file(&files, "interface/a/b/types/moon.pkg.json"); + assert!( + package.contains("\"supported-targets\": \"+wasm\""), + "{package}" + ); + let link_package = file(&files, "gen/moon.pkg.json"); + assert!( + link_package.contains("\"supported-targets\": \"+wasm\""), + "{link_package}" + ); file(&files, "async-core/moon.pkg.json"); file(&files, "async-core/async_trait.mbt"); } @@ -3644,7 +3663,10 @@ mod tests { let import = file(&imports, "interface/test/moonbit-nested/nested/top.mbt"); assert!(import.contains("if before_started")); assert!(import.contains(".drop_sync()")); - assert!(import.contains("defer mbt_ffi_free(_result_ptr)")); + assert!(import.contains("defer mbt_ffi_free(result_ptr)")); + for misleading_name in ["_lower_ptr", "_lower_arg", "_subtask_code", "_result_ptr"] { + assert!(!import.contains(misleading_name), "{import}"); + } assert_eq!(import.matches("FutureLower(value)").count(), 1, "{import}"); assert_eq!(import.matches("StreamLower(value)").count(), 1, "{import}"); assert!(!import.contains("cleanup_list")); @@ -3733,9 +3755,9 @@ mod tests { "{import}" ); assert!( - import.contains("mbt_ffi_free(mbt_ffi_load32((_result_ptr) + 0))") - && import.contains("mbt_ffi_free(mbt_ffi_load32((_result_ptr) + 8))") - && import.contains("defer mbt_ffi_free(_result_ptr)"), + import.contains("mbt_ffi_free(mbt_ffi_load32((result_ptr) + 0))") + && import.contains("mbt_ffi_free(mbt_ffi_load32((result_ptr) + 8))") + && import.contains("defer mbt_ffi_free(result_ptr)"), "cancelled returned result must free its strings, lists, and result area: {import}" ); } diff --git a/tests/runtime/moonbit/async-import-cancel/runner.mbt b/tests/runtime/moonbit/async-import-cancel/runner.mbt index 03d87d85d..a847b733b 100644 --- a/tests/runtime/moonbit/async-import-cancel/runner.mbt +++ b/tests/runtime/moonbit/async-import-cancel/runner.mbt @@ -1,7 +1,7 @@ //@ wasmtime-flags = '-Wcomponent-model-async' //@ [lang] //@ path = 'gen/world/runner/stub.mbt' -//@ pkg_config = """{ "import": [{ "path": "test/async-import-cancel/async-core", "alias": "async-core" }, { "path": "test/async-import-cancel/interface/test/async-import-cancel/pending", "alias": "pending" }, { "path": "test/async-import-cancel/interface/test/async-import-cancel/gate-control", "alias": "gate-control" }] }""" +//@ pkg_config = """{ "supported-targets": "+wasm", "import": [{ "path": "test/async-import-cancel/async-core", "alias": "async-core" }, { "path": "test/async-import-cancel/interface/test/async-import-cancel/pending", "alias": "pending" }, { "path": "test/async-import-cancel/interface/test/async-import-cancel/gate-control", "alias": "gate-control" }] }""" ///| async fn settle_leaf_counts(expected_drops : UInt) -> Unit { diff --git a/tests/runtime/moonbit/concurrent-export-isolation/runner.mbt b/tests/runtime/moonbit/concurrent-export-isolation/runner.mbt index e6478cb77..9d184a3c2 100644 --- a/tests/runtime/moonbit/concurrent-export-isolation/runner.mbt +++ b/tests/runtime/moonbit/concurrent-export-isolation/runner.mbt @@ -1,7 +1,7 @@ //@ wasmtime-flags = '-Wcomponent-model-async' //@ [lang] //@ path = 'gen/world/runner/stub.mbt' -//@ pkg_config = """{ "warn-list": "-44", "import": [{ "path": "test/moonbit-concurrent-export-isolation/async-core", "alias": "async-core" }, { "path": "test/moonbit-concurrent-export-isolation/interface/test/moonbit-concurrent-export-isolation/operations", "alias": "operations" }] }""" +//@ pkg_config = """{ "warn-list": "-44", "supported-targets": "+wasm", "import": [{ "path": "test/moonbit-concurrent-export-isolation/async-core", "alias": "async-core" }, { "path": "test/moonbit-concurrent-export-isolation/interface/test/moonbit-concurrent-export-isolation/operations", "alias": "operations" }] }""" ///| pub async fn run(background_group : @async-core.TaskGroup[Unit]) -> Unit { diff --git a/tests/runtime/moonbit/http-background-hook/runner.mbt b/tests/runtime/moonbit/http-background-hook/runner.mbt index d01bff8bc..7fe4bf046 100644 --- a/tests/runtime/moonbit/http-background-hook/runner.mbt +++ b/tests/runtime/moonbit/http-background-hook/runner.mbt @@ -1,7 +1,7 @@ //@ wasmtime-flags = '-Wcomponent-model-async -S p3 -S http' //@ [lang] //@ path = 'gen/world/runner/stub.mbt' -//@ pkg_config = """{ "warn-list": "-44", "import": [{ "path": "example/background-hook/async-core", "alias": "async-core" }, { "path": "example/background-hook/interface/wasi/http/handler", "alias": "handler" }, { "path": "example/background-hook/interface/wasi/http/types", "alias": "types" }] }""" +//@ pkg_config = """{ "warn-list": "-44", "supported-targets": "+wasm", "import": [{ "path": "example/background-hook/async-core", "alias": "async-core" }, { "path": "example/background-hook/interface/wasi/http/handler", "alias": "handler" }, { "path": "example/background-hook/interface/wasi/http/types", "alias": "types" }] }""" ///| async fn run_hook(payload : Bytes, expected_error : String?) -> Unit { diff --git a/tests/runtime/moonbit/http-background-hook/test.mbt b/tests/runtime/moonbit/http-background-hook/test.mbt index ed1114014..8e490d36b 100644 --- a/tests/runtime/moonbit/http-background-hook/test.mbt +++ b/tests/runtime/moonbit/http-background-hook/test.mbt @@ -1,7 +1,7 @@ //@ wasmtime-flags = '-Wcomponent-model-async -S p3 -S http' //@ [lang] //@ path = 'gen/interface/wasi/http/handler/stub.mbt' -//@ pkg_config = """{ "import": [{ "path": "moonbitlang/core/buffer", "alias": "buffer" }, { "path": "example/background-hook/async-core", "alias": "async-core" }, { "path": "example/background-hook/interface/wasi/http/types", "alias": "types" }] }""" +//@ pkg_config = """{ "supported-targets": "+wasm", "import": [{ "path": "moonbitlang/core/buffer", "alias": "buffer" }, { "path": "example/background-hook/async-core", "alias": "async-core" }, { "path": "example/background-hook/interface/wasi/http/types", "alias": "types" }] }""" ///| pub async fn handle( diff --git a/tests/runtime/moonbit/local-async-primitives/runner.mbt b/tests/runtime/moonbit/local-async-primitives/runner.mbt index 60adaac55..e45b4e32e 100644 --- a/tests/runtime/moonbit/local-async-primitives/runner.mbt +++ b/tests/runtime/moonbit/local-async-primitives/runner.mbt @@ -1,7 +1,7 @@ //@ wasmtime-flags = '-Wcomponent-model-async' //@ [lang] //@ path = 'gen/world/runner/stub.mbt' -//@ pkg_config = """{ "import": [{ "path": "test/moonbit-local-async-primitives/async-core", "alias": "async-core" }, { "path": "test/moonbit-local-async-primitives/interface/test/moonbit-local-async-primitives/operations", "alias": "operations" }] }""" +//@ pkg_config = """{ "supported-targets": "+wasm", "import": [{ "path": "test/moonbit-local-async-primitives/async-core", "alias": "async-core" }, { "path": "test/moonbit-local-async-primitives/interface/test/moonbit-local-async-primitives/operations", "alias": "operations" }] }""" ///| pub async fn run(_background_group : @async-core.TaskGroup[Unit]) -> Unit { diff --git a/tests/runtime/moonbit/resource-payloads/leaf.mbt b/tests/runtime/moonbit/resource-payloads/leaf.mbt index b7c1c28a1..69ec8cabb 100644 --- a/tests/runtime/moonbit/resource-payloads/leaf.mbt +++ b/tests/runtime/moonbit/resource-payloads/leaf.mbt @@ -1,6 +1,6 @@ //@ [lang] //@ path = 'gen/interface/my/test/leaf-interface/stub.mbt' -//@ pkg_config = """{ "warn-list": "-44", "import": [{ "path": "my/test/async-core", "alias": "async-core" }, { "path": "moonbitlang/core/encoding/utf8", "alias": "utf8" }] }""" +//@ pkg_config = """{ "warn-list": "-44", "supported-targets": "+wasm", "import": [{ "path": "my/test/async-core", "alias": "async-core" }, { "path": "moonbitlang/core/encoding/utf8", "alias": "utf8" }] }""" ///| priv struct BodyState { diff --git a/tests/runtime/moonbit/resource-payloads/runner.mbt b/tests/runtime/moonbit/resource-payloads/runner.mbt index e7770fae1..f951cfd9b 100644 --- a/tests/runtime/moonbit/resource-payloads/runner.mbt +++ b/tests/runtime/moonbit/resource-payloads/runner.mbt @@ -1,7 +1,7 @@ //@ wasmtime-flags = '-Wcomponent-model-async' //@ [lang] //@ path = 'gen/world/runner/stub.mbt' -//@ pkg_config = """{ "warn-list": "-44", "import": [{ "path": "my/test/async-core", "alias": "async-core" }, { "path": "my/test/interface/my/test/leaf-interface", "alias": "leaf-interface" }, { "path": "my/test/interface/my/test/test-interface", "alias": "test-interface" }] }""" +//@ pkg_config = """{ "warn-list": "-44", "supported-targets": "+wasm", "import": [{ "path": "my/test/async-core", "alias": "async-core" }, { "path": "my/test/interface/my/test/leaf-interface", "alias": "leaf-interface" }, { "path": "my/test/interface/my/test/test-interface", "alias": "test-interface" }] }""" ///| async fn read_one_leaf( diff --git a/tests/runtime/moonbit/stream-write-cancel/test.mbt b/tests/runtime/moonbit/stream-write-cancel/test.mbt index 630427372..87b633d87 100644 --- a/tests/runtime/moonbit/stream-write-cancel/test.mbt +++ b/tests/runtime/moonbit/stream-write-cancel/test.mbt @@ -1,6 +1,6 @@ //@ [lang] //@ path = 'gen/interface/test/moonbit-stream-write-cancel/controller/stub.mbt' -//@ pkg_config = """{ "import": [{ "path": "test/moonbit-stream-write-cancel/async-core", "alias": "async-core" }, { "path": "test/moonbit-stream-write-cancel/interface/test/moonbit-stream-write-cancel/holder", "alias": "holder" }] }""" +//@ pkg_config = """{ "supported-targets": "+wasm", "import": [{ "path": "test/moonbit-stream-write-cancel/async-core", "alias": "async-core" }, { "path": "test/moonbit-stream-write-cancel/interface/test/moonbit-stream-write-cancel/holder", "alias": "holder" }] }""" ///| let never : (@async-core.Future[Unit], @async-core.Promise[Unit]) = diff --git a/tests/runtime/moonbit/wasi-cli-p3-stdout/runner.mbt b/tests/runtime/moonbit/wasi-cli-p3-stdout/runner.mbt index 86d82c972..c35e88179 100644 --- a/tests/runtime/moonbit/wasi-cli-p3-stdout/runner.mbt +++ b/tests/runtime/moonbit/wasi-cli-p3-stdout/runner.mbt @@ -1,7 +1,7 @@ //@ wasmtime-flags = '-Wcomponent-model-async -S p3' //@ [lang] //@ path = 'gen/world/runner/stub.mbt' -//@ pkg_config = """{ "warn-list": "-44", "import": [{ "path": "my/test/async-core", "alias": "async-core" }, { "path": "my/test/interface/my/test/i", "alias": "i" }] }""" +//@ pkg_config = """{ "warn-list": "-44", "supported-targets": "+wasm", "import": [{ "path": "my/test/async-core", "alias": "async-core" }, { "path": "my/test/interface/my/test/i", "alias": "i" }] }""" ///| pub async fn run(_background_group : @async-core.TaskGroup[Unit]) -> Unit { diff --git a/tests/runtime/moonbit/wasi-cli-p3-stdout/test.mbt b/tests/runtime/moonbit/wasi-cli-p3-stdout/test.mbt index 753229c10..89a72002d 100644 --- a/tests/runtime/moonbit/wasi-cli-p3-stdout/test.mbt +++ b/tests/runtime/moonbit/wasi-cli-p3-stdout/test.mbt @@ -1,6 +1,7 @@ //@ [lang] //@ path = 'gen/interface/my/test/i/stub.mbt' //@ pkg_config = """{ +//@ "supported-targets": "+wasm", //@ "import": [ //@ { "path": "my/test/async-core", "alias": "async-core" }, //@ { "path": "my/test/interface/wasi/cli/stdout", "alias": "stdout" } From b62422a7b62f9af01bb14c39bf9713dd64396148 Mon Sep 17 00:00:00 2001 From: zihang Date: Tue, 21 Jul 2026 11:58:10 +0800 Subject: [PATCH 19/23] fix(moonbit): correct async future rejection --- crates/moonbit/src/async/promise.mbt | 11 ++++- crates/moonbit/src/async/trait.mbt | 45 ++++++++++++++++--- crates/moonbit/src/async_support.rs | 18 ++++---- crates/moonbit/src/lib.rs | 22 +++++++++ .../moonbit/async-import-cancel/runner.mbt | 23 +++++++--- .../moonbit/async-import-cancel/test.rs | 4 ++ .../moonbit/async-import-cancel/test.wit | 1 + 7 files changed, 101 insertions(+), 23 deletions(-) diff --git a/crates/moonbit/src/async/promise.mbt b/crates/moonbit/src/async/promise.mbt index cce7b5cc2..e1be2004f 100644 --- a/crates/moonbit/src/async/promise.mbt +++ b/crates/moonbit/src/async/promise.mbt @@ -172,7 +172,10 @@ async fn[X] promised_future_get(cell : Ref[PromiseCell[X]]) -> X { } ///| -fn[X] promised_future_drop(cell : Ref[PromiseCell[X]]) -> Unit { +fn[X] promised_future_drop( + cell : Ref[PromiseCell[X]], + fallback_cleanup : ((X) -> Unit)?, +) -> Unit { if cell.val.reading { match cell.val.state { Pending => { @@ -191,7 +194,11 @@ fn[X] promised_future_drop(cell : Ref[PromiseCell[X]]) -> Unit { cell.val.state = Consumed match cell.val.cleanup { Some(cleanup) => cleanup(value) - None => () + None => + match fallback_cleanup { + Some(cleanup) => cleanup(value) + None => () + } } } Failed(_) | Closed => cell.val.state = Consumed diff --git a/crates/moonbit/src/async/trait.mbt b/crates/moonbit/src/async/trait.mbt index 516a3a0e1..e219e6695 100644 --- a/crates/moonbit/src/async/trait.mbt +++ b/crates/moonbit/src/async/trait.mbt @@ -179,7 +179,10 @@ priv struct LocalFutureState[X] { } ///| -fn[X] local_future_close(state : Ref[LocalFutureState[X]]) -> Unit { +fn[X] local_future_close( + state : Ref[LocalFutureState[X]], + fallback_cleanup : ((X) -> Unit)?, +) -> Unit { if state.val.closed { return } @@ -188,7 +191,11 @@ fn[X] local_future_close(state : Ref[LocalFutureState[X]]) -> Unit { state.val.value = None match state.val.cleanup { Some(cleanup) => cleanup(value) - None => () + None => + match fallback_cleanup { + Some(cleanup) => cleanup(value) + None => () + } } } None => () @@ -610,7 +617,7 @@ pub async fn[X] Future::get(self : Future[X]) -> X { pub async fn[X] Future::drop(self : Future[X]) -> Unit noraise { match self.inner { Source(source) => (source.close)() catch { _ => () } - Local(state) => local_future_close(state) + Local(state) => local_future_close(state, None) Pending(state) => { let cleanup = state.val.on_unstarted_drop state.val.producer = None @@ -620,7 +627,7 @@ pub async fn[X] Future::drop(self : Future[X]) -> Unit noraise { None => () } } - Promised(cell) => promised_future_drop(cell) + Promised(cell) => promised_future_drop(cell, None) } } @@ -629,7 +636,33 @@ pub async fn[X] Future::drop(self : Future[X]) -> Unit noraise { pub fn[X] Future::drop_sync(self : Future[X]) -> Unit { match self.inner { Source(source) => (source.close_sync)() - Local(state) => local_future_close(state) + Local(state) => local_future_close(state, None) + Pending(state) => { + let cleanup = state.val.on_unstarted_drop + state.val.producer = None + state.val.on_unstarted_drop = None + match cleanup { + Some(cleanup) => cleanup() + None => () + } + } + Promised(cell) => promised_future_drop(cell, None) + } +} + +///| +/// Rejects a future that was prepared for a component-model transfer but was +/// never transferred. A locally available value uses its registered cleanup, +/// falling back to the generated canonical-ABI cleanup. An unstarted producer +/// is discarded without running it. +#internal(wit_bindgen, "generated binding code only") +pub async fn[X] Future::reject( + self : Future[X], + cleanup : (X) -> Unit, +) -> Unit noraise { + match self.inner { + Source(source) => (source.close)() catch { _ => () } + Local(state) => local_future_close(state, Some(cleanup)) Pending(state) => { let cleanup = state.val.on_unstarted_drop state.val.producer = None @@ -639,7 +672,7 @@ pub fn[X] Future::drop_sync(self : Future[X]) -> Unit { None => () } } - Promised(cell) => promised_future_drop(cell) + Promised(cell) => promised_future_drop(cell, Some(cleanup)) } } diff --git a/crates/moonbit/src/async_support.rs b/crates/moonbit/src/async_support.rs index 52ce72a12..8b3c0420f 100644 --- a/crates/moonbit/src/async_support.rs +++ b/crates/moonbit/src/async_support.rs @@ -1458,9 +1458,14 @@ fn wasm{symbol_name}FutureRejectPrepared(handle : Int) -> Bool {{ defer wasmImport{symbol_name}DropWritable(writer) {ffi}protect_from_cancel( () => {{ - let value = producer.future.get() + producer.future.reject((value : {result}) => {{ + let ptr = wasm{symbol_name}Malloc(1) + wasm{symbol_name}Lower(value, ptr) + wasm{symbol_name}Reject(ptr, 0, 1) + {free_outer} + }}) let ptr = wasm{symbol_name}Malloc(1) - wasm{symbol_name}Lower(value, ptr) + defer {{ {free_outer} }} let terminal : Ref[Bool?] = {{ val: None }} while terminal.val is None {{ terminal.val = {ffi}suspend_for_future_write_terminal( @@ -1470,15 +1475,12 @@ fn wasm{symbol_name}FutureRejectPrepared(handle : Int) -> Bool {{ }} guard terminal.val is Some(transferred) if transferred {{ - wasm{symbol_name}Commit(ptr, 0, 1) - }} else {{ - wasm{symbol_name}Reject(ptr, 0, 1) + abort("rejected component future unexpectedly transferred a value") }} - {free_outer} }}, resume_on_cancel=true, ) catch {{ - _ => abort("rejected component future producer ended without a value") + _ => abort("failed to reject component future producer") }} }}) true @@ -2271,7 +2273,7 @@ fn wasm{symbol_name}Reject( package: &str, async_state: &mut AsyncFunctionState, ) -> String { - let mut f = FunctionBindgen::new(self, Box::new([])) + let mut f = FunctionBindgen::new(self, Box::new([address.to_string(), value.to_string()])) .with_type_context(package) .with_async_state(mem::take(async_state)) .without_block_cleanup(); diff --git a/crates/moonbit/src/lib.rs b/crates/moonbit/src/lib.rs index bd377de42..820274d00 100644 --- a/crates/moonbit/src/lib.rs +++ b/crates/moonbit/src/lib.rs @@ -3299,6 +3299,26 @@ mod tests { assert!(ffi.contains("extern \"wasm\" fn mbt_ffi_malloc"), "{ffi}"); } + #[test] + fn endpoint_payload_lowering_reserves_destination_pointer() { + let files = generate( + r#" + package a:b; + + interface api { + consume: async func(value: future); + } + + world client { import api; } + "#, + "client", + ); + + let ffi = file(&files, "interface/a/b/api/ffi.mbt"); + assert!(ffi.contains("let ptr0 = mbt_ffi_str2ptr(value)"), "{ffi}"); + assert!(ffi.contains("mbt_ffi_store32((ptr) + 0, ptr0)"), "{ffi}"); + } + #[test] fn type_only_endpoints_emit_async_runtime() { let files = generate( @@ -3621,6 +3641,8 @@ mod tests { assert!(!ffi.contains("wait_until")); assert!(ffi.contains("RelayFuture5FutureLowerCommitted")); assert!(ffi.contains("RelayFuture4FutureRejectPrepared")); + assert!(ffi.contains("producer.future.reject((value :")); + assert!(ffi.contains("rejected component future unexpectedly transferred a value")); assert!(ffi.contains("RelayStream3StreamRejectPrepared")); assert!(!ffi.contains("RelayFuture5FutureRejectPrepared")); diff --git a/tests/runtime/moonbit/async-import-cancel/runner.mbt b/tests/runtime/moonbit/async-import-cancel/runner.mbt index a847b733b..86cbc7b3b 100644 --- a/tests/runtime/moonbit/async-import-cancel/runner.mbt +++ b/tests/runtime/moonbit/async-import-cancel/runner.mbt @@ -20,14 +20,20 @@ async fn settle_leaf_counts(expected_drops : UInt) -> Unit { pub async fn run(background_group : @async-core.TaskGroup[Unit]) -> Unit { guard @pending.leaf_live_count() == 0U else { panic() } guard @pending.leaf_drop_count() == 0U else { panic() } + guard @pending.consume_future_string(@async-core.Future::ready("hello")) else { + panic() + } @pending.backpressure_set(true) let leaf = @pending.Leaf::leaf() - let future = @async-core.Future::ready_with_cleanup(leaf, fn( - value : @pending.Leaf, - ) { - value.drop() - }) + let future_producer_ran = Ref(false) + let future = @async-core.Future::from( + async fn() { + future_producer_ran.val = true + leaf + }, + on_unstarted_drop=fn() { leaf.drop() }, + ) let (lowered, lowered_sink) = @async-core.Stream::new(capacity=1) let task = background_group.spawn(async fn() -> Unit { let signal : FixedArray[Unit] = [()] @@ -42,6 +48,7 @@ pub async fn run(background_group : @async-core.TaskGroup[Unit]) -> Unit { task.cancel() @pending.settle() guard !@pending.pending_started() else { panic() } + guard !future_producer_ran.val else { panic() } settle_leaf_counts(1U) @@ -100,9 +107,11 @@ pub async fn run(background_group : @async-core.TaskGroup[Unit]) -> Unit { settle_leaf_counts(5U) let cleanup_leaf = fn(value : @pending.Leaf) { value.drop() } + let (promised_future, promised_writer) = @async-core.Future::new() + guard promised_writer.complete(@pending.Leaf::leaf()) else { panic() } let stream_values : FixedArray[@async-core.Future[@pending.Leaf]] = [ - @async-core.Future::ready_with_cleanup(@pending.Leaf::leaf(), cleanup_leaf), - @async-core.Future::ready_with_cleanup(@pending.Leaf::leaf(), cleanup_leaf), + @async-core.Future::ready(@pending.Leaf::leaf()), + promised_future, @async-core.Future::ready_with_cleanup(@pending.Leaf::leaf(), cleanup_leaf), ] let cleanup_stream = @async-core.Stream::produce(async fn(sink) { diff --git a/tests/runtime/moonbit/async-import-cancel/test.rs b/tests/runtime/moonbit/async-import-cancel/test.rs index a432a3b78..2cb0513c2 100644 --- a/tests/runtime/moonbit/async-import-cancel/test.rs +++ b/tests/runtime/moonbit/async-import-cancel/test.rs @@ -41,6 +41,10 @@ impl Guest for Component { let _ = value.await; } + async fn consume_future_string(value: FutureReader) -> bool { + value.await == "hello" + } + fn pending_started() -> bool { PENDING_STARTED.load(Ordering::SeqCst) } diff --git a/tests/runtime/moonbit/async-import-cancel/test.wit b/tests/runtime/moonbit/async-import-cancel/test.wit index 1589ed5be..a1dd30ada 100644 --- a/tests/runtime/moonbit/async-import-cancel/test.wit +++ b/tests/runtime/moonbit/async-import-cancel/test.wit @@ -29,6 +29,7 @@ interface pending { } pending: async func(value: future); + consume-future-string: async func(value: future) -> bool; pending-started: func() -> bool; pending-stream: async func(value: stream); pending-stream-started: func() -> bool; From 9d5806eede1476712f03124d259f5aca4a8cebf3 Mon Sep 17 00:00:00 2001 From: zihang Date: Tue, 21 Jul 2026 19:27:20 +0800 Subject: [PATCH 20/23] fix(moonbit): harden async endpoint boundaries --- crates/moonbit/docs/async-design.md | 6 ++++ crates/moonbit/src/async_support.rs | 11 +++++-- crates/moonbit/src/lib.rs | 1 + tests/runtime/cancel-import/test.mbt | 10 ++++-- .../future-close-after-coming-back/test.mbt | 6 ---- .../moonbit/nested-future-stream/runner.rs | 6 +++- .../moonbit/nested-future-stream/test.mbt | 5 +-- .../moonbit/resource-payloads/runner.mbt | 24 +++++++++----- .../moonbit/resource-payloads/test.mbt | 5 +-- tests/runtime/simple-stream-payload/test.mbt | 31 ++++++++++--------- 10 files changed, 63 insertions(+), 42 deletions(-) delete mode 100644 tests/runtime/future-close-after-coming-back/test.mbt diff --git a/crates/moonbit/docs/async-design.md b/crates/moonbit/docs/async-design.md index 589a10515..1563f9a7e 100644 --- a/crates/moonbit/docs/async-design.md +++ b/crates/moonbit/docs/async-design.md @@ -163,6 +163,9 @@ background_group : @async-core.TaskGroup[Unit] The generated adapter publishes component task return when the implementation returns its result, then allows this group to finish hook-style work. The work remains structurally owned in MoonBit but cannot alter the published result. +If the implementation raises before publishing task return, the adapter traps: +WIT does not encode arbitrary MoonBit errors, and generic component +`error-context` integration is outside this MVP. ## Boundary Conversion @@ -355,6 +358,9 @@ Known limits: - explicitly captured resources require `on_unstarted_drop` before lazy production; lazy streams of managed values also require per-element cleanup after production starts; +- MoonBit cannot enforce linear ownership, so synchronous drop during an + aliased in-flight read is best effort; the active read retains responsibility + for eventual cleanup; - generated glue and user implementations still share a MoonBit package, so `#internal` is documentation control rather than an access boundary. diff --git a/crates/moonbit/src/async_support.rs b/crates/moonbit/src/async_support.rs index 8b3c0420f..02c25a040 100644 --- a/crates/moonbit/src/async_support.rs +++ b/crates/moonbit/src/async_support.rs @@ -1234,6 +1234,7 @@ impl<'a> InterfaceGenerator<'a> { .unwrap_or(0); let read_chunk_owns_buffer = result_type.is_some_and(|ty| self.is_list_canonical(ty)); let staging_window = if payload_sites.is_empty() { 64 } else { 1 }; + let max_read_count = 64; let EndpointPayloadFragments { lift, @@ -1898,7 +1899,13 @@ async fn Wasm{symbol_name}StreamSource::read( if self.reading {{ raise {ffi}EndpointBusy::Read }} - let ptr = wasm{symbol_name}Malloc(count) + // `Stream.read` promises at most `count`; bound one canonical allocation. + let read_count = if count < {max_read_count} {{ + count + }} else {{ + {max_read_count} + }} + let ptr = wasm{symbol_name}Malloc(read_count) let mut owns_buffer = false defer {{ if !owns_buffer {{ @@ -1912,7 +1919,7 @@ async fn Wasm{symbol_name}StreamSource::read( self.read_cleanup_done = false let (progress, end) = {ffi}suspend_for_stream_read( self.handle, - wasmImport{symbol_name}Read(self.handle, ptr, count), + wasmImport{symbol_name}Read(self.handle, ptr, read_count), ) catch {{ err => {{ if self.read_discarding {{ diff --git a/crates/moonbit/src/lib.rs b/crates/moonbit/src/lib.rs index 820274d00..802c804c2 100644 --- a/crates/moonbit/src/lib.rs +++ b/crates/moonbit/src/lib.rs @@ -3632,6 +3632,7 @@ mod tests { ); assert!(!ffi.contains("defer close_writer()")); assert!(ffi.contains("read_cleanup : @async-core.CondVar")); + assert!(ffi.contains("let read_count = if count < 64")); assert!(ffi.contains("self.read_cleanup.broadcast()")); assert!(ffi.contains("@async-core.cancel_future_read(")); assert!(ffi.contains("@async-core.cancel_stream_read(")); diff --git a/tests/runtime/cancel-import/test.mbt b/tests/runtime/cancel-import/test.mbt index e24774938..cf48531d2 100644 --- a/tests/runtime/cancel-import/test.mbt +++ b/tests/runtime/cancel-import/test.mbt @@ -1,6 +1,12 @@ //@ [lang] //@ path = 'gen/interface/my/test/i/stub.mbt' +///| +fn wasmBackpressureInc() = "$root" "[backpressure-inc]" + +///| +fn wasmBackpressureDec() = "$root" "[backpressure-dec]" + ///| pub async fn pending_import( x : @async-core.Future[Unit], @@ -12,8 +18,8 @@ pub async fn pending_import( ///| pub fn backpressure_set(x : Bool) -> Unit { if x { - @async-core.backpressure_inc() + wasmBackpressureInc() } else { - @async-core.backpressure_dec() + wasmBackpressureDec() } } diff --git a/tests/runtime/future-close-after-coming-back/test.mbt b/tests/runtime/future-close-after-coming-back/test.mbt deleted file mode 100644 index b6fc91dcf..000000000 --- a/tests/runtime/future-close-after-coming-back/test.mbt +++ /dev/null @@ -1,6 +0,0 @@ -//@ [lang] -//@ path = 'gen/interface/a/b/the-test/stub.mbt' - -pub fn f(_param : @async-core.Future[Unit]) -> @async-core.Future[Unit] { - _param -} diff --git a/tests/runtime/moonbit/nested-future-stream/runner.rs b/tests/runtime/moonbit/nested-future-stream/runner.rs index 52842639d..7087fc699 100644 --- a/tests/runtime/moonbit/nested-future-stream/runner.rs +++ b/tests/runtime/moonbit/nested-future-stream/runner.rs @@ -129,7 +129,11 @@ async fn rejects_nested_endpoints() { drop(output); outer_writer.write(inner_reader).await.unwrap(); - inner_writer.write(stream_reader).await.unwrap(); + if let Err(error) = inner_writer.write(stream_reader).await { + // A relay may stop at this rejected nested endpoint instead of + // consuming the returned stream. Release the returned reader here. + drop(error.value); + } let (result, remaining) = stream_writer.write(vec![9]).await; match result { StreamResult::Dropped => assert_eq!(remaining.remaining(), 1), diff --git a/tests/runtime/moonbit/nested-future-stream/test.mbt b/tests/runtime/moonbit/nested-future-stream/test.mbt index 6dde2fd03..cff97da26 100644 --- a/tests/runtime/moonbit/nested-future-stream/test.mbt +++ b/tests/runtime/moonbit/nested-future-stream/test.mbt @@ -18,10 +18,7 @@ pub async fn relay( value : @async-core.Future[@async-core.Future[@async-core.Stream[Byte]]], _background_group : @async-core.TaskGroup[Unit], ) -> @async-core.Future[@async-core.Future[@async-core.Stream[Byte]]] { - @async-core.Future::from(async fn() { - let inner = value.get() - @async-core.Future::from(async fn() { inner.get() }) - }) + value } ///| diff --git a/tests/runtime/moonbit/resource-payloads/runner.mbt b/tests/runtime/moonbit/resource-payloads/runner.mbt index f951cfd9b..6e39b9aca 100644 --- a/tests/runtime/moonbit/resource-payloads/runner.mbt +++ b/tests/runtime/moonbit/resource-payloads/runner.mbt @@ -57,14 +57,22 @@ async fn assert_no_live_resources() -> Unit { fn nested_leaf_stream( prefix : String, ) -> @async-core.Stream[@leaf-interface.LeafThing] { - @async-core.Stream::produce(async fn(sink) { - let values : FixedArray[@leaf-interface.LeafThing] = [ - @leaf-interface.LeafThing::leaf_thing(prefix + "-a"), - @leaf-interface.LeafThing::leaf_thing(prefix + "-b"), - ] - let _ = sink.write_all(values[:]) - sink.close() - }) + let values : FixedArray[@leaf-interface.LeafThing] = [ + @leaf-interface.LeafThing::leaf_thing(prefix + "-a"), + @leaf-interface.LeafThing::leaf_thing(prefix + "-b"), + ] + @async-core.Stream::produce( + async fn(sink) { + let _ = sink.write_all(values[:]) + sink.close() + }, + cleanup=fn(value) { value.drop() }, + on_unstarted_drop=fn() { + for value in values { + value.drop() + } + }, + ) } ///| diff --git a/tests/runtime/moonbit/resource-payloads/test.mbt b/tests/runtime/moonbit/resource-payloads/test.mbt index 477223aae..49330d123 100644 --- a/tests/runtime/moonbit/resource-payloads/test.mbt +++ b/tests/runtime/moonbit/resource-payloads/test.mbt @@ -467,10 +467,7 @@ pub async fn relay_nested_leaf( ) -> @async-core.Future[ @async-core.Future[@async-core.Stream[@leaf-interface.LeafThing]], ] { - @async-core.Future::from(async fn() { - let inner = value.get() - @async-core.Future::from(async fn() { inner.get() }) - }) + value } ///| diff --git a/tests/runtime/simple-stream-payload/test.mbt b/tests/runtime/simple-stream-payload/test.mbt index 48a18ef5d..6fd4086c1 100644 --- a/tests/runtime/simple-stream-payload/test.mbt +++ b/tests/runtime/simple-stream-payload/test.mbt @@ -1,26 +1,27 @@ //@ [lang] //@ path = 'gen/interface/my/test/i/stub.mbt' +///| pub async fn read_stream( x : @async-core.Stream[Byte], _background_group : @async-core.TaskGroup[Unit], -) -> Unit raise { - let chunk0 = x.read(1).unwrap() - assert_eq(chunk0.length(), 1) - assert_eq(chunk0[0], b'\x00') +) -> Unit { + let chunk0 = x.read(1).unwrap() + assert_eq(chunk0.length(), 1) + assert_eq(chunk0[0], b'\x00') - let chunk1 = x.read(2).unwrap() - assert_eq(chunk1.length(), 2) - assert_eq(chunk1[0], b'\x01') - assert_eq(chunk1[1], b'\x02') + let chunk1 = x.read(2).unwrap() + assert_eq(chunk1.length(), 2) + assert_eq(chunk1[0], b'\x01') + assert_eq(chunk1[1], b'\x02') - let chunk2 = x.read(1).unwrap() - assert_eq(chunk2.length(), 1) - assert_eq(chunk2[0], b'\x03') + let chunk2 = x.read(1).unwrap() + assert_eq(chunk2.length(), 1) + assert_eq(chunk2[0], b'\x03') - let chunk3 = x.read(1).unwrap() - assert_eq(chunk3.length(), 1) - assert_eq(chunk3[0], b'\x04') + let chunk3 = x.read(0x7fffffff).unwrap() + assert_eq(chunk3.length(), 1) + assert_eq(chunk3[0], b'\x04') - x.drop() + x.drop() } From 16a60a99a17d9228e31dcd867efeab6b477f5418 Mon Sep 17 00:00:00 2001 From: Daniel Vigovszky Date: Mon, 22 Jun 2026 19:02:52 +0200 Subject: [PATCH 21/23] Outline lift helpers --- crates/c/src/lib.rs | 4 + crates/core/src/abi.rs | 78 ++++++++++++++- crates/cpp/src/lib.rs | 4 + crates/csharp/src/function.rs | 4 + crates/go/src/lib.rs | 4 + crates/moonbit/src/lib.rs | 5 + crates/rust/src/bindgen.rs | 32 +++++++ crates/rust/src/interface.rs | 173 +++++++++++++++++++++++++++++++++- crates/rust/src/lib.rs | 2 + 9 files changed, 303 insertions(+), 3 deletions(-) diff --git a/crates/c/src/lib.rs b/crates/c/src/lib.rs index 91d30b14e..148c23b8c 100644 --- a/crates/c/src/lib.rs +++ b/crates/c/src/lib.rs @@ -3921,6 +3921,10 @@ impl Bindgen for FunctionBindgen<'_, '_> { self.load("uint8_t *", *offset, operands, results) } Instruction::LengthLoad { offset } => self.load("size_t", *offset, operands, results), + Instruction::LiftNamedFromMemory { .. } => unreachable!( + "LiftNamedFromMemory is only emitted by generators that implement \ + Bindgen::lift_helper_name, which this generator does not" + ), Instruction::I32Store { offset } => self.store("int32_t", *offset, operands), Instruction::I64Store { offset } => self.store("int64_t", *offset, operands), Instruction::F32Store { offset } => self.store("float", *offset, operands), diff --git a/crates/core/src/abi.rs b/crates/core/src/abi.rs index 73238fc93..b4e50b487 100644 --- a/crates/core/src/abi.rs +++ b/crates/core/src/abi.rs @@ -122,6 +122,18 @@ def_instruction! { /// Like `I32Load` or `I64Load`, but for loading array length values. LengthLoad { offset: ArchitectureSize } : [1] => [1], + /// Pops a base pointer from the stack, calls a pre-generated, shared + /// per-type lift helper function to read and lift the named aggregate + /// type `ty` stored at the constant `offset` from that pointer, and + /// pushes the lifted value. + /// + /// This is used to "outline" the canonical-ABI lift of large named + /// record/variant types into shared helper functions instead of + /// inlining the full recursive lift at every use site. Keeping each + /// generated function small avoids the super-linear native-compile cost + /// of a single huge function. + LiftNamedFromMemory { ty: TypeId, offset: ArchitectureSize } : [1] => [1], + /// Pops a pointer from the stack and then an `i32` value. /// Stores the value in little-endian at the pointer specified plus the /// constant `offset`. @@ -791,6 +803,19 @@ pub trait Bindgen { /// "canonical" form for lists. This dictates whether the `ListCanonLower` /// and `ListCanonLift` instructions are used or not. fn is_list_canonical(&self, resolve: &Resolve, element: &Type) -> bool; + + /// Returns the name of a pre-generated, shared lift helper function for the + /// named aggregate type `id`, if one exists. + /// + /// When this returns `Some`, the canonical-ABI lift of that type (in + /// `read_from_memory`) is "outlined" into a call to the named helper + /// (emitted as `Instruction::LiftNamedFromMemory`) instead of being inlined + /// recursively. Generators that do not implement helper outlining should + /// return `None` (the default). + fn lift_helper_name(&self, resolve: &Resolve, id: TypeId) -> Option { + let _ = (resolve, id); + None + } } /// Generates an abstract sequence of instructions which represents this @@ -860,6 +885,35 @@ pub fn lift_from_memory( generator.stack.pop().unwrap() } +/// Like [`lift_from_memory`], but used to generate the *body* of an outlined +/// lift helper for the named type `skip_outline_root`. +/// +/// The root type itself is lifted inline (one level deep) while nested named +/// aggregate types are outlined into calls to their own shared helpers (see +/// [`Bindgen::lift_helper_name`] and [`Instruction::LiftNamedFromMemory`]). +/// +/// `skip_outline_root` must be the id of the named aggregate (record/variant) +/// being lifted, and `ty` must be exactly `Type::Id(skip_outline_root)` (not an +/// alias to it) so that the single-shot inline of the root in `read_from_memory` +/// lands on the intended type. +pub fn lift_from_memory_root( + resolve: &Resolve, + bindgen: &mut B, + address: B::Operand, + ty: &Type, + skip_outline_root: TypeId, +) -> B::Operand { + assert!( + matches!(ty, Type::Id(id) if *id == skip_outline_root), + "lift_from_memory_root requires `ty` to be `Type::Id(skip_outline_root)`, \ + not an alias or other type" + ); + let mut generator = Generator::new(resolve, bindgen); + generator.lift_outline_root = Some(skip_outline_root); + generator.read_from_memory(ty, address, Default::default()); + generator.stack.pop().unwrap() +} + /// Used in a similar manner as the `Interface::call` function except is /// used to generate the `post-return` callback for `func`. /// @@ -1001,6 +1055,11 @@ struct Generator<'a, B: Bindgen> { stack: Vec, return_pointer: Option, realloc: Option, + /// When generating the body of an outlined lift helper for a named type, + /// this holds that type's id so its *own* top-level lift is inlined (one + /// level) while nested named aggregates are still outlined into their own + /// helpers. `None` everywhere else (so all eligible named types outline). + lift_outline_root: Option, } const MAX_FLAT_PARAMS: usize = 16; @@ -1016,6 +1075,7 @@ impl<'a, B: Bindgen> Generator<'a, B> { stack: Vec::new(), return_pointer: None, realloc: None, + lift_outline_root: None, } } @@ -2177,7 +2237,20 @@ impl<'a, B: Bindgen> Generator<'a, B> { Type::String => self.read_list_from_memory(ty, addr, offset), Type::ErrorContext => self.emit_and_lift(ty, addr, &I32Load { offset }), - Type::Id(id) => match &self.resolve.types[id].kind { + Type::Id(id) => { + // Outline the lift of large named aggregate types into shared + // helper functions instead of inlining the full recursive lift + // here. `lift_outline_root` is `Some(id)` only while generating + // that type's own helper body, in which case its top level is + // inlined (one level) and nested types still outline. + let is_outline_root = self.lift_outline_root == Some(id); + self.lift_outline_root = None; + if !is_outline_root && self.bindgen.lift_helper_name(self.resolve, id).is_some() { + self.stack.push(addr); + self.emit(&Instruction::LiftNamedFromMemory { ty: id, offset }); + return; + } + match &self.resolve.types[id].kind { TypeDefKind::Type(t) => self.read_from_memory(t, addr, offset), TypeDefKind::List(_) => self.read_list_from_memory(ty, addr, offset), @@ -2284,7 +2357,8 @@ impl<'a, B: Bindgen> Generator<'a, B> { id, }); } - }, + } + } } } diff --git a/crates/cpp/src/lib.rs b/crates/cpp/src/lib.rs index 2d7b30099..60a90290e 100644 --- a/crates/cpp/src/lib.rs +++ b/crates/cpp/src/lib.rs @@ -3571,6 +3571,10 @@ impl<'a, 'b> Bindgen for FunctionBindgen<'a, 'b> { abi::Instruction::LengthLoad { offset } => { self.load("size_t", *offset, operands, results) } + abi::Instruction::LiftNamedFromMemory { .. } => unreachable!( + "LiftNamedFromMemory is only emitted by generators that implement \ + Bindgen::lift_helper_name, which this generator does not" + ), abi::Instruction::PointerStore { offset } => { let ptr_type = self.r#gen.r#gen.opts.ptr_type(); self.store(ptr_type, *offset, operands) diff --git a/crates/csharp/src/function.rs b/crates/csharp/src/function.rs index 9de5852cf..2e3d82cdb 100644 --- a/crates/csharp/src/function.rs +++ b/crates/csharp/src/function.rs @@ -464,6 +464,10 @@ impl Bindgen for FunctionBindgen<'_, '_> { offset = offset.size_wasm32() )) } + Instruction::LiftNamedFromMemory { .. } => unreachable!( + "LiftNamedFromMemory is only emitted by generators that implement \ + Bindgen::lift_helper_name, which this generator does not" + ), Instruction::PointerLoad { offset } => results.push(format!( "new global::System.Span((void*)((byte*){} + {offset}), 1)[0]", operands[0], diff --git a/crates/go/src/lib.rs b/crates/go/src/lib.rs index faa5e5b8d..556112763 100644 --- a/crates/go/src/lib.rs +++ b/crates/go/src/lib.rs @@ -1995,6 +1995,10 @@ return {results}" Instruction::LengthLoad { offset } => { load(self, results, &operands[0], offset, "uint32", &|v| v) } + Instruction::LiftNamedFromMemory { .. } => unreachable!( + "LiftNamedFromMemory is only emitted by generators that implement \ + Bindgen::lift_helper_name, which this generator does not" + ), Instruction::PointerLoad { offset } => { load(self, results, &operands[0], offset, "uint32", &|v| { format!("uintptr({v})") diff --git a/crates/moonbit/src/lib.rs b/crates/moonbit/src/lib.rs index 802c804c2..adf8edcec 100644 --- a/crates/moonbit/src/lib.rs +++ b/crates/moonbit/src/lib.rs @@ -2376,6 +2376,11 @@ impl Bindgen for FunctionBindgen<'_, '_> { )) } + Instruction::LiftNamedFromMemory { .. } => unreachable!( + "LiftNamedFromMemory is only emitted by generators that implement \ + Bindgen::lift_helper_name, which this generator does not" + ), + Instruction::I32Load8U { offset } => { self.use_ffi(ffi::LOAD8_U); results.push(format!( diff --git a/crates/rust/src/bindgen.rs b/crates/rust/src/bindgen.rs index 767beb6a9..11460c424 100644 --- a/crates/rust/src/bindgen.rs +++ b/crates/rust/src/bindgen.rs @@ -22,6 +22,16 @@ pub(super) struct FunctionBindgen<'a, 'b> { pub handle_decls: Vec, always_owned: bool, return_self: bool, + /// Whether outlined lift helpers may be used for this snippet. + /// + /// Lift helpers are emitted as free functions in the interface module, and + /// outlined calls to them are unqualified, so they only resolve for code + /// emitted directly in that same module (import wrappers and the helper + /// bodies themselves). Code emitted into a nested submodule — notably the + /// `future`/`stream` payload vtables, which live in a separate module tree — + /// must not outline; it lifts inline instead. Such payload `lift` functions + /// are already isolated single-value lifts, so this loses no benefit. + pub(super) outline_lifts: bool, } pub const POINTER_SIZE_EXPRESSION: &str = "::core::mem::size_of::<*const u8>()"; @@ -48,6 +58,7 @@ impl<'a, 'b> FunctionBindgen<'a, 'b> { handle_decls: Vec::new(), always_owned, return_self, + outline_lifts: true, } } @@ -267,6 +278,13 @@ impl Bindgen for FunctionBindgen<'_, '_> { self.r#gen.is_list_canonical(ty) } + fn lift_helper_name(&self, _resolve: &Resolve, id: TypeId) -> Option { + if !self.outline_lifts { + return None; + } + self.r#gen.lift_helpers.get(&id).cloned() + } + fn emit( &mut self, resolve: &Resolve, @@ -1176,6 +1194,20 @@ impl Bindgen for FunctionBindgen<'_, '_> { results.push(format!("l{tmp}")); } + Instruction::LiftNamedFromMemory { ty, offset } => { + let name = self + .lift_helper_name(resolve, *ty) + .expect("lift helper must be registered before it is emitted"); + let tmp = self.tmp(); + uwriteln!( + self.src, + "let result{tmp} = {name}({base}.add({offset}));", + base = operands[0], + offset = offset.format_term(POINTER_SIZE_EXPRESSION, true), + ); + results.push(format!("result{tmp}")); + } + Instruction::I32Store { offset } => { self.push_str(&format!( "*{}.add({}).cast::() = {};\n", diff --git a/crates/rust/src/interface.rs b/crates/rust/src/interface.rs index 148249be0..77ad1a78b 100644 --- a/crates/rust/src/interface.rs +++ b/crates/rust/src/interface.rs @@ -26,6 +26,14 @@ pub struct InterfaceGenerator<'a> { pub return_pointer_area_align: Alignment, pub(super) needs_runtime_module: bool, pub(super) needs_wit_map: bool, + /// Map of named aggregate type -> name of a shared, outlined lift helper + /// function generated for that type. Populated for guest imports so the + /// canonical-ABI lift of large types is shared across all wrappers in this + /// interface instead of being inlined into each one. + pub(super) lift_helpers: BTreeMap, + /// Generated source for the bodies of the helpers in `lift_helpers`, + /// flushed into `src` once at the end of import generation. + pub(super) lift_helper_bodies: Source, } /// A description of the "mode" in which a type is printed. @@ -406,11 +414,150 @@ macro_rules! {macro_name} {{ funcs: impl Iterator, interface: Option<&WorldKey>, ) { + let funcs: Vec<&Function> = funcs.collect(); + + // Register shared lift helpers for the result types of all imported + // functions (transitively over all named aggregate types), then emit + // their bodies once. Wrappers below then lift via calls to these shared + // helpers instead of inlining the full recursive lift each time. + // + // Only register a helper when the import actually lifts its result from + // memory; otherwise the generated helper would be unused: + // * skipped functions emit no wrapper at all, and + // * a synchronous result that fits in flat returns is lifted directly + // (`abi::call` uses `lift`, not `read_from_memory`). + // Async imports always deliver their result via memory. Under-registering + // is always safe: `read_from_memory` simply inlines the lift when no + // helper exists for a type. + for func in &funcs { + if self.r#gen.skip.contains(&func.name) { + continue; + } + let Some(result) = func.result.as_ref() else { + continue; + }; + let async_ = self.r#gen.is_async(self.resolve, interface, func, true); + let memory_lifted = async_ + || self + .resolve + .wasm_signature(AbiVariant::GuestImport, func) + .retptr; + if memory_lifted { + self.register_lift_helpers(result); + } + } + self.generate_lift_helper_bodies(); + let bodies = mem::take(&mut self.lift_helper_bodies); + self.src.push_str(&String::from(bodies)); + for func in funcs { self.generate_guest_import(func, interface); } } + /// Recursively registers a shared lift helper for `ty` and every named + /// aggregate type (record/variant) reachable from it. Only registers names + /// here; bodies are generated later by `generate_lift_helper_bodies`. + fn register_lift_helpers(&mut self, ty: &Type) { + let Type::Id(id) = ty else { return }; + let id = *id; + match &self.resolve.types[id].kind { + TypeDefKind::Type(t) => { + let t = *t; + self.register_lift_helpers(&t); + } + TypeDefKind::Record(_) | TypeDefKind::Variant(_) => { + if self.lift_helpers.contains_key(&id) { + return; + } + let name = format!("__wit_bindgen_lift_t{}", id.index()); + self.lift_helpers.insert(id, name); + // Collect child types first to avoid borrow conflicts. + let children: Vec = match &self.resolve.types[id].kind { + TypeDefKind::Record(r) => r.fields.iter().map(|f| f.ty).collect(), + TypeDefKind::Variant(v) => v.cases.iter().filter_map(|c| c.ty).collect(), + _ => Vec::new(), + }; + for child in children { + self.register_lift_helpers(&child); + } + } + TypeDefKind::List(t) | TypeDefKind::Option(t) | TypeDefKind::FixedLengthList(t, _) => { + let t = *t; + self.register_lift_helpers(&t); + } + TypeDefKind::Tuple(tuple) => { + let tys: Vec = tuple.types.clone(); + for t in tys { + self.register_lift_helpers(&t); + } + } + TypeDefKind::Result(r) => { + let ok = r.ok; + let err = r.err; + if let Some(t) = ok { + self.register_lift_helpers(&t); + } + if let Some(t) = err { + self.register_lift_helpers(&t); + } + } + TypeDefKind::Map(k, v) => { + let k = *k; + let v = *v; + self.register_lift_helpers(&k); + self.register_lift_helpers(&v); + } + _ => {} + } + } + + /// Generates the body of every registered lift helper into + /// `lift_helper_bodies`. + fn generate_lift_helper_bodies(&mut self) { + let ids: Vec = self.lift_helpers.keys().copied().collect(); + let resolve = self.resolve; + let module = self.wasm_import_module; + for id in ids { + let name = self.lift_helpers[&id].clone(); + let ret_ty = self.type_path(id, true); + let ty = Type::Id(id); + + let mut f = FunctionBindgen::new(self, Vec::new(), module, true, false); + let expr = abi::lift_from_memory_root(resolve, &mut f, "ptr".to_string(), &ty, id); + let body = String::from(mem::take(&mut f.src)); + + // A pure memory-lift of a record/variant must not require any of the + // side state that `generate_guest_import_body_sync` would otherwise + // flush into the surrounding function. These are dropped here, so + // assert the assumption holds rather than silently emitting broken + // code if a future lift path starts depending on them. + assert!( + !f.needs_cleanup_list, + "outlined lift helper unexpectedly requires a cleanup list" + ); + assert!( + f.import_return_pointer_area_size.is_empty(), + "outlined lift helper unexpectedly requires a return pointer area" + ); + assert!( + f.handle_decls.is_empty(), + "outlined lift helper unexpectedly produced handle declarations" + ); + + uwriteln!( + self.lift_helper_bodies, + "#[allow(dead_code, unused_unsafe, clippy::all)]\n\ + unsafe fn {name}(ptr: *mut u8) -> {ret_ty} {{\n\ + unsafe {{\n\ + {body}\n\ + {expr}\n\ + }}\n\ + }}" + ); + } + } + pub fn align_area(&mut self, alignment: Alignment) { match alignment { Alignment::Pointer => uwriteln!( @@ -652,7 +799,12 @@ macro_rules! {macro_name} {{ let lower; let dealloc_lists; if let Some(payload_type) = payload_type { - lift = self.lift_from_memory("ptr", &payload_type, &module); + // This `lift` snippet is emitted inside a nested `pub mod vtable{N}` + // that lives in a separate module tree from the interface's lift + // helpers, so outlining (which emits unqualified helper calls) would + // not resolve. Lift inline instead; this payload `lift` is already an + // isolated single-value function. + lift = self.lift_from_memory_no_outline("ptr", &payload_type, &module); dealloc_lists = self.deallocate_lists( std::slice::from_ref(payload_type), &["ptr".to_string()], @@ -848,7 +1000,26 @@ pub mod vtable{ordinal} {{ } fn lift_from_memory(&mut self, address: &str, ty: &Type, module: &str) -> String { + self.lift_from_memory_inner(address, ty, module, true) + } + + /// Like [`Self::lift_from_memory`], but never outlines into shared lift + /// helpers. Used for snippets emitted into a nested submodule (e.g. a + /// `future`/`stream` payload vtable), where the unqualified helper calls + /// would not resolve because the helpers live in a different module. + fn lift_from_memory_no_outline(&mut self, address: &str, ty: &Type, module: &str) -> String { + self.lift_from_memory_inner(address, ty, module, false) + } + + fn lift_from_memory_inner( + &mut self, + address: &str, + ty: &Type, + module: &str, + outline_lifts: bool, + ) -> String { let mut f = FunctionBindgen::new(self, Vec::new(), module, true, false); + f.outline_lifts = outline_lifts; let result = abi::lift_from_memory(f.r#gen.resolve, &mut f, address.into(), ty); format!("unsafe {{ {}\n{result} }}", String::from(f.src)) } diff --git a/crates/rust/src/lib.rs b/crates/rust/src/lib.rs index e7f4a36ed..09e1a243e 100644 --- a/crates/rust/src/lib.rs +++ b/crates/rust/src/lib.rs @@ -380,6 +380,8 @@ impl RustWasm { return_pointer_area_align: Default::default(), needs_runtime_module: false, needs_wit_map: false, + lift_helpers: Default::default(), + lift_helper_bodies: Default::default(), } } From 0f378ea9ed5ce5da3658fe2601984d4b4ca06596 Mon Sep 17 00:00:00 2001 From: Daniel Vigovszky Date: Thu, 25 Jun 2026 16:55:24 +0200 Subject: [PATCH 22/23] LowerNamedToMemory for MoonBit --- crates/c/src/lib.rs | 4 + crates/core/src/abi.rs | 80 +++++++++++++++- crates/cpp/src/lib.rs | 4 + crates/csharp/src/function.rs | 4 + crates/go/src/lib.rs | 4 + crates/moonbit/src/lib.rs | 170 +++++++++++++++++++++++++++++++++- crates/rust/src/bindgen.rs | 5 + 7 files changed, 269 insertions(+), 2 deletions(-) diff --git a/crates/c/src/lib.rs b/crates/c/src/lib.rs index 148c23b8c..92f4c04c9 100644 --- a/crates/c/src/lib.rs +++ b/crates/c/src/lib.rs @@ -3925,6 +3925,10 @@ impl Bindgen for FunctionBindgen<'_, '_> { "LiftNamedFromMemory is only emitted by generators that implement \ Bindgen::lift_helper_name, which this generator does not" ), + Instruction::LowerNamedToMemory { .. } => unreachable!( + "LowerNamedToMemory is only emitted by generators that implement \ + Bindgen::lower_helper_name, which this generator does not" + ), Instruction::I32Store { offset } => self.store("int32_t", *offset, operands), Instruction::I64Store { offset } => self.store("int64_t", *offset, operands), Instruction::F32Store { offset } => self.store("float", *offset, operands), diff --git a/crates/core/src/abi.rs b/crates/core/src/abi.rs index b4e50b487..08a1bfa06 100644 --- a/crates/core/src/abi.rs +++ b/crates/core/src/abi.rs @@ -134,6 +134,18 @@ def_instruction! { /// of a single huge function. LiftNamedFromMemory { ty: TypeId, offset: ArchitectureSize } : [1] => [1], + /// Pops a value to lower and then a base pointer from the stack, calls a + /// pre-generated, shared per-type lower helper function to write the + /// named aggregate type `ty` at the constant `offset` from that + /// pointer, producing no result. + /// + /// This is the lower-side counterpart of `LiftNamedFromMemory`: it + /// outlines the canonical-ABI "write to memory" of large named + /// record/variant types into shared helper functions instead of + /// inlining the full recursive lower at every use site. The value + /// operand is `operands[0]` and the base pointer is `operands[1]`. + LowerNamedToMemory { ty: TypeId, offset: ArchitectureSize } : [2] => [0], + /// Pops a pointer from the stack and then an `i32` value. /// Stores the value in little-endian at the pointer specified plus the /// constant `offset`. @@ -816,6 +828,19 @@ pub trait Bindgen { let _ = (resolve, id); None } + + /// Returns the name of a pre-generated, shared lower helper function for + /// the named aggregate type `id`, if one exists. + /// + /// When this returns `Some`, the canonical-ABI lower of that type (in + /// `write_to_memory`) is "outlined" into a call to the named helper + /// (emitted as `Instruction::LowerNamedToMemory`) instead of being inlined + /// recursively. Generators that do not implement helper outlining should + /// return `None` (the default). + fn lower_helper_name(&self, resolve: &Resolve, id: TypeId) -> Option { + let _ = (resolve, id); + None + } } /// Generates an abstract sequence of instructions which represents this @@ -914,6 +939,38 @@ pub fn lift_from_memory_root( generator.stack.pop().unwrap() } +/// Like [`lower_to_memory`], but used to generate the *body* of an outlined +/// lower helper for the named type `skip_outline_root`. +/// +/// The root type itself is lowered inline (one level deep) while nested named +/// aggregate types are outlined into calls to their own shared helpers (see +/// [`Bindgen::lower_helper_name`] and [`Instruction::LowerNamedToMemory`]). +/// +/// `skip_outline_root` must be the id of the named aggregate (record/variant) +/// being lowered, and `ty` must be exactly `Type::Id(skip_outline_root)` (not +/// an alias to it) so that the single-shot inline of the root in +/// `write_to_memory` lands on the intended type. +pub fn lower_to_memory_root( + resolve: &Resolve, + bindgen: &mut B, + address: B::Operand, + value: B::Operand, + ty: &Type, + skip_outline_root: TypeId, +) { + assert!( + matches!(ty, Type::Id(id) if *id == skip_outline_root), + "lower_to_memory_root requires `ty` to be `Type::Id(skip_outline_root)`, \ + not an alias or other type" + ); + let mut generator = Generator::new(resolve, bindgen); + generator.realloc = Some(Realloc::Export("cabi_realloc")); + generator.lower_outline_root = Some(skip_outline_root); + generator.stack.push(value); + generator.write_to_memory(ty, address, Default::default()); + debug_assert!(generator.stack.is_empty()); +} + /// Used in a similar manner as the `Interface::call` function except is /// used to generate the `post-return` callback for `func`. /// @@ -1060,6 +1117,11 @@ struct Generator<'a, B: Bindgen> { /// level) while nested named aggregates are still outlined into their own /// helpers. `None` everywhere else (so all eligible named types outline). lift_outline_root: Option, + /// Like `lift_outline_root`, but for the lower side: when generating the + /// body of an outlined lower helper for a named type, this holds that + /// type's id so its own top-level lower is inlined (one level) while + /// nested named aggregates are still outlined. `None` everywhere else. + lower_outline_root: Option, } const MAX_FLAT_PARAMS: usize = 16; @@ -1076,6 +1138,7 @@ impl<'a, B: Bindgen> Generator<'a, B> { return_pointer: None, realloc: None, lift_outline_root: None, + lower_outline_root: None, } } @@ -2029,7 +2092,21 @@ impl<'a, B: Bindgen> Generator<'a, B> { Type::String => self.write_list_to_memory(ty, addr, offset), Type::ErrorContext => self.lower_and_emit(ty, addr, &I32Store { offset }), - Type::Id(id) => match &self.resolve.types[id].kind { + Type::Id(id) => { + // Outline the lower of large named aggregate types into shared + // helper functions instead of inlining the full recursive + // lower here. `lower_outline_root` is `Some(id)` only while + // generating that type's own helper body, in which case its + // top level is inlined (one level) and nested types still + // outline. + let is_outline_root = self.lower_outline_root == Some(id); + self.lower_outline_root = None; + if !is_outline_root && self.bindgen.lower_helper_name(self.resolve, id).is_some() { + self.stack.push(addr); + self.emit(&Instruction::LowerNamedToMemory { ty: id, offset }); + return; + } + match &self.resolve.types[id].kind { TypeDefKind::Type(t) => self.write_to_memory(t, addr, offset), TypeDefKind::List(_) => self.write_list_to_memory(ty, addr, offset), // Maps have the same linear memory layout as list>. @@ -2143,6 +2220,7 @@ impl<'a, B: Bindgen> Generator<'a, B> { id, }); } + } }, } } diff --git a/crates/cpp/src/lib.rs b/crates/cpp/src/lib.rs index 60a90290e..8c4fb5ea2 100644 --- a/crates/cpp/src/lib.rs +++ b/crates/cpp/src/lib.rs @@ -3575,6 +3575,10 @@ impl<'a, 'b> Bindgen for FunctionBindgen<'a, 'b> { "LiftNamedFromMemory is only emitted by generators that implement \ Bindgen::lift_helper_name, which this generator does not" ), + abi::Instruction::LowerNamedToMemory { .. } => unreachable!( + "LowerNamedToMemory is only emitted by generators that implement \ + Bindgen::lower_helper_name, which this generator does not" + ), abi::Instruction::PointerStore { offset } => { let ptr_type = self.r#gen.r#gen.opts.ptr_type(); self.store(ptr_type, *offset, operands) diff --git a/crates/csharp/src/function.rs b/crates/csharp/src/function.rs index 2e3d82cdb..866f111e3 100644 --- a/crates/csharp/src/function.rs +++ b/crates/csharp/src/function.rs @@ -468,6 +468,10 @@ impl Bindgen for FunctionBindgen<'_, '_> { "LiftNamedFromMemory is only emitted by generators that implement \ Bindgen::lift_helper_name, which this generator does not" ), + Instruction::LowerNamedToMemory { .. } => unreachable!( + "LowerNamedToMemory is only emitted by generators that implement \ + Bindgen::lower_helper_name, which this generator does not" + ), Instruction::PointerLoad { offset } => results.push(format!( "new global::System.Span((void*)((byte*){} + {offset}), 1)[0]", operands[0], diff --git a/crates/go/src/lib.rs b/crates/go/src/lib.rs index 556112763..01cdf7166 100644 --- a/crates/go/src/lib.rs +++ b/crates/go/src/lib.rs @@ -1999,6 +1999,10 @@ return {results}" "LiftNamedFromMemory is only emitted by generators that implement \ Bindgen::lift_helper_name, which this generator does not" ), + Instruction::LowerNamedToMemory { .. } => unreachable!( + "LowerNamedToMemory is only emitted by generators that implement \ + Bindgen::lower_helper_name, which this generator does not" + ), Instruction::PointerLoad { offset } => { load(self, results, &operands[0], offset, "uint32", &|v| { format!("uintptr({v})") diff --git a/crates/moonbit/src/lib.rs b/crates/moonbit/src/lib.rs index adf8edcec..d367b96cd 100644 --- a/crates/moonbit/src/lib.rs +++ b/crates/moonbit/src/lib.rs @@ -2,7 +2,7 @@ use anyhow::Result; use core::panic; use heck::{ToShoutySnakeCase, ToUpperCamelCase}; use std::{ - collections::{HashMap, HashSet}, + collections::{BTreeMap, HashMap, HashSet}, fmt::Write, mem, ops::Deref, @@ -152,6 +152,7 @@ impl MoonBit { ffi_imports: HashSet::new(), derive_opts, interface, + lower_helpers: BTreeMap::new(), } } @@ -424,6 +425,8 @@ impl WorldGenerator for MoonBit { r#gen.export(func); } + r#gen.generate_lower_helper_bodies(); + let fragment = r#gen.finish(); // Write files @@ -488,6 +491,8 @@ impl WorldGenerator for MoonBit { r#gen.export(func); } + r#gen.generate_lower_helper_bodies(); + let fragment = r#gen.finish(); // Write files @@ -585,6 +590,14 @@ struct InterfaceGenerator<'a> { // Options for deriving traits derive_opts: DeriveOpts, + + // Shared lower-to-memory helper functions for named aggregate types + // (records/variants) reached from memory-lowered guest-export results. The + // helper for a given `TypeId` is emitted once and called from every + // `wasmExport*` glue function that lowers a value of that type, instead of + // inlining the full recursive lower at each call site. This keeps the + // generated `wasmExport*` functions small enough for `moonc` to compile. + lower_helpers: BTreeMap, } impl InterfaceGenerator<'_> { @@ -596,6 +609,131 @@ impl InterfaceGenerator<'_> { } } + /// Recursively registers a shared lower-to-memory helper for `ty` and + /// every named aggregate type (record/variant) reachable from it. Only + /// records names here; bodies are generated later by + /// [`Self::generate_lower_helper_bodies`]. + /// + /// Mirrors the Rust backend's `register_lift_helpers`, but for the lower + /// side. Outlining the recursive lower into one helper per aggregate type + /// keeps the generated `wasmExport*` glue functions small enough for + /// `moonc` to compile (a single inlined lower of a deeply nested variant + /// can balloon the per-function IR past `moonc`'s limits). + fn register_lower_helpers(&mut self, ty: &Type) { + let Type::Id(id) = ty else { return }; + let id = *id; + match &self.resolve.types[id].kind { + TypeDefKind::Type(t) => { + let t = *t; + self.register_lower_helpers(&t); + } + TypeDefKind::Record(_) | TypeDefKind::Variant(_) => { + if self.lower_helpers.contains_key(&id) { + return; + } + let name = format!("__wit_bindgen_lower_t{}", id.index()); + self.lower_helpers.insert(id, name); + // Collect child types first to avoid borrow conflicts. + let children: Vec = match &self.resolve.types[id].kind { + TypeDefKind::Record(r) => r.fields.iter().map(|f| f.ty).collect(), + TypeDefKind::Variant(v) => v.cases.iter().filter_map(|c| c.ty).collect(), + _ => Vec::new(), + }; + for child in children { + self.register_lower_helpers(&child); + } + } + TypeDefKind::List(t) | TypeDefKind::Option(t) | TypeDefKind::FixedLengthList(t, _) => { + let t = *t; + self.register_lower_helpers(&t); + } + TypeDefKind::Tuple(tuple) => { + let tys: Vec = tuple.types.clone(); + for t in tys { + self.register_lower_helpers(&t); + } + } + TypeDefKind::Result(r) => { + if let Some(t) = r.ok { + self.register_lower_helpers(&t); + } + if let Some(t) = r.err { + self.register_lower_helpers(&t); + } + } + TypeDefKind::Map(k, v) => { + let k = *k; + let v = *v; + self.register_lower_helpers(&k); + self.register_lower_helpers(&v); + } + _ => {} + } + } + + /// Generates the body of every registered lower helper into `self.ffi`. + /// + /// Each helper has the signature `fn name(ptr : Int, value : T) -> Unit` and + /// writes `value` to linear memory at `ptr` using the canonical ABI. The + /// root type is lowered inline (one level deep) via + /// [`abi::lower_to_memory_root`]; nested named aggregates are outlined + /// into calls to their own helpers. + fn generate_lower_helper_bodies(&mut self) { + let ids: Vec = self.lower_helpers.keys().copied().collect(); + let resolve = self.resolve; + for id in ids { + let name = self.lower_helpers[&id].clone(); + let ty = Type::Id(id); + let value_ty = self.world_gen.pkg_resolver.type_name(self.name, &ty); + + // The helper body is generated in a borrow scope so that + // `FunctionBindgen`'s borrow of `self` ends before we write the + // finished body into `self.ffi` below. + let body = { + // Reserve the helper's parameter names (`ptr`, `value`) in the + // local namespace so that lowering temps such as the string / + // canonical-list pointer (`locals.tmp("ptr")`) become `ptr0`, + // `ptr1`, ... instead of `ptr`, which would shadow the `ptr` + // parameter and make subsequent `(ptr) + offset` stores write + // to the string's own buffer instead of the result base. + let mut f = FunctionBindgen::new( + self, + Box::new(["ptr".to_string(), "value".to_string()]), + ); + abi::lower_to_memory_root( + resolve, + &mut f, + "ptr".to_string(), + "value".to_string(), + &ty, + id, + ); + let body = mem::take(&mut f.src); + + // A pure lower-to-memory of a record/variant allocates the + // result buffers via `cabi_realloc` (handed off to the caller) + // and must not require any of the side state that `export` + // would otherwise flush into the surrounding function. Assert + // the assumption holds rather than silently emitting broken + // code if a future lower path starts depending on them. + assert!( + !f.needs_cleanup_list, + "outlined lower helper unexpectedly requires a cleanup list" + ); + assert!( + f.cleanup.is_empty(), + "outlined lower helper unexpectedly produced cleanup entries" + ); + body + }; + + uwriteln!( + self.ffi, + "\n#doc(hidden)\nfn {name}(ptr : Int, value : {value_ty}) -> Unit {{\n{body}\n}}\n" + ); + } + } + fn import(&mut self, func: &Function) { let async_plan = self.world_gen.async_support.import_plan( &mut self.world_gen.opts.async_, @@ -705,6 +843,19 @@ impl InterfaceGenerator<'_> { ); let endpoint_plan = self.export_async_function_plan(self.interface, func); + + // Register shared lower-to-memory helpers for the named aggregate + // types reached from this export's result so that the recursive lower + // is outlined into per-type helpers instead of being inlined into the + // `wasmExport*` glue. Only memory-lowered results (return-pointer + // results) go through `write_to_memory`, so helpers are only needed + // there. + if sig.retptr { + if let Some(result) = &func.result { + self.register_lower_helpers(result); + } + } + let mut bindgen = FunctionBindgen::new( self, (0..sig.params.len()).map(|i| format!("p{i}")).collect(), @@ -2381,6 +2532,19 @@ impl Bindgen for FunctionBindgen<'_, '_> { Bindgen::lift_helper_name, which this generator does not" ), + Instruction::LowerNamedToMemory { ty, offset } => { + let name = self + .lower_helper_name(self.interface_gen.resolve, *ty) + .expect("lower helper must be registered before it is emitted"); + uwriteln!( + self.src, + "{name}(({}) + {offset}, {})", + operands[1], + operands[0], + offset = offset.size_wasm32() + ) + } + Instruction::I32Load8U { offset } => { self.use_ffi(ffi::LOAD8_U); results.push(format!( @@ -2905,6 +3069,10 @@ impl Bindgen for FunctionBindgen<'_, '_> { Type::U8 | Type::U32 | Type::U64 | Type::S32 | Type::S64 | Type::F32 | Type::F64 ) } + + fn lower_helper_name(&self, _resolve: &Resolve, id: TypeId) -> Option { + self.interface_gen.lower_helpers.get(&id).cloned() + } } fn perform_cast(op: &str, cast: &Bitcast) -> String { diff --git a/crates/rust/src/bindgen.rs b/crates/rust/src/bindgen.rs index 11460c424..0d2f6b473 100644 --- a/crates/rust/src/bindgen.rs +++ b/crates/rust/src/bindgen.rs @@ -1208,6 +1208,11 @@ impl Bindgen for FunctionBindgen<'_, '_> { results.push(format!("result{tmp}")); } + Instruction::LowerNamedToMemory { .. } => unreachable!( + "LowerNamedToMemory is only emitted by generators that implement \ + Bindgen::lower_helper_name, which this generator does not" + ), + Instruction::I32Store { offset } => { self.push_str(&format!( "*{}.add({}).cast::() = {};\n", From 4407232ead86d9bcbd06cbebd790a52120a4087a Mon Sep 17 00:00:00 2001 From: Daniel Vigovszky Date: Thu, 25 Jun 2026 19:25:41 +0200 Subject: [PATCH 23/23] Export disambiguator for MoonBit --- crates/core/src/abi.rs | 398 ++++++++++++++-------------- crates/moonbit/src/async_support.rs | 3 +- crates/moonbit/src/lib.rs | 86 +++++- 3 files changed, 284 insertions(+), 203 deletions(-) diff --git a/crates/core/src/abi.rs b/crates/core/src/abi.rs index 08a1bfa06..1b87bcc0a 100644 --- a/crates/core/src/abi.rs +++ b/crates/core/src/abi.rs @@ -2107,121 +2107,125 @@ impl<'a, B: Bindgen> Generator<'a, B> { return; } match &self.resolve.types[id].kind { - TypeDefKind::Type(t) => self.write_to_memory(t, addr, offset), - TypeDefKind::List(_) => self.write_list_to_memory(ty, addr, offset), - // Maps have the same linear memory layout as list>. - TypeDefKind::Map(_, _) => self.write_list_to_memory(ty, addr, offset), + TypeDefKind::Type(t) => self.write_to_memory(t, addr, offset), + TypeDefKind::List(_) => self.write_list_to_memory(ty, addr, offset), + // Maps have the same linear memory layout as list>. + TypeDefKind::Map(_, _) => self.write_list_to_memory(ty, addr, offset), - TypeDefKind::Future(_) | TypeDefKind::Stream(_) | TypeDefKind::Handle(_) => { - self.lower_and_emit(ty, addr, &I32Store { offset }) - } + TypeDefKind::Future(_) | TypeDefKind::Stream(_) | TypeDefKind::Handle(_) => { + self.lower_and_emit(ty, addr, &I32Store { offset }) + } - // Decompose the record into its components and then write all - // the components into memory one-by-one. - TypeDefKind::Record(record) => { - self.emit(&RecordLower { - record, - ty: id, - name: self.resolve.types[id].name.as_deref().unwrap(), - }); - self.write_fields_to_memory(record.fields.iter().map(|f| &f.ty), addr, offset); - } - TypeDefKind::Resource => { - todo!() - } - TypeDefKind::Tuple(tuple) => { - self.emit(&TupleLower { tuple, ty: id }); - self.write_fields_to_memory(tuple.types.iter(), addr, offset); - } + // Decompose the record into its components and then write all + // the components into memory one-by-one. + TypeDefKind::Record(record) => { + self.emit(&RecordLower { + record, + ty: id, + name: self.resolve.types[id].name.as_deref().unwrap(), + }); + self.write_fields_to_memory( + record.fields.iter().map(|f| &f.ty), + addr, + offset, + ); + } + TypeDefKind::Resource => { + todo!() + } + TypeDefKind::Tuple(tuple) => { + self.emit(&TupleLower { tuple, ty: id }); + self.write_fields_to_memory(tuple.types.iter(), addr, offset); + } - TypeDefKind::Flags(f) => { - self.lower(ty); - match f.repr() { - FlagsRepr::U8 => { - self.stack.push(addr); - self.store_intrepr(offset, Int::U8); - } - FlagsRepr::U16 => { - self.stack.push(addr); - self.store_intrepr(offset, Int::U16); - } - FlagsRepr::U32(n) => { - for i in (0..n).rev() { - self.stack.push(addr.clone()); - self.emit(&I32Store { - offset: offset.add_bytes(i * 4), - }); + TypeDefKind::Flags(f) => { + self.lower(ty); + match f.repr() { + FlagsRepr::U8 => { + self.stack.push(addr); + self.store_intrepr(offset, Int::U8); + } + FlagsRepr::U16 => { + self.stack.push(addr); + self.store_intrepr(offset, Int::U16); + } + FlagsRepr::U32(n) => { + for i in (0..n).rev() { + self.stack.push(addr.clone()); + self.emit(&I32Store { + offset: offset.add_bytes(i * 4), + }); + } } } } - } - // Each case will get its own block, and the first item in each - // case is writing the discriminant. After that if we have a - // payload we write the payload after the discriminant, aligned up - // to the type's alignment. - TypeDefKind::Variant(v) => { - self.write_variant_arms_to_memory( - offset, - addr, - v.tag(), - v.cases.iter().map(|c| c.ty.as_ref()), - ); - self.emit(&VariantLower { - variant: v, - ty: id, - results: &[], - name: self.resolve.types[id].name.as_deref().unwrap(), - }); - } + // Each case will get its own block, and the first item in each + // case is writing the discriminant. After that if we have a + // payload we write the payload after the discriminant, aligned up + // to the type's alignment. + TypeDefKind::Variant(v) => { + self.write_variant_arms_to_memory( + offset, + addr, + v.tag(), + v.cases.iter().map(|c| c.ty.as_ref()), + ); + self.emit(&VariantLower { + variant: v, + ty: id, + results: &[], + name: self.resolve.types[id].name.as_deref().unwrap(), + }); + } - TypeDefKind::Option(t) => { - self.write_variant_arms_to_memory(offset, addr, Int::U8, [None, Some(t)]); - self.emit(&OptionLower { - payload: t, - ty: id, - results: &[], - }); - } + TypeDefKind::Option(t) => { + self.write_variant_arms_to_memory(offset, addr, Int::U8, [None, Some(t)]); + self.emit(&OptionLower { + payload: t, + ty: id, + results: &[], + }); + } - TypeDefKind::Result(r) => { - self.write_variant_arms_to_memory( - offset, - addr, - Int::U8, - [r.ok.as_ref(), r.err.as_ref()], - ); - self.emit(&ResultLower { - result: r, - ty: id, - results: &[], - }); - } + TypeDefKind::Result(r) => { + self.write_variant_arms_to_memory( + offset, + addr, + Int::U8, + [r.ok.as_ref(), r.err.as_ref()], + ); + self.emit(&ResultLower { + result: r, + ty: id, + results: &[], + }); + } - TypeDefKind::Enum(e) => { - self.lower(ty); - self.stack.push(addr); - self.store_intrepr(offset, e.tag()); - } + TypeDefKind::Enum(e) => { + self.lower(ty); + self.stack.push(addr); + self.store_intrepr(offset, e.tag()); + } - TypeDefKind::Unknown => unreachable!(), - TypeDefKind::FixedLengthList(element, size) => { - // resembles write_list_to_memory - self.push_block(); - self.emit(&IterElem { element }); - self.emit(&IterBasePointer); - let elem_addr = self.stack.pop().unwrap(); - self.write_to_memory(element, elem_addr, offset); - self.finish_block(0); - self.stack.push(addr); - self.emit(&FixedLengthListLowerToMemory { - element, - size: *size, - id, - }); - } + TypeDefKind::Unknown => unreachable!(), + TypeDefKind::FixedLengthList(element, size) => { + // resembles write_list_to_memory + self.push_block(); + self.emit(&IterElem { element }); + self.emit(&IterBasePointer); + let elem_addr = self.stack.pop().unwrap(); + self.write_to_memory(element, elem_addr, offset); + self.finish_block(0); + self.stack.push(addr); + self.emit(&FixedLengthListLowerToMemory { + element, + size: *size, + id, + }); + } } - }, + } } } @@ -2329,112 +2333,116 @@ impl<'a, B: Bindgen> Generator<'a, B> { return; } match &self.resolve.types[id].kind { - TypeDefKind::Type(t) => self.read_from_memory(t, addr, offset), + TypeDefKind::Type(t) => self.read_from_memory(t, addr, offset), - TypeDefKind::List(_) => self.read_list_from_memory(ty, addr, offset), - // Maps have the same linear memory layout as list>. - TypeDefKind::Map(_, _) => self.read_list_from_memory(ty, addr, offset), + TypeDefKind::List(_) => self.read_list_from_memory(ty, addr, offset), + // Maps have the same linear memory layout as list>. + TypeDefKind::Map(_, _) => self.read_list_from_memory(ty, addr, offset), - TypeDefKind::Future(_) | TypeDefKind::Stream(_) | TypeDefKind::Handle(_) => { - self.emit_and_lift(ty, addr, &I32Load { offset }) - } + TypeDefKind::Future(_) | TypeDefKind::Stream(_) | TypeDefKind::Handle(_) => { + self.emit_and_lift(ty, addr, &I32Load { offset }) + } - TypeDefKind::Resource => { - todo!(); - } + TypeDefKind::Resource => { + todo!(); + } - // Read and lift each field individually, adjusting the offset - // as we go along, then aggregate all the fields into the - // record. - TypeDefKind::Record(record) => { - self.read_fields_from_memory(record.fields.iter().map(|f| &f.ty), addr, offset); - self.emit(&RecordLift { - record, - ty: id, - name: self.resolve.types[id].name.as_deref().unwrap(), - }); - } + // Read and lift each field individually, adjusting the offset + // as we go along, then aggregate all the fields into the + // record. + TypeDefKind::Record(record) => { + self.read_fields_from_memory( + record.fields.iter().map(|f| &f.ty), + addr, + offset, + ); + self.emit(&RecordLift { + record, + ty: id, + name: self.resolve.types[id].name.as_deref().unwrap(), + }); + } - TypeDefKind::Tuple(tuple) => { - self.read_fields_from_memory(&tuple.types, addr, offset); - self.emit(&TupleLift { tuple, ty: id }); - } + TypeDefKind::Tuple(tuple) => { + self.read_fields_from_memory(&tuple.types, addr, offset); + self.emit(&TupleLift { tuple, ty: id }); + } - TypeDefKind::Flags(f) => { - match f.repr() { - FlagsRepr::U8 => { - self.stack.push(addr); - self.load_intrepr(offset, Int::U8); - } - FlagsRepr::U16 => { - self.stack.push(addr); - self.load_intrepr(offset, Int::U16); - } - FlagsRepr::U32(n) => { - for i in 0..n { - self.stack.push(addr.clone()); - self.emit(&I32Load { - offset: offset.add_bytes(i * 4), - }); + TypeDefKind::Flags(f) => { + match f.repr() { + FlagsRepr::U8 => { + self.stack.push(addr); + self.load_intrepr(offset, Int::U8); + } + FlagsRepr::U16 => { + self.stack.push(addr); + self.load_intrepr(offset, Int::U16); + } + FlagsRepr::U32(n) => { + for i in 0..n { + self.stack.push(addr.clone()); + self.emit(&I32Load { + offset: offset.add_bytes(i * 4), + }); + } } } + self.lift(ty); } - self.lift(ty); - } - // Each case will get its own block, and we'll dispatch to the - // right block based on the `i32.load` we initially perform. Each - // individual block is pretty simple and just reads the payload type - // from the corresponding offset if one is available. - TypeDefKind::Variant(variant) => { - self.read_variant_arms_from_memory( - offset, - addr, - variant.tag(), - variant.cases.iter().map(|c| c.ty.as_ref()), - ); - self.emit(&VariantLift { - variant, - ty: id, - name: self.resolve.types[id].name.as_deref().unwrap(), - }); - } + // Each case will get its own block, and we'll dispatch to the + // right block based on the `i32.load` we initially perform. Each + // individual block is pretty simple and just reads the payload type + // from the corresponding offset if one is available. + TypeDefKind::Variant(variant) => { + self.read_variant_arms_from_memory( + offset, + addr, + variant.tag(), + variant.cases.iter().map(|c| c.ty.as_ref()), + ); + self.emit(&VariantLift { + variant, + ty: id, + name: self.resolve.types[id].name.as_deref().unwrap(), + }); + } - TypeDefKind::Option(t) => { - self.read_variant_arms_from_memory(offset, addr, Int::U8, [None, Some(t)]); - self.emit(&OptionLift { payload: t, ty: id }); - } + TypeDefKind::Option(t) => { + self.read_variant_arms_from_memory(offset, addr, Int::U8, [None, Some(t)]); + self.emit(&OptionLift { payload: t, ty: id }); + } - TypeDefKind::Result(r) => { - self.read_variant_arms_from_memory( - offset, - addr, - Int::U8, - [r.ok.as_ref(), r.err.as_ref()], - ); - self.emit(&ResultLift { result: r, ty: id }); - } + TypeDefKind::Result(r) => { + self.read_variant_arms_from_memory( + offset, + addr, + Int::U8, + [r.ok.as_ref(), r.err.as_ref()], + ); + self.emit(&ResultLift { result: r, ty: id }); + } - TypeDefKind::Enum(e) => { - self.stack.push(addr.clone()); - self.load_intrepr(offset, e.tag()); - self.lift(ty); - } + TypeDefKind::Enum(e) => { + self.stack.push(addr.clone()); + self.load_intrepr(offset, e.tag()); + self.lift(ty); + } - TypeDefKind::Unknown => unreachable!(), - TypeDefKind::FixedLengthList(ty, size) => { - self.push_block(); - self.emit(&IterBasePointer); - let elemaddr = self.stack.pop().unwrap(); - self.read_from_memory(ty, elemaddr, offset); - self.finish_block(1); - self.stack.push(addr.clone()); - self.emit(&FixedLengthListLiftFromMemory { - element: ty, - size: *size, - id, - }); - } + TypeDefKind::Unknown => unreachable!(), + TypeDefKind::FixedLengthList(ty, size) => { + self.push_block(); + self.emit(&IterBasePointer); + let elemaddr = self.stack.pop().unwrap(); + self.read_from_memory(ty, elemaddr, offset); + self.finish_block(1); + self.stack.push(addr.clone()); + self.emit(&FixedLengthListLiftFromMemory { + element: ty, + size: *size, + id, + }); + } } } } diff --git a/crates/moonbit/src/async_support.rs b/crates/moonbit/src/async_support.rs index 02c25a040..da93aa44f 100644 --- a/crates/moonbit/src/async_support.rs +++ b/crates/moonbit/src/async_support.rs @@ -819,6 +819,7 @@ impl<'a> InterfaceGenerator<'a> { interface: Option<&WorldKey>, func: &Function, camel_name: &str, + disambig: &str, async_state: AsyncFunctionState, ) -> bool { if !plan.is_async() { @@ -828,7 +829,7 @@ impl<'a> InterfaceGenerator<'a> { let export_func_name = self .world_gen .export_ns - .tmp(&format!("wasmExportAsync{camel_name}")); + .tmp(&format!("wasmExportAsync{camel_name}{disambig}")); let AsyncTaskReturnState::Emitted { body: task_return_body, needs_cleanup_list: task_return_needs_cleanup, diff --git a/crates/moonbit/src/lib.rs b/crates/moonbit/src/lib.rs index d367b96cd..d9aa133ac 100644 --- a/crates/moonbit/src/lib.rs +++ b/crates/moonbit/src/lib.rs @@ -16,7 +16,7 @@ use wit_bindgen_core::{ Alignment, ArchitectureSize, Docs, Enum, Flags, FlagsRepr, Function, Int, InterfaceId, LiftLowerAbi, LiveTypes, ManglingAndAbi, Param, Record, Resolve, ResourceIntrinsic, Result_, SizeAlign, Tuple, Type, TypeDefKind, TypeId, Variant, WasmExport, WasmExportKind, - WasmImport, WorldId, WorldKey, + WasmImport, WorldId, WorldItem, WorldKey, }, }; @@ -113,6 +113,34 @@ impl InterfaceFragment { } } +/// Sanitizes a WIT export identity into a stable MoonBit-identifier suffix used +/// to disambiguate `wasmExport*` names that collide across the world's exports. +/// `golem:agent/guest` -> `GolemAgentGuest`, world `agent-guest` -> `AgentGuest`. +/// Any `@version` segment is stripped so the suffix does not change when a +/// dependency is bumped. +fn export_disambiguator(resolve: &Resolve, key: Option<&WorldKey>, world: WorldId) -> String { + let raw: String = match key { + Some(k) => resolve.name_world_key(k), + None => resolve.worlds[world].name.clone(), + }; + let raw = match raw.rfind('@') { + Some(i) => &raw[..i], + None => raw.as_str(), + }; + let mut out = String::new(); + for part in raw.split([':', '/', '-', '_']) { + if part.is_empty() { + continue; + } + let mut chars = part.chars(); + if let Some(c) = chars.next() { + out.push(c.to_ascii_uppercase()); + out.push_str(chars.as_str()); + } + } + out +} + #[derive(Default)] pub struct MoonBit { opts: Opts, @@ -130,6 +158,14 @@ pub struct MoonBit { export_ns: Ns, + /// Stable per-export disambiguators for `wasmExport*` names that collide + /// across the world's exports (e.g. `invoke` exported by both + /// `golem:agent/guest` and `golem:tool/guest`). Computed once in + /// `preprocess` from the exporting interface's identity, so the generated + /// names no longer depend on `world.exports` iteration order. Keyed by + /// `(interface key, function name)`; unique exports are absent. + disambiguators: HashMap<(Option, String), String>, + async_support: AsyncSupport, } @@ -264,6 +300,36 @@ impl WorldGenerator for MoonBit { })) .unwrap_or("generated".into()); self.sizes.fill(resolve); + + let mut groups: HashMap, String)>> = HashMap::new(); + for (key, item) in &resolve.worlds[world].exports { + match item { + WorldItem::Interface { id, .. } => { + for (fname, _) in &resolve.interfaces[*id].functions { + let base = format!("wasmExport{}", fname.to_upper_camel_case()); + groups + .entry(base) + .or_default() + .push((Some(key.clone()), fname.clone())); + } + } + WorldItem::Function(f) => { + let base = format!("wasmExport{}", f.name.to_upper_camel_case()); + groups.entry(base).or_default().push((None, f.name.clone())); + } + WorldItem::Type { .. } => {} + } + } + let mut disambiguators = HashMap::new(); + for entries in groups.values() { + if entries.len() > 1 { + for (key, fname) in entries { + let disambig = export_disambiguator(resolve, key.as_ref(), world); + disambiguators.insert((key.clone(), fname.clone()), disambig); + } + } + } + self.disambiguators = disambiguators; Ok(()) } @@ -696,10 +762,8 @@ impl InterfaceGenerator<'_> { // `ptr1`, ... instead of `ptr`, which would shadow the `ptr` // parameter and make subsequent `(ptr) + offset` stores write // to the string's own buffer instead of the result base. - let mut f = FunctionBindgen::new( - self, - Box::new(["ptr".to_string(), "value".to_string()]), - ); + let mut f = + FunctionBindgen::new(self, Box::new(["ptr".to_string(), "value".to_string()])); abi::lower_to_memory_root( resolve, &mut f, @@ -887,10 +951,17 @@ impl InterfaceGenerator<'_> { let camel_name = func.name.to_upper_camel_case(); + let disambig = self + .world_gen + .disambiguators + .get(&(self.interface.cloned(), func.name.to_string())) + .cloned() + .unwrap_or_default(); + let func_name = self .world_gen .export_ns - .tmp(&format!("wasmExport{camel_name}")); + .tmp(&format!("wasmExport{camel_name}{disambig}")); let params = sig .params @@ -959,6 +1030,7 @@ impl InterfaceGenerator<'_> { self.interface, func, &camel_name, + &disambig, async_state, ) && abi::guest_export_needs_post_return(self.resolve, func) { @@ -985,7 +1057,7 @@ impl InterfaceGenerator<'_> { let func_name = self .world_gen .export_ns - .tmp(&format!("wasmExport{camel_name}PostReturn")); + .tmp(&format!("wasmExport{camel_name}{disambig}PostReturn")); uwrite!( self.ffi,