From c09c29dd17ebf9d78cd4a2da3dabfda9aaa82cd0 Mon Sep 17 00:00:00 2001 From: Luke Wagner Date: Mon, 31 Aug 2026 18:44:50 -0500 Subject: [PATCH 1/2] CABI: refactor/simplify stream/future logic; fix DROPPED/CANCELLED delivery --- design/mvp/CanonicalABI.md | 1120 +++++++++-------------- design/mvp/Concurrency.md | 11 +- design/mvp/Explainer.md | 6 +- design/mvp/canonical-abi/definitions.py | 567 +++++------- design/mvp/canonical-abi/run_tests.py | 577 ++++++------ test/async/cancel-stream.wast | 193 ++++ test/async/futures-must-write.wast | 139 +++ test/async/trap-if-done.wast | 57 +- 8 files changed, 1335 insertions(+), 1335 deletions(-) diff --git a/design/mvp/CanonicalABI.md b/design/mvp/CanonicalABI.md index 8ed6e64b..5d2010ef 100644 --- a/design/mvp/CanonicalABI.md +++ b/design/mvp/CanonicalABI.md @@ -22,8 +22,7 @@ specified here. * [Waitable State](#waitable-state) * [Subtask State](#subtask-state) * [Buffer State](#buffer-state) - * [Stream State](#stream-state) - * [Future State](#future-state) + * [Stream and Future State](#stream-and-future-state) * [Despecialization](#despecialization) * [Type Predicates](#type-predicates) * [Alignment](#alignment) @@ -54,8 +53,7 @@ specified here. * [`canon subtask.cancel`](#-canon-subtaskcancel) ๐Ÿ”€ * [`canon subtask.drop`](#-canon-subtaskdrop) ๐Ÿ”€ * [`canon {stream,future}.new`](#-canon-streamfuturenew) ๐Ÿ”€ - * [`canon stream.{read,write}`](#-canon-streamreadwrite) ๐Ÿ”€ - * [`canon future.{read,write}`](#-canon-futurereadwrite) ๐Ÿ”€ + * [`canon {stream,future}.{read,write}`](#-canon-streamfuturereadwrite) ๐Ÿ”€ * [`canon {stream,future}.cancel-{read,write}`](#-canon-streamfuturecancel-readwrite) ๐Ÿ”€ * [`canon {stream,future}.drop-{readable,writable}`](#-canon-streamfuturedrop-readablewritable) ๐Ÿ”€ * [`canon thread.index`](#-canon-threadindex) ๐Ÿงต @@ -1477,7 +1475,6 @@ class Buffer: MAX_LENGTH = 2**28 - 1 t: ValType remain: Callable[[], int] - is_zero_length: Callable[[], bool] class ReadableBuffer(Buffer): read: Callable[[int], list[any]] @@ -1498,12 +1495,12 @@ memory over time). The `ReadableBuffer` and `WritableBuffer` abstract classes may either be implemented by the host or by another wasm component. In the latter case, these -abstract classes are implemented by the concrete `ReadableBufferGuestImpl` and -`WritableBufferGuestImpl` classes which eagerly check alignment and range -when the buffer is constructed so that `read` and `write` are infallible -operations (modulo traps): +abstract classes are implemented by the concrete `ReadableGuestBuffer` and +`WritableGuestBuffer` classes which eagerly check alignment and range when the +buffer is constructed so that `read` and `write` are infallible operations +(modulo traps): ```python -class BufferGuestImpl(Buffer): +class GuestBuffer(Buffer): cx: LiftLowerContext t: ValType ptr: int @@ -1524,10 +1521,7 @@ class BufferGuestImpl(Buffer): def remain(self): return self.length - self.progress - def is_zero_length(self): - return self.length == 0 - -class ReadableBufferGuestImpl(BufferGuestImpl, ReadableBuffer): +class ReadableGuestBuffer(GuestBuffer, ReadableBuffer): def read(self, n): assert(n <= self.remain()) if self.t: @@ -1538,7 +1532,7 @@ class ReadableBufferGuestImpl(BufferGuestImpl, ReadableBuffer): self.progress += n return vs -class WritableBufferGuestImpl(BufferGuestImpl, WritableBuffer): +class WritableGuestBuffer(GuestBuffer, WritableBuffer): def write(self, vs): assert(len(vs) <= self.remain()) if self.t: @@ -1559,400 +1553,296 @@ that do all the heavy lifting are shared with function parameter/result lifting and lowering and defined below. -### Stream State +### Stream and Future State -Values of `stream` type are represented in the Canonical ABI as `i32` indices -into the current component instance's `handles` table referring to either the -[readable or writable end] of a stream. Reading from the readable end of a -stream is achieved by calling `stream.read` and supplying a `WritableBuffer`. -Conversely, writing to the writable end of a stream is achieved by calling -`stream.write` and supplying a `ReadableBuffer`. The runtime waits until both -a readable and writable buffer have been supplied and then performs a direct -copy between the two buffers. This rendezvous-based design avoids the need -for an intermediate buffer and copy (unlike, e.g., a Unix pipe; a Unix pipe -would instead be implemented as a resource type owning the buffer memory and -*two* streams; on going in and one coming out). +`stream` and `future` types used in function parameters and results are +represented in the Canonical ABI as `i32` indices into the component instance's +`handles` table that refer to the *readable* [end] of a stream or future. +*Writable* ends are never passed across component boundaries and are instead +added directly to the `handles` table, along with a paired readable end, via the +`{stream,future}.new` built-ins. Stream and future readable and writable ends +are represented by 4 concrete classes: `{Readable,Writable}{Stream,Future}End`. +These 4 classes derive from 2 common `{Stream,Future}End` base classes which +themselves derive from a common `End` base class. -The result of a `{stream,future}.{read,write}` is communicated to the wasm -guest via a `CopyResult` code: +The `End` base class derives from `Waitable`, which means that stream and future +ends can be added to waitable sets and waited on via `waitable-set.wait` or the +`callback` event loop. Each `End` maintains its own independent state that +reflects what *that end* is currently doing or has done and is used to enforce +that each end upholds its respective end of the stream/future control-flow +communication protocol. ```python -class CopyResult(IntEnum): - COMPLETED = 0 - DROPPED = 1 - CANCELLED = 2 -``` -The `DROPPED` code indicates that the *other* end has since been dropped and -thus no more reads/writes are possible. The `CANCELLED` code is only possible -after *this* end has performed a `{stream,future}.{read,write}` followed by a -`{stream,future}.cancel-{read,write}`; `CANCELLED` notifies the wasm code -that the cancellation finished and so ownership of the memory buffer has been -returned to the wasm code. Lastly, `COMPLETED` indicates that the copy is done -and neither `DROPPED` nor `CANCELLED` apply. - -As with functions and buffers, native host code can be on either side of a -stream. Thus, streams are defined in terms of abstract interfaces that can be -implemented and consumed by wasm or host code (with all {wasm,host} pairings -being possible and well-defined). Since a `stream` in a function parameter or -result type always represents the transfer of the *readable* end of a stream, -only the `ReadableStream` interface can be implemented by either wasm or the -host; the `WritableStream` interface is always written to by wasm via a -writable stream end created by `stream.new`. -```python -ReclaimBuffer = Callable[[], None] -OnCopy = Callable[[ReclaimBuffer], None] -OnCopyDone = Callable[[CopyResult], None] +class End(Waitable): + class State(Enum): + IDLE = 1 + COPYING = 2 + CANCELLING_COPY = 3 + DONE = 4 -class SharedBase: t: ValType - cancel: Callable[[], None] - drop: Callable[[], None] - -class ReadableStream(SharedBase): - read: Callable[[ComponentInstance, WritableBuffer, OnCopy, OnCopyDone], None] + state: State + other: Optional[End] + buffer: Optional[Buffer] + owner: Optional[ComponentInstance] + index: Optional[int] + event_code: EventCode -class WritableStream(SharedBase): - write: Callable[[ComponentInstance, ReadableBuffer, OnCopy, OnCopyDone], None] -``` -The key operations in these interfaces are `read` and `write` which work as -follows: -* `read` never blocks and returns its values by either synchronously or - asynchronously writing to the given `WritableBuffer` and then calling the - given `OnCopy*` callbacks to notify the caller of progress. -* Symmetrically, `write` never blocks and takes the value to be written - from the given `ReadableBuffer`, calling the given `OnCopy*` callbacks to - notify the caller of progress. -* `OnCopyDone` is called to indicate that the `read` or `write` is finished - copying and that the caller has regained ownership of the buffer. -* `OnCopy` is called to indicate a copy has been made to or from the buffer. - However, there may be further copies made in the future, so the caller has - *not* regained ownership of the buffer. -* The `ReclaimBuffer` callback passed to `OnCopy` allows the caller of `read` or - `write` to immediately regain ownership of the buffer once the first copy has - completed. -* `cancel` is non-blocking, but does **not** guarantee that ownership of - the buffer has been returned; `cancel` only lets the caller *request* that - one of the `OnCopy*` callbacks be called ASAP (which may or may not happen - during `cancel`). -* The client may not call `read`, `write` or `drop` while there is a previous - `read` or `write` in progress. - -The `OnCopy*` callbacks are a spec-internal detail used to specify the allowed -concurrent behaviors of `stream.{read,write}` and not exposed directly to core -wasm code. Specifically, the point of the `OnCopy*` callbacks is to specify that -*multiple* reads or writes are allowed into the same `Buffer` up until the point -where either the buffer is full or the calling core wasm code receives a -`STREAM_READ` or `STREAM_WRITE` progress event (in which case `ReclaimBuffer` is -called). This reduces the number of context-switches required by the spec, -particularly when streaming between two components. - -The `SharedStreamImpl` class implements both `ReadableStream` and -`WritableStream` for streams created by wasm (via `stream.new`) and tracks the -common state shared by both the readable and writable ends of streams (defined -below). - -Introducing `SharedStreamImpl` in chunks, starting with the fields and initialization: -```python -class SharedStreamImpl(ReadableStream, WritableStream): - dropped: bool - pending_inst: Optional[ComponentInstance] - pending_buffer: Optional[Buffer] - pending_on_copy: Optional[OnCopy] - pending_on_copy_done: Optional[OnCopyDone] - - def __init__(self, t): + def __init__(self, t, owner, event_code): + Waitable.__init__(self) self.t = t - self.dropped = False - self.reset_pending() - - def reset_pending(self): - self.set_pending(None, None, None, None) - - def set_pending(self, inst, buffer, on_copy, on_copy_done): - self.pending_inst = inst - self.pending_buffer = buffer - self.pending_on_copy = on_copy - self.pending_on_copy_done = on_copy_done -``` -If set, the `pending_*` fields record the `Buffer` and `OnCopy*` callbacks of a -`read` or `write` that is waiting to rendezvous with a complementary `write` or -`read`. Dropping the readable or writable end of a stream or cancelling a -`read` or `write` notifies any pending `read` or `write` via its `OnCopyDone` -callback: -```python - def reset_and_notify_pending(self, result): - pending_on_copy_done = self.pending_on_copy_done - self.reset_pending() - pending_on_copy_done(result) - - def cancel(self): - self.reset_and_notify_pending(CopyResult.CANCELLED) - - def drop(self): - if not self.dropped: - self.dropped = True - if self.pending_buffer: - self.reset_and_notify_pending(CopyResult.DROPPED) -``` -While the abstract `ReadableStream` and `WritableStream` interfaces *allow* -`cancel` to return without having returned ownership of the buffer (which, in -general, is necessary for [various][OIO] [host][io_uring] APIs), when *wasm* is -implementing the stream, `cancel` always returns ownership of the buffer -immediately. - -Note that `cancel` and `drop` notify in opposite directions: -* `cancel` *must* be called on a readable or writable end with an operation - pending, and thus `cancel` notifies the same end that called it. -* `drop` *must not* be called on a readable or writable end with an operation - pending, and thus `drop` notifies the opposite end. - -The `read` method implements `ReadableStream.read` and is called by either -`stream.read` or the host, depending on who is passed the readable end of the -stream. If the reader is first to rendezvous, then all the parameters are -stored in the `pending_*` fields, requiring the reader to wait for the writer -to rendezvous. If the writer was first to rendezvous, then there is already a -pending `ReadableBuffer` to read from, and so the reader copies as much as it -can (which may be less than a full buffer's worth) and eagerly completes the -copy without blocking. In the final special case where the pending writer has a -zero-length buffer, the writer is notified, but the reader remains blocked: -```python - def read(self, inst, dst_buffer, on_copy, on_copy_done): - if self.dropped: - on_copy_done(CopyResult.DROPPED) - elif not self.pending_buffer: - self.set_pending(inst, dst_buffer, on_copy, on_copy_done) - else: - assert(self.t == dst_buffer.t == self.pending_buffer.t) - trap_if(inst is self.pending_inst and not none_or_number_type(self.t)) # temporary - if self.pending_buffer.remain() > 0: - if dst_buffer.remain() > 0: - n = min(dst_buffer.remain(), self.pending_buffer.remain()) - dst_buffer.write(self.pending_buffer.read(n)) - self.pending_on_copy(self.reset_pending) - on_copy_done(CopyResult.COMPLETED) + self.state = End.State.IDLE + self.other = None + self.buffer = None + self.owner = owner + self.index = None + self.event_code = event_code +``` +Going through each of these fields: +* `t`: the (immutable) `t` in `stream` or `future`; needed for the dynamic + type checks performed when dereferencing untyped `i32` indices. +* `state`: one of 4 states enumerated above whose meaning is described below. +* `other`: the readable/writable end paired with this writable/readable end; + if `None`, the other end has been dropped. +* `buffer`: while a `{stream,future}.{read,write}` is in progress, the + linear-memory buffer region passed by the call; `buffer.remain() == 0` means a + pending [zero-length read or write][Stream Readiness]. +* `owner`: each stream/future end must be uniquely owned by either the host or a + single component instance; `owner` tracks which one, with `None` meaning "the + host". +* `index`: if `owner` is non-`None`, the `i32` index of this end in the + component instance's `handles` table; only needed to deliver progress events + returned from `waitable-set.wait` et al. +* `event_code`: (immutably) one of `EventCode.{STREAM,FUTURE}_{READ,WRITE}`; + only needed to deliver progress events. + +The `End.copy` method is called by `{Readable,Writable}{Stream,Future}End.copy` +below given their `{Writable,Readable}Buffer` and a boolean `is_read` flag +indicating which one is calling. As shown in the code below, there are 5 +relevant cases that need to be handled. Because the `future.{read,write}` +built-ins have no explicit length parameter and thus always implicitly create a +buffer of length `1`, the second half of case `3` and cases `4` and `5` do not +apply to futures. Enumerating the cases in the order that they are handled in +the code below: +1. The other end was racily dropped before this end could be notified, in which + case the call immediately completes, reporting `DROPPED` and nothing copied. +2. The other end has not currently provided a buffer, in which case this end + must block until the other end shows up with a buffer. +3. Both this and the other end have provided buffers that can copy at least 1 + element, in which case the maximal amount is copied, notifying both sides of + the progress and leaving the other end's buffer pending if it has more + remaining. +4. The other end is performing a zero-length `stream.{read,write}`, in which + case the other end is notified if this end's buffer is non-zero-length *or* + this end is performing a `read` (since, as part of how [stream readiness] + works, writes are always asymmetrically notified in a both-zero-length + rendezvous). +5. Otherwise, this end must be doing a zero-length `stream.{read,write}` (where, + in the case of a `read`, the corresponding `write` buffer is + non-zero-length), in which case the call immediately returns `COMPLETED` with + no elements copied. + +```python + def copy(self, buffer: Buffer, is_read: bool): + assert(self.buffer is None) + self.state = End.State.COPYING + if self.other is None: + self.notify(progress = 0) + elif self.other.buffer is None: + self.buffer = buffer + elif buffer.remain() > 0 and self.other.buffer.remain() > 0: + trap_if(self.owner and self.owner is self.other.owner and not none_or_number_type(self.t)) + n = min(buffer.remain(), self.other.buffer.remain()) + if is_read: + buffer.write(self.other.buffer.read(n)) else: - self.reset_and_notify_pending(CopyResult.COMPLETED) - self.set_pending(inst, dst_buffer, on_copy, on_copy_done) -``` -Currently, there is a trap when both the `read` and `write` come from the same -component instance and there is a non-empty, non-number element type. This trap -will be removed in a subsequent release; the reason for the trap is that when -lifting and lowering can alias the same memory, interleavings can be complex -and must be handled carefully. Future improvements to the Canonical ABI ([lazy -lowering]) can greatly simplify this interleaving and be more practical to -implement. - -The `write` method implements `WritableStream.write` and is called by the -`stream.write` built-in (noting that the host cannot be passed the writable end -of a stream but may instead *implement* the `ReadableStream` interface and pass -the readable end into a component). The steps for `write` are the same as -`read` except for when a zero-length `write` rendezvous with a zero-length -`read`, in which case the `write` eagerly completes, leaving the `read` -pending: -```python - def write(self, inst, src_buffer, on_copy, on_copy_done): - if self.dropped: - on_copy_done(CopyResult.DROPPED) - elif not self.pending_buffer: - self.set_pending(inst, src_buffer, on_copy, on_copy_done) + self.other.buffer.write(buffer.read(n)) + self.notify(buffer.progress) + self.other.notify(self.other.buffer.progress) + if self.other.buffer.remain() == 0: + self.other.buffer = None + elif buffer.remain() > 0 or (is_read and self.other.buffer.remain() == 0): + self.other.notify(progress = 0) + self.other.buffer = None + self.buffer = buffer else: - assert(self.t == src_buffer.t == self.pending_buffer.t) - trap_if(inst is self.pending_inst and not none_or_number_type(self.t)) # temporary - if self.pending_buffer.remain() > 0: - if src_buffer.remain() > 0: - n = min(src_buffer.remain(), self.pending_buffer.remain()) - self.pending_buffer.write(src_buffer.read(n)) - self.pending_on_copy(self.reset_pending) - on_copy_done(CopyResult.COMPLETED) - elif src_buffer.is_zero_length() and self.pending_buffer.is_zero_length(): - on_copy_done(CopyResult.COMPLETED) - else: - self.reset_and_notify_pending(CopyResult.COMPLETED) - self.set_pending(inst, src_buffer, on_copy, on_copy_done) -``` -Putting together the behavior of zero-length `read` and `write` above, we can -see that, when *both* the reader and writer are zero-length, regardless of who -was first, the zero-length `write` always completes, leaving the zero-length -`read` pending. To avoid livelock, the Canonical ABI requires that a writer -*must* (eventually) follow a completed zero-length `write` with a -non-zero-length `write` that is allowed to block. This will break the loop, -notifying the reader end and allowing it to rendezvous with a non-zero-length -`read` and make progress. See the [stream readiness] section in the async -explainer for more background on purpose of zero-length reads and writes. - -The `none_or_number_type` predicate used above includes both the integer and -floating point number types: + self.notify(progress = 0) +``` +As a temporary measure (until [lazy lowering] obviates the problem), there is +also a trap when both the `read` and `write` come from the same component +instance and there is a non-empty, non-number element type. The reason for this +trap is that when lifting and lowering can alias the same memory, the eager +interleaving semantics of copying compound values would otherwise be complex to +precisely specify and implement. + +The `End.cancel` method is called by `{stream,future}.cancel-{read,write}` to +transition an end from `COPYING` (set by `End.copy` above) to `CANCELLING_COPY`. +The pending event conditionally set by `notify` is immediately delivered by +`canon_{stream,future}_cancel_{read,write}` below and prevents the built-in from +returning `BLOCKED`. If there is already a pending event, it is not overwritten, +as this might lose the previous `progress` argument. As reflected in the second +conjunct of the condition: (currently) only a host-owned end has the +nondeterministic option to block copy cancellation. When cancellation blocks, +the host takes responsibility for manually calling `notify` if and when the host +determines that the cancellation has completed. This enables the host to +efficiently use [completion-based][OIO] [APIs][io_uring] with asynchronous +cancellation. In the future, guest components may be given the same capability. ```python -def none_or_number_type(t): - return t is None or isinstance(t, U8Type | U16Type | U32Type | U64Type | - S8Type | S16Type | S32Type | S64Type | - F32Type | F64Type) + def cancel(self): + assert(self.state == End.State.COPYING) + self.state = End.State.CANCELLING_COPY + if (not self.has_pending_event() + and (self.other.owner is not None + or DETERMINISTIC_PROFILE + or random.randint(0,1))): + self.notify(progress = 0) ``` -The two ends of a stream are stored as separate elements in the component -instance `handles` table and each end has a separate `CopyState` that reflects -what *that end* is currently doing or has done. This `state` field is factored -out into the `CopyEnd` class that is derived below. The two ends also share some -state which is referenced by the `shared` field and either points to a -`SharedStreamImpl` (for component-created streams) or something host-defined for -(host-created streams). +The `End.drop` method is called by `{stream,future}.drop-{readable,writable}` to +update the `other` end's state and possibly set a pending notification for the +other end, if doing so wouldn't clobber an already-pending notification. ```python -class CopyState(Enum): - IDLE = 1 - COPYING = 2 - CANCELLING_COPY = 3 - DONE = 4 - -class CopyEnd(Waitable): - state: CopyState - shared: SharedBase - - def __init__(self, shared): - Waitable.__init__(self) - self.state = CopyState.IDLE - self.shared = shared - - def copying(self): - match self.state: - case CopyState.IDLE | CopyState.DONE: - return False - case CopyState.COPYING | CopyState.CANCELLING_COPY: - return True - assert(False) - def drop(self): - trap_if(self.copying()) - self.shared.drop() + assert(not self.copying_or_cancelling()) + if self.other is not None: + assert(self is self.other.other) + self.other.other = None + if self.other.copying_or_cancelling() and not self.other.has_pending_event(): + self.other.notify(progress = 0) + self.other = None Waitable.drop(self) -class ReadableStreamEnd(CopyEnd): - def copy(self, inst, dst, on_copy, on_copy_done): - self.shared.read(inst, dst, on_copy, on_copy_done) - -class WritableStreamEnd(CopyEnd): - def copy(self, inst, src, on_copy, on_copy_done): - self.shared.write(inst, src, on_copy, on_copy_done) -``` -As shown in `drop`, attempting to drop a readable or writable end while a copy -is in progress or in the process of being cancelled traps. This means that -client code must take care to wait for these operations to finish (potentially -cancelling them via `stream.cancel-{read,write}`) before dropping. - -The polymorphic `copy` method dispatches to either `ReadableStream.read` or -`WritableStream.write` and allows the implementations of `stream.{read,write}` -to share a single definition (in `stream_copy` below). - - -### Future State - -Futures are similar to streams, except that instead of passing 0..N values, -exactly one value is passed from the writer end to the reader end unless the -reader end is explicitly dropped first. - -Futures are defined in terms of abstract `ReadableFuture` and `WritableFuture` -interfaces: -```python -class ReadableFuture(SharedBase): - read: Callable[[ComponentInstance, WritableBuffer, OnCopyDone], None] - -class WritableFuture(SharedBase): - write: Callable[[ComponentInstance, ReadableBuffer, OnCopyDone], None] + def copying_or_cancelling(self): + return self.state in { End.State.COPYING, End.State.CANCELLING_COPY } ``` -These interfaces work like `ReadableStream` and `WritableStream` except that -there is no `OnCopy` callback passed to `read` or `write` to report partial -progress (since at most 1 value is copied) and the given `Buffer` must have -`remain() == 1`. -Introducing `SharedFutureImpl` in chunks, the first part is exactly -symmetric to `SharedStreamImpl` in how initialization and cancellation work: +Next, the intermediate `{Stream,Future}End` base classes are defined to implement +the `notify` method that is called by the `End.{copy,cancel,drop}` methods +above. `notify`'s behavior does not exhibit the same symmetry as the other `End` +methods above, which is why it is pushed down into stream- and future-specific +classes. `{Stream,Future}End.notify` both define closures that return a +`CopyResult` code saying what happened: ```python -class SharedFutureImpl(ReadableFuture, WritableFuture): - dropped: bool - pending_inst: Optional[ComponentInstance] - pending_buffer: Optional[Buffer] - pending_on_copy_done: Optional[OnCopyDone] - - def __init__(self, t): - self.t = t - self.dropped = False - self.reset_pending() - - def reset_pending(self): - self.set_pending(None, None, None) - - def set_pending(self, inst, buffer, on_copy_done): - self.pending_inst = inst - self.pending_buffer = buffer - self.pending_on_copy_done = on_copy_done - - def reset_and_notify_pending(self, result): - pending_on_copy_done = self.pending_on_copy_done - self.reset_pending() - pending_on_copy_done(result) - - def cancel(self): - self.reset_and_notify_pending(CopyResult.CANCELLED) + class CopyResult(IntEnum): + COMPLETED = 0 + DROPPED = 1 + CANCELLED = 2 ``` -Dropping works the same in futures as in streams, except that a future -writable end cannot be dropped without having written a value. This is guarded -by `WritableFutureEnd.drop` so it can be asserted here: -```python - def drop(self): - if not self.dropped: - self.dropped = True - if self.pending_buffer: - assert(isinstance(self.pending_buffer, ReadableBuffer)) - self.reset_and_notify_pending(CopyResult.DROPPED) -``` -Lastly, `read` and `write` work mostly like streams, but simplified based on -the fact that we're copying at most 1 value. The only asymmetric difference is -that, as mentioned above, only the writable end can observe that the readable -end was dropped before receiving a value. -```python - def read(self, inst, dst_buffer, on_copy_done): - assert(not self.dropped and dst_buffer.remain() == 1) - if not self.pending_buffer: - self.set_pending(inst, dst_buffer, on_copy_done) - else: - trap_if(inst is self.pending_inst and not none_or_number_type(self.t)) # temporary - dst_buffer.write(self.pending_buffer.read(1)) - self.reset_and_notify_pending(CopyResult.COMPLETED) - on_copy_done(CopyResult.COMPLETED) - - def write(self, inst, src_buffer, on_copy_done): - assert(src_buffer.remain() == 1) - if self.dropped: - on_copy_done(CopyResult.DROPPED) - elif not self.pending_buffer: - self.set_pending(inst, src_buffer, on_copy_done) - else: - trap_if(inst is self.pending_inst and not none_or_number_type(self.t)) # temporary - self.pending_buffer.write(src_buffer.read(1)) - self.reset_and_notify_pending(CopyResult.COMPLETED) - on_copy_done(CopyResult.COMPLETED) -``` -As with streams, the `# temporary` limitation shown above is that a future -cannot be read and written from the same component instance when it has a -non-empty, non-number value type. - -Lastly, the `{Readable,Writable}FutureEnd` classes are mostly symmetric with -`{Readable,Writable}StreamEnd`, defining a polymorphic `copy` method that -dispatches to either `ReadableFuture.read` or `WritableFuture.write`, which -allows the implementation of `future.{read,write}` to share a single -definition (in `future_copy` below). The only difference is that -`WritableFutureEnd.drop` traps if the writer hasn't successfully written a value -or been notified of the reader dropping their end: -```python -class ReadableFutureEnd(CopyEnd): - def copy(self, inst, dst_buffer, on_copy_done): - self.shared.read(inst, dst_buffer, on_copy_done) - -class WritableFutureEnd(CopyEnd): - def copy(self, inst, src_buffer, on_copy_done): - self.shared.write(inst, src_buffer, on_copy_done) +The `DROPPED` code indicates that the `other` end has since been dropped and +thus no more reads/writes are possible. The `CANCELLED` code is only possible +after *this* end has performed a `{stream,future}.{read,write}` followed by a +`{stream,future}.cancel-{read,write}`; `CANCELLED` notifies the wasm code that +the cancellation finished and so ownership of the memory buffer has been +returned to the wasm code. Lastly, `COMPLETED` indicates that the copy is done +and neither `DROPPED` nor `CANCELLED` apply. - def drop(self): - trap_if(self.state != CopyState.DONE) - CopyEnd.drop(self) +`StreamEnd.notify` sets a pending `stream_event` closure on the current `End` +(which is a `Waitable`) that may be delivered either synchronously (calling +`stream_event` during `stream.{cancel-,}{read,write}`) or asynchronously +(calling `stream_event` from `waitable-set.{wait,poll}` or the `callback` event +loop). In either case, the call to `stream_event` happens "right before" wasm +code runs and thus defines state transitions that are only observable once an +event is *delivered* to core wasm (not just *set pending*). In particular, a +stream end doesn't officially transition to the `DONE` or `IDLE` states (as used +to gate various `stream.*` built-in calls) until wasm code is notified of the +corresponding result. In computing the `CopyResult` returned to wasm code, +`CANCELLED` takes precedence over `COMPLETED` and `DROPPED` takes precedence +over the other two. Finally, `CopyResult` is packed into an `i32` along with the +total number of elements that have been copied to/from the supplied `buffer`. +```python +class StreamEnd(End): + def notify(self, progress): + def stream_event(): + self.buffer = None + if self.other is None: + assert(self.state != End.State.DONE) + result = CopyResult.DROPPED + self.state = End.State.DONE + elif self.state == End.State.CANCELLING_COPY: + result = CopyResult.CANCELLED + self.state = End.State.IDLE + else: + assert(self.state == End.State.COPYING) + result = CopyResult.COMPLETED + self.state = End.State.IDLE + assert(0 <= result < 2**4) + assert(progress <= Buffer.MAX_LENGTH < 2**28) + packed_result = result | (progress << 4) + return (self.event_code, self.index, packed_result) + Waitable.set_pending_event(self, stream_event) +``` + +`FutureEnd.notify` is similar to `StreamEnd.notify`, but with two key +differences. First, the number of elements copied (which is either `0` or `1`) +is not packed into the high bits since it's always implied by the `CopyResult`. +Second, future ends immediately transition to the `DONE` state (where the only +valid operation is to call `future.drop-{readable,writable}`) when delivering +*both* `DROPPED` and `COMPLETED` results (unlike streams, which only transition +to `DONE` after delivering a `DROPPED` result). +```python +class FutureEnd(End): + def notify(self, progress): + assert(0 <= progress <= 1) + def future_event(): + if progress == 1: + assert(self.copying_or_cancelling()) + assert(self.buffer is None) + self.state = End.State.DONE + result = CopyResult.COMPLETED + elif self.other is None: + assert(self.state != End.State.DONE) + self.buffer = None + self.state = End.State.DONE + result = CopyResult.DROPPED + else: + assert(self.state == End.State.CANCELLING_COPY) + self.buffer = None + self.state = End.State.IDLE + result = CopyResult.CANCELLED + return (self.event_code, self.index, result) + Waitable.set_pending_event(self, future_event) +``` +Juxtaposing the two `{stream,future}_event` functions, we can see that the +"precedence" of `CopyResult`s for streams is `DROPPED` > `CANCELLED` > +`COMPLETED` whereas for futures the precedence is `COMPLETED` > `DROPPED` > +`CANCELLED`. This priority of `COMPLETED` reflects the fact that, for futures, +`COMPLETED` conveys more essential information. + +Lastly, the 4 concrete `{Readable,Writable}{Stream,Future}End` classes are +trivially defined by fixing the `is_read` argument of `copy`. Given these 4 +classes, the top-level `new_{stream,future}` functions (that are called by the +`{stream,future}.new` built-ins as well as by the host to create host streams +and futures) show that "streams" and "futures" are really just pairs of readable +and writable ends linked together. +```python +class ReadableStreamEnd(StreamEnd): + def copy(self, dst: WritableBuffer): + End.copy(self, dst, is_read = True) + +class WritableStreamEnd(StreamEnd): + def copy(self, src: ReadableBuffer): + End.copy(self, src, is_read = False) + +class ReadableFutureEnd(FutureEnd): + def copy(self, dst: WritableBuffer): + End.copy(self, dst, is_read = True) + +class WritableFutureEnd(FutureEnd): + def copy(self, src: ReadableBuffer): + End.copy(self, src, is_read = False) + +def new_stream(t: ValType, owner: Optional[ComponentInstance]): + reader = ReadableStreamEnd(t, owner, EventCode.STREAM_READ) + writer = WritableStreamEnd(t, owner, EventCode.STREAM_WRITE) + reader.other = writer + writer.other = reader + return (reader, writer) + +def new_future(t, owner: Optional[ComponentInstance]): + reader = ReadableFutureEnd(t, owner, EventCode.FUTURE_READ) + writer = WritableFutureEnd(t, owner, EventCode.FUTURE_WRITE) + reader.other = writer + writer.other = reader + return (reader, writer) ``` @@ -2009,6 +1899,15 @@ def contains(t, p): assert(False) ``` +The `none_or_number_type` predicate is used above for the temporary +same-instance stream/future copy restriction: +```python +def none_or_number_type(t): + return t is None or isinstance(t, U8Type | U16Type | U32Type | U64Type | + S8Type | S16Type | S32Type | S64Type | + F32Type | F64Type) +``` + ## Alignment Each value type is assigned an [alignment] which is used by subsequent @@ -2448,10 +2347,13 @@ transitively-borrowed handle. Streams and futures are entirely symmetric, transferring ownership of the readable end from the lifting component to the host or lowering component and -trapping if the readable end is in the middle of copying (which would create -a dangling-pointer situation) or is in the `DONE` state (in which case the only +trapping if the readable end is in the middle of copying (which would create a +dangling-pointer situation) or is in the `DONE` state (in which case the only valid operation is `{stream,future}.drop-{readable,writable}`) or in a waitable -set (in which case it must be removed first via `waitable.join(0)`). +set (in which case it must be removed first via `waitable.join(0)`). By clearing +the `owner` and `index` fields, the end becomes officially owned by the host. If +the lifted `End` is then passed into another component, `lower_async_value` will +transition ownership from the host into the receiving component instance. ```python def lift_stream(cx, i, t): return lift_async_value(ReadableStreamEnd, cx, i, t) @@ -2461,12 +2363,15 @@ def lift_future(cx, i, t): def lift_async_value(ReadableEndT, cx, i, t): assert(not contains_borrow(t)) - e = cx.inst.handles.remove(i) - trap_if(not isinstance(e, ReadableEndT)) - trap_if(e.shared.t != t) - trap_if(e.state != CopyState.IDLE) - trap_if(e.in_waitable_set()) - return e.shared + end = cx.inst.handles.remove(i) + trap_if(not isinstance(end, ReadableEndT)) + trap_if(end.t != t) + trap_if(end.state != End.State.IDLE) + trap_if(end.in_waitable_set()) + assert(end.owner is cx.inst and end.index == i) + end.owner = None + end.index = None + return end ``` @@ -2887,19 +2792,24 @@ type, the only thing the borrowed handle is good for is calling `resource.rep`, so lowering might as well avoid the overhead of creating an intermediate borrow handle. -Lowering a `stream` or `future` is entirely symmetric and simply adds a new -readable end to the current component instance's `handles` table, passing the -index of the new element to core wasm: +Lowering a `stream` or `future` simply adds the given readable end to the +current component instance's `handles` table, establishing unique ownership of +the end and passing the newly-allocated `handles`-table index to wasm code: ```python -def lower_stream(cx, v, t): - assert(isinstance(v, ReadableStream)) - assert(not contains_borrow(t)) - return cx.inst.handles.add(ReadableStreamEnd(v)) +def lower_stream(cx, end, t): + return lower_async_value(ReadableStreamEnd, cx, end, t) -def lower_future(cx, v, t): - assert(isinstance(v, ReadableFuture)) +def lower_future(cx, end, t): + return lower_async_value(ReadableFutureEnd, cx, end, t) + +def lower_async_value(ReadableEndT, cx, end, t): assert(not contains_borrow(t)) - return cx.inst.handles.add(ReadableFutureEnd(v)) + assert(isinstance(end, ReadableEndT)) + assert(end.t == t) + assert(end.state == End.State.IDLE) + end.owner = cx.inst + end.index = cx.inst.handles.add(end) + return end.index ``` @@ -3062,9 +2972,6 @@ class CoreValueIter: case 'f64': assert(isinstance(v, (int,float))) case _ : assert(False) return v - - def done(self): - return self.i == len(self.values) ``` The `match` is only used for spec-level assertions; no runtime typecase is required. @@ -4309,234 +4216,115 @@ above). def canon_stream_new(stream_t): inst = current_instance() trap_if(not inst.may_leave) - shared = SharedStreamImpl(stream_t.t) - ri = inst.handles.add(ReadableStreamEnd(shared)) - wi = inst.handles.add(WritableStreamEnd(shared)) - return [ ri | (wi << 32) ] + (readable_end, writable_end) = new_stream(stream_t.t, owner = inst) + readable_end.index = inst.handles.add(readable_end) + writable_end.index = inst.handles.add(writable_end) + return [ readable_end.index | (writable_end.index << 32) ] def canon_future_new(future_t): inst = current_instance() trap_if(not inst.may_leave) - shared = SharedFutureImpl(future_t.t) - ri = inst.handles.add(ReadableFutureEnd(shared)) - wi = inst.handles.add(WritableFutureEnd(shared)) - return [ ri | (wi << 32) ] + (readable_end, writable_end) = new_future(future_t.t, owner = inst) + readable_end.index = inst.handles.add(readable_end) + writable_end.index = inst.handles.add(writable_end) + return [ readable_end.index | (writable_end.index << 32) ] ``` -### ๐Ÿ”€ `canon stream.{read,write}` +### ๐Ÿ”€ `canon {stream,future}.{read,write}` For canonical definitions: ```wat -(canon stream.read $stream_t $opts (core func $f)) -(canon stream.write $stream_t $opts (core func $f)) +(canon stream.read $stream_t $opts (core func $stream_copy)) +(canon stream.write $stream_t $opts (core func $stream_copy)) +(canon future.read $future_t $opts (core func $future_copy)) +(canon future.write $future_t $opts (core func $future_copy)) ``` In addition to [general validation of `$opts`](#canonopt-validation) validation specifies: -* `$f` is given type `(func (param i32 T T) (result T))` where `T` is `i32` +* `$stream_copy` is given type `(func (param i32 T T) (result T))` where `T` is `i32` +* `$future_copy` is given type `(func (param i32 T) (result i32))` where `T` is `i32` * ๐Ÿ˜ - `T` is `i32` or `i64` as determined by the address type of `memory` from `$opts` (or `i32` by default if no `memory` is present) -* `$stream_t` must be a type of the form `(stream $t?)` +* `$stream_t`/`$future_t` must be a type of the form `(stream $t?)`/`(future $t?)` * If `$t` is present: - * [`lower($t)` above](#canonopt-validation) defines required options for `stream.write` - * [`lift($t)` above](#canonopt-validation) defines required options for `stream.read` + * [`lower($t)` above](#canonopt-validation) defines required options for `write` + * [`lift($t)` above](#canonopt-validation) defines required options for `read` * `memory` is required to be present * ๐Ÿš - `async` is allowed to be omitted, otherwise it must be present -The implementation of these built-ins funnels down to a single `stream_copy` -function that is parameterized by the direction of the copy: -```python -def canon_stream_read(stream_t, opts, i, ptr, n): - return stream_copy(ReadableStreamEnd, WritableBufferGuestImpl, EventCode.STREAM_READ, - stream_t, opts, i, ptr, n) - -def canon_stream_write(stream_t, opts, i, ptr, n): - return stream_copy(WritableStreamEnd, ReadableBufferGuestImpl, EventCode.STREAM_WRITE, - stream_t, opts, i, ptr, n) -``` - -Introducing the `stream_copy` function in chunks, first, the element at index -`i` is checked to be of the right type and allowed to start a new copy. (In the -future, the "trap if not `IDLE`" condition could be relaxed to allow multiple -pipelined reads or writes.) There is also a trap if attempting to synchronously -read or write from a stream that is already being asynchronously waited on via -waitable set. +The implementations of these 4 built-ins all funnel down to a single +parameterized `copy` function: ```python -def stream_copy(EndT, BufferT, event_code, stream_t, opts, i, ptr, n): - thread = current_thread() - trap_if(not thread.task.inst.may_leave) - e = thread.task.inst.handles.get(i) - trap_if(not isinstance(e, EndT)) - trap_if(e.shared.t != stream_t.t) - trap_if(e.state != CopyState.IDLE) - trap_if(e.in_waitable_set() and not opts.async_) -``` - -Then a readable or writable buffer is created which (in `Buffer`'s constructor) -eagerly checks the alignment and bounds of (`ptr`, `n`). (In the future, the -restriction on futures/streams containing `borrow`s could be relaxed by -maintaining sufficient bookkeeping state to ensure that borrowed handles *or -streams/futures of borrowed handles* could not outlive their originating call. -Additionally, `stream` will be allowed and defined to encode and decode -according to the `string-encoding`.) -```python - assert(not isinstance(stream_t, CharType)) - assert(not contains_borrow(stream_t)) - cx = LiftLowerContext(opts, thread.task.inst, borrow_scope = None) - buffer = BufferT(stream_t.t, cx, ptr, n) -``` - -Next, the `copy` method of `{Readable,Writable}{Stream,Future}End` is called to -perform the actual read/write. The `on_copy*` callbacks passed to `copy` bind -and store a `stream_event` closure on the readable/writable end (via the -inherited `Waitable.set_pending_event`) which will be called right before the -event is delivered to core wasm. `stream_event` first calls `reclaim_buffer` to -regain ownership of `buffer` and prevent any further partial reads/writes. -Thus, up until event delivery, the other end of the stream is free to -repeatedly read/write from/to `buffer`, ideally filling it up and minimizing -context switches. Next, the stream's `state` is updated based on the result -being delivered to core wasm so that, once a stream end has been notified that -the other end dropped, calling anything other than `stream.drop-*` traps. -Lastly, `stream_event` packs the `CopyResult` and number of elements copied up -until this point into a single `i32` or `i64`-sized payload for core wasm. The -size is determined by the `addrtype` coming from the [`memtype`] of the -`memory` immediate. Note that even though the number of elements copied is -packed into an `addrtype`, the maximum length of the buffer is fixed at `2^28 - 1` -independently of the `addrtype`. -```python - def stream_event(result, reclaim_buffer): - reclaim_buffer() - assert(e.copying()) - if result == CopyResult.DROPPED: - e.state = CopyState.DONE - else: - e.state = CopyState.IDLE - assert(0 <= result < 2**4) - assert(buffer.progress <= Buffer.MAX_LENGTH < 2**28) - packed_result = result | (buffer.progress << 4) - return (event_code, i, packed_result) - - def on_copy(reclaim_buffer): - e.set_pending_event(partial(stream_event, CopyResult.COMPLETED, reclaim_buffer)) - - def on_copy_done(result): - e.set_pending_event(partial(stream_event, result, reclaim_buffer = lambda:())) - - e.state = CopyState.COPYING - e.copy(thread.task.inst, buffer, on_copy, on_copy_done) -``` - -When this `copy` makes progress, a `stream_event` is set on the stream end's -`Waitable` base object. If `stream.{read,write}` is called synchronously, the -call suspends the current thread until an event is set, so that the event can -be returned. Otherwise, asynchronous calls deliver the event if it was produced -synchronously and return `BLOCKED` if not: -```python - if not e.has_pending_event(): - if not opts.async_: - e.wait_for_pending_event() - else: - return [BLOCKED] - code,index,payload = e.get_pending_event() - assert(code == event_code and index == i and payload != BLOCKED) - return [payload] -``` - +def canon_stream_read(stream_t, opts, i, ptr, length): + return copy(ReadableStreamEnd, WritableGuestBuffer, stream_t, opts, i, ptr, length) -### ๐Ÿ”€ `canon future.{read,write}` +def canon_stream_write(stream_t, opts, i, ptr, length): + return copy(WritableStreamEnd, ReadableGuestBuffer, stream_t, opts, i, ptr, length) -For canonical definitions: -```wat -(canon future.read $future_t $opts (core func $f)) -(canon future.write $future_t $opts (core func $f)) -``` -In addition to [general validation of `$opts`](#canonopt-validation) validation -specifies: -* `$f` is given type `(func (param i32 T) (result i32))` where `T` is `i32` - * ๐Ÿ˜ - `T` is `i32` or `i64` as determined by the address type of `memory` from - `$opts` (or `i32` by default if no `memory` is present) -* `$future_t` must be a type of the form `(future $t?)` -* If `$t` is present: - * [`lift($t)` above](#canonopt-validation) defines required options for `future.read` - * [`lower($t)` above](#canonopt-validation) defines required options for `future.write` - * `memory` is required to be present -* ๐Ÿš - `async` is allowed to be omitted, otherwise it must be present - -The implementation of these built-ins funnels down to a single `future_copy` -function that is parameterized by the direction of the copy: -```python def canon_future_read(future_t, opts, i, ptr): - return future_copy(ReadableFutureEnd, WritableBufferGuestImpl, EventCode.FUTURE_READ, - future_t, opts, i, ptr) + return copy(ReadableFutureEnd, WritableGuestBuffer, future_t, opts, i, ptr, 1) def canon_future_write(future_t, opts, i, ptr): - return future_copy(WritableFutureEnd, ReadableBufferGuestImpl, EventCode.FUTURE_WRITE, - future_t, opts, i, ptr) -``` + return copy(WritableFutureEnd, ReadableGuestBuffer, future_t, opts, i, ptr, 1) -Introducing the `future_copy` function in chunks, `future_copy` starts with the -same set of guards on the element `i` as `stream_copy`, except checking for a -*future* end instead of a *stream* end: -```python -def future_copy(EndT, BufferT, event_code, future_t, opts, i, ptr): +def copy(EndT, BufferT, stream_or_future_t, opts, i, ptr, length): thread = current_thread() trap_if(not thread.task.inst.may_leave) - e = thread.task.inst.handles.get(i) - trap_if(not isinstance(e, EndT)) - trap_if(e.shared.t != future_t.t) - trap_if(e.state != CopyState.IDLE) - trap_if(e.in_waitable_set() and not opts.async_) -``` - -Next, a readable or writable buffer is created, as with streams, except that the -buffer length is fixed to `1` and there is no validation-time prohibition on -`future`: -```python - assert(not contains_borrow(future_t)) + end = thread.task.inst.handles.get(i) + trap_if(not isinstance(end, EndT)) + trap_if(end.t != stream_or_future_t.t) + trap_if(end.state != End.State.IDLE) + trap_if(end.in_waitable_set() and not opts.async_) cx = LiftLowerContext(opts, thread.task.inst, borrow_scope = None) - buffer = BufferT(future_t.t, cx, ptr, 1) -``` - -Next, the `copy` method of `{Readable,Writable}FutureEnd.copy` is called to -perform the actual read/write. Other than the simplifications allowed by the -absence of repeated partial copies, the main difference in the following code -from the stream code is that `future_event` transitions the end to the `DONE` -state (in which the only valid operation is to call `future.drop-*`) on -*either* the `DROPPED` and `COMPLETED` results. This ensures that futures are -read/written at most once and futures are only passed to other components in a -state where they are ready to be read/written. Another important difference is -that, since the buffer length is always implied by the `CopyResult`, the number -of elements copied is not packed in the high 28 bits; they're always zero. -```python - def future_event(result): - assert((buffer.remain() == 0) == (result == CopyResult.COMPLETED)) - assert(e.copying()) - if result == CopyResult.DROPPED or result == CopyResult.COMPLETED: - e.state = CopyState.DONE - else: - e.state = CopyState.IDLE - return (event_code, i, result) - - def on_copy_done(result): - assert(result != CopyResult.DROPPED or event_code == EventCode.FUTURE_WRITE) - e.set_pending_event(partial(future_event, result)) - - e.state = CopyState.COPYING - e.copy(thread.task.inst, buffer, on_copy_done) -``` - -The end of `future_copy` is the exact same as `stream_copy`: waiting if called -synchronously and returning either the progress made or `BLOCKED`. -```python - if not e.has_pending_event(): + buffer = BufferT(end.t, cx, ptr, length) + end.copy(buffer) + if not end.has_pending_event(): if not opts.async_: - e.wait_for_pending_event() + end.wait_for_pending_event() else: return [BLOCKED] - code,index,payload = e.get_pending_event() - assert(code == event_code and index == i) + code,index,payload = end.get_pending_event() + assert(code == end.event_code and index == i and payload != BLOCKED) return [payload] ``` +First, the `i`th handle is checked to have the right type and to be in the +`IDLE` state. There is also a trap if attempting to synchronously read or write +from a stream or future end that is already being asynchronously waited on via +waitable set, as this might result in the waitable set "stealing" an event from +the synchronous operation, leaving it hung. After these, a readable or writable +buffer is created which (in `GuestBuffer`'s constructor) also eagerly guards the +alignment and bounds of (`ptr`, `length`). The `Buffer` object captures the +`$opts` immediate passed to `{stream,future}.{read,write}` so that subsequent +lifting and lowering of elements is well-defined to use these same `$opts`. + +Next, `end.copy(buffer)` is called to actually perform the copy. The `End.copy` +method never blocks: if it's able to make some progress without blocking, it +returns with `end.has_pending_event()` set to true, otherwise it returns +immediately without blocking with `end.has_pending_event()` set to false. In the +latter case, the copy operation will execute in the background until either +progress is made, which will set `end.has_pending_event()`, or wasm code calls +`{stream,future}.cancel-{read,write}` to cancel the copy operation. While the +copy is executing, wasm code must keep the (`ptr`, `length`) region stored in +`buffer` available, since it will be concurrently read from or written into. +Once `end.has_pending_event()` is true and `end.get_pending_event()` is called, +ownership of (`ptr`, `length`) will be returned. (This "ownership" is conceptual +and not enforced; if wasm code uses a buffer region that is conceptually "owned" +by a copy operation, it will just result in racy loads/stores, not a trap.) + +If `end.has_pending_event()` is true before returning, the event is +synchronously "delivered" and the `i32` event `payload` (containing a +`CopyResult` code packed with, for streams, the number of elements copied) is +the return value of `{stream,future}.{read,write}`. This `payload` value is +computed by the `{stream,future}_event` functions, defined above, which may also +transition `end.state` to `DONE` based on the `CopyResult`. + +If `{stream,future}.{read,write}` is called without `async` set in `$opts`, the +call blocks until `end.has_pending_event()` is true and thus always returns the +event `payload`. Otherwise, if `end.has_pending_event()` is false, the call +immediately returns the sentinel `BLOCKED` code (`-1`) and the caller must add +`end` to a waitable set and call `waitable-set.{wait,poll}` or return to a +`callback` event loop to be notified of progress. ### ๐Ÿ”€ `canon {stream,future}.cancel-{read,write}` @@ -4553,68 +4341,62 @@ validation specifies: * `$stream_t`/`$future_t` must be a type of the form `(stream $t?)`/`(future $t?)` * ๐Ÿš - `async` is allowed (otherwise it must be absent) -The implementation of these four built-ins all funnel down to a single +The implementations of these 4 built-ins all funnel down to a single parameterized `cancel_copy` function: ```python def canon_stream_cancel_read(stream_t, async_, i): - return cancel_copy(ReadableStreamEnd, EventCode.STREAM_READ, stream_t, async_, i) + return cancel_copy(ReadableStreamEnd, stream_t, async_, i) def canon_stream_cancel_write(stream_t, async_, i): - return cancel_copy(WritableStreamEnd, EventCode.STREAM_WRITE, stream_t, async_, i) + return cancel_copy(WritableStreamEnd, stream_t, async_, i) def canon_future_cancel_read(future_t, async_, i): - return cancel_copy(ReadableFutureEnd, EventCode.FUTURE_READ, future_t, async_, i) + return cancel_copy(ReadableFutureEnd, future_t, async_, i) def canon_future_cancel_write(future_t, async_, i): - return cancel_copy(WritableFutureEnd, EventCode.FUTURE_WRITE, future_t, async_, i) + return cancel_copy(WritableFutureEnd, future_t, async_, i) -def cancel_copy(EndT, event_code, stream_or_future_t, async_, i): +def cancel_copy(EndT, stream_or_future_t, async_, i): thread = current_thread() trap_if(not thread.task.inst.may_leave) - e = thread.task.inst.handles.get(i) - trap_if(not isinstance(e, EndT)) - trap_if(e.shared.t != stream_or_future_t.t) - trap_if(e.state != CopyState.COPYING or e.has_sync_waiter) - trap_if(e.in_waitable_set() and not async_) - e.state = CopyState.CANCELLING_COPY - if not e.has_pending_event(): - e.shared.cancel() - if not e.has_pending_event(): - if not async_: - e.wait_for_pending_event() - else: - return [BLOCKED] - code,index,payload = e.get_pending_event() - assert(not e.copying() and code == event_code and index == i) + end = thread.task.inst.handles.get(i) + trap_if(not isinstance(end, EndT)) + trap_if(end.t != stream_or_future_t.t) + trap_if(end.state != End.State.COPYING) + trap_if(end.has_sync_waiter) + trap_if(end.in_waitable_set() and not async_) + end.cancel() + if not end.has_pending_event(): + if not async_: + end.wait_for_pending_event() + else: + return [BLOCKED] + code,index,payload = end.get_pending_event() + assert(not end.copying_or_cancelling()) + assert(code == end.event_code and index == i) return [payload] ``` -Cancellation traps if there is not currently an async copy in progress (sync -copies do not expect or check for cancellation and thus cannot be cancelled, and -repeatedly cancelling the same async copy after the first call blocked is not -allowed). There is also a trap if attempting to synchronously cancel a stream -operation when the stream end is already being asynchronously waited on by a -waitable set. - -The *first* check for `e.has_pending_event()` catches the case where the copy has -already racily finished, in which case we must *not* call `cancel()`. Calling -`cancel()` may, but is not required to, recursively call one of the `on_*` -callbacks (passed by `canon_{stream,future}_{read,write}` above) which will set -a pending event that is caught by the *second* check for -`e.has_pending_event()`. - -If the copy hasn't been cancelled, the synchronous case suspends the thread to -wait for one of the `on_*` callbacks to eventually be called (which will set -the pending event). - -The asynchronous case simply returns `BLOCKED` and the client code must wait -as usual for a `{STREAM,FUTURE}_{READ,WRITE}` event. In this case, cancellation -has served only to asynchronously request that the host relinquish the buffer -ASAP without waiting for anything to be read or written. - -If `BLOCKED` is *not* returned, the pending event (which is necessarily a -`stream_event` or `future_event`) is eagerly delivered to core wasm as the return value, thereby -saving an additional turn of the event loop. In this case, the core wasm -caller can assume that ownership of the buffer has been returned. +Cancellation traps if the given index `i` has the wrong type or if there is not +an asynchronous copy in progress (sync copies do not expect or check for +cancellation and thus cannot be cancelled). Repeatedly cancelling the same async +copy (after the first call blocks) also traps. Lastly, there is a trap if +attempting to synchronously cancel a stream or future operation when the end is +already being asynchronously waited on by a waitable set. + +After these guards, `end.cancel()` is called to request the cancellation. Like +`End.copy`, the `End.cancel` method never blocks: if it's able to make some +progress without blocking, it returns with `end.has_pending_event()` set to +true, otherwise it returns immediately without blocking with +`end.has_pending_event()` set to false. If `{stream,future}.cancel-{read,write}` +is called without the `$async` immediate set, the call blocks until +`end.has_pending_event()` is true and thus always returns an event `payload`. +Otherwise, if `end.has_pending_event()` is false, the call immediately returns +the sentinel `BLOCKED` code (`-1`) and the caller must add `end` to a waitable +set and call `waitable-set.{wait,poll}` or return to a `callback` event loop to +be notified of progress. + +See `stream_event` and `future_event` definitions above for how the returned +`payload` is computed. ### ๐Ÿ”€ `canon {stream,future}.drop-{readable,writable}` @@ -4630,30 +4412,36 @@ validation specifies: * `$f` is given type `(func (param i32))` * `$stream_t`/`$future_t` must be a type of the form `(stream $t?)`/`(future $t?)` -Calling `$f` removes the readable or writable end of the stream or future at -the given index from the current component instance's `handles` table, -performing the guards and bookkeeping defined by -`{Readable,Writable}{Stream,Future}End.drop()` above. +Calling `$f` drops the readable or writable end of a stream or future at the +given index from the current component instance's `handles` table after checking +that the index is valid, has the right type, and the end is not in the middle of +a copy operation. Additionally, dropping the writable end of a future traps if a +value has not been written and the writable end hasn't already been notified that +the readable end was dropped. Lastly, the `End.drop` method is called to notify +the other end of the stream or future and also perform the waitable set +bookkeeping updates in `Waitable.drop`. ```python def canon_stream_drop_readable(stream_t, i): return drop(ReadableStreamEnd, stream_t, i) -def canon_stream_drop_writable(stream_t, hi): - return drop(WritableStreamEnd, stream_t, hi) +def canon_stream_drop_writable(stream_t, i): + return drop(WritableStreamEnd, stream_t, i) def canon_future_drop_readable(future_t, i): return drop(ReadableFutureEnd, future_t, i) -def canon_future_drop_writable(future_t, hi): - return drop(WritableFutureEnd, future_t, hi) +def canon_future_drop_writable(future_t, i): + return drop(WritableFutureEnd, future_t, i) -def drop(EndT, stream_or_future_t, hi): +def drop(EndT, stream_or_future_t, i): inst = current_instance() trap_if(not inst.may_leave) - e = inst.handles.remove(hi) - trap_if(not isinstance(e, EndT)) - trap_if(e.shared.t != stream_or_future_t.t) - e.drop() + end = inst.handles.remove(i) + trap_if(not isinstance(end, EndT)) + trap_if(end.t != stream_or_future_t.t) + trap_if(end.copying_or_cancelling()) + trap_if(isinstance(end, WritableFutureEnd) and end.state != End.State.DONE) + end.drop() return [] ``` @@ -5118,8 +4906,8 @@ def canon_thread_available_parallelism(): [Blocked]: Concurrency.md#blocking [Waiting On External I/O And Yielding]: Concurrency.md#blocking [Subtasks]: Concurrency.md#subtasks-and-supertasks +[End]: Concurrency.md#streams-and-futures [Readable and Writable Ends]: Concurrency.md#streams-and-futures -[Readable or Writable End]: Concurrency.md#streams-and-futures [Thread-Local Storage]: Concurrency.md#thread-local-storage [Cancellation]: Concurrency.md#cancellation [Subtask State Machine]: Concurrency.md#cancellation diff --git a/design/mvp/Concurrency.md b/design/mvp/Concurrency.md index b6e69663..08b9282b 100644 --- a/design/mvp/Concurrency.md +++ b/design/mvp/Concurrency.md @@ -600,8 +600,8 @@ and writable ends of streams and futures can then be progress signals *completion* of a read or write (i.e., the bytes have already been copied into the buffer). Additionally, *readiness* (to perform a read or write in the future) can be queried and signalled by performing a `0`-length -read or write (see the [Stream State] section in the Canonical ABI explainer -for details). +read or write (see the [Stream and Future State] section in the Canonical ABI +explainer for details). As a temporary limitation, if a `read` and `write` for a single stream or future occur from within the same component and the element type is a @@ -619,8 +619,8 @@ without requiring an explicit `future` return type. Thus, a function like which point the caller receives the readable end of a `future` that, when successfully read, conveys the completion of a second event. -The [Stream State] and [Future State] sections describe the runtime state -maintained for streams and futures by the Canonical ABI. +The [Stream and Future State] section describes the runtime state maintained for +streams and futures by the Canonical ABI. ### Stream Readiness @@ -1574,8 +1574,7 @@ the concurrency story: [`ComponentInstance`]: CanonicalABI.md#component-instances [`Thread`]: CanonicalABI.md#threads [`Task`]: CanonicalABI.md#tasks -[Stream State]: CanonicalABI.md#stream-state -[Future State]: CanonicalABI.md#future-state +[Stream and Future State]: CanonicalABI.md#stream-and-future-state [Binary Format]: Binary.md [WIT]: WIT.md diff --git a/design/mvp/Explainer.md b/design/mvp/Explainer.md index b290b14e..b0cee058 100644 --- a/design/mvp/Explainer.md +++ b/design/mvp/Explainer.md @@ -2005,7 +2005,7 @@ the buffer parameter is ignored. If the return value is a `stream-result`, then the `progress` field indicates how many `T` elements were read or written from the given buffer before the `copy-result` was reached. For example, a return value of `{progress: 4, -result: dropped}` from a `stream.read` means that 32 bytes were copied +result: dropped}` from a `stream.read` means that 16 bytes were copied into the given buffer before the writer end dropped the stream. The `cancelled` case can only arise as the result of a call to `stream.cancel-{read,write}`. @@ -3419,8 +3419,8 @@ For some use-case-focused, worked examples, see: [`canon_waitable_set_drop`]: CanonicalABI.md#-canon-waitable-setdrop [`canon_waitable_join`]: CanonicalABI.md#-canon-waitablejoin [`canon_stream_new`]: CanonicalABI.md#-canon-streamfuturenew -[`canon_stream_read`]: CanonicalABI.md#-canon-streamreadwrite -[`canon_future_read`]: CanonicalABI.md#-canon-futurereadwrite +[`canon_stream_read`]: CanonicalABI.md#-canon-streamfuturereadwrite +[`canon_future_read`]: CanonicalABI.md#-canon-streamfuturereadwrite [`canon_stream_cancel_read`]: CanonicalABI.md#-canon-streamfuturecancel-readwrite [`canon_stream_drop_readable`]: CanonicalABI.md#-canon-streamfuturedrop-readablewritable [`canon_subtask_cancel`]: CanonicalABI.md#-canon-subtaskcancel diff --git a/design/mvp/canonical-abi/definitions.py b/design/mvp/canonical-abi/definitions.py index 08f5109a..b5d4e2c4 100644 --- a/design/mvp/canonical-abi/definitions.py +++ b/design/mvp/canonical-abi/definitions.py @@ -6,7 +6,6 @@ from __future__ import annotations from dataclasses import dataclass -from functools import partial from typing import Any, Optional, Callable, TypeVar, Generic, Literal from enum import Enum, IntEnum import math @@ -861,7 +860,6 @@ class Buffer: MAX_LENGTH = 2**28 - 1 t: ValType remain: Callable[[], int] - is_zero_length: Callable[[], bool] class ReadableBuffer(Buffer): read: Callable[[int], list[any]] @@ -869,7 +867,7 @@ class ReadableBuffer(Buffer): class WritableBuffer(Buffer): write: Callable[[list[any]]] -class BufferGuestImpl(Buffer): +class GuestBuffer(Buffer): cx: LiftLowerContext t: ValType ptr: int @@ -890,10 +888,7 @@ def __init__(self, t, cx, ptr, length): def remain(self): return self.length - self.progress - def is_zero_length(self): - return self.length == 0 - -class ReadableBufferGuestImpl(BufferGuestImpl, ReadableBuffer): +class ReadableGuestBuffer(GuestBuffer, ReadableBuffer): def read(self, n): assert(n <= self.remain()) if self.t: @@ -904,7 +899,7 @@ def read(self, n): self.progress += n return vs -class WritableBufferGuestImpl(BufferGuestImpl, WritableBuffer): +class WritableGuestBuffer(GuestBuffer, WritableBuffer): def write(self, vs): assert(len(vs) <= self.remain()) if self.t: @@ -914,217 +909,157 @@ def write(self, vs): assert(all(v == () for v in vs)) self.progress += len(vs) -### Stream State - -class CopyResult(IntEnum): - COMPLETED = 0 - DROPPED = 1 - CANCELLED = 2 +### Stream and Future State -ReclaimBuffer = Callable[[], None] -OnCopy = Callable[[ReclaimBuffer], None] -OnCopyDone = Callable[[CopyResult], None] +class End(Waitable): + class State(Enum): + IDLE = 1 + COPYING = 2 + CANCELLING_COPY = 3 + DONE = 4 -class SharedBase: t: ValType - cancel: Callable[[], None] - drop: Callable[[], None] - -class ReadableStream(SharedBase): - read: Callable[[ComponentInstance, WritableBuffer, OnCopy, OnCopyDone], None] - -class WritableStream(SharedBase): - write: Callable[[ComponentInstance, ReadableBuffer, OnCopy, OnCopyDone], None] - -class SharedStreamImpl(ReadableStream, WritableStream): - dropped: bool - pending_inst: Optional[ComponentInstance] - pending_buffer: Optional[Buffer] - pending_on_copy: Optional[OnCopy] - pending_on_copy_done: Optional[OnCopyDone] + state: State + other: Optional[End] + buffer: Optional[Buffer] + owner: Optional[ComponentInstance] + index: Optional[int] + event_code: EventCode - def __init__(self, t): + def __init__(self, t, owner, event_code): + Waitable.__init__(self) self.t = t - self.dropped = False - self.reset_pending() - - def reset_pending(self): - self.set_pending(None, None, None, None) - - def set_pending(self, inst, buffer, on_copy, on_copy_done): - self.pending_inst = inst - self.pending_buffer = buffer - self.pending_on_copy = on_copy - self.pending_on_copy_done = on_copy_done - - def reset_and_notify_pending(self, result): - pending_on_copy_done = self.pending_on_copy_done - self.reset_pending() - pending_on_copy_done(result) - - def cancel(self): - self.reset_and_notify_pending(CopyResult.CANCELLED) - - def drop(self): - if not self.dropped: - self.dropped = True - if self.pending_buffer: - self.reset_and_notify_pending(CopyResult.DROPPED) - - def read(self, inst, dst_buffer, on_copy, on_copy_done): - if self.dropped: - on_copy_done(CopyResult.DROPPED) - elif not self.pending_buffer: - self.set_pending(inst, dst_buffer, on_copy, on_copy_done) - else: - assert(self.t == dst_buffer.t == self.pending_buffer.t) - trap_if(inst is self.pending_inst and not none_or_number_type(self.t)) # temporary - if self.pending_buffer.remain() > 0: - if dst_buffer.remain() > 0: - n = min(dst_buffer.remain(), self.pending_buffer.remain()) - dst_buffer.write(self.pending_buffer.read(n)) - self.pending_on_copy(self.reset_pending) - on_copy_done(CopyResult.COMPLETED) + self.state = End.State.IDLE + self.other = None + self.buffer = None + self.owner = owner + self.index = None + self.event_code = event_code + + def copy(self, buffer: Buffer, is_read: bool): + assert(self.buffer is None) + self.state = End.State.COPYING + if self.other is None: + self.notify(progress = 0) + elif self.other.buffer is None: + self.buffer = buffer + elif buffer.remain() > 0 and self.other.buffer.remain() > 0: + trap_if(self.owner and self.owner is self.other.owner and not none_or_number_type(self.t)) + n = min(buffer.remain(), self.other.buffer.remain()) + if is_read: + buffer.write(self.other.buffer.read(n)) else: - self.reset_and_notify_pending(CopyResult.COMPLETED) - self.set_pending(inst, dst_buffer, on_copy, on_copy_done) - - def write(self, inst, src_buffer, on_copy, on_copy_done): - if self.dropped: - on_copy_done(CopyResult.DROPPED) - elif not self.pending_buffer: - self.set_pending(inst, src_buffer, on_copy, on_copy_done) + self.other.buffer.write(buffer.read(n)) + self.notify(buffer.progress) + self.other.notify(self.other.buffer.progress) + if self.other.buffer.remain() == 0: + self.other.buffer = None + elif buffer.remain() > 0 or (is_read and self.other.buffer.remain() == 0): + self.other.notify(progress = 0) + self.other.buffer = None + self.buffer = buffer else: - assert(self.t == src_buffer.t == self.pending_buffer.t) - trap_if(inst is self.pending_inst and not none_or_number_type(self.t)) # temporary - if self.pending_buffer.remain() > 0: - if src_buffer.remain() > 0: - n = min(src_buffer.remain(), self.pending_buffer.remain()) - self.pending_buffer.write(src_buffer.read(n)) - self.pending_on_copy(self.reset_pending) - on_copy_done(CopyResult.COMPLETED) - elif src_buffer.is_zero_length() and self.pending_buffer.is_zero_length(): - on_copy_done(CopyResult.COMPLETED) - else: - self.reset_and_notify_pending(CopyResult.COMPLETED) - self.set_pending(inst, src_buffer, on_copy, on_copy_done) - -def none_or_number_type(t): - return t is None or isinstance(t, U8Type | U16Type | U32Type | U64Type | - S8Type | S16Type | S32Type | S64Type | - F32Type | F64Type) - -class CopyState(Enum): - IDLE = 1 - COPYING = 2 - CANCELLING_COPY = 3 - DONE = 4 - -class CopyEnd(Waitable): - state: CopyState - shared: SharedBase - - def __init__(self, shared): - Waitable.__init__(self) - self.state = CopyState.IDLE - self.shared = shared - - def copying(self): - match self.state: - case CopyState.IDLE | CopyState.DONE: - return False - case CopyState.COPYING | CopyState.CANCELLING_COPY: - return True - assert(False) - - def drop(self): - trap_if(self.copying()) - self.shared.drop() - Waitable.drop(self) - -class ReadableStreamEnd(CopyEnd): - def copy(self, inst, dst, on_copy, on_copy_done): - self.shared.read(inst, dst, on_copy, on_copy_done) - -class WritableStreamEnd(CopyEnd): - def copy(self, inst, src, on_copy, on_copy_done): - self.shared.write(inst, src, on_copy, on_copy_done) - -### Future State - -class ReadableFuture(SharedBase): - read: Callable[[ComponentInstance, WritableBuffer, OnCopyDone], None] - -class WritableFuture(SharedBase): - write: Callable[[ComponentInstance, ReadableBuffer, OnCopyDone], None] - -class SharedFutureImpl(ReadableFuture, WritableFuture): - dropped: bool - pending_inst: Optional[ComponentInstance] - pending_buffer: Optional[Buffer] - pending_on_copy_done: Optional[OnCopyDone] - - def __init__(self, t): - self.t = t - self.dropped = False - self.reset_pending() - - def reset_pending(self): - self.set_pending(None, None, None) - - def set_pending(self, inst, buffer, on_copy_done): - self.pending_inst = inst - self.pending_buffer = buffer - self.pending_on_copy_done = on_copy_done - - def reset_and_notify_pending(self, result): - pending_on_copy_done = self.pending_on_copy_done - self.reset_pending() - pending_on_copy_done(result) + self.notify(progress = 0) def cancel(self): - self.reset_and_notify_pending(CopyResult.CANCELLED) + assert(self.state == End.State.COPYING) + self.state = End.State.CANCELLING_COPY + if (not self.has_pending_event() + and (self.other.owner is not None + or DETERMINISTIC_PROFILE + or random.randint(0,1))): + self.notify(progress = 0) def drop(self): - if not self.dropped: - self.dropped = True - if self.pending_buffer: - assert(isinstance(self.pending_buffer, ReadableBuffer)) - self.reset_and_notify_pending(CopyResult.DROPPED) - - def read(self, inst, dst_buffer, on_copy_done): - assert(not self.dropped and dst_buffer.remain() == 1) - if not self.pending_buffer: - self.set_pending(inst, dst_buffer, on_copy_done) - else: - trap_if(inst is self.pending_inst and not none_or_number_type(self.t)) # temporary - dst_buffer.write(self.pending_buffer.read(1)) - self.reset_and_notify_pending(CopyResult.COMPLETED) - on_copy_done(CopyResult.COMPLETED) - - def write(self, inst, src_buffer, on_copy_done): - assert(src_buffer.remain() == 1) - if self.dropped: - on_copy_done(CopyResult.DROPPED) - elif not self.pending_buffer: - self.set_pending(inst, src_buffer, on_copy_done) - else: - trap_if(inst is self.pending_inst and not none_or_number_type(self.t)) # temporary - self.pending_buffer.write(src_buffer.read(1)) - self.reset_and_notify_pending(CopyResult.COMPLETED) - on_copy_done(CopyResult.COMPLETED) + assert(not self.copying_or_cancelling()) + if self.other is not None: + assert(self is self.other.other) + self.other.other = None + if self.other.copying_or_cancelling() and not self.other.has_pending_event(): + self.other.notify(progress = 0) + self.other = None + Waitable.drop(self) -class ReadableFutureEnd(CopyEnd): - def copy(self, inst, dst_buffer, on_copy_done): - self.shared.read(inst, dst_buffer, on_copy_done) + def copying_or_cancelling(self): + return self.state in { End.State.COPYING, End.State.CANCELLING_COPY } -class WritableFutureEnd(CopyEnd): - def copy(self, inst, src_buffer, on_copy_done): - self.shared.write(inst, src_buffer, on_copy_done) +class CopyResult(IntEnum): + COMPLETED = 0 + DROPPED = 1 + CANCELLED = 2 - def drop(self): - trap_if(self.state != CopyState.DONE) - CopyEnd.drop(self) +class StreamEnd(End): + def notify(self, progress): + def stream_event(): + self.buffer = None + if self.other is None: + assert(self.state != End.State.DONE) + result = CopyResult.DROPPED + self.state = End.State.DONE + elif self.state == End.State.CANCELLING_COPY: + result = CopyResult.CANCELLED + self.state = End.State.IDLE + else: + assert(self.state == End.State.COPYING) + result = CopyResult.COMPLETED + self.state = End.State.IDLE + assert(0 <= result < 2**4) + assert(progress <= Buffer.MAX_LENGTH < 2**28) + packed_result = result | (progress << 4) + return (self.event_code, self.index, packed_result) + Waitable.set_pending_event(self, stream_event) + +class FutureEnd(End): + def notify(self, progress): + assert(0 <= progress <= 1) + def future_event(): + if progress == 1: + assert(self.copying_or_cancelling()) + assert(self.buffer is None) + self.state = End.State.DONE + result = CopyResult.COMPLETED + elif self.other is None: + assert(self.state != End.State.DONE) + self.buffer = None + self.state = End.State.DONE + result = CopyResult.DROPPED + else: + assert(self.state == End.State.CANCELLING_COPY) + self.buffer = None + self.state = End.State.IDLE + result = CopyResult.CANCELLED + return (self.event_code, self.index, result) + Waitable.set_pending_event(self, future_event) + +class ReadableStreamEnd(StreamEnd): + def copy(self, dst: WritableBuffer): + End.copy(self, dst, is_read = True) + +class WritableStreamEnd(StreamEnd): + def copy(self, src: ReadableBuffer): + End.copy(self, src, is_read = False) + +class ReadableFutureEnd(FutureEnd): + def copy(self, dst: WritableBuffer): + End.copy(self, dst, is_read = True) + +class WritableFutureEnd(FutureEnd): + def copy(self, src: ReadableBuffer): + End.copy(self, src, is_read = False) + +def new_stream(t: ValType, owner: Optional[ComponentInstance]): + reader = ReadableStreamEnd(t, owner, EventCode.STREAM_READ) + writer = WritableStreamEnd(t, owner, EventCode.STREAM_WRITE) + reader.other = writer + writer.other = reader + return (reader, writer) + +def new_future(t, owner: Optional[ComponentInstance]): + reader = ReadableFutureEnd(t, owner, EventCode.FUTURE_READ) + writer = WritableFutureEnd(t, owner, EventCode.FUTURE_WRITE) + reader.other = writer + writer.other = reader + return (reader, writer) ## Despecialization @@ -1163,6 +1098,11 @@ def contains(t, p): case _: assert(False) +def none_or_number_type(t): + return t is None or isinstance(t, U8Type | U16Type | U32Type | U64Type | + S8Type | S16Type | S32Type | S64Type | + F32Type | F64Type) + ## Alignment @@ -1471,12 +1411,15 @@ def lift_future(cx, i, t): def lift_async_value(ReadableEndT, cx, i, t): assert(not contains_borrow(t)) - e = cx.inst.handles.remove(i) - trap_if(not isinstance(e, ReadableEndT)) - trap_if(e.shared.t != t) - trap_if(e.state != CopyState.IDLE) - trap_if(e.in_waitable_set()) - return e.shared + end = cx.inst.handles.remove(i) + trap_if(not isinstance(end, ReadableEndT)) + trap_if(end.t != t) + trap_if(end.state != End.State.IDLE) + trap_if(end.in_waitable_set()) + assert(end.owner is cx.inst and end.index == i) + end.owner = None + end.index = None + return end ## Storing @@ -1767,15 +1710,20 @@ def lower_borrow(cx, rep, t): h.borrow_scope.num_borrows += 1 return cx.inst.handles.add(h) -def lower_stream(cx, v, t): - assert(isinstance(v, ReadableStream)) - assert(not contains_borrow(t)) - return cx.inst.handles.add(ReadableStreamEnd(v)) +def lower_stream(cx, end, t): + return lower_async_value(ReadableStreamEnd, cx, end, t) + +def lower_future(cx, end, t): + return lower_async_value(ReadableFutureEnd, cx, end, t) -def lower_future(cx, v, t): - assert(isinstance(v, ReadableFuture)) +def lower_async_value(ReadableEndT, cx, end, t): assert(not contains_borrow(t)) - return cx.inst.handles.add(ReadableFutureEnd(v)) + assert(isinstance(end, ReadableEndT)) + assert(end.t == t) + assert(end.state == End.State.IDLE) + end.owner = cx.inst + end.index = cx.inst.handles.add(end) + return end.index ## Flattening @@ -1883,9 +1831,6 @@ def next(self, t): case _ : assert(False) return v - def done(self): - return self.i == len(self.values) - def lift_flat(cx, vi, t): match despecialize(t): case BoolType() : return convert_int_to_bool(vi.next('i32')) @@ -2451,153 +2396,85 @@ def canon_subtask_drop(i): def canon_stream_new(stream_t): inst = current_instance() trap_if(not inst.may_leave) - shared = SharedStreamImpl(stream_t.t) - ri = inst.handles.add(ReadableStreamEnd(shared)) - wi = inst.handles.add(WritableStreamEnd(shared)) - return [ ri | (wi << 32) ] + (readable_end, writable_end) = new_stream(stream_t.t, owner = inst) + readable_end.index = inst.handles.add(readable_end) + writable_end.index = inst.handles.add(writable_end) + return [ readable_end.index | (writable_end.index << 32) ] def canon_future_new(future_t): inst = current_instance() trap_if(not inst.may_leave) - shared = SharedFutureImpl(future_t.t) - ri = inst.handles.add(ReadableFutureEnd(shared)) - wi = inst.handles.add(WritableFutureEnd(shared)) - return [ ri | (wi << 32) ] + (readable_end, writable_end) = new_future(future_t.t, owner = inst) + readable_end.index = inst.handles.add(readable_end) + writable_end.index = inst.handles.add(writable_end) + return [ readable_end.index | (writable_end.index << 32) ] ### ๐Ÿ”€ `canon stream.{read,write}` -def canon_stream_read(stream_t, opts, i, ptr, n): - return stream_copy(ReadableStreamEnd, WritableBufferGuestImpl, EventCode.STREAM_READ, - stream_t, opts, i, ptr, n) - -def canon_stream_write(stream_t, opts, i, ptr, n): - return stream_copy(WritableStreamEnd, ReadableBufferGuestImpl, EventCode.STREAM_WRITE, - stream_t, opts, i, ptr, n) - -def stream_copy(EndT, BufferT, event_code, stream_t, opts, i, ptr, n): - thread = current_thread() - trap_if(not thread.task.inst.may_leave) - e = thread.task.inst.handles.get(i) - trap_if(not isinstance(e, EndT)) - trap_if(e.shared.t != stream_t.t) - trap_if(e.state != CopyState.IDLE) - trap_if(e.in_waitable_set() and not opts.async_) - - assert(not isinstance(stream_t, CharType)) - assert(not contains_borrow(stream_t)) - cx = LiftLowerContext(opts, thread.task.inst, borrow_scope = None) - buffer = BufferT(stream_t.t, cx, ptr, n) - - def stream_event(result, reclaim_buffer): - reclaim_buffer() - assert(e.copying()) - if result == CopyResult.DROPPED: - e.state = CopyState.DONE - else: - e.state = CopyState.IDLE - assert(0 <= result < 2**4) - assert(buffer.progress <= Buffer.MAX_LENGTH < 2**28) - packed_result = result | (buffer.progress << 4) - return (event_code, i, packed_result) - - def on_copy(reclaim_buffer): - e.set_pending_event(partial(stream_event, CopyResult.COMPLETED, reclaim_buffer)) +def canon_stream_read(stream_t, opts, i, ptr, length): + return copy(ReadableStreamEnd, WritableGuestBuffer, stream_t, opts, i, ptr, length) - def on_copy_done(result): - e.set_pending_event(partial(stream_event, result, reclaim_buffer = lambda:())) - - e.state = CopyState.COPYING - e.copy(thread.task.inst, buffer, on_copy, on_copy_done) - - if not e.has_pending_event(): - if not opts.async_: - e.wait_for_pending_event() - else: - return [BLOCKED] - code,index,payload = e.get_pending_event() - assert(code == event_code and index == i and payload != BLOCKED) - return [payload] - -### ๐Ÿ”€ `canon future.{read,write}` +def canon_stream_write(stream_t, opts, i, ptr, length): + return copy(WritableStreamEnd, ReadableGuestBuffer, stream_t, opts, i, ptr, length) def canon_future_read(future_t, opts, i, ptr): - return future_copy(ReadableFutureEnd, WritableBufferGuestImpl, EventCode.FUTURE_READ, - future_t, opts, i, ptr) + return copy(ReadableFutureEnd, WritableGuestBuffer, future_t, opts, i, ptr, 1) def canon_future_write(future_t, opts, i, ptr): - return future_copy(WritableFutureEnd, ReadableBufferGuestImpl, EventCode.FUTURE_WRITE, - future_t, opts, i, ptr) + return copy(WritableFutureEnd, ReadableGuestBuffer, future_t, opts, i, ptr, 1) -def future_copy(EndT, BufferT, event_code, future_t, opts, i, ptr): +def copy(EndT, BufferT, stream_or_future_t, opts, i, ptr, length): thread = current_thread() trap_if(not thread.task.inst.may_leave) - e = thread.task.inst.handles.get(i) - trap_if(not isinstance(e, EndT)) - trap_if(e.shared.t != future_t.t) - trap_if(e.state != CopyState.IDLE) - trap_if(e.in_waitable_set() and not opts.async_) - - assert(not contains_borrow(future_t)) + end = thread.task.inst.handles.get(i) + trap_if(not isinstance(end, EndT)) + trap_if(end.t != stream_or_future_t.t) + trap_if(end.state != End.State.IDLE) + trap_if(end.in_waitable_set() and not opts.async_) cx = LiftLowerContext(opts, thread.task.inst, borrow_scope = None) - buffer = BufferT(future_t.t, cx, ptr, 1) - - def future_event(result): - assert((buffer.remain() == 0) == (result == CopyResult.COMPLETED)) - assert(e.copying()) - if result == CopyResult.DROPPED or result == CopyResult.COMPLETED: - e.state = CopyState.DONE - else: - e.state = CopyState.IDLE - return (event_code, i, result) - - def on_copy_done(result): - assert(result != CopyResult.DROPPED or event_code == EventCode.FUTURE_WRITE) - e.set_pending_event(partial(future_event, result)) - - e.state = CopyState.COPYING - e.copy(thread.task.inst, buffer, on_copy_done) - - if not e.has_pending_event(): + buffer = BufferT(end.t, cx, ptr, length) + end.copy(buffer) + if not end.has_pending_event(): if not opts.async_: - e.wait_for_pending_event() + end.wait_for_pending_event() else: return [BLOCKED] - code,index,payload = e.get_pending_event() - assert(code == event_code and index == i) + code,index,payload = end.get_pending_event() + assert(code == end.event_code and index == i and payload != BLOCKED) return [payload] ### ๐Ÿ”€ `canon {stream,future}.cancel-{read,write}` def canon_stream_cancel_read(stream_t, async_, i): - return cancel_copy(ReadableStreamEnd, EventCode.STREAM_READ, stream_t, async_, i) + return cancel_copy(ReadableStreamEnd, stream_t, async_, i) def canon_stream_cancel_write(stream_t, async_, i): - return cancel_copy(WritableStreamEnd, EventCode.STREAM_WRITE, stream_t, async_, i) + return cancel_copy(WritableStreamEnd, stream_t, async_, i) def canon_future_cancel_read(future_t, async_, i): - return cancel_copy(ReadableFutureEnd, EventCode.FUTURE_READ, future_t, async_, i) + return cancel_copy(ReadableFutureEnd, future_t, async_, i) def canon_future_cancel_write(future_t, async_, i): - return cancel_copy(WritableFutureEnd, EventCode.FUTURE_WRITE, future_t, async_, i) + return cancel_copy(WritableFutureEnd, future_t, async_, i) -def cancel_copy(EndT, event_code, stream_or_future_t, async_, i): +def cancel_copy(EndT, stream_or_future_t, async_, i): thread = current_thread() trap_if(not thread.task.inst.may_leave) - e = thread.task.inst.handles.get(i) - trap_if(not isinstance(e, EndT)) - trap_if(e.shared.t != stream_or_future_t.t) - trap_if(e.state != CopyState.COPYING or e.has_sync_waiter) - trap_if(e.in_waitable_set() and not async_) - e.state = CopyState.CANCELLING_COPY - if not e.has_pending_event(): - e.shared.cancel() - if not e.has_pending_event(): - if not async_: - e.wait_for_pending_event() - else: - return [BLOCKED] - code,index,payload = e.get_pending_event() - assert(not e.copying() and code == event_code and index == i) + end = thread.task.inst.handles.get(i) + trap_if(not isinstance(end, EndT)) + trap_if(end.t != stream_or_future_t.t) + trap_if(end.state != End.State.COPYING) + trap_if(end.has_sync_waiter) + trap_if(end.in_waitable_set() and not async_) + end.cancel() + if not end.has_pending_event(): + if not async_: + end.wait_for_pending_event() + else: + return [BLOCKED] + code,index,payload = end.get_pending_event() + assert(not end.copying_or_cancelling()) + assert(code == end.event_code and index == i) return [payload] ### ๐Ÿ”€ `canon {stream,future}.drop-{readable,writable}` @@ -2605,22 +2482,24 @@ def cancel_copy(EndT, event_code, stream_or_future_t, async_, i): def canon_stream_drop_readable(stream_t, i): return drop(ReadableStreamEnd, stream_t, i) -def canon_stream_drop_writable(stream_t, hi): - return drop(WritableStreamEnd, stream_t, hi) +def canon_stream_drop_writable(stream_t, i): + return drop(WritableStreamEnd, stream_t, i) def canon_future_drop_readable(future_t, i): return drop(ReadableFutureEnd, future_t, i) -def canon_future_drop_writable(future_t, hi): - return drop(WritableFutureEnd, future_t, hi) +def canon_future_drop_writable(future_t, i): + return drop(WritableFutureEnd, future_t, i) -def drop(EndT, stream_or_future_t, hi): +def drop(EndT, stream_or_future_t, i): inst = current_instance() trap_if(not inst.may_leave) - e = inst.handles.remove(hi) - trap_if(not isinstance(e, EndT)) - trap_if(e.shared.t != stream_or_future_t.t) - e.drop() + end = inst.handles.remove(i) + trap_if(not isinstance(end, EndT)) + trap_if(end.t != stream_or_future_t.t) + trap_if(end.copying_or_cancelling()) + trap_if(isinstance(end, WritableFutureEnd) and end.state != End.State.DONE) + end.drop() return [] ### ๐Ÿงต `canon thread.index` diff --git a/design/mvp/canonical-abi/run_tests.py b/design/mvp/canonical-abi/run_tests.py index 90dc344e..75a235a3 100644 --- a/design/mvp/canonical-abi/run_tests.py +++ b/design/mvp/canonical-abi/run_tests.py @@ -1,6 +1,6 @@ - import definitions from definitions import * +from functools import partial definitions.DETERMINISTIC_PROFILE = True @@ -1305,161 +1305,154 @@ def on_resolve(results): pass lift_and_run(mk_opts(), consumer_inst, ft, core_func, on_start, on_resolve) -class HostSource(ReadableStream): - remaining: list[int] - destroy_if_empty: bool - chunk: int - cancel_lock: Optional[threading.Lock] - cancelled_lock: Optional[threading.Lock] - pending_dst: Optional[WritableBuffer] - pending_on_copy: Optional[OnCopy] - pending_on_copy_done: Optional[OnCopyDone] +class HostReadableBuffer(ReadableBuffer): + vs: list[any] + progress: int - def __init__(self, t, contents, chunk, destroy_if_empty = True): + def __init__(self, t, vs): self.t = t - self.remaining = contents - self.destroy_if_empty = destroy_if_empty - self.chunk = chunk - self.cancel_lock = None - self.cancelled_lock = None - self.reset_pending() - def reset_pending(self): - self.pending_dst = None - self.pending_on_copy = None - self.pending_on_copy_done = None - - def closed(self): - return not self.remaining and self.destroy_if_empty - - def drop(self): - self.remaining = [] - self.destroy_if_empty = True - if self.pending_dst: - self.pending_on_copy_done(CopyResult.DROPPED) - self.reset_pending() - - def destroy_once_empty(self): - self.destroy_if_empty = True - if not self.remaining: - self.drop() - - def read(self, inst, dst, on_copy, on_copy_done): - if self.closed(): - on_copy_done(CopyResult.DROPPED) - elif self.remaining: - self.actually_copy(dst) - if self.closed(): - on_copy_done(CopyResult.DROPPED) - else: - on_copy_done(CopyResult.COMPLETED) - else: - self.pending_dst = dst - self.pending_on_copy = on_copy - self.pending_on_copy_done = on_copy_done - - def actually_copy(self, dst): - n = min(dst.remain(), len(self.remaining), self.chunk) - dst.write(self.remaining[:n]) - del self.remaining[:n] - - def block_cancel(self): - self.cancel_lock = threading.Lock() - self.cancel_lock.acquire() - self.cancelled_lock = threading.Lock() - self.cancelled_lock.acquire() - - def unblock_cancel(self): - self.cancel_lock.release() - self.cancelled_lock.acquire() - - def cancel(self): - if not self.cancel_lock: - self.actually_cancel() - else: - def async_cancel(): - self.cancel_lock.acquire() - self.actually_cancel() - self.cancelled_lock.release() - threading.Thread(target = async_cancel).start() + self.vs = vs + self.progress = 0 - def actually_cancel(self): - self.pending_on_copy_done(CopyResult.CANCELLED) - self.reset_pending() + def remain(self): + return len(self.vs) - self.progress - def write(self, vs): - assert(vs and not self.closed()) - self.remaining += vs - if self.pending_dst: - self.actually_copy(self.pending_dst) - if self.pending_dst.remain(): - self.pending_on_copy(self.reset_pending) - else: - self.pending_on_copy_done(CopyResult.COMPLETED) - self.reset_pending() + def read(self, n): + assert(n <= self.remain()) + vs = self.vs[self.progress : self.progress + n] + self.progress += n + return vs -class HostSink: - shared: ReadableStream - t: ValType - received: list[int] - chunk: int - write_remain: int - write_event: threading.Event - ready_to_consume: bool - closed: bool - - def __init__(self, shared, chunk, remain = 2**64): - self.shared = shared - self.t = shared.t +class HostWritableBuffer(WritableBuffer): + length: int + progress: int + received: list[any] + + def __init__(self, t, length): + self.t = t + self.length = length + self.progress = 0 self.received = [] - self.chunk = chunk - self.write_remain = remain - self.write_event = threading.Event() - if remain: - self.write_event.set() - self.ready_to_consume = threading.Event() - self.closed = False - def read_all(): - while True: - self.write_event.wait() - copy_event = threading.Event() - def on_copy(reclaim_buffer): - reclaim_buffer() - copy_event.set() - def on_copy_done(result): - if result == CopyResult.DROPPED: - self.closed = True - copy_event.set() - self.shared.read(None, self, on_copy, on_copy_done) - copy_event.wait() - if self.closed: - break - self.ready_to_consume.set() - threading.Thread(target = read_all).start() - - def set_remain(self, n): - self.write_remain = n - if self.write_remain > 0: - self.write_event.set() def remain(self): - return self.write_remain + return self.length - self.progress def write(self, vs): + assert(len(vs) <= self.remain()) self.received += vs - self.ready_to_consume.set() - self.write_remain -= len(vs) - if self.write_remain == 0: - self.write_event.clear() - - def consume(self, n): - while n > len(self.received): - if self.closed: - return None - self.ready_to_consume.clear() - self.ready_to_consume.wait() - ret = self.received[:n]; - del self.received[:n] - return ret + self.progress += len(vs) + +# To avoid using a whole separate host thread to concurrently write to or read +# from a given stream/future, wrap the other end's copy/drop operations (which +# are the only way the stream makes progress) and call `pump` after each one to +# allow "the other end" to read/write the next batch from the host. +def pump_after_other_end_makes_progress(end, pump): + if end.other is not None: + copy = end.other.copy + drop = end.other.drop + def copy_then_pump(buffer): + copy(buffer) + pump() + def drop_then_pump(): + drop() + pump() + end.other.copy = copy_then_pump + end.other.drop = drop_then_pump + +class HostWriter: + readable_end: ReadableStreamEnd + end: WritableStreamEnd + queue: list[any] + chunk: int + drop_when_empty: bool + pumping: bool + + def __init__(self, t, vs = (), chunk = Buffer.MAX_LENGTH, drop_when_empty = True): + (self.readable_end, self.end) = new_stream(t, owner = None) + pump_after_other_end_makes_progress(self.end, self.pump) + self.queue = list(vs) + self.chunk = chunk + self.drop_when_empty = drop_when_empty + self.pumping = False + self.pump() + + def write(self, vs): + self.queue += vs + self.pump() + + def end_when_empty(self): + self.drop_when_empty = True + self.pump() + + def pump(self): + if self.pumping: + return + self.pumping = True + while self.end.other is not None: + if self.end.has_pending_event(): + _,_,packed = self.end.get_pending_event() + _,progress = unpack_result(packed) + del self.queue[:progress] + elif self.end.state != End.State.IDLE: + break # the peer hasn't finished the write in flight + elif self.queue: + self.end.copy(HostReadableBuffer(self.end.t, self.queue[:self.chunk])) + elif self.drop_when_empty: + self.end.drop() + else: + break + self.pumping = False + +class HostReader: + end: ReadableStreamEnd + buffer: Optional[HostWritableBuffer] + received: list[any] + remain: int + dropped: bool + on_data: Optional[Callable[[HostReader], None]] + pumping: bool + + def __init__(self, end, remain = Buffer.MAX_LENGTH, on_data = None): + self.end = end + pump_after_other_end_makes_progress(self.end, self.pump) + self.buffer = None + self.received = [] + self.remain = remain + self.dropped = False + self.on_data = on_data + self.pumping = False + self.pump() + + def set_remain(self, remain): + self.remain = remain + self.pump() + + def take(self): + received = self.received + self.received = [] + return received + + def pump(self): + if self.pumping: + return + self.pumping = True + while not self.dropped: + if self.end.copying_or_cancelling() and self.end.has_pending_event(): + _,_,packed = self.end.get_pending_event() + result,progress = unpack_result(packed) + assert(progress == len(self.buffer.received)) + self.received += self.buffer.received + self.remain -= progress + self.dropped = (result == CopyResult.DROPPED) + self.buffer = None + if self.on_data: + self.on_data(self) + elif self.end.state == End.State.IDLE and self.remain > 0: + self.buffer = HostWritableBuffer(self.end.t, self.remain) + self.end.copy(self.buffer) + else: + break + self.pumping = False def test_eager_stream_completion(): store = Store() @@ -1470,30 +1463,29 @@ def test_eager_stream_completion(): ft = FuncType([StreamType(U8Type())], [StreamType(U8Type())]) def host_func(on_start, on_resolve, wait_until): - args = on_start() - assert(len(args) == 1) - assert(isinstance(args[0], ReadableStream)) - incoming = HostSink(args[0], chunk=4) - outgoing = HostSource(U8Type(), [], chunk=4, destroy_if_empty=False) - on_resolve([outgoing]) - def add10(): - while (vs := incoming.consume(4)): - for i in range(len(vs)): - vs[i] += 10 - outgoing.write(vs) - outgoing.drop() - threading.Thread(target = add10).start() + [incoming_readable_end] = on_start() + assert(isinstance(incoming_readable_end, ReadableStreamEnd)) + host_writer = HostWriter(U8Type(), chunk=4, drop_when_empty=False) + def add10(reader): + vs = reader.take() + if vs: + host_writer.write([v + 10 for v in vs]) + if reader.dropped: + host_writer.end_when_empty() + HostReader(incoming_readable_end, on_data = add10) + on_resolve([host_writer.readable_end]) host_func_inst = mk_host_func(store, host_func, ft) - src_stream = HostSource(U8Type(), [1,2,3,4,5,6,7,8], chunk=4) + host_writer = HostWriter(U8Type(), [1,2,3,4,5,6,7,8], chunk=4) def on_start(): - return [src_stream] + return [host_writer.readable_end] - dst_stream = None + host_reader = None def on_resolve(results): - assert(len(results) == 1) - nonlocal dst_stream - dst_stream = HostSink(results[0], chunk=4) + [readable_end] = results + assert(isinstance(readable_end, ReadableStreamEnd)) + nonlocal host_reader + host_reader = HostReader(readable_end) def core_func(args): assert(len(args) == 1) @@ -1541,7 +1533,7 @@ def core_func(args): return [] lift_and_run(opts, inst, ft, core_func, on_start, on_resolve) - assert(dst_stream.received == [11,12,13,14,15,16,17,18]) + assert(host_reader.received == [11,12,13,14,15,16,17,18]) def test_async_stream_ops(): @@ -1551,44 +1543,33 @@ def test_async_stream_ops(): opts = mk_opts(memory=MemInst(mem, 'i32'), async_=True) sync_opts = mk_opts(memory=MemInst(mem, 'i32'), async_=False) - host_import_incoming = None - host_import_outgoing = None + host_reader = None + host_writer = None ft = FuncType([StreamType(U8Type())], [StreamType(U8Type())], async_ = True) def host_func(on_start, on_resolve, wait_until): - nonlocal host_import_incoming, host_import_outgoing - args = on_start() - assert(len(args) == 1) - assert(isinstance(args[0], ReadableStream)) - host_import_incoming = HostSink(args[0], chunk=4, remain = 0) - host_import_outgoing = HostSource(U8Type(), [], chunk=4, destroy_if_empty=False) - on_resolve([host_import_outgoing]) + nonlocal host_reader, host_writer + [readable_end] = on_start() + assert(isinstance(readable_end, ReadableStreamEnd)) + host_reader = HostReader(readable_end, remain = 0) + host_writer = HostWriter(U8Type(), chunk=4, drop_when_empty=False) + on_resolve([host_writer.readable_end]) while True: - vs = None - results_ready = RacyBool(False) - def consume_results(): - nonlocal vs - vs = host_import_incoming.consume(4) - results_ready.set() - threading.Thread(target = consume_results).start() - wait_until(results_ready.is_set) - if vs: - for i in range(len(vs)): - vs[i] += 10 - else: + wait_until(lambda: host_reader.received or host_reader.dropped) + vs = host_reader.take() + if not vs: break - host_import_outgoing.write(vs) - host_import_outgoing.destroy_once_empty() + host_writer.write([v + 10 for v in vs]) host_func_inst = mk_host_func(store, host_func, ft) - src_stream = HostSource(U8Type(), [], chunk=4, destroy_if_empty = False) + host_writer2 = HostWriter(U8Type(), chunk=4, drop_when_empty = False) def on_start(): - return [src_stream] + return [host_writer2.readable_end] - dst_stream = None + host_reader2 = None def on_resolve(results): - assert(len(results) == 1) - nonlocal dst_stream - dst_stream = HostSink(results[0], chunk=4, remain = 0) + [readable_end] = results + nonlocal host_reader2 + host_reader2 = HostReader(readable_end, remain = 0) def core_func(args): [rsi1] = args @@ -1598,11 +1579,10 @@ def core_func(args): [] = canon_task_return([StreamType(U8Type())], opts, [rsi2]) [ret] = canon_stream_read(StreamType(U8Type()), opts, rsi1, 0, 4) assert(ret == definitions.BLOCKED) - src_stream.write([1,2,3,4]) + host_writer2.write([1,2,3,4]) retp = 16 [seti] = canon_waitable_set_new() [] = canon_waitable_join(rsi1, seti) - definitions.throw_it = True [event] = canon_waitable_set_wait(MemInst(mem, 'i32'), seti, retp) ## assert(event == EventCode.STREAM_READ) assert(mem[retp+0] == rsi1) @@ -1617,7 +1597,7 @@ def core_func(args): assert(rsi4 == 4) [ret] = canon_stream_write(StreamType(U8Type()), opts, wsi3, 0, 4) assert(ret == definitions.BLOCKED) - host_import_incoming.set_remain(100) + host_reader.set_remain(100) [] = canon_waitable_join(wsi3, seti) [event] = canon_waitable_set_wait(MemInst(mem, 'i32'), seti, retp) assert(event == EventCode.STREAM_WRITE) @@ -1629,20 +1609,20 @@ def core_func(args): assert(n == 4 and result == CopyResult.COMPLETED) [ret] = canon_stream_write(StreamType(U8Type()), opts, wsi2, 0, 4) assert(ret == definitions.BLOCKED) - dst_stream.set_remain(100) + host_reader2.set_remain(100) [] = canon_waitable_join(wsi2, seti) [event] = canon_waitable_set_wait(MemInst(mem, 'i32'), seti, retp) assert(event == EventCode.STREAM_WRITE) assert(mem[retp+0] == wsi2) result,n = unpack_result(mem[retp+4]) assert(n == 4 and result == CopyResult.COMPLETED) - src_stream.write([5,6,7,8]) - src_stream.destroy_once_empty() + host_writer2.write([5,6,7,8]) + host_writer2.end_when_empty() [ret] = canon_stream_read(StreamType(U8Type()), opts, rsi1, 0, 4) result,n = unpack_result(ret) assert(n == 4 and result == CopyResult.DROPPED) - [] = canon_stream_drop_readable(StreamType(U8Type()), rsi1) assert(mem[0:4] == b'\x05\x06\x07\x08') + [] = canon_stream_drop_readable(StreamType(U8Type()), rsi1) [ret] = canon_stream_write(StreamType(U8Type()), opts, wsi3, 0, 4) result,n = unpack_result(ret) assert(n == 4 and result == CopyResult.COMPLETED) @@ -1656,6 +1636,7 @@ def core_func(args): [] = canon_waitable_join(rsi4, 0) result,n = unpack_result(mem[retp+4]) assert(n == 4 and result == CopyResult.COMPLETED) + host_writer.end_when_empty() [ret] = canon_stream_read(StreamType(U8Type()), sync_opts, rsi4, 0, 4) assert(ret == CopyResult.DROPPED) [] = canon_stream_drop_readable(StreamType(U8Type()), rsi4) @@ -1667,19 +1648,18 @@ def core_func(args): return [] lift_and_run(opts, inst, ft, core_func, on_start, on_resolve) - assert(dst_stream.received == [11,12,13,14,15,16,17,18]) + assert(host_reader2.received == [11,12,13,14,15,16,17,18]) def test_stream_forward(): - src_stream = HostSource(U8Type(), [1,2,3,4], chunk=4) + host_writer = HostWriter(U8Type(), [1,2,3,4], chunk=4) def on_start(): - return [src_stream] + return [host_writer.readable_end] - dst_stream = None + return_value = None def on_resolve(results): - assert(len(results) == 1) - nonlocal dst_stream - dst_stream = results[0] + nonlocal return_value + [return_value] = results def core_func(args): assert(len(args) == 1) @@ -1691,7 +1671,7 @@ def core_func(args): inst = ComponentInstance(Store()) ft = FuncType([StreamType(U8Type())], [StreamType(U8Type())]) lift_and_run(opts, inst, ft, core_func, on_start, on_resolve) - assert(src_stream is dst_stream) + assert(host_writer.readable_end is return_value) def test_receive_own_stream(): @@ -1704,7 +1684,7 @@ def test_receive_own_stream(): def host_func(on_start, on_resolve, wait_until): args = on_start() assert(len(args) == 1) - assert(isinstance(args[0], ReadableStream)) + assert(isinstance(args[0], ReadableStreamEnd)) on_resolve(args) host_func_inst = mk_host_func(store, host_func, host_ft) @@ -1739,19 +1719,19 @@ def test_host_partial_reads_writes(): opts = mk_opts(memory=MemInst(mem, 'i32'), async_=True) inst = ComponentInstance(store) - src = HostSource(U8Type(), [1,2,3,4], chunk=2, destroy_if_empty = False) + host_writer = HostWriter(U8Type(), [1,2], drop_when_empty = False) source_ft = FuncType([], [StreamType(U8Type())]) def host_source_func(on_start, on_resolve, wait_until): [] = on_start() - on_resolve([src]) + on_resolve([host_writer.readable_end]) host_source_func_inst = mk_host_func(store, host_source_func, source_ft) - dst = None + host_reader = None sink_ft = FuncType([StreamType(U8Type())], []) def host_sink_func(on_start, on_resolve, wait_until): - nonlocal dst - [s] = on_start() - dst = HostSink(s, chunk=1, remain=2) + nonlocal host_reader + [readable_end] = on_start() + host_reader = HostReader(readable_end, remain=2) on_resolve([]) host_sink_func_inst = mk_host_func(store, host_sink_func, sink_ft) @@ -1766,13 +1746,14 @@ def core_func(args): result,n = unpack_result(ret) assert(n == 2 and result == CopyResult.COMPLETED) assert(mem[0:2] == b'\x01\x02') + host_writer.write([3,4]) [ret] = canon_stream_read(StreamType(U8Type()), opts, rsi, 0, 4) result,n = unpack_result(ret) assert(n == 2 and result == CopyResult.COMPLETED) assert(mem[0:2] == b'\x03\x04') [ret] = canon_stream_read(StreamType(U8Type()), opts, rsi, 0, 4) assert(ret == definitions.BLOCKED) - src.write([5,6]) + host_writer.write([5,6]) [seti] = canon_waitable_set_new() [] = canon_waitable_join(rsi, seti) @@ -1795,18 +1776,18 @@ def core_func(args): assert(n == 2 and result == CopyResult.COMPLETED) [ret] = canon_stream_write(StreamType(U8Type()), opts, wsi, 2, 4) assert(ret == definitions.BLOCKED) - dst.set_remain(4) + host_reader.set_remain(4) [] = canon_waitable_join(wsi, seti) [event] = canon_waitable_set_wait(MemInst(mem, 'i32'), seti, retp) assert(event == EventCode.STREAM_WRITE) assert(mem[retp+0] == wsi) result,n = unpack_result(mem[retp+4]) assert(n == 4 and result == CopyResult.COMPLETED) - assert(dst.received == [1,2,3,4,5,6]) + assert(host_reader.received == [1,2,3,4,5,6]) [] = canon_stream_drop_writable(StreamType(U8Type()), wsi) [] = canon_waitable_set_drop(seti) - dst.set_remain(100) - assert(dst.consume(100) is None) + host_reader.set_remain(100) + assert(host_reader.dropped) return [] opts2 = mk_opts() @@ -2055,21 +2036,21 @@ def test_cancel_copy(): lower_opts = mk_opts(memory=MemInst(mem, 'i32'), async_=True) host_ft1 = FuncType([StreamType(U8Type())],[]) - host_sink = None + host_reader = None def host_func1(on_start, on_resolve, wait_until): - nonlocal host_sink - [stream] = on_start() - host_sink = HostSink(stream, 2, remain = 0) + nonlocal host_reader + [readable_end] = on_start() + host_reader = HostReader(readable_end, remain = 0) on_resolve([]) host_func1_inst = mk_host_func(store, host_func1, host_ft1) host_ft2 = FuncType([], [StreamType(U8Type())]) - host_source = None + host_writer = None def host_func2(on_start, on_resolve, wait_until): - nonlocal host_source + nonlocal host_writer [] = on_start() - host_source = HostSource(U8Type(), [], chunk=2, destroy_if_empty = False) - on_resolve([host_source]) + host_writer = HostWriter(U8Type(), chunk=2, drop_when_empty = False) + on_resolve([host_writer.readable_end]) host_func2_inst = mk_host_func(store, host_func2, host_ft2) lift_opts = mk_opts() @@ -2083,15 +2064,14 @@ def core_func(args): mem[0:4] = b'\x0a\x0b\x0c\x0d' [ret] = canon_stream_write(StreamType(U8Type()), lower_opts, wsi, 0, 4) assert(ret == definitions.BLOCKED) - host_sink.set_remain(2) - got = host_sink.consume(2) - assert(got == [0xa, 0xb]) + host_reader.set_remain(2) + assert(host_reader.take() == [0xa, 0xb]) [ret] = canon_stream_cancel_write(StreamType(U8Type()), False, wsi) result,n = unpack_result(ret) - assert(n == 2 and result == CopyResult.COMPLETED) + assert(n == 2 and result == CopyResult.CANCELLED) [] = canon_stream_drop_writable(StreamType(U8Type()), wsi) - host_sink.set_remain(100) - assert(host_sink.consume(100) is None) + host_reader.set_remain(100) + assert(host_reader.dropped) [packed] = canon_stream_new(StreamType(U8Type())) rsi,wsi = unpack_new_ends(packed) @@ -2100,15 +2080,14 @@ def core_func(args): mem[0:4] = b'\x01\x02\x03\x04' [ret] = canon_stream_write(StreamType(U8Type()), lower_opts, wsi, 0, 4) assert(ret == definitions.BLOCKED) - host_sink.set_remain(2) - got = host_sink.consume(2) - assert(got == [1, 2]) + host_reader.set_remain(2) + assert(host_reader.take() == [1, 2]) [ret] = canon_stream_cancel_write(StreamType(U8Type()), True, wsi) result,n = unpack_result(ret) - assert(n == 2 and result == CopyResult.COMPLETED) + assert(n == 2 and result == CopyResult.CANCELLED) [] = canon_stream_drop_writable(StreamType(U8Type()), wsi) - host_sink.set_remain(100) - assert(host_sink.consume(100) is None) + host_reader.set_remain(100) + assert(host_reader.dropped) retp = 16 [ret] = store.lower(host_func2_inst, host_ft2, lower_opts, inst)([retp]) @@ -2126,26 +2105,22 @@ def core_func(args): rsi = mem[retp] [ret] = canon_stream_read(StreamType(U8Type()), lower_opts, rsi, 0, 4) assert(ret == definitions.BLOCKED) - host_source.block_cancel() [ret] = canon_stream_cancel_read(StreamType(U8Type()), True, rsi) + result,n = unpack_result(ret) + assert(n == 0 and result == CopyResult.CANCELLED) + [] = canon_stream_drop_readable(StreamType(U8Type()), rsi) + + [ret] = store.lower(host_func2_inst, host_ft2, lower_opts, inst)([retp]) + assert(ret == Subtask.State.RETURNED) + rsi = mem[retp] + [ret] = canon_stream_read(StreamType(U8Type()), lower_opts, rsi, 0, 4) assert(ret == definitions.BLOCKED) - try: - canon_stream_cancel_read(StreamType(U8Type()), True, rsi) - assert(False) - except Trap: - pass - host_source.write([7,8]) - host_source.unblock_cancel() - [seti] = canon_waitable_set_new() - [] = canon_waitable_join(rsi, seti) - [event] = canon_waitable_set_wait(MemInst(mem, 'i32'), seti, retp) - assert(event == EventCode.STREAM_READ) - assert(mem[retp+0] == rsi) - result,n = unpack_result(mem[retp+4]) + host_writer.write([7,8]) + [ret] = canon_stream_cancel_read(StreamType(U8Type()), True, rsi) + result,n = unpack_result(ret) assert(n == 2 and result == CopyResult.CANCELLED) assert(mem[0:2] == b'\x07\x08') [] = canon_stream_drop_readable(StreamType(U8Type()), rsi) - [] = canon_waitable_set_drop(seti) return [] @@ -2153,56 +2128,6 @@ def core_func(args): lift_and_run(lift_opts, inst, caller_ft, core_func, lambda:[], lambda _:()) -class HostFutureSink: - t: ValType - v: Optional[any] - has_v: RacyBool - - def __init__(self, t): - self.t = t - self.v = None - self.has_v = RacyBool(False) - - def remain(self): - return 1 if self.v is None else 0 - - def write(self, v): - assert(not self.v) - assert(len(v) == 1) - self.v = v[0] - self.has_v.set() - -class HostFutureSource(ReadableFuture): - v: Optional[any] - pending_buffer: Optional[WritableBuffer] - pending_on_copy_done: Optional[OnCopyDone] - def __init__(self, t): - self.t = t - self.v = None - self.reset_pending() - def reset_pending(self): - self.pending_buffer = None - self.pending_on_copy_done = None - def read(self, inst, buffer, on_copy_done): - if self.v: - buffer.write([self.v]) - on_copy_done(CopyResult.COMPLETED) - else: - self.pending_buffer = buffer - self.pending_on_copy_done = on_copy_done - def cancel(self): - self.pending_on_copy_done(CopyResult.CANCELLED) - self.reset_pending() - def drop(self): - pass - def set_result(self, v): - if self.pending_buffer: - self.pending_buffer.write([v]) - self.pending_on_copy_done(CopyResult.COMPLETED) - self.reset_pending() - else: - self.v = v - def test_futures(): store = Store() inst = ComponentInstance(store) @@ -2211,14 +2136,14 @@ def test_futures(): host_ft1 = FuncType([FutureType(U8Type())],[FutureType(U8Type())], async_ = True) def host_func(on_start, on_resolve, wait_until): - [future] = on_start() - outgoing = HostFutureSource(U8Type()) - on_resolve([outgoing]) - incoming = HostFutureSink(U8Type()) - future.read(None, incoming, lambda why:()) - wait_until(incoming.has_v.is_set) - assert(incoming.v == 42) - outgoing.set_result(43) + [incoming_readable_end] = on_start() + (outgoing_readable_end, outgoing_writable_end) = new_future(U8Type(), owner = None) + on_resolve([outgoing_readable_end]) + buffer = HostWritableBuffer(U8Type(), 1) + incoming_readable_end.copy(buffer) + wait_until(incoming_readable_end.has_pending_event) + assert(buffer.received == [42]) + outgoing_writable_end.copy(HostReadableBuffer(U8Type(), [43])) host_func_inst = mk_host_func(store, host_func, host_ft1) lift_opts = mk_opts() @@ -2743,6 +2668,30 @@ def core_func(args): assert(n == 3) [] = canon_stream_drop_writable(StreamType(elemt), wsi) + # A zero-length read that has already been completed (here by a write that + # then blocked and was cancelled) must not be taken for a still-pending + # zero-length read: a later zero-length write blocks, and cancelling the + # completed read reports the cancellation. + [packed] = canon_stream_new(StreamType(elemt)) + rsi,wsi = unpack_new_ends(packed) + [ret] = canon_stream_read(StreamType(elemt), async_opts, rsi, 0, 0) + assert(ret == definitions.BLOCKED) + [ret] = canon_stream_write(StreamType(elemt), async_opts, wsi, 0, 1) + assert(ret == definitions.BLOCKED) + [ret] = canon_stream_cancel_write(StreamType(elemt), True, wsi) + result,n = unpack_result(ret) + assert(n == 0 and result == CopyResult.CANCELLED) + [ret] = canon_stream_write(StreamType(elemt), async_opts, wsi, 0, 0) + assert(ret == definitions.BLOCKED) + [ret] = canon_stream_cancel_read(StreamType(elemt), True, rsi) + result,n = unpack_result(ret) + assert(n == 0 and result == CopyResult.CANCELLED) + [ret] = canon_stream_cancel_write(StreamType(elemt), True, wsi) + result,n = unpack_result(ret) + assert(n == 0 and result == CopyResult.CANCELLED) + [] = canon_stream_drop_readable(StreamType(elemt), rsi) + [] = canon_stream_drop_writable(StreamType(elemt), wsi) + [] = canon_waitable_set_drop(seti) return [] diff --git a/test/async/cancel-stream.wast b/test/async/cancel-stream.wast index 4bc9a0e9..5245bfdc 100644 --- a/test/async/cancel-stream.wast +++ b/test/async/cancel-stream.wast @@ -153,6 +153,26 @@ (then unreachable)) (call $stream.drop-readable (local.get $sr)) + ;; get a new $sr + (local.set $sr (call $start-stream)) + (if (i32.ne (i32.const 1) (local.get $sr)) + (then unreachable)) + + ;; same as above, but with a 4-byte buffer that $C's write fills + ;; exactly, so the read has already fully completed by the time $C + ;; drops. Cancelling still shows "4+dropped", not "4+completed", + ;; because the stream is dropped when the event is delivered. + (local.set $ret (call $stream.read (local.get $sr) (i32.const 8) (i32.const 4))) + (if (i32.ne (i32.const -1 (; BLOCKED;)) (local.get $ret)) + (then unreachable)) + (call $write4-and-drop) + (local.set $ret (call $stream.cancel-read (local.get $sr))) + (if (i32.ne (i32.const 0x41 (; DROPPED=1 | (4<<4) ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 0xabcd) (i32.load (i32.const 8))) + (then unreachable)) + (call $stream.drop-readable (local.get $sr)) + ;; get a new $sr (local.set $sr (call $start-stream)) (if (i32.ne (i32.const 1) (local.get $sr)) @@ -200,3 +220,176 @@ (func (export "run") (alias export $d "run")) ) (assert_return (invoke "run") (u32.const 42)) + +;; The cases above cancel reads/writes that are still genuinely in flight. +;; $Tester below covers cancelling an operation whose completion has already +;; happened but has not yet been observed. +(component definition $Tester + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $M + (import "" "mem" (memory 1)) + (import "" "stream.new" (func $stream.new (result i64))) + (import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32))) + (import "" "stream.write" (func $stream.write (param i32 i32 i32) (result i32))) + (import "" "stream.cancel-read" (func $stream.cancel-read (param i32) (result i32))) + (import "" "stream.cancel-write" (func $stream.cancel-write (param i32) (result i32))) + (import "" "stream.drop-readable" (func $stream.drop-readable (param i32))) + (import "" "stream.drop-writable" (func $stream.drop-writable (param i32))) + + (global $rx (mut i32) (i32.const 0)) + (global $tx (mut i32) (i32.const 0)) + (func $new-stream + (local $r i64) + (local.set $r (call $stream.new)) + (global.set $rx (i32.wrap_i64 (local.get $r))) + (global.set $tx (i32.wrap_i64 (i64.shr_u (local.get $r) (i64.const 32))))) + + ;; A 2-byte write is fully drained by a 2-byte read, so the writer's copy is + ;; finished and its buffer released, but the writer has not observed the + ;; event yet. Cancelling now reports CANCELLED with the full progress, and + ;; leaves the writable end IDLE and reusable. + (func (export "cancel-write-after-completion") (result i32) + (call $new-stream) + (if (i32.ne (call $stream.write (global.get $tx) (i32.const 64) (i32.const 2)) + (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + (if (i32.ne (call $stream.read (global.get $rx) (i32.const 128) (i32.const 2)) + (i32.const 0x20 (; COMPLETED=0 | (2<<4) ;))) + (then unreachable)) + (if (i32.ne (call $stream.cancel-write (global.get $tx)) + (i32.const 0x22 (; CANCELLED=2 | (2<<4) ;))) + (then unreachable)) + ;; the end is IDLE again: a fresh write blocks, and cancelling that one + ;; reports a plain CANCELLED with no progress + (if (i32.ne (call $stream.write (global.get $tx) (i32.const 64) (i32.const 1)) + (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + (if (i32.ne (call $stream.cancel-write (global.get $tx)) + (i32.const 0x2 (; CANCELLED=2 | (0<<4) ;))) + (then unreachable)) + (i32.const 42) + ) + ;; The converse case: + (func (export "cancel-read-after-completion") (result i32) + (call $new-stream) + (if (i32.ne (call $stream.read (global.get $rx) (i32.const 128) (i32.const 2)) + (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + (if (i32.ne (call $stream.write (global.get $tx) (i32.const 64) (i32.const 2)) + (i32.const 0x20 (; COMPLETED=0 | (2<<4) ;))) + (then unreachable)) + (if (i32.ne (call $stream.cancel-read (global.get $rx)) + (i32.const 0x22 (; CANCELLED=2 | (2<<4) ;))) + (then unreachable)) + ;; the end is IDLE again: a fresh read blocks, and cancelling that one + ;; reports a plain CANCELLED with no progress + (if (i32.ne (call $stream.read (global.get $rx) (i32.const 128) (i32.const 1)) + (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + (if (i32.ne (call $stream.cancel-read (global.get $rx)) + (i32.const 0x2 (; CANCELLED=2 | (0<<4) ;))) + (then unreachable)) + (i32.const 42) + ) + + ;; Zero-length ops complete by signalling readiness rather than by copying, + ;; and that completion is reported the same way: a blocked zero-length write + ;; is completed by the zero-length read arriving, and cancelling it + ;; afterwards reports CANCELLED with no progress. + (func (export "cancel-write-after-zero-length-completion") (result i32) + (call $new-stream) + (if (i32.ne (call $stream.write (global.get $tx) (i32.const 64) (i32.const 0)) + (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + ;; the reader learns the writer is ready and blocks in turn + (if (i32.ne (call $stream.read (global.get $rx) (i32.const 128) (i32.const 0)) + (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + (if (i32.ne (call $stream.cancel-write (global.get $tx)) + (i32.const 0x2 (; CANCELLED=2 | (0<<4) ;))) + (then unreachable)) + (i32.const 42) + ) + (func (export "cancel-read-after-zero-length-completion") (result i32) + (call $new-stream) + (if (i32.ne (call $stream.read (global.get $rx) (i32.const 128) (i32.const 0)) + (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + (if (i32.ne (call $stream.write (global.get $tx) (i32.const 64) (i32.const 1)) + (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + (if (i32.ne (call $stream.cancel-read (global.get $rx)) + (i32.const 0x2 (; CANCELLED=2 | (0<<4) ;))) + (then unreachable)) + (i32.const 42) + ) + + ;; A drop that lands before the event is observed wins over the cancel: the + ;; writer is told DROPPED, still carrying the 1 byte that was copied. The + ;; end is then done and may be dropped. + (func (export "cancel-write-after-partial-then-drop") (result i32) + (call $new-stream) + (if (i32.ne (call $stream.write (global.get $tx) (i32.const 64) (i32.const 2)) + (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + (if (i32.ne (call $stream.read (global.get $rx) (i32.const 128) (i32.const 1)) + (i32.const 0x10 (; COMPLETED=0 | (1<<4) ;))) + (then unreachable)) + (call $stream.drop-readable (global.get $rx)) + (if (i32.ne (call $stream.cancel-write (global.get $tx)) + (i32.const 0x11 (; DROPPED=1 | (1<<4) ;))) + (then unreachable)) + (call $stream.drop-writable (global.get $tx)) + (i32.const 42) + ) + (func (export "cancel-write-after-drop") (result i32) + (call $new-stream) + (if (i32.ne (call $stream.write (global.get $tx) (i32.const 64) (i32.const 2)) + (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + (call $stream.drop-readable (global.get $rx)) + (if (i32.ne (call $stream.cancel-write (global.get $tx)) + (i32.const 0x01 (; DROPPED=1 | (0<<4) ;))) + (then unreachable)) + (call $stream.drop-writable (global.get $tx)) + (i32.const 42) + ) + ) + (type $ST (stream u8)) + (canon stream.new $ST (core func $stream.new)) + (canon stream.read $ST async (memory (core memory $memory "mem")) (core func $stream.read)) + (canon stream.write $ST async (memory (core memory $memory "mem")) (core func $stream.write)) + (canon stream.cancel-read $ST async (core func $stream.cancel-read)) + (canon stream.cancel-write $ST async (core func $stream.cancel-write)) + (canon stream.drop-readable $ST (core func $stream.drop-readable)) + (canon stream.drop-writable $ST (core func $stream.drop-writable)) + (core instance $m (instantiate $M (with "" (instance + (export "mem" (memory $memory "mem")) + (export "stream.new" (func $stream.new)) + (export "stream.read" (func $stream.read)) + (export "stream.write" (func $stream.write)) + (export "stream.cancel-read" (func $stream.cancel-read)) + (export "stream.cancel-write" (func $stream.cancel-write)) + (export "stream.drop-readable" (func $stream.drop-readable)) + (export "stream.drop-writable" (func $stream.drop-writable)) + )))) + (func (export "cancel-write-after-completion") (result u32) (canon lift (core func $m "cancel-write-after-completion"))) + (func (export "cancel-read-after-completion") (result u32) (canon lift (core func $m "cancel-read-after-completion"))) + (func (export "cancel-write-after-zero-length-completion") (result u32) (canon lift (core func $m "cancel-write-after-zero-length-completion"))) + (func (export "cancel-read-after-zero-length-completion") (result u32) (canon lift (core func $m "cancel-read-after-zero-length-completion"))) + (func (export "cancel-write-after-partial-then-drop") (result u32) (canon lift (core func $m "cancel-write-after-partial-then-drop"))) + (func (export "cancel-write-after-drop") (result u32) (canon lift (core func $m "cancel-write-after-drop"))) +) +(component instance $i $Tester) +(assert_return (invoke "cancel-write-after-completion") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "cancel-read-after-completion") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "cancel-write-after-zero-length-completion") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "cancel-read-after-zero-length-completion") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "cancel-write-after-partial-then-drop") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "cancel-write-after-drop") (u32.const 42)) diff --git a/test/async/futures-must-write.wast b/test/async/futures-must-write.wast index 53e04aac..2676c205 100644 --- a/test/async/futures-must-write.wast +++ b/test/async/futures-must-write.wast @@ -116,3 +116,142 @@ (assert_return (invoke "drop-readable-future-before-read") (u32.const 42)) (assert_trap (invoke "drop-writable-future-before-write") "cannot drop future write end without first writing a value") + +;; Test interactions with cancellation +(component definition $Tester + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $M + (import "" "mem" (memory 1)) + (import "" "future.new" (func $future.new (result i64))) + (import "" "future.read" (func $future.read (param i32 i32) (result i32))) + (import "" "future.write" (func $future.write (param i32 i32) (result i32))) + (import "" "future.cancel-read" (func $future.cancel-read (param i32) (result i32))) + (import "" "future.cancel-write" (func $future.cancel-write (param i32) (result i32))) + (import "" "future.drop-readable" (func $future.drop-readable (param i32))) + (import "" "future.drop-writable" (func $future.drop-writable (param i32))) + + (global $rx (mut i32) (i32.const 0)) + (global $tx (mut i32) (i32.const 0)) + (func $new-future + (local $r i64) + (local.set $r (call $future.new)) + (global.set $rx (i32.wrap_i64 (local.get $r))) + (global.set $tx (i32.wrap_i64 (i64.shr_u (local.get $r) (i64.const 32))))) + + ;; The write blocked, then the read took the value. Cancelling afterwards + ;; reports COMPLETED, not CANCELLED -- the value was delivered and cannot + ;; be unsent -- so the writable end is done and may be dropped. + (func (export "cancel-write-after-completion") (result i32) + (call $new-future) + (i32.store8 (i32.const 64) (i32.const 7)) + (if (i32.ne (call $future.write (global.get $tx) (i32.const 64)) + (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + (if (i32.ne (call $future.read (global.get $rx) (i32.const 128)) + (i32.const 0 (; COMPLETED ;))) + (then unreachable)) + (if (i32.ne (call $future.cancel-write (global.get $tx)) + (i32.const 0 (; COMPLETED ;))) + (then unreachable)) + (if (i32.ne (i32.load8_u (i32.const 128)) (i32.const 7)) + (then unreachable)) + (call $future.drop-writable (global.get $tx)) + (call $future.drop-readable (global.get $rx)) + (i32.const 42) + ) + ;; The converse case: + (func (export "cancel-read-after-completion") (result i32) + (call $new-future) + (if (i32.ne (call $future.read (global.get $rx) (i32.const 128)) + (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + (i32.store8 (i32.const 64) (i32.const 7)) + (if (i32.ne (call $future.write (global.get $tx) (i32.const 64)) + (i32.const 0 (; COMPLETED ;))) + (then unreachable)) + (if (i32.ne (call $future.cancel-read (global.get $rx)) + (i32.const 0 (; COMPLETED ;))) + (then unreachable)) + (if (i32.ne (i32.load8_u (i32.const 128)) (i32.const 7)) + (then unreachable)) + (call $future.drop-readable (global.get $rx)) + (call $future.drop-writable (global.get $tx)) + (i32.const 42) + ) + + ;; Nothing was ever copied, so the cancel really does cancel: CANCELLED, + ;; and the end goes back to IDLE, still owing a value. + (func (export "cancel-write-with-nothing-written") (result i32) + (call $new-future) + (if (i32.ne (call $future.write (global.get $tx) (i32.const 64)) + (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + (if (i32.ne (call $future.cancel-write (global.get $tx)) + (i32.const 2 (; CANCELLED ;))) + (then unreachable)) + (i32.const 42) + ) + ;; ... so dropping it still traps, exactly as if no write had been started. + (func (export "drop-writable-after-cancelled-write") + (call $new-future) + (if (i32.ne (call $future.write (global.get $tx) (i32.const 64)) + (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + (if (i32.ne (call $future.cancel-write (global.get $tx)) + (i32.const 2 (; CANCELLED ;))) + (then unreachable)) + ;; boom + (call $future.drop-writable (global.get $tx)) + ) + + ;; If the readable end is dropped while the write is in flight, the cancel + ;; reports DROPPED rather than CANCELLED, and that does make the writable + ;; end done: it may be dropped without ever having written a value. + (func (export "cancel-write-after-readable-dropped") (result i32) + (call $new-future) + (if (i32.ne (call $future.write (global.get $tx) (i32.const 64)) + (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + (call $future.drop-readable (global.get $rx)) + (if (i32.ne (call $future.cancel-write (global.get $tx)) + (i32.const 1 (; DROPPED ;))) + (then unreachable)) + (call $future.drop-writable (global.get $tx)) + (i32.const 42) + ) + ) + (type $FT (future u8)) + (canon future.new $FT (core func $future.new)) + (canon future.read $FT async (memory (core memory $memory "mem")) (core func $future.read)) + (canon future.write $FT async (memory (core memory $memory "mem")) (core func $future.write)) + (canon future.cancel-read $FT async (core func $future.cancel-read)) + (canon future.cancel-write $FT async (core func $future.cancel-write)) + (canon future.drop-readable $FT (core func $future.drop-readable)) + (canon future.drop-writable $FT (core func $future.drop-writable)) + (core instance $m (instantiate $M (with "" (instance + (export "mem" (memory $memory "mem")) + (export "future.new" (func $future.new)) + (export "future.read" (func $future.read)) + (export "future.write" (func $future.write)) + (export "future.cancel-read" (func $future.cancel-read)) + (export "future.cancel-write" (func $future.cancel-write)) + (export "future.drop-readable" (func $future.drop-readable)) + (export "future.drop-writable" (func $future.drop-writable)) + )))) + (func (export "cancel-write-after-completion") (result u32) (canon lift (core func $m "cancel-write-after-completion"))) + (func (export "cancel-read-after-completion") (result u32) (canon lift (core func $m "cancel-read-after-completion"))) + (func (export "cancel-write-with-nothing-written") (result u32) (canon lift (core func $m "cancel-write-with-nothing-written"))) + (func (export "drop-writable-after-cancelled-write") (canon lift (core func $m "drop-writable-after-cancelled-write"))) + (func (export "cancel-write-after-readable-dropped") (result u32) (canon lift (core func $m "cancel-write-after-readable-dropped"))) +) +(component instance $i $Tester) +(assert_return (invoke "cancel-write-after-completion") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "cancel-read-after-completion") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "cancel-write-with-nothing-written") (u32.const 42)) +(component instance $i $Tester) +(assert_trap (invoke "drop-writable-after-cancelled-write") "cannot drop future write end without first writing a value") +(component instance $i $Tester) +(assert_return (invoke "cancel-write-after-readable-dropped") (u32.const 42)) diff --git a/test/async/trap-if-done.wast b/test/async/trap-if-done.wast index 414f497f..173214f8 100644 --- a/test/async/trap-if-done.wast +++ b/test/async/trap-if-done.wast @@ -77,7 +77,9 @@ (call $stream.write (global.get $writable-end) (i32.const 16) (i32.const 1)) ) (func $acknowledge-stream-write (export "acknowledge-stream-write") - ;; confirm we got a STREAM_WRITE $writable-end COMPLETED event + ;; confirm we got a STREAM_WRITE $writable-end event; the write is + ;; reported as DROPPED, not COMPLETED, because the readable end was + ;; dropped before we observed the event (local $ret i32) (local.set $ret (call $waitable-set.wait (global.get $ws) (i32.const 0))) (if (i32.ne (i32.const 3 (; STREAM_WRITE ;)) (local.get $ret)) @@ -318,7 +320,8 @@ ;; then drop our readable end (call $stream.drop-readable (local.get $sr)) - ;; let $C see that it's stream.write COMPLETED and wrote 1 elem + ;; let $C observe its write: because we dropped in the meantime, the + ;; fully-copied write is reported as DROPPED with its 1 elem of progress (call $acknowledge-stream-write) ;; now calling stream.write again in $C will trap @@ -379,6 +382,50 @@ )) unreachable ) + (func $trap-after-stream-writer-dropped-after-write (export "trap-after-stream-writer-dropped-after-write") (param $bool i32) (result i32) + (local $ret i32) (local $ws i32) + (local $sr i32) + (local.set $sr (call $start-stream)) + + ;; start a read on our end first which will block; the 1-elem buffer is + ;; exactly filled by $C's write below, so both copies complete + (local.set $ret (call $stream.read (local.get $sr) (i32.const 16) (i32.const 1))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + + ;; $C's write rendezvouses with our read and fills our buffer completely + (local.set $ret (call $stream-write)) + (if (i32.ne (i32.const 0x10 (; COMPLETED=0 | (1<<4) ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 42) (i32.load8_u (i32.const 16))) + (then unreachable)) + + ;; drop the writable end before we've observed our own event + (call $stream-drop-writable) + + ;; even though the read already copied everything it asked for, it is + ;; reported as DROPPED (keeping its progress) because the stream is + ;; dropped by the time the event is delivered + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $sr) (local.get $ws)) + (local.set $ret (call $waitable-set.wait (local.get $ws) (i32.const 0))) + (if (i32.ne (i32.const 2 (; STREAM_READ ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (local.get $sr) (i32.load (i32.const 0))) + (then unreachable)) + (if (i32.ne (i32.const 0x11 (; DROPPED=1 | (1<<4) ;)) (i32.load (i32.const 4))) + (then unreachable)) + + ;; ... and so the readable end is now done + (if (i32.eqz (local.get $bool)) (then + ;; calling stream.read again should then trap + (drop (call $stream.read (local.get $sr) (i32.const 16) (i32.const 1))) + ) (else + ;; lifting the stream by returning it should also trap + (return (local.get $sr)) + )) + unreachable + ) ) (type $FT (future u8)) (type $ST (stream u8)) @@ -428,6 +475,7 @@ (func (export "trap-after-stream-reader-async-dropped") async (canon lift (core func $core "trap-after-stream-reader-async-dropped"))) (func (export "trap-after-stream-writer-eager-dropped") async (param "bool" bool) (result $ST) (canon lift (core func $core "trap-after-stream-writer-eager-dropped"))) (func (export "trap-after-stream-writer-async-dropped") async (param "bool" bool) (result $ST) (canon lift (core func $core "trap-after-stream-writer-async-dropped"))) + (func (export "trap-after-stream-writer-dropped-after-write") async (param "bool" bool) (result $ST) (canon lift (core func $core "trap-after-stream-writer-dropped-after-write"))) ) (instance $c (instantiate $C)) (instance $d (instantiate $D (with "c" (instance $c)))) @@ -440,6 +488,7 @@ (func (export "trap-after-stream-reader-async-dropped") (alias export $d "trap-after-stream-reader-async-dropped")) (func (export "trap-after-stream-writer-eager-dropped") (alias export $d "trap-after-stream-writer-eager-dropped")) (func (export "trap-after-stream-writer-async-dropped") (alias export $d "trap-after-stream-writer-async-dropped")) + (func (export "trap-after-stream-writer-dropped-after-write") (alias export $d "trap-after-stream-writer-dropped-after-write")) ) (component instance $i1 $Tester) @@ -468,3 +517,7 @@ (assert_trap (invoke "trap-after-stream-writer-async-dropped" (bool.const false)) "cannot read from stream after being notified that the writable end dropped") (component instance $i9.2 $Tester) (assert_trap (invoke "trap-after-stream-writer-async-dropped" (bool.const true)) "cannot lift stream after being notified that the writable end dropped") +(component instance $i10.1 $Tester) +(assert_trap (invoke "trap-after-stream-writer-dropped-after-write" (bool.const false)) "cannot read from stream after being notified that the writable end dropped") +(component instance $i10.2 $Tester) +(assert_trap (invoke "trap-after-stream-writer-dropped-after-write" (bool.const true)) "cannot lift stream after being notified that the writable end dropped") From b812be5ed75e90b06ac773c4cb396e160a2b4d6e Mon Sep 17 00:00:00 2001 From: Luke Wagner Date: Mon, 14 Sep 2026 17:19:23 -0500 Subject: [PATCH 2/2] Deliver stream/future dropped events even when not reading/writing (#720) --- design/mvp/CanonicalABI.md | 10 +- design/mvp/canonical-abi/definitions.py | 5 +- design/mvp/canonical-abi/run_tests.py | 70 +++ test/async/idle-drop.wast | 572 ++++++++++++++++++++++++ test/nyi.txt | 1 - 5 files changed, 649 insertions(+), 9 deletions(-) create mode 100644 test/async/idle-drop.wast diff --git a/design/mvp/CanonicalABI.md b/design/mvp/CanonicalABI.md index 5d2010ef..2b05c97b 100644 --- a/design/mvp/CanonicalABI.md +++ b/design/mvp/CanonicalABI.md @@ -1265,7 +1265,6 @@ class Waitable: wset.elems.append(self) def drop(self): - assert(not self.has_pending_event()) assert(not self.has_sync_waiter) self.join(None) ``` @@ -1624,7 +1623,7 @@ buffer of length `1`, the second half of case `3` and cases `4` and `5` do not apply to futures. Enumerating the cases in the order that they are handled in the code below: 1. The other end was racily dropped before this end could be notified, in which - case the call immediately completes, reporting `DROPPED` and nothing copied. + case `End.drop` already left a pending event and so there's nothing to do. 2. The other end has not currently provided a buffer, in which case this end must block until the other end shows up with a buffer. 3. Both this and the other end have provided buffers that can copy at least 1 @@ -1646,7 +1645,7 @@ the code below: assert(self.buffer is None) self.state = End.State.COPYING if self.other is None: - self.notify(progress = 0) + assert(self.has_pending_event()) elif self.other.buffer is None: self.buffer = buffer elif buffer.remain() > 0 and self.other.buffer.remain() > 0: @@ -1699,14 +1698,15 @@ cancellation. In the future, guest components may be given the same capability. The `End.drop` method is called by `{stream,future}.drop-{readable,writable}` to update the `other` end's state and possibly set a pending notification for the -other end, if doing so wouldn't clobber an already-pending notification. +other end, if the other end isn't already `DONE` and doing so wouldn't clobber +an already-pending notification. ```python def drop(self): assert(not self.copying_or_cancelling()) if self.other is not None: assert(self is self.other.other) self.other.other = None - if self.other.copying_or_cancelling() and not self.other.has_pending_event(): + if self.other.state != End.State.DONE and not self.other.has_pending_event(): self.other.notify(progress = 0) self.other = None Waitable.drop(self) diff --git a/design/mvp/canonical-abi/definitions.py b/design/mvp/canonical-abi/definitions.py index b5d4e2c4..edcd26ba 100644 --- a/design/mvp/canonical-abi/definitions.py +++ b/design/mvp/canonical-abi/definitions.py @@ -742,7 +742,6 @@ def join(self, wset): wset.elems.append(self) def drop(self): - assert(not self.has_pending_event()) assert(not self.has_sync_waiter) self.join(None) @@ -940,7 +939,7 @@ def copy(self, buffer: Buffer, is_read: bool): assert(self.buffer is None) self.state = End.State.COPYING if self.other is None: - self.notify(progress = 0) + assert(self.has_pending_event()) elif self.other.buffer is None: self.buffer = buffer elif buffer.remain() > 0 and self.other.buffer.remain() > 0: @@ -975,7 +974,7 @@ def drop(self): if self.other is not None: assert(self is self.other.other) self.other.other = None - if self.other.copying_or_cancelling() and not self.other.has_pending_event(): + if self.other.state != End.State.DONE and not self.other.has_pending_event(): self.other.notify(progress = 0) self.other = None Waitable.drop(self) diff --git a/design/mvp/canonical-abi/run_tests.py b/design/mvp/canonical-abi/run_tests.py index 75a235a3..1d1e9fbc 100644 --- a/design/mvp/canonical-abi/run_tests.py +++ b/design/mvp/canonical-abi/run_tests.py @@ -1673,6 +1673,15 @@ def core_func(args): lift_and_run(opts, inst, ft, core_func, on_start, on_resolve) assert(host_writer.readable_end is return_value) + # A stream whose writable end has already been dropped but the readable end + # has not yet received the event can still be lowered into and lifted out of + # the component; whoever finally reads it sees DROPPED. + host_writer = HostWriter(U8Type(), []) + lift_and_run(opts, inst, ft, core_func, on_start, on_resolve) + assert(host_writer.readable_end is return_value) + host_reader = HostReader(return_value) + assert(host_reader.dropped and host_reader.take() == []) + def test_receive_own_stream(): store = Store() @@ -2254,6 +2263,66 @@ def core_func(args): lift_and_run(opts, inst, caller_ft, core_func, lambda:[], lambda _:()) +def test_stream_drop_both_ends_while_idle(): + store = Store() + inst = ComponentInstance(store) + mem = bytearray(24) + opts = mk_opts(memory=MemInst(mem, 'i32'), async_ = True) + stream_t = StreamType(U8Type()) + future_t = FutureType(U8Type()) + + def core_func(args): + assert(len(args) == 0) + [] = canon_task_return([], opts, []) + [packed] = canon_stream_new(stream_t) + rsi,wsi = unpack_new_ends(packed) + [] = canon_stream_drop_writable(stream_t, wsi) + [] = canon_stream_drop_readable(stream_t, rsi) + + # Dropping one end notifies the other end even when it has no read or write + # in flight: the DROPPED event is delivered (once) through the waitable set, + # after which the end is done and further reads trap. + retp = 8 + [seti] = canon_waitable_set_new() + [packed] = canon_stream_new(stream_t) + rsi,wsi = unpack_new_ends(packed) + [] = canon_waitable_join(rsi, seti) + [] = canon_stream_drop_writable(stream_t, wsi) + [event] = canon_waitable_set_poll(MemInst(mem, 'i32'), seti, retp) + assert(event == EventCode.STREAM_READ) + assert(mem[retp+0] == rsi) + result,n = unpack_result(mem[retp+4]) + assert(n == 0 and result == CopyResult.DROPPED) + [event] = canon_waitable_set_poll(MemInst(mem, 'i32'), seti, retp) + assert(event == EventCode.NONE) + trapped = False + try: + canon_stream_read(stream_t, opts, rsi, 0, 4) + except Trap: + trapped = True + assert(trapped) + [] = canon_waitable_join(rsi, 0) + [] = canon_stream_drop_readable(stream_t, rsi) + + # Same for the writable end of a future (using wait instead of poll); once + # notified, the writable end may be dropped without having written. + [packed] = canon_future_new(future_t) + rfi,wfi = unpack_new_ends(packed) + [] = canon_waitable_join(wfi, seti) + [] = canon_future_drop_readable(future_t, rfi) + [event] = canon_waitable_set_wait(MemInst(mem, 'i32'), seti, retp) + assert(event == EventCode.FUTURE_WRITE) + assert(mem[retp+0] == wfi) + assert(mem[retp+4] == CopyResult.DROPPED) + [] = canon_waitable_join(wfi, 0) + [] = canon_future_drop_writable(future_t, wfi) + [] = canon_waitable_set_drop(seti) + return [] + + caller_ft = FuncType([], [], async_ = True) + lift_and_run(opts, inst, caller_ft, core_func, lambda:[], lambda _:()) + + def test_cancel_subtask(): store = Store() ft = FuncType([U8Type()], [U8Type()], async_ = True) @@ -2982,6 +3051,7 @@ def on_resolve(v): test_cancel_copy() test_futures() test_future_drop_readable_with_pending_write() +test_stream_drop_both_ends_while_idle() test_cancel_subtask() test_self_copy(None) test_self_copy(U8Type()) diff --git a/test/async/idle-drop.wast b/test/async/idle-drop.wast new file mode 100644 index 00000000..11d20a33 --- /dev/null +++ b/test/async/idle-drop.wast @@ -0,0 +1,572 @@ +;; This test checks that dropping one end of a stream or future notifies the +;; other end even when that other end is idle, i.e. has no read or write in +;; flight. The notification is recorded at the time of the drop and delivered +;; exactly once, either through whatever waitable set the idle end is in or +;; inline by a subsequent read. Once it has been delivered, the end is "done" +;; and behaves like any other done end. +(component definition $Tester + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $M + (import "" "mem" (memory 1)) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (import "" "waitable-set.poll" (func $waitable-set.poll (param i32 i32) (result i32))) + (import "" "waitable-set.drop" (func $waitable-set.drop (param i32))) + (import "" "stream.new" (func $stream.new (result i64))) + (import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32))) + (import "" "stream.read-sync" (func $stream.read-sync (param i32 i32 i32) (result i32))) + (import "" "stream.write" (func $stream.write (param i32 i32 i32) (result i32))) + (import "" "stream.drop-readable" (func $stream.drop-readable (param i32))) + (import "" "stream.drop-writable" (func $stream.drop-writable (param i32))) + (import "" "future.new" (func $future.new (result i64))) + (import "" "future.write" (func $future.write (param i32 i32) (result i32))) + (import "" "future.drop-readable" (func $future.drop-readable (param i32))) + (import "" "future.drop-writable" (func $future.drop-writable (param i32))) + + ;; The event returned by waitable-set.{wait,poll} is stored at [0,8): the + ;; waitable index at 0 and the payload at 4. Its event code is stashed at + ;; 0x100 and stream/future payloads land at 16. + (global $ws (mut i32) (i32.const 0)) + (global $rx (mut i32) (i32.const 0)) + (global $tx (mut i32) (i32.const 0)) + + (func $start (global.set $ws (call $waitable-set.new))) + (start $start) + + (func $poll + (i32.store (i32.const 0x100) (call $waitable-set.poll (global.get $ws) (i32.const 0))) + ) + (func $wait + (i32.store (i32.const 0x100) (call $waitable-set.wait (global.get $ws) (i32.const 0))) + ) + (func $check-event (param $code i32) (param $index i32) (param $payload i32) + (if (i32.ne (local.get $code) (i32.load (i32.const 0x100))) (then unreachable)) + (if (i32.ne (local.get $index) (i32.load (i32.const 0))) (then unreachable)) + (if (i32.ne (local.get $payload) (i32.load (i32.const 4))) (then unreachable)) + ) + (func $assert-no-event + (call $poll) + (if (i32.ne (i32.const 0 (; NONE ;)) (i32.load (i32.const 0x100))) (then unreachable)) + ) + (func $new-stream + (local $ret64 i64) + (local.set $ret64 (call $stream.new)) + (global.set $rx (i32.wrap_i64 (local.get $ret64))) + (global.set $tx (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + ) + (func $new-future + (local $ret64 i64) + (local.set $ret64 (call $future.new)) + (global.set $rx (i32.wrap_i64 (local.get $ret64))) + (global.set $tx (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + ) + + ;; Park the readable end of a fresh stream in the waitable set, drop the + ;; writable end and consume the resulting DROPPED notification. The + ;; readable end is left joined to the set unless $unjoin. + (func $notify-reader (param $unjoin i32) + (call $new-stream) + (call $waitable.join (global.get $rx) (global.get $ws)) + (call $stream.drop-writable (global.get $tx)) + (call $poll) + (call $check-event (i32.const 2 (; STREAM_READ ;)) (global.get $rx) + (i32.const 0x01 (; DROPPED=1 | (0<<4) ;))) + (if (local.get $unjoin) + (then (call $waitable.join (global.get $rx) (i32.const 0)))) + ) + ;; The mirror: park the writable end and drop the readable end. + (func $notify-writer + (call $new-stream) + (call $waitable.join (global.get $tx) (global.get $ws)) + (call $stream.drop-readable (global.get $rx)) + (call $poll) + (call $check-event (i32.const 3 (; STREAM_WRITE ;)) (global.get $tx) + (i32.const 0x01 (; DROPPED=1 | (0<<4) ;))) + ) + ;; Same for a future: park the writable end and drop the readable end. + (func $notify-future-writer + (call $new-future) + (call $waitable.join (global.get $tx) (global.get $ws)) + (call $future.drop-readable (global.get $rx)) + (call $poll) + (call $check-event (i32.const 5 (; FUTURE_WRITE ;)) (global.get $tx) + (i32.const 1 (; DROPPED ;))) + (call $waitable.join (global.get $tx) (i32.const 0)) + ) + + ;; A parked (idle, in a waitable set) readable stream end is notified when + ;; the writable end is dropped, and the notification is one-shot. + (func (export "reader-parked-poll") (result i32) + (call $notify-reader (i32.const 0)) + (call $assert-no-event) + (call $stream.drop-readable (global.get $rx)) + (call $waitable-set.drop (global.get $ws)) + (i32.const 42) + ) + + ;; Same, but delivered by waitable-set.wait, which must return immediately + ;; instead of blocking forever: this is the motivating case for delivering + ;; drops to idle ends at all. + (func (export "reader-parked-wait") (result i32) + (call $new-stream) + (call $waitable.join (global.get $rx) (global.get $ws)) + (call $stream.drop-writable (global.get $tx)) + (call $wait) + (call $check-event (i32.const 2 (; STREAM_READ ;)) (global.get $rx) + (i32.const 0x01 (; DROPPED=1 | (0<<4) ;))) + (call $assert-no-event) + (call $stream.drop-readable (global.get $rx)) + (call $waitable-set.drop (global.get $ws)) + (i32.const 42) + ) + + ;; The mirror case: a parked writable stream end is notified when the + ;; readable end is dropped. + (func (export "writer-parked") (result i32) + (call $notify-writer) + (call $assert-no-event) + (call $stream.drop-writable (global.get $tx)) + (call $waitable-set.drop (global.get $ws)) + (i32.const 42) + ) + + ;; A parked writable future end is notified when the readable end is + ;; dropped. (Dropping it afterwards is "future-drop-writable-after- + ;; notification" below.) + (func (export "future-writer-parked") (result i32) + (call $notify-future-writer) + (call $assert-no-event) + (call $waitable-set.drop (global.get $ws)) + (i32.const 42) + ) + + ;; With both ends in the same waitable set, dropping the writable end + ;; produces exactly one event (for the surviving readable end). + (func (export "both-ends-in-set") (result i32) + (call $new-stream) + (call $waitable.join (global.get $rx) (global.get $ws)) + (call $waitable.join (global.get $tx) (global.get $ws)) + (call $stream.drop-writable (global.get $tx)) + (call $poll) + (call $check-event (i32.const 2 (; STREAM_READ ;)) (global.get $rx) + (i32.const 0x01 (; DROPPED=1 | (0<<4) ;))) + (call $assert-no-event) + (call $stream.drop-readable (global.get $rx)) + (call $waitable-set.drop (global.get $ws)) + (i32.const 42) + ) + + ;; The notification is set pending when the peer is dropped, independently of + ;; waitable set membership: joining the set afterwards still delivers it. + (func (export "drop-before-join") (result i32) + (call $new-stream) + (call $stream.drop-writable (global.get $tx)) + (call $waitable.join (global.get $rx) (global.get $ws)) + (call $poll) + (call $check-event (i32.const 2 (; STREAM_READ ;)) (global.get $rx) + (i32.const 0x01 (; DROPPED=1 | (0<<4) ;))) + (call $assert-no-event) + (call $stream.drop-readable (global.get $rx)) + (call $waitable-set.drop (global.get $ws)) + (i32.const 42) + ) + + ;; A synchronous stream.read on an end that is in no waitable set: the + ;; pending notification means the read reports DROPPED without blocking. + (func (export "sync-read-consumes-notification") (result i32) + (call $new-stream) + (call $stream.drop-writable (global.get $tx)) + (if (i32.ne (i32.const 0x01 (; DROPPED=1 | (0<<4) ;)) + (call $stream.read-sync (global.get $rx) (i32.const 16) (i32.const 4))) + (then unreachable)) + (call $stream.drop-readable (global.get $rx)) + (i32.const 42) + ) + + ;; An async stream.read consumes the pending notification inline, returning + ;; DROPPED immediately; the waitable set must then be empty (no double + ;; delivery of the same drop). + (func (export "read-consumes-notification") (result i32) + (call $new-stream) + (call $waitable.join (global.get $rx) (global.get $ws)) + (call $stream.drop-writable (global.get $tx)) + (if (i32.ne (i32.const 0x01 (; DROPPED=1 | (0<<4) ;)) + (call $stream.read (global.get $rx) (i32.const 16) (i32.const 4))) + (then unreachable)) + (call $assert-no-event) + (call $stream.drop-readable (global.get $rx)) + (call $waitable-set.drop (global.get $ws)) + (i32.const 42) + ) + + ;; A notified writable future end may be dropped without having written. + (func (export "future-drop-writable-after-notification") (result i32) + (call $notify-future-writer) + (call $future.drop-writable (global.get $tx)) + (call $waitable-set.drop (global.get $ws)) + (i32.const 42) + ) + + (func (export "trap-read-after-notification") + (call $notify-reader (i32.const 0)) + (drop (call $stream.read (global.get $rx) (i32.const 16) (i32.const 4))) + unreachable + ) + + (func (export "trap-write-after-notification") + (call $notify-writer) + (drop (call $stream.write (global.get $tx) (i32.const 16) (i32.const 4))) + unreachable + ) + + (func (export "trap-future-write-after-notification") + (call $notify-future-writer) + (drop (call $future.write (global.get $tx) (i32.const 16))) + unreachable + ) + + ;; Lifting a notified readable stream end out of the component traps just + ;; like lifting any other done end. + (func (export "trap-lift-after-notification") (result i32) + (call $notify-reader (i32.const 1)) + (global.get $rx) + ) + ) + (type $ST (stream u8)) + (type $FT (future u8)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.wait (memory (core memory $memory "mem")) (core func $waitable-set.wait)) + (canon waitable-set.poll (memory (core memory $memory "mem")) (core func $waitable-set.poll)) + (canon waitable-set.drop (core func $waitable-set.drop)) + (canon stream.new $ST (core func $stream.new)) + (canon stream.read $ST async (memory (core memory $memory "mem")) (core func $stream.read)) + (canon stream.read $ST (memory (core memory $memory "mem")) (core func $stream.read-sync)) + (canon stream.write $ST async (memory (core memory $memory "mem")) (core func $stream.write)) + (canon stream.drop-readable $ST (core func $stream.drop-readable)) + (canon stream.drop-writable $ST (core func $stream.drop-writable)) + (canon future.new $FT (core func $future.new)) + (canon future.write $FT async (memory (core memory $memory "mem")) (core func $future.write)) + (canon future.drop-readable $FT (core func $future.drop-readable)) + (canon future.drop-writable $FT (core func $future.drop-writable)) + (core instance $m (instantiate $M (with "" (instance + (export "mem" (memory $memory "mem")) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait" (func $waitable-set.wait)) + (export "waitable-set.poll" (func $waitable-set.poll)) + (export "waitable-set.drop" (func $waitable-set.drop)) + (export "stream.new" (func $stream.new)) + (export "stream.read" (func $stream.read)) + (export "stream.read-sync" (func $stream.read-sync)) + (export "stream.write" (func $stream.write)) + (export "stream.drop-readable" (func $stream.drop-readable)) + (export "stream.drop-writable" (func $stream.drop-writable)) + (export "future.new" (func $future.new)) + (export "future.write" (func $future.write)) + (export "future.drop-readable" (func $future.drop-readable)) + (export "future.drop-writable" (func $future.drop-writable)) + )))) + (func (export "reader-parked-poll") (result u32) (canon lift (core func $m "reader-parked-poll"))) + (func (export "reader-parked-wait") (result u32) (canon lift (core func $m "reader-parked-wait"))) + (func (export "writer-parked") (result u32) (canon lift (core func $m "writer-parked"))) + (func (export "future-writer-parked") (result u32) (canon lift (core func $m "future-writer-parked"))) + (func (export "both-ends-in-set") (result u32) (canon lift (core func $m "both-ends-in-set"))) + (func (export "drop-before-join") (result u32) (canon lift (core func $m "drop-before-join"))) + (func (export "sync-read-consumes-notification") (result u32) (canon lift (core func $m "sync-read-consumes-notification"))) + (func (export "read-consumes-notification") (result u32) (canon lift (core func $m "read-consumes-notification"))) + (func (export "future-drop-writable-after-notification") (result u32) (canon lift (core func $m "future-drop-writable-after-notification"))) + (func (export "trap-read-after-notification") (canon lift (core func $m "trap-read-after-notification"))) + (func (export "trap-write-after-notification") (canon lift (core func $m "trap-write-after-notification"))) + (func (export "trap-future-write-after-notification") (canon lift (core func $m "trap-future-write-after-notification"))) + (func (export "trap-lift-after-notification") (result (stream u8)) (canon lift (core func $m "trap-lift-after-notification"))) +) + +;; $TransferTester checks that a readable stream end whose writable end has +;; already been dropped is an ordinary, transferrable value: it can be passed +;; as a parameter or returned as a result, and whichever component ends up +;; holding it observes the DROPPED notification (by reading, or by parking the +;; idle end in a waitable set). It also checks that the notification produced +;; by dropping one end reaches an idle peer held by a *different* component +;; instance, in both directions. +(component definition $TransferTester + (component $C + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $CM + (import "" "mem" (memory 1)) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.poll" (func $waitable-set.poll (param i32 i32) (result i32))) + (import "" "waitable-set.drop" (func $waitable-set.drop (param i32))) + (import "" "stream.new" (func $stream.new (result i64))) + (import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32))) + (import "" "stream.drop-readable" (func $stream.drop-readable (param i32))) + (import "" "stream.drop-writable" (func $stream.drop-writable (param i32))) + + (global $ws (mut i32) (i32.const 0)) + (global $tx (mut i32) (i32.const 0)) + + (func $start (global.set $ws (call $waitable-set.new))) + (start $start) + + ;; Create a stream, drop the writable end immediately and hand the + ;; readable end (which now has a pending DROPPED notification) to the + ;; caller. + (func (export "make-dropped-stream") (result i32) + (local $ret64 i64) + (local.set $ret64 (call $stream.new)) + (call $stream.drop-writable + (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (i32.wrap_i64 (local.get $ret64)) + ) + + ;; Create a stream, park the writable end in our waitable set and hand + ;; the readable end to the caller. + (func (export "make-stream") (result i32) + (local $ret64 i64) + (local.set $ret64 (call $stream.new)) + (global.set $tx (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (call $waitable.join (global.get $tx) (global.get $ws)) + (i32.wrap_i64 (local.get $ret64)) + ) + + (func (export "drop-writable") + (call $stream.drop-writable (global.get $tx)) + ) + + ;; Confirm that our parked writable end was notified that the caller + ;; dropped the readable end, exactly once. + (func (export "check-writer-notified") (result i32) + (if (i32.ne (i32.const 3 (; STREAM_WRITE ;)) + (call $waitable-set.poll (global.get $ws) (i32.const 0))) + (then unreachable)) + (if (i32.ne (global.get $tx) (i32.load (i32.const 0))) (then unreachable)) + (if (i32.ne (i32.const 0x01 (; DROPPED=1 | (0<<4) ;)) (i32.load (i32.const 4))) + (then unreachable)) + (if (i32.ne (i32.const 0 (; NONE ;)) + (call $waitable-set.poll (global.get $ws) (i32.const 0))) + (then unreachable)) + (call $stream.drop-writable (global.get $tx)) + (call $waitable-set.drop (global.get $ws)) + (i32.const 42) + ) + + ;; Read from a stream handed to us whose writable end is already gone. + (func (export "consume-dropped") (param $rx i32) (result i32) + (if (i32.ne (i32.const 0x01 (; DROPPED=1 | (0<<4) ;)) + (call $stream.read (local.get $rx) (i32.const 16) (i32.const 4))) + (then unreachable)) + (call $stream.drop-readable (local.get $rx)) + (i32.const 42) + ) + ) + (type $ST (stream u8)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.poll (memory (core memory $memory "mem")) (core func $waitable-set.poll)) + (canon waitable-set.drop (core func $waitable-set.drop)) + (canon stream.new $ST (core func $stream.new)) + (canon stream.read $ST async (memory (core memory $memory "mem")) (core func $stream.read)) + (canon stream.drop-readable $ST (core func $stream.drop-readable)) + (canon stream.drop-writable $ST (core func $stream.drop-writable)) + (core instance $cm (instantiate $CM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.poll" (func $waitable-set.poll)) + (export "waitable-set.drop" (func $waitable-set.drop)) + (export "stream.new" (func $stream.new)) + (export "stream.read" (func $stream.read)) + (export "stream.drop-readable" (func $stream.drop-readable)) + (export "stream.drop-writable" (func $stream.drop-writable)) + )))) + (func (export "make-dropped-stream") (result (stream u8)) (canon lift (core func $cm "make-dropped-stream"))) + (func (export "make-stream") (result (stream u8)) (canon lift (core func $cm "make-stream"))) + (func (export "drop-writable") (canon lift (core func $cm "drop-writable"))) + (func (export "check-writer-notified") (result u32) (canon lift (core func $cm "check-writer-notified"))) + (func (export "consume-dropped") (param "rx" (stream u8)) (result u32) (canon lift (core func $cm "consume-dropped"))) + ) + + (component $D + (import "c" (instance $c + (export "make-dropped-stream" (func (result (stream u8)))) + (export "make-stream" (func (result (stream u8)))) + (export "drop-writable" (func)) + (export "check-writer-notified" (func (result u32))) + (export "consume-dropped" (func (param "rx" (stream u8)) (result u32))) + )) + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $DM + (import "" "mem" (memory 1)) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.poll" (func $waitable-set.poll (param i32 i32) (result i32))) + (import "" "waitable-set.drop" (func $waitable-set.drop (param i32))) + (import "" "stream.new" (func $stream.new (result i64))) + (import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32))) + (import "" "stream.drop-readable" (func $stream.drop-readable (param i32))) + (import "" "stream.drop-writable" (func $stream.drop-writable (param i32))) + (import "" "make-dropped-stream" (func $make-dropped-stream (result i32))) + (import "" "make-stream" (func $make-stream (result i32))) + (import "" "drop-writable" (func $drop-writable)) + (import "" "check-writer-notified" (func $check-writer-notified (result i32))) + (import "" "consume-dropped" (func $consume-dropped (param i32) (result i32))) + + (func $check-dropped-event (param $ws i32) (param $rx i32) + (if (i32.ne (i32.const 2 (; STREAM_READ ;)) + (call $waitable-set.poll (local.get $ws) (i32.const 0))) + (then unreachable)) + (if (i32.ne (local.get $rx) (i32.load (i32.const 0))) (then unreachable)) + (if (i32.ne (i32.const 0x01 (; DROPPED=1 | (0<<4) ;)) (i32.load (i32.const 4))) + (then unreachable)) + (if (i32.ne (i32.const 0 (; NONE ;)) + (call $waitable-set.poll (local.get $ws) (i32.const 0))) + (then unreachable)) + ) + + ;; Pass an already-dropped-peer readable end to $C as a parameter. + (func (export "transfer-as-param") (result i32) + (local $ret64 i64) (local $rx i32) + (local.set $ret64 (call $stream.new)) + (local.set $rx (i32.wrap_i64 (local.get $ret64))) + (call $stream.drop-writable + (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (if (i32.ne (i32.const 42) (call $consume-dropped (local.get $rx))) + (then unreachable)) + (i32.const 42) + ) + + ;; Receive an already-dropped-peer readable end from $C as a result and + ;; read from it. + (func (export "transfer-as-result") (result i32) + (local $rx i32) + (local.set $rx (call $make-dropped-stream)) + (if (i32.ne (i32.const 0x01 (; DROPPED=1 | (0<<4) ;)) + (call $stream.read (local.get $rx) (i32.const 16) (i32.const 4))) + (then unreachable)) + (call $stream.drop-readable (local.get $rx)) + (i32.const 42) + ) + + ;; Same, but instead of reading, park the received end in a waitable set: + ;; the notification travelled with the end and is reported under the + ;; receiver's own index. + (func (export "transfer-then-park") (result i32) + (local $rx i32) (local $ws i32) + (local.set $ws (call $waitable-set.new)) + (local.set $rx (call $make-dropped-stream)) + (call $waitable.join (local.get $rx) (local.get $ws)) + (call $check-dropped-event (local.get $ws) (local.get $rx)) + (call $stream.drop-readable (local.get $rx)) + (call $waitable-set.drop (local.get $ws)) + (i32.const 42) + ) + + ;; $C parks its writable end; we drop our readable end; $C is notified. + (func (export "notify-writer-in-other-component") (result i32) + (call $stream.drop-readable (call $make-stream)) + (if (i32.ne (i32.const 42) (call $check-writer-notified)) + (then unreachable)) + (i32.const 42) + ) + + ;; We park the readable end; $C drops its writable end; we are notified. + (func (export "notify-reader-in-other-component") (result i32) + (local $rx i32) (local $ws i32) + (local.set $ws (call $waitable-set.new)) + (local.set $rx (call $make-stream)) + (call $waitable.join (local.get $rx) (local.get $ws)) + (call $drop-writable) + (call $check-dropped-event (local.get $ws) (local.get $rx)) + (call $stream.drop-readable (local.get $rx)) + (call $waitable-set.drop (local.get $ws)) + (i32.const 42) + ) + ) + (type $ST (stream u8)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.poll (memory (core memory $memory "mem")) (core func $waitable-set.poll)) + (canon waitable-set.drop (core func $waitable-set.drop)) + (canon stream.new $ST (core func $stream.new)) + (canon stream.read $ST async (memory (core memory $memory "mem")) (core func $stream.read)) + (canon stream.drop-readable $ST (core func $stream.drop-readable)) + (canon stream.drop-writable $ST (core func $stream.drop-writable)) + (canon lower (func $c "make-dropped-stream") (core func $make-dropped-stream')) + (canon lower (func $c "make-stream") (core func $make-stream')) + (canon lower (func $c "drop-writable") (core func $drop-writable')) + (canon lower (func $c "check-writer-notified") (core func $check-writer-notified')) + (canon lower (func $c "consume-dropped") (core func $consume-dropped')) + (core instance $dm (instantiate $DM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.poll" (func $waitable-set.poll)) + (export "waitable-set.drop" (func $waitable-set.drop)) + (export "stream.new" (func $stream.new)) + (export "stream.read" (func $stream.read)) + (export "stream.drop-readable" (func $stream.drop-readable)) + (export "stream.drop-writable" (func $stream.drop-writable)) + (export "make-dropped-stream" (func $make-dropped-stream')) + (export "make-stream" (func $make-stream')) + (export "drop-writable" (func $drop-writable')) + (export "check-writer-notified" (func $check-writer-notified')) + (export "consume-dropped" (func $consume-dropped')) + )))) + (func (export "transfer-as-param") (result u32) (canon lift (core func $dm "transfer-as-param"))) + (func (export "transfer-as-result") (result u32) (canon lift (core func $dm "transfer-as-result"))) + (func (export "transfer-then-park") (result u32) (canon lift (core func $dm "transfer-then-park"))) + (func (export "notify-writer-in-other-component") (result u32) (canon lift (core func $dm "notify-writer-in-other-component"))) + (func (export "notify-reader-in-other-component") (result u32) (canon lift (core func $dm "notify-reader-in-other-component"))) + ) + (instance $c (instantiate $C)) + (instance $d (instantiate $D (with "c" (instance $c)))) + (func (export "transfer-as-param") (alias export $d "transfer-as-param")) + (func (export "transfer-as-result") (alias export $d "transfer-as-result")) + (func (export "transfer-then-park") (alias export $d "transfer-then-park")) + (func (export "notify-writer-in-other-component") (alias export $d "notify-writer-in-other-component")) + (func (export "notify-reader-in-other-component") (alias export $d "notify-reader-in-other-component")) +) +;; Cases that only require the idle-end notification to be delivered. +(component instance $i $Tester) +(assert_return (invoke "reader-parked-poll") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "reader-parked-wait") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "writer-parked") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "future-writer-parked") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "both-ends-in-set") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "drop-before-join") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "sync-read-consumes-notification") (u32.const 42)) +(component instance $i $TransferTester) +(assert_return (invoke "transfer-as-param") (u32.const 42)) +(component instance $i $TransferTester) +(assert_return (invoke "transfer-as-result") (u32.const 42)) +(component instance $i $TransferTester) +(assert_return (invoke "transfer-then-park") (u32.const 42)) +(component instance $i $TransferTester) +(assert_return (invoke "notify-writer-in-other-component") (u32.const 42)) +(component instance $i $TransferTester) +(assert_return (invoke "notify-reader-in-other-component") (u32.const 42)) + +;; Cases that additionally require the notified end to become "done". +(component instance $i $Tester) +(assert_return (invoke "read-consumes-notification") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "future-drop-writable-after-notification") (u32.const 42)) +(component instance $i $Tester) +(assert_trap (invoke "trap-read-after-notification") "cannot read from stream after being notified that the writable end dropped") +(component instance $i $Tester) +(assert_trap (invoke "trap-write-after-notification") "cannot write to stream after being notified that the readable end dropped") +(component instance $i $Tester) +(assert_trap (invoke "trap-future-write-after-notification") "cannot write to future after previous write succeeded or readable end dropped") +(component instance $i $Tester) +(assert_trap (invoke "trap-lift-after-notification") "cannot lift stream after being notified that the writable end dropped") diff --git a/test/nyi.txt b/test/nyi.txt index ba0b84d1..cdc233f7 100644 --- a/test/nyi.txt +++ b/test/nyi.txt @@ -1,4 +1,3 @@ # See README.md ./validation/kebab.wast ./binary/binary.wast -./async/zero-length.wast