diff --git a/design/mvp/Binary.md b/design/mvp/Binary.md index 9a022d72..80903f5a 100644 --- a/design/mvp/Binary.md +++ b/design/mvp/Binary.md @@ -316,6 +316,7 @@ canon ::= 0x00 0x00 f: opts: ft: => (canon lift | 0x12 t: async?: => (canon stream.cancel-write t async? (core func)) ๐Ÿ”€ | 0x13 t: => (canon stream.drop-readable t (core func)) ๐Ÿ”€ | 0x14 t: => (canon stream.drop-writable t (core func)) ๐Ÿ”€ + | 0x2e t: => (canon stream.forward t (core func)) โžก๏ธ | 0x15 t: => (canon future.new t (core func)) ๐Ÿ”€ | 0x16 t: opts: => (canon future.read t opts (core func)) ๐Ÿ”€ | 0x17 t: opts: => (canon future.write t opts (core func)) ๐Ÿ”€ @@ -323,6 +324,7 @@ canon ::= 0x00 0x00 f: opts: ft: => (canon lift | 0x19 t: async?: => (canon future.cancel-write t async? (core func)) ๐Ÿ”€ | 0x1a t: => (canon future.drop-readable t (core func)) ๐Ÿ”€ | 0x1b t: => (canon future.drop-writable t (core func)) ๐Ÿ”€ + | 0x2f t: => (canon future.forward t (core func)) โžก๏ธ | 0x1c opts: => (canon error-context.new opts (core func)) ๐Ÿ“ | 0x1d opts: => (canon error-context.debug-message opts (core func)) ๐Ÿ“ | 0x1e => (canon error-context.drop (core func)) ๐Ÿ“ diff --git a/design/mvp/CanonicalABI.md b/design/mvp/CanonicalABI.md index 8ed6e64b..55c72aa3 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,10 +53,10 @@ 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 {stream,future}.forward`](#-canon-streamfutureforward) โžก๏ธ * [`canon thread.index`](#-canon-threadindex) ๐Ÿงต * [`canon thread.new-indirect`](#-canon-threadnew-indirect) ๐Ÿงต * [`canon thread.resume-later`](#-canon-threadresume-later) ๐Ÿงต @@ -1477,7 +1476,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 +1496,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 +1522,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 +1533,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 +1554,332 @@ 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 SharedBase: - t: ValType - cancel: Callable[[], None] - drop: Callable[[], None] +class End(Waitable): + class State(Enum): + IDLE = 1 + COPYING = 2 + CANCELLING_COPY = 3 + DONE = 4 -class ReadableStream(SharedBase): - read: Callable[[ComponentInstance, WritableBuffer, OnCopy, OnCopyDone], None] + t: ValType + 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. + +When guest code calls a `{stream,future}.{cancel-,}{read,write}` built-in, the +result returned to wasm code can be one of the following 3 `End.Result` codes +(or the sentinel `BLOCKED` (`-1`) code, if the operation is `async` and blocks). +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. For streams, the `End.Result` code +is returned in a packed `i32` along with the number of values copied. +```python + class Result(IntEnum): + COMPLETED = 0 + DROPPED = 1 + CANCELLED = 2 +``` +`End.Result` values are passed to the `{Stream,Future}End.notify` method that is +called by the following `End` methods. `notify` delivers the given `End.Result` +code to wasm code the next time `Waitable.get_pending_event` is called for this +`End`. + +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(End.Result.DROPPED, 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(End.Result.COMPLETED, buffer.progress) + self.other.notify(End.Result.COMPLETED, 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(End.Result.COMPLETED, 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(End.Result.COMPLETED, 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 `self.notify(CANCELLED)` 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 previous copy progress. 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(End.Result.CANCELLED) +``` + +The `End.forward` function is called by `{stream,future}.forward` to efficiently +forward all the values from a given readable end into a given writable end, +propagating `DROPPED` results in both directions. This causes the given readable +end and writable end to disappear, leaving only the `other` writable and +readable ends, which are now linked together directly to form a single stream or +future. If *both* of these formerly-separate ends have pending copy operations +with pending `buffer`s, the forwarding operation performs a synchronous copy, +leaving at most one pending buffer (the bigger of the two) and notifying one or +both sides of the progress made. In the corner case where a stream or future's +readable end is forwarded to its own writable end (creating a trivial recursive +loop), there is no trap since the whole thing simply disappears. +```python + def forward(src: End, dst: End): + if src.other is dst or src.other is None or dst.other is None: + src.drop() + dst.drop() + else: + writable_end = src.other + readable_end = dst.other + writable_end.other = readable_end + readable_end.other = writable_end + if writable_end.buffer is not None and readable_end.buffer is not None: + if readable_end.buffer.remain() > writable_end.buffer.remain(): + bigger_end, smaller_end = readable_end, writable_end + else: + bigger_end, smaller_end = writable_end, readable_end + buffer = smaller_end.buffer + smaller_end.buffer = None + smaller_end.copy(buffer) ``` -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}` +and `End.forward` 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(End.Result.DROPPED) + 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] -``` -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: -```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) -``` -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) - - def drop(self): - trap_if(self.state != CopyState.DONE) - CopyEnd.drop(self) + def copying_or_cancelling(self): + return self.state in { End.State.COPYING, End.State.CANCELLING_COPY } +``` + +Next, the intermediate `{Stream,Future}End` base classes are defined to implement +the `{Stream,Future}End.notify` methods that are 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 separate classes. + +`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. Additionally, the `buffer` passed to `stream.{read,write}` +stays available for repeated (smaller) copies until wasm code is notified. If +between setting and delivering a `stream_event` the other stream end was +dropped, the original `result` is "upgraded" to `DROPPED` (as this conveys more +information sooner). Finally, this possibly-upgraded result 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, result: End.Result, progress = 0): + def stream_event(): + self.buffer = None + if self.other is None: + upgraded_result = End.Result.DROPPED + self.state = End.State.DONE + else: + upgraded_result = result + self.state = End.State.IDLE + assert(0 <= upgraded_result < 2**4) + assert(progress <= Buffer.MAX_LENGTH < 2**28) + packed_result = upgraded_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 `End.Result` +(as asserted below). Because of this, the only case where it's necessary to +"upgrade" a result is when cancellation blocks and races with the readable end +dropping. Second, future ends immediately transition to the `DONE` state (where +the only valid operation is to call `future.drop-{readable,writable}`) after +notifying wasm of a `COMPLETED` `End.Result` (unlike streams, which only +transition to `DONE` after receiving `DROPPED`). +```python +class FutureEnd(End): + def notify(self, result: End.Result, progress = 0): + def future_event(): + upgraded_result = result + match result: + case End.Result.COMPLETED: + assert(self.buffer is None and progress == 1) + self.state = End.State.DONE + case End.Result.DROPPED: + assert(self.other is None and progress == 0) + self.buffer = None + self.state = End.State.DONE + case End.Result.CANCELLED: + assert(progress == 0) + self.buffer = None + if self.other is None: + upgraded_result = End.Result.DROPPED + self.state = End.State.DONE + else: + self.state = End.State.IDLE + return (self.event_code, self.index, upgraded_result) + Waitable.set_pending_event(self, future_event) +``` + +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 +1936,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 +2384,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 +2400,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 +2829,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, 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 ``` @@ -3062,9 +3009,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 +4253,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: +The implementations of these 4 built-ins all funnel down to a single +parameterized `copy` function: ```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_read(stream_t, opts, i, ptr, length): + return copy(ReadableStreamEnd, WritableGuestBuffer, stream_t, opts, i, ptr, length) -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. -```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] -``` - - -### ๐Ÿ”€ `canon future.{read,write}` - -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 +def canon_stream_write(stream_t, opts, i, ptr, length): + return copy(WritableStreamEnd, ReadableGuestBuffer, stream_t, opts, i, ptr, length) -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 the +`End.Result` 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 `End.Result` code. + +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 +4378,63 @@ 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. In any case, the cancellation request +races with the completion of the earlier copy operation and so the `End.Result` +returned by the event `payload` may or may not be `CANCELLED` depending on who +wins the race. + +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 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}.drop-{readable,writable}` @@ -4630,30 +4450,76 @@ 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, i): + inst = current_instance() + trap_if(not inst.may_leave) + 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 {stream,future}.forward` + +For canonical definitions: +```wat +(canon stream.forward $stream_t (core func $forward)) +(canon future.forward $future_t (core func $forward)) +``` +validation specifies: +* `$forward` is given type `(func (param $ri i32) (param $wi i32))` +* `$stream_t`/`$future_t` must be a type of the form `(stream $t?)`/`(future $t?)` + +Calling `$forward` removes the readable and writable ends at the given indices, +after checking that all the types match, the ends are in the `IDLE` state, and +the ends are not currently part of a waitable set. Then the readable end is +forwarded into the writable end as defined by `End.forward` above. +```python +def canon_stream_forward(stream_t, ri, wi): + return forward(ReadableStreamEnd, WritableStreamEnd, stream_t, ri, wi) + +def canon_future_forward(future_t, ri, wi): + return forward(ReadableFutureEnd, WritableFutureEnd, future_t, ri, wi) -def drop(EndT, stream_or_future_t, hi): +def forward(ReadableEndT, WritableEndT, stream_or_future_t, ri, wi): 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() + readable_end = inst.handles.remove(ri) + trap_if(not isinstance(readable_end, ReadableEndT)) + trap_if(readable_end.t != stream_or_future_t.t) + trap_if(readable_end.state != End.State.IDLE) + trap_if(readable_end.in_waitable_set()) + writable_end = inst.handles.remove(wi) + trap_if(not isinstance(writable_end, WritableEndT)) + trap_if(writable_end.t != stream_or_future_t.t) + trap_if(writable_end.state != End.State.IDLE) + trap_if(writable_end.in_waitable_set()) + End.forward(readable_end, writable_end) return [] ``` @@ -5118,8 +4984,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..282e441c 100644 --- a/design/mvp/Concurrency.md +++ b/design/mvp/Concurrency.md @@ -577,7 +577,7 @@ or `future`. When *producing* a `stream` or `future` value as a parameter (of an import call) or result (of an export call), the producer can *transfer ownership* of a readable end that it has either been given by the outside world or freshly created via `{stream,future}.new` (which also return a fresh paired -writable end that is permanently owned by the calling component instance). +writable end). Based on this, `stream` and `future` values can be passed between functions as if they were synchronous `list` and `T` values, resp. For @@ -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,15 @@ 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. +Given the readable end of one stream/future and the writable end of another, the +`{stream,future}.forward` built-ins can be called to efficiently forward all +remaining values from the readable end into the writable end, avoiding any +intermediate copies. Doing so relinquishes ownership of both handles, allowing +the calling component instance to be eagerly torn down while the forwarding is +in progress. + +The [Stream and Future State] section describes the runtime state maintained for +streams and futures by the Canonical ABI. ### Stream Readiness @@ -1478,7 +1485,6 @@ specified, the following features are being considered for addition to complete the concurrency story: * remove the temporary trap mentioned above that occurs when a `read` and `write` of a stream/future happen from within the same component instance -* zero-copy forwarding/splicing * allow `async` functions using the stackful ABI to be notified of cancellation * allow the `stream` type to validate; make it use `string-encoding` @@ -1574,8 +1580,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..3065f311 100644 --- a/design/mvp/Explainer.md +++ b/design/mvp/Explainer.md @@ -76,6 +76,7 @@ shipped as part of a future WASI Developer Preview release: * ๐Ÿ”—: canonical interface names * ๐Ÿ˜: [memory64] * ๐Ÿ“ก: getters and setters +* โžก๏ธ: `stream.forward` and `future.forward` built-ins ## Grammar @@ -1574,6 +1575,7 @@ canon ::= ... | (canon stream.cancel-write async? (core func ?)) ๐Ÿ”€ | (canon stream.drop-readable (core func ?)) ๐Ÿ”€ | (canon stream.drop-writable (core func ?)) ๐Ÿ”€ + | (canon stream.forward (core func ?)) โžก๏ธ | (canon future.new (core func ?)) ๐Ÿ”€ | (canon future.read * (core func ?)) ๐Ÿ”€ | (canon future.write * (core func ?)) ๐Ÿ”€ @@ -1581,6 +1583,7 @@ canon ::= ... | (canon future.cancel-write async? (core func ?)) ๐Ÿ”€ | (canon future.drop-readable (core func ?)) ๐Ÿ”€ | (canon future.drop-writable (core func ?)) ๐Ÿ”€ + | (canon future.forward (core func ?)) โžก๏ธ | (canon thread.index (core func ?)) ๐Ÿงต | (canon thread.new-indirect core-prefix() core-prefix() (core func ?)) ๐Ÿงต | (canon thread.resume-later (core func ?)) ๐Ÿงต @@ -2005,7 +2008,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}`. @@ -2146,6 +2149,25 @@ already been dropped. For details, see [Streams and Futures] in the concurrency explainer and [`canon_stream_drop_readable`] in the Canonical ABI explainer. +###### โžก๏ธ `stream.forward` and `future.forward` + +| Synopsis | | +| ---------------------------------------------- | -------------------------------------------------------------------------- | +| Approximate WIT signature for `stream.forward` | `func>(r: readable-stream-end, w: writable-stream-end)` | +| Approximate WIT signature for `future.forward` | `func>(r: readable-future-end, w: writable-future-end)` | +| Canonical ABI signature | `[ri:i32 wi:i32] -> []` | + +The `{stream,future}.forward` built-ins remove the given readable and writable +ends from the caller's handle table and logically forward everything from the +readable end into the writable end (propagating drops in both directions), but +do so without an intermediate copy. The call traps if either end has a +mismatched direction or element type, is in the middle of a read or write, is a +member of a waitable set, or has received its final `dropped` or, for futures, +`completed` result. + +For details, see [Streams and Futures] in the concurrency explainer and +[`canon_stream_forward`] in the Canonical ABI explainer. + ###### ๐Ÿงต `thread.index` | Synopsis | | @@ -3419,10 +3441,11 @@ 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_stream_forward`]: CanonicalABI.md#-canon-streamfutureforward [`canon_subtask_cancel`]: CanonicalABI.md#-canon-subtaskcancel [`canon_subtask_drop`]: CanonicalABI.md#-canon-subtaskdrop [`canon_resource_new`]: CanonicalABI.md#canon-resourcenew diff --git a/design/mvp/canonical-abi/definitions.py b/design/mvp/canonical-abi/definitions.py index 08f5109a..3daee8dc 100644 --- a/design/mvp/canonical-abi/definitions.py +++ b/design/mvp/canonical-abi/definitions.py @@ -4,10 +4,8 @@ ### Boilerplate -from __future__ import annotations from dataclasses import dataclass -from functools import partial -from typing import Any, Optional, Callable, TypeVar, Generic, Literal +from typing import Optional, Callable, Literal from enum import Enum, IntEnum import math import struct @@ -861,7 +859,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 +866,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 +887,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 +898,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 +908,171 @@ 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 + + class Result(IntEnum): + COMPLETED = 0 + DROPPED = 1 + CANCELLED = 2 + + 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(End.Result.DROPPED, 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(End.Result.COMPLETED, buffer.progress) + self.other.notify(End.Result.COMPLETED, 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(End.Result.COMPLETED, 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(End.Result.COMPLETED, progress = 0) def cancel(self): - self.reset_and_notify_pending(CopyResult.CANCELLED) - - 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) + 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(End.Result.CANCELLED) + + def forward(src: End, dst: End): + if src.other is dst or src.other is None or dst.other is None: + src.drop() + dst.drop() 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) + writable_end = src.other + readable_end = dst.other + writable_end.other = readable_end + readable_end.other = writable_end + if writable_end.buffer is not None and readable_end.buffer is not None: + if readable_end.buffer.remain() > writable_end.buffer.remain(): + bigger_end, smaller_end = readable_end, writable_end + else: + bigger_end, smaller_end = writable_end, readable_end + buffer = smaller_end.buffer + smaller_end.buffer = None + smaller_end.copy(buffer) -class ReadableFutureEnd(CopyEnd): - def copy(self, inst, dst_buffer, on_copy_done): - self.shared.read(inst, dst_buffer, on_copy_done) + 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(): + self.other.notify(End.Result.DROPPED) + self.other = None + Waitable.drop(self) -class WritableFutureEnd(CopyEnd): - def copy(self, inst, src_buffer, on_copy_done): - self.shared.write(inst, src_buffer, on_copy_done) + def copying_or_cancelling(self): + return self.state in { End.State.COPYING, End.State.CANCELLING_COPY } - def drop(self): - trap_if(self.state != CopyState.DONE) - CopyEnd.drop(self) +class StreamEnd(End): + def notify(self, result: End.Result, progress = 0): + def stream_event(): + self.buffer = None + if self.other is None: + upgraded_result = End.Result.DROPPED + self.state = End.State.DONE + else: + upgraded_result = result + self.state = End.State.IDLE + assert(0 <= upgraded_result < 2**4) + assert(progress <= Buffer.MAX_LENGTH < 2**28) + packed_result = upgraded_result | (progress << 4) + return (self.event_code, self.index, packed_result) + Waitable.set_pending_event(self, stream_event) + +class FutureEnd(End): + def notify(self, result: End.Result, progress = 0): + def future_event(): + upgraded_result = result + match result: + case End.Result.COMPLETED: + assert(self.buffer is None and progress == 1) + self.state = End.State.DONE + case End.Result.DROPPED: + assert(self.other is None and progress == 0) + self.buffer = None + self.state = End.State.DONE + case End.Result.CANCELLED: + assert(progress == 0) + self.buffer = None + if self.other is None: + upgraded_result = End.Result.DROPPED + self.state = End.State.DONE + else: + self.state = End.State.IDLE + return (self.event_code, self.index, upgraded_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 +1111,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 +1424,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 +1723,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 +1844,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 +2409,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 on_copy_done(result): - e.set_pending_event(partial(stream_event, result, reclaim_buffer = lambda:())) +def canon_stream_read(stream_t, opts, i, ptr, length): + return copy(ReadableStreamEnd, WritableGuestBuffer, stream_t, opts, i, ptr, length) - 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 +2495,48 @@ 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, i): + inst = current_instance() + trap_if(not inst.may_leave) + 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 {stream,future}.forward` + +def canon_stream_forward(stream_t, ri, wi): + return forward(ReadableStreamEnd, WritableStreamEnd, stream_t, ri, wi) + +def canon_future_forward(future_t, ri, wi): + return forward(ReadableFutureEnd, WritableFutureEnd, future_t, ri, wi) -def drop(EndT, stream_or_future_t, hi): +def forward(ReadableEndT, WritableEndT, stream_or_future_t, ri, wi): 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() + readable_end = inst.handles.remove(ri) + trap_if(not isinstance(readable_end, ReadableEndT)) + trap_if(readable_end.t != stream_or_future_t.t) + trap_if(readable_end.state != End.State.IDLE) + trap_if(readable_end.in_waitable_set()) + writable_end = inst.handles.remove(wi) + trap_if(not isinstance(writable_end, WritableEndT)) + trap_if(writable_end.t != stream_or_future_t.t) + trap_if(writable_end.state != End.State.IDLE) + trap_if(writable_end.in_waitable_set()) + End.forward(readable_end, writable_end) return [] ### ๐Ÿงต `canon thread.index` diff --git a/design/mvp/canonical-abi/run_tests.py b/design/mvp/canonical-abi/run_tests.py index 90dc344e..237540b9 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 @@ -862,7 +862,7 @@ def core_producer(args): sync_opts = mk_opts() [ret] = canon_future_read(FutureType(None), sync_opts, fut1, 0xdeadbeef) - assert(ret == CopyResult.COMPLETED) + assert(ret == End.Result.COMPLETED) [seti] = canon_waitable_set_new() @@ -876,7 +876,7 @@ def core_producer(args): def core_producer_callback(args): [event,payload1,payload2] = args assert(event == EventCode.FUTURE_READ) - assert(payload2 == CopyResult.COMPLETED) + assert(payload2 == End.Result.COMPLETED) [i] = canon_context_get('i32', 0) [] = canon_task_return([U32Type()], mk_opts(), [42 + i]) @@ -884,7 +884,7 @@ def core_producer_callback(args): fut3 = fut3s[i] sync_opts = mk_opts() [ret] = canon_future_read(FutureType(None), sync_opts, fut3, 0xdeadbeef) - assert(ret == CopyResult.COMPLETED) + assert(ret == End.Result.COMPLETED) return [CallbackCode.EXIT] producer_opts = mk_opts() @@ -949,7 +949,7 @@ def core_consumer(args): [] = canon_waitable_join(subi2, seti) [ret] = canon_future_write(FutureType(None), consumer_opts, wfut11, 0xdeadbeef) - assert(ret == CopyResult.COMPLETED) + assert(ret == End.Result.COMPLETED) retp = 0 [event] = canon_waitable_set_wait(MemInst(consumer_mem, 'i32'), seti, retp) @@ -958,7 +958,7 @@ def core_consumer(args): assert(consumer_mem[retp+4] == Subtask.State.STARTED) [ret] = canon_future_write(FutureType(None), consumer_opts, wfut12, 0xdeadbeef) - assert(ret == CopyResult.COMPLETED) + assert(ret == End.Result.COMPLETED) for i in range(10): canon_thread_yield() @@ -967,7 +967,7 @@ def core_consumer(args): assert(ret == EventCode.NONE) [ret] = canon_future_write(FutureType(None), consumer_opts, wfut21, 0xdeadbeef) - assert(ret == CopyResult.COMPLETED) + assert(ret == End.Result.COMPLETED) retp = 0 [event] = canon_waitable_set_wait(MemInst(consumer_mem, 'i32'), seti, retp) @@ -978,7 +978,7 @@ def core_consumer(args): [] = canon_subtask_drop(subi1) [ret] = canon_future_write(FutureType(None), consumer_opts, wfut22, 0xdeadbeef) - assert(ret == CopyResult.COMPLETED) + assert(ret == End.Result.COMPLETED) for i in range(10): canon_thread_yield() @@ -987,7 +987,7 @@ def core_consumer(args): assert(ret == EventCode.NONE) [ret] = canon_future_write(FutureType(None), consumer_opts, wfut13, 0xdeadbeef) - assert(ret == CopyResult.COMPLETED) + assert(ret == End.Result.COMPLETED) retp = 0 [event] = canon_waitable_set_wait(MemInst(consumer_mem, 'i32'), seti, retp) @@ -1004,7 +1004,7 @@ def core_consumer(args): [] = canon_waitable_join(subi3, seti) [ret] = canon_future_write(FutureType(None), consumer_opts, wfut23, 0xdeadbeef) - assert(ret == CopyResult.COMPLETED) + assert(ret == End.Result.COMPLETED) retp = 0 [event] = canon_waitable_set_wait(MemInst(consumer_mem, 'i32'), seti, retp) @@ -1029,7 +1029,7 @@ def test_sync_ignores_backpressure(): def core_callee1(args): [i,fut] = args [ret] = canon_future_read(FutureType(None), sync_opts, fut, 0xdeadbeef) - assert(ret == CopyResult.COMPLETED) + assert(ret == End.Result.COMPLETED) return [42 + i] async_callee = store.lift(core_callee1, async_ft, sync_opts, callee_inst) @@ -1060,7 +1060,7 @@ def core_caller(args): assert(caller_mem[retp2] == 86) [ret] = canon_future_write(FutureType(None), sync_opts, wfut, 0xdeadbeef) - assert(ret == CopyResult.COMPLETED) + assert(ret == End.Result.COMPLETED) [seti] = canon_waitable_set_new() [] = canon_waitable_join(subi, seti) @@ -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 remain(self): + return len(self.vs) - self.progress - def actually_cancel(self): - self.pending_on_copy_done(CopyResult.CANCELLED) - self.reset_pending() + def read(self, n): + assert(n <= self.remain()) + vs = self.vs[self.progress : self.progress + n] + self.progress += n + return vs - 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() +class HostWritableBuffer(WritableBuffer): + length: int + progress: int + received: list[any] -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 + 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 == End.Result.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) @@ -1504,7 +1496,7 @@ def core_func(args): [] = canon_task_return([StreamType(U8Type())], opts, [rsi2]) [ret] = canon_stream_read(StreamType(U8Type()), opts, rsi1, 0, 4) result,n = unpack_result(ret) - assert(n == 4 and result == CopyResult.COMPLETED) + assert(n == 4 and result == End.Result.COMPLETED) assert(mem[0:4] == b'\x01\x02\x03\x04') [packed] = canon_stream_new(StreamType(U8Type())) rsi3,wsi3 = unpack_new_ends(packed) @@ -1514,26 +1506,26 @@ def core_func(args): rsi4 = mem[retp] [ret] = canon_stream_write(StreamType(U8Type()), opts, wsi3, 0, 4) result,n = unpack_result(ret) - assert(n == 4 and result == CopyResult.COMPLETED) + assert(n == 4 and result == End.Result.COMPLETED) [ret] = canon_stream_read(StreamType(U8Type()), sync_opts, rsi4, 0, 4) result,n = unpack_result(ret) - assert(n == 4 and result == CopyResult.COMPLETED) + assert(n == 4 and result == End.Result.COMPLETED) [ret] = canon_stream_write(StreamType(U8Type()), opts, wsi2, 0, 4) result,n = unpack_result(ret) - assert(n == 4 and result == CopyResult.COMPLETED) + assert(n == 4 and result == End.Result.COMPLETED) [ret] = canon_stream_read(StreamType(U8Type()), opts, rsi1, 0, 4) result,n = unpack_result(ret) - assert(n == 4 and result == CopyResult.DROPPED) + assert(n == 4 and result == End.Result.DROPPED) assert(mem[0:4] == b'\x05\x06\x07\x08') [ret] = canon_stream_write(StreamType(U8Type()), sync_opts, wsi3, 0, 4) result,n = unpack_result(ret) - assert(n == 4 and result == CopyResult.COMPLETED) + assert(n == 4 and result == End.Result.COMPLETED) [ret] = canon_stream_read(StreamType(U8Type()), sync_opts, rsi4, 0, 4) result,n = unpack_result(ret) - assert(n == 4 and result == CopyResult.COMPLETED) + assert(n == 4 and result == End.Result.COMPLETED) [ret] = canon_stream_write(StreamType(U8Type()), sync_opts, wsi2, 0, 4) result,n = unpack_result(ret) - assert(n == 4 and result == CopyResult.COMPLETED) + assert(n == 4 and result == End.Result.COMPLETED) [] = canon_stream_drop_readable(StreamType(U8Type()), rsi1) [] = canon_stream_drop_readable(StreamType(U8Type()), rsi4) [] = canon_stream_drop_writable(StreamType(U8Type()), wsi2) @@ -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,16 +1579,15 @@ 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) result,n = unpack_result(mem[retp+4]) - assert(n == 4 and result == CopyResult.COMPLETED) + assert(n == 4 and result == End.Result.COMPLETED) assert(mem[0:4] == b'\x01\x02\x03\x04') [packed] = canon_stream_new(StreamType(U8Type())) rsi3,wsi3 = unpack_new_ends(packed) @@ -1617,35 +1597,35 @@ 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) assert(mem[retp+0] == wsi3) result,n = unpack_result(mem[retp+4]) - assert(n == 4 and result == CopyResult.COMPLETED) + assert(n == 4 and result == End.Result.COMPLETED) [ret] = canon_stream_read(StreamType(U8Type()), sync_opts, rsi4, 0, 4) result,n = unpack_result(ret) - assert(n == 4 and result == CopyResult.COMPLETED) + assert(n == 4 and result == End.Result.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() + assert(n == 4 and result == End.Result.COMPLETED) + 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(n == 4 and result == End.Result.DROPPED) 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) + assert(n == 4 and result == End.Result.COMPLETED) [] = canon_stream_drop_writable(StreamType(U8Type()), wsi3) [ret] = canon_stream_read(StreamType(U8Type()), opts, rsi4, 0, 4) assert(ret == definitions.BLOCKED) @@ -1655,31 +1635,31 @@ def core_func(args): assert(mem[retp+0] == rsi4) [] = canon_waitable_join(rsi4, 0) result,n = unpack_result(mem[retp+4]) - assert(n == 4 and result == CopyResult.COMPLETED) + assert(n == 4 and result == End.Result.COMPLETED) + host_writer.end_when_empty() [ret] = canon_stream_read(StreamType(U8Type()), sync_opts, rsi4, 0, 4) - assert(ret == CopyResult.DROPPED) + assert(ret == End.Result.DROPPED) [] = canon_stream_drop_readable(StreamType(U8Type()), rsi4) [ret] = canon_stream_write(StreamType(U8Type()), opts, wsi2, 0, 4) result,n = unpack_result(ret) - assert(n == 4 and result == CopyResult.COMPLETED) + assert(n == 4 and result == End.Result.COMPLETED) [] = canon_stream_drop_writable(StreamType(U8Type()), wsi2) [] = canon_waitable_set_drop(seti) 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) +def test_transfer_readable_end(): + 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,98 @@ 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_forward(): + store = Store() + mem = bytearray(32) + opts = mk_opts(memory=MemInst(mem, 'i32'), async_=True) + inst = ComponentInstance(store) + st = StreamType(U8Type()) + ft = FutureType(U8Type()) + + def core_func(args): + def new_stream(t = st): + [packed] = canon_stream_new(t) + return unpack_new_ends(packed) + def new_future(): + [packed] = canon_future_new(ft) + return unpack_new_ends(packed) + + rsi,wsi = new_stream() + [] = canon_stream_forward(st, rsi, wsi) + + rsi1,wsi1 = new_stream() + rsi2,wsi2 = new_stream() + [] = canon_stream_drop_readable(st, rsi2) + [] = canon_stream_forward(st, rsi1, wsi2) + [ret] = canon_stream_write(st, opts, wsi1, 0, 4) + result,n = unpack_result(ret) + assert(n == 0 and result == End.Result.DROPPED) + [] = canon_stream_drop_writable(st, wsi1) + + rsi1,wsi1 = new_stream() + rsi2,wsi2 = new_stream() + [] = canon_stream_forward(st, rsi1, wsi2) + mem[0:4] = b'\x01\x02\x03\x04' + [ret] = canon_stream_write(st, opts, wsi1, 0, 4) + assert(ret == definitions.BLOCKED) + [ret] = canon_stream_read(st, opts, rsi2, 8, 4) + result,n = unpack_result(ret) + assert(n == 4 and result == End.Result.COMPLETED) + assert(mem[8:12] == b'\x01\x02\x03\x04') + + rsi1,wsi1 = new_stream() + rsi2,wsi2 = new_stream() + [ret] = canon_stream_read(st, opts, rsi2, 8, 4) + assert(ret == definitions.BLOCKED) + [] = canon_stream_forward(st, rsi1, wsi2) + mem[0:4] = b'\x05\x06\x07\x08' + [ret] = canon_stream_write(st, opts, wsi1, 0, 4) + result,n = unpack_result(ret) + assert(n == 4 and result == End.Result.COMPLETED) + assert(mem[8:12] == b'\x05\x06\x07\x08') + [seti] = canon_waitable_set_new() + [] = canon_waitable_join(rsi2, seti) + [event] = canon_waitable_set_wait(MemInst(mem, 'i32'), seti, 16) + assert(event == EventCode.STREAM_READ) + assert(mem[16] == rsi2) + result,n = unpack_result(mem[20]) + assert(n == 4 and result == End.Result.COMPLETED) + + rfi1,wfi1 = new_future() + rfi2,wfi2 = new_future() + [ret] = canon_future_read(ft, opts, rfi2, 8) + assert(ret == definitions.BLOCKED) + [] = canon_future_forward(ft, rfi1, wfi2) + mem[0] = 42 + [ret] = canon_future_write(ft, opts, wfi1, 0) + assert(ret == End.Result.COMPLETED) + assert(mem[8] == 42) + + rfi1,wfi1 = new_future() + rfi2,wfi2 = new_future() + mem[0] = 43 + [ret] = canon_future_write(ft, opts, wfi1, 0) + assert(ret == definitions.BLOCKED) + [ret] = canon_future_read(ft, opts, rfi2, 8) + assert(ret == definitions.BLOCKED) + [] = canon_future_forward(ft, rfi1, wfi2) + assert(mem[8] == 43) + + rfi1,wfi1 = new_future() + rfi2,wfi2 = new_future() + [] = canon_future_drop_readable(ft, rfi2) + [] = canon_future_forward(ft, rfi1, wfi2) + [ret] = canon_future_write(ft, opts, wfi1, 0) + assert(ret == End.Result.DROPPED) + [] = canon_future_drop_writable(ft, wfi1) + + return [] + + caller_ft = FuncType([], [], async_ = True) + lift_and_run(mk_opts(), inst, caller_ft, core_func, lambda:[], lambda _:()) def test_receive_own_stream(): @@ -1704,7 +1775,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) @@ -1723,7 +1794,7 @@ def core_func(args): assert(rsi2 == 1) [ret] = canon_stream_cancel_write(StreamType(U8Type()), False, wsi) result,n = unpack_result(ret) - assert(result == CopyResult.CANCELLED and n == 0) + assert(result == End.Result.CANCELLED and n == 0) [] = canon_stream_drop_writable(StreamType(U8Type()), wsi) return [] @@ -1739,19 +1810,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) @@ -1764,15 +1835,16 @@ def core_func(args): assert(rsi == 1) [ret] = canon_stream_read(StreamType(U8Type()), opts, rsi, 0, 4) result,n = unpack_result(ret) - assert(n == 2 and result == CopyResult.COMPLETED) + assert(n == 2 and result == End.Result.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(n == 2 and result == End.Result.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) @@ -1780,7 +1852,7 @@ def core_func(args): assert(event == EventCode.STREAM_READ) assert(mem[retp+0] == rsi) result,n = unpack_result(mem[retp+4]) - assert(n == 2 and result == CopyResult.COMPLETED) + assert(n == 2 and result == End.Result.COMPLETED) [] = canon_stream_drop_readable(StreamType(U8Type()), rsi) [packed] = canon_stream_new(StreamType(U8Type())) @@ -1792,21 +1864,21 @@ def core_func(args): mem[0:6] = b'\x01\x02\x03\x04\x05\x06' [ret] = canon_stream_write(StreamType(U8Type()), opts, wsi, 0, 6) result,n = unpack_result(ret) - assert(n == 2 and result == CopyResult.COMPLETED) + assert(n == 2 and result == End.Result.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(n == 4 and result == End.Result.COMPLETED) + 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() @@ -1836,16 +1908,16 @@ def core_func1(args): mem1[0:4] = b'\x01\x02\x03\x04' [ret] = canon_stream_write(StreamType(U8Type()), opts1, wsi, 0, 4) result,n = unpack_result(ret) - assert(n == 4 and result == CopyResult.COMPLETED) + assert(n == 4 and result == End.Result.COMPLETED) [ret] = canon_stream_write(StreamType(U8Type()), opts1, wsi, 0, 4) result,n = unpack_result(ret) - assert(n == 4 and result == CopyResult.COMPLETED) + assert(n == 4 and result == End.Result.COMPLETED) [ret] = canon_stream_write(StreamType(U8Type()), opts1, wsi, 0, 0) assert(ret == definitions.BLOCKED) [ret] = canon_stream_cancel_write(StreamType(U8Type()), True, wsi) result,n = unpack_result(ret) - assert(n == 0 and result == CopyResult.CANCELLED) + assert(n == 0 and result == End.Result.CANCELLED) thread.wait_until(fut2.is_set) @@ -1862,7 +1934,7 @@ def core_func1(args): assert(event == EventCode.STREAM_WRITE) assert(mem1[retp+0] == wsi) result,n = unpack_result(mem1[retp+4]) - assert(n == 4 and result == CopyResult.COMPLETED) + assert(n == 4 and result == End.Result.COMPLETED) [ret] = canon_stream_write(StreamType(U8Type()), opts1, wsi, 12345, 0) assert(ret == definitions.BLOCKED) @@ -1912,7 +1984,7 @@ def core_func2(args): assert(event == EventCode.STREAM_READ) assert(mem2[retp+0] == rsi) result,n = unpack_result(mem2[retp+4]) - assert(n == 8 and result == CopyResult.COMPLETED) + assert(n == 8 and result == End.Result.COMPLETED) assert(mem2[0:8] == b'\x01\x02\x03\x04\x01\x02\x03\x04') fut2.set() @@ -1924,11 +1996,11 @@ def core_func2(args): mem2[0:8] = bytes(8) [ret] = canon_stream_read(StreamType(U8Type()), opts2, rsi, 0, 2) result,n = unpack_result(ret) - assert(n == 2 and result == CopyResult.COMPLETED) + assert(n == 2 and result == End.Result.COMPLETED) assert(mem2[0:6] == b'\x05\x06\x00\x00\x00\x00') [ret] = canon_stream_read(StreamType(U8Type()), opts2, rsi, 2, 2) result,n = unpack_result(ret) - assert(n == 2 and result == CopyResult.COMPLETED) + assert(n == 2 and result == End.Result.COMPLETED) assert(mem2[0:6] == b'\x05\x06\x07\x08\x00\x00') thread.wait_until(fut4.is_set) @@ -1940,7 +2012,7 @@ def core_func2(args): assert(event == EventCode.STREAM_READ) assert(mem2[retp+0] == rsi) p2 = int.from_bytes(mem2[retp+4 : retp+8], 'little', signed=False) - assert(p2 == (CopyResult.DROPPED | 1)) + assert(p2 == (End.Result.DROPPED | 1)) [] = canon_stream_drop_readable(StreamType(U8Type()), rsi) [] = canon_waitable_set_drop(seti) @@ -1968,10 +2040,10 @@ def core_func1(args): [ret] = canon_stream_write(StreamType(None), opts1, wsi, 10000, 2) result,n = unpack_result(ret) - assert(n == 2 and result == CopyResult.COMPLETED) + assert(n == 2 and result == End.Result.COMPLETED) [ret] = canon_stream_write(StreamType(None), opts1, wsi, 10000, 2) result,n = unpack_result(ret) - assert(n == 2 and result == CopyResult.COMPLETED) + assert(n == 2 and result == End.Result.COMPLETED) thread.wait_until(fut2.is_set) @@ -1987,7 +2059,7 @@ def core_func1(args): assert(event == EventCode.STREAM_WRITE) assert(mem1[retp+0] == wsi) result,n = unpack_result(mem1[retp+4]) - assert(n == 4 and result == CopyResult.COMPLETED) + assert(n == 4 and result == End.Result.COMPLETED) fut4.set() @@ -2025,23 +2097,23 @@ def core_func2(args): assert(event == EventCode.STREAM_READ) assert(mem2[retp+0] == rsi) result,n = unpack_result(mem2[retp+4]) - assert(n == 4 and result == CopyResult.COMPLETED) + assert(n == 4 and result == End.Result.COMPLETED) fut2.set() thread.wait_until(fut3.is_set) [ret] = canon_stream_read(StreamType(None), opts2, rsi, 1000000, 2) result,n = unpack_result(ret) - assert(n == 2 and result == CopyResult.COMPLETED) + assert(n == 2 and result == End.Result.COMPLETED) [ret] = canon_stream_read(StreamType(None), opts2, rsi, 1000000, 2) result,n = unpack_result(ret) - assert(n == 2 and result == CopyResult.COMPLETED) + assert(n == 2 and result == End.Result.COMPLETED) thread.wait_until(fut4.is_set) [ret] = canon_stream_read(StreamType(None), opts2, rsi, 1000000, 2) result,n = unpack_result(ret) - assert(n == 0 and result == CopyResult.DROPPED) + assert(n == 0 and result == End.Result.DROPPED) [] = canon_stream_drop_readable(StreamType(None), rsi) return [] @@ -2055,21 +2127,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 +2155,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 == End.Result.COMPLETED) [] = 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 +2171,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 == End.Result.COMPLETED) [] = 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]) @@ -2118,7 +2188,7 @@ def core_func(args): assert(ret == definitions.BLOCKED) [ret] = canon_stream_cancel_read(StreamType(U8Type()), False, rsi) result,n = unpack_result(ret) - assert(n == 0 and result == CopyResult.CANCELLED) + assert(n == 0 and result == End.Result.CANCELLED) [] = canon_stream_drop_readable(StreamType(U8Type()), rsi) [ret] = store.lower(host_func2_inst, host_ft2, lower_opts, inst)([retp]) @@ -2126,26 +2196,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 == End.Result.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]) - assert(n == 2 and result == CopyResult.CANCELLED) + host_writer.write([7,8]) + [ret] = canon_stream_cancel_read(StreamType(U8Type()), True, rsi) + result,n = unpack_result(ret) + assert(n == 2 and result == End.Result.COMPLETED) assert(mem[0:2] == b'\x07\x08') [] = canon_stream_drop_readable(StreamType(U8Type()), rsi) - [] = canon_waitable_set_drop(seti) return [] @@ -2153,56 +2219,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 +2227,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() @@ -2239,14 +2255,14 @@ def core_func(args): writep = 8 mem[writep] = 42 [ret] = canon_future_write(FutureType(U8Type()), lower_opts, wfi, writep) - assert(ret == CopyResult.COMPLETED) + assert(ret == End.Result.COMPLETED) [seti] = canon_waitable_set_new() [] = canon_waitable_join(rfi, seti) [event] = canon_waitable_set_wait(MemInst(mem, 'i32'), seti, retp) assert(event == EventCode.FUTURE_READ) assert(mem[retp+0] == rfi) - assert(mem[retp+4] == CopyResult.COMPLETED) + assert(mem[retp+4] == End.Result.COMPLETED) assert(mem[readp] == 43) [] = canon_future_drop_writable(FutureType(U8Type()), wfi) @@ -2266,13 +2282,13 @@ def core_func(args): writep = 8 mem[writep] = 42 [ret] = canon_future_write(FutureType(U8Type()), lower_opts, wfi, writep) - assert(ret == CopyResult.COMPLETED) + assert(ret == End.Result.COMPLETED) while not thread.task.inst.handles.get(rfi).has_pending_event(): canon_thread_yield() [ret] = canon_future_cancel_read(FutureType(U8Type()), False, rfi) - assert(ret == CopyResult.COMPLETED) + assert(ret == End.Result.COMPLETED) assert(mem[readp] == 43) [] = canon_future_drop_writable(FutureType(U8Type()), wfi) @@ -2319,7 +2335,7 @@ def core_func(args): [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) + assert(mem[retp+4] == End.Result.DROPPED) [] = canon_waitable_join(wfi, 0) [] = canon_waitable_set_drop(seti) [] = canon_future_drop_writable(future_t, wfi) @@ -2711,14 +2727,14 @@ def core_func(args): assert(ret == definitions.BLOCKED) [ret] = canon_future_read(FutureType(elemt), async_opts, rfi, 0) - assert(ret == CopyResult.COMPLETED) + assert(ret == End.Result.COMPLETED) [] = canon_future_drop_readable(FutureType(elemt), rfi) [] = canon_waitable_join(wfi, seti) [event] = canon_waitable_set_wait(MemInst(mem, 'i32'), seti, 0) assert(event == EventCode.FUTURE_WRITE) assert(mem[0] == wfi) - assert(mem[4] == CopyResult.COMPLETED) + assert(mem[4] == End.Result.COMPLETED) [] = canon_future_drop_writable(FutureType(elemt), wfi) [packed] = canon_stream_new(StreamType(elemt)) @@ -2728,10 +2744,10 @@ def core_func(args): [ret] = canon_stream_read(StreamType(elemt), async_opts, rsi, 0, 1) result,n = unpack_result(ret) - assert(n == 1 and result == CopyResult.COMPLETED) + assert(n == 1 and result == End.Result.COMPLETED) [ret] = canon_stream_read(StreamType(elemt), async_opts, rsi, 0, 4) result,n = unpack_result(ret) - assert(n == 2 and result == CopyResult.COMPLETED) + assert(n == 2 and result == End.Result.COMPLETED) [] = canon_stream_drop_readable(StreamType(elemt), rsi) [] = canon_waitable_join(wsi, seti) @@ -2739,10 +2755,34 @@ def core_func(args): assert(event == EventCode.STREAM_WRITE) assert(mem[0] == wsi) result,n = unpack_result(mem[4]) - assert(result == CopyResult.DROPPED) + assert(result == End.Result.DROPPED) 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 its completion. + [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 == End.Result.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 == End.Result.COMPLETED) + [ret] = canon_stream_cancel_write(StreamType(elemt), True, wsi) + result,n = unpack_result(ret) + assert(n == 0 and result == End.Result.CANCELLED) + [] = canon_stream_drop_readable(StreamType(elemt), rsi) + [] = canon_stream_drop_writable(StreamType(elemt), wsi) + [] = canon_waitable_set_drop(seti) return [] @@ -2940,7 +2980,7 @@ def thread_func4_2(args): assert(ret == definitions.BLOCKED) [] = canon_waitable_join(futr, wsi) [ret] = canon_future_write(FutureType(None), opts, futw, 0xdeadbeef) - assert(ret == CopyResult.COMPLETED) + assert(ret == End.Result.COMPLETED) return [] fi4_2 = ftbl.add(CoreFuncRef(ft, thread_func4_2)) @@ -3025,7 +3065,8 @@ def on_resolve(v): test_sync_using_wait() test_eager_stream_completion() test_async_stream_ops() -test_stream_forward() +test_transfer_readable_end() +test_forward() test_receive_own_stream() test_host_partial_reads_writes() test_wasm_to_wasm_stream() diff --git a/test/async/big-interleaving-test.wast b/test/async/big-interleaving-test.wast index 5ca9bf85..71ee132e 100644 --- a/test/async/big-interleaving-test.wast +++ b/test/async/big-interleaving-test.wast @@ -168,6 +168,7 @@ (import "" "stream.cancel-write" (func $stream.cancel-write (param i32) (result i32))) (import "" "stream.drop-readable" (func $stream.drop-readable (param i32))) (import "" "stream.drop-writable" (func $stream.drop-writable (param i32))) + (import "" "stream.forward" (func $stream.forward (param i32 i32))) (import "" "future.new" (func $future.new (result i64))) (import "" "future.read" (func $future.read (param i32 i32) (result i32))) (import "" "future.write" (func $future.write (param i32 i32) (result i32))) @@ -175,6 +176,7 @@ (import "" "future.cancel-write" (func $future.cancel-write (param i32) (result i32))) (import "" "future.drop-readable" (func $future.drop-readable (param i32))) (import "" "future.drop-writable" (func $future.drop-writable (param i32))) + (import "" "future.forward" (func $future.forward (param i32 i32))) (import "" "task.return" (func $task.return)) (import "" "waitable.join" (func $waitable.join (param i32 i32))) (import "" "waitable-set.new" (func $waitable-set.new (result i32))) @@ -314,6 +316,11 @@ (func (export "drop-writable") (param $slot i32) (call $stream.drop-writable (call $tx (local.get $slot)))) + (func (export "stream-forward") (param $src i32) (param $dst i32) + (call $stream.forward (call $rx (local.get $src)) (call $tx (local.get $dst)))) + (func (export "future-forward") (param $src i32) (param $dst i32) + (call $future.forward (call $rx (local.get $src)) (call $tx (local.get $dst)))) + (func (export "poll") (param $slot i32) (param $expected-event i32) (param $expected-payload i32) (local $ws i32) (local $event i32) (local.set $ws (call $waitable-set.new)) @@ -373,6 +380,7 @@ (canon stream.cancel-write $ST async (core func $stream.cancel-write)) (canon stream.drop-readable $ST (core func $stream.drop-readable)) (canon stream.drop-writable $ST (core func $stream.drop-writable)) + (canon stream.forward $ST (core func $stream.forward)) (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)) @@ -380,6 +388,7 @@ (canon future.cancel-write $FT async (core func $future.cancel-write)) (canon future.drop-readable $FT (core func $future.drop-readable)) (canon future.drop-writable $FT (core func $future.drop-writable)) + (canon future.forward $FT (core func $future.forward)) (canon task.return (core func $task.return)) (canon waitable.join (core func $waitable.join)) (canon waitable-set.new (core func $waitable-set.new)) @@ -411,6 +420,7 @@ (export "stream.cancel-write" (func $stream.cancel-write)) (export "stream.drop-readable" (func $stream.drop-readable)) (export "stream.drop-writable" (func $stream.drop-writable)) + (export "stream.forward" (func $stream.forward)) (export "future.new" (func $future.new)) (export "future.read" (func $future.read)) (export "future.write" (func $future.write)) @@ -418,6 +428,7 @@ (export "future.cancel-write" (func $future.cancel-write)) (export "future.drop-readable" (func $future.drop-readable)) (export "future.drop-writable" (func $future.drop-writable)) + (export "future.forward" (func $future.forward)) (export "task.return" (func $task.return)) (export "waitable.join" (func $waitable.join)) (export "waitable-set.new" (func $waitable-set.new)) @@ -453,6 +464,8 @@ (func (export "future-drop-writable") (param "slot" u8) (canon lift (core func $tm "future-drop-writable"))) (func (export "drop-readable") (param "slot" u8) (canon lift (core func $tm "drop-readable"))) (func (export "drop-writable") (param "slot" u8) (canon lift (core func $tm "drop-writable"))) + (func (export "stream-forward") (param "src" u8) (param "dst" u8) (canon lift (core func $tm "stream-forward"))) + (func (export "future-forward") (param "src" u8) (param "dst" u8) (canon lift (core func $tm "future-forward"))) (func (export "poll") (param "slot" u8) (param "event" u8) (param "payload" u32) (canon lift (core func $tm "poll"))) (func (export "await") async (param "slot" u8) (param "event" u8) (param "payload" u32) (canon lift (core func $tm "await") async (memory (core memory $memory "mem")))) @@ -469,6 +482,8 @@ (export $sub-args-e "sub-args" (type $sub-args)) (type $sub-expect (record (field "sub" u8) (field "state" u8))) (export $sub-expect-e "sub-expect" (type $sub-expect)) + (type $forward-args (record (field "src" u8) (field "dst" u8))) + (export $forward-args-e "forward-args" (type $forward-args)) (type $command (variant (case "stream-new" u8) (case "future-new" u8) @@ -501,7 +516,9 @@ (case "await-subtask" $sub-expect-e) (case "mock-bp-inc") (case "mock-bp-dec") - (case "subtask-cancel-await" $sub-expect-e))) + (case "subtask-cancel-await" $sub-expect-e) + (case "stream-forward" $forward-args-e) + (case "future-forward" $forward-args-e))) (export $command-e "command" (type $command)) (import "call-import" (func $call-import (param "slot" u8) (result s32))) (import "stream-new" (func $stream-new (param "slot" u8))) @@ -509,6 +526,8 @@ (import "testee-read" (func $testee-read (param "handle" u8) (param "bytes" u32) (result s32))) (import "drop-readable" (func $drop-readable (param "slot" u8))) (import "drop-writable" (func $drop-writable (param "slot" u8))) + (import "stream-forward" (func $stream-forward (param "src" u8) (param "dst" u8))) + (import "future-forward" (func $future-forward (param "src" u8) (param "dst" u8))) (import "poll" (func $poll (param "slot" u8) (param "event" u8) (param "payload" u32))) (import "await" (func $await async (param "slot" u8) (param "event" u8) (param "payload" u32))) (import "future-new" (func $future-new (param "slot" u8))) @@ -545,6 +564,8 @@ (import "" "call-import-future" (func $call-import-future (param i32) (result i32))) (import "" "drop-readable" (func $drop-readable (param i32))) (import "" "drop-writable" (func $drop-writable (param i32))) + (import "" "stream-forward" (func $stream-forward (param i32 i32))) + (import "" "future-forward" (func $future-forward (param i32 i32))) (import "" "poll" (func $poll (param i32 i32 i32))) (import "" "await" (func $await (param i32 i32 i32) (result i32))) (import "" "poll-readable" (func $poll-readable (param i32 i32 i32))) @@ -603,6 +624,8 @@ (global $MOCK_BP_INC i32 (i32.const 29)) (global $MOCK_BP_DEC i32 (i32.const 30)) (global $SUBTASK_CANCEL_AWAIT i32 (i32.const 31)) + (global $STREAM_FORWARD i32 (i32.const 32)) + (global $FUTURE_FORWARD i32 (i32.const 33)) (global $last (mut i32) (i32.const 0)) (global $VOID_OK i32 (i32.const 1337)) @@ -661,6 +684,19 @@ (call $drop-writable (i32.load8_u offset=4 (local.get $insn))) (global.set $last (global.get $VOID_OK)))) + (if (i32.eq (local.get $op) (global.get $STREAM_FORWARD)) + (then + (call $stream-forward + (i32.load8_u offset=4 (local.get $insn)) + (i32.load8_u offset=5 (local.get $insn))) + (global.set $last (global.get $VOID_OK)))) + (if (i32.eq (local.get $op) (global.get $FUTURE_FORWARD)) + (then + (call $future-forward + (i32.load8_u offset=4 (local.get $insn)) + (i32.load8_u offset=5 (local.get $insn))) + (global.set $last (global.get $VOID_OK)))) + (if (i32.eq (local.get $op) (global.get $CALL_IMPORT)) (then (global.set $last (call $call-import (i32.load8_u offset=4 (local.get $insn)))))) @@ -762,6 +798,8 @@ (canon lower (func $future-drop-writable) (core func $future-drop-writable')) (canon lower (func $drop-readable) (core func $drop-readable')) (canon lower (func $drop-writable) (core func $drop-writable')) + (canon lower (func $stream-forward) (core func $stream-forward')) + (canon lower (func $future-forward) (core func $future-forward')) (canon lower (func $poll) (core func $poll')) (canon lower (func $await) async (core func $await')) (canon lower (func $poll-readable) (core func $poll-readable')) @@ -791,6 +829,8 @@ (export "future-drop-writable" (func $future-drop-writable')) (export "drop-readable" (func $drop-readable')) (export "drop-writable" (func $drop-writable')) + (export "stream-forward" (func $stream-forward')) + (export "future-forward" (func $future-forward')) (export "poll" (func $poll')) (export "await" (func $await')) (export "poll-readable" (func $poll-readable')) @@ -831,6 +871,8 @@ (with "future-drop-writable" (func $testee "future-drop-writable")) (with "drop-readable" (func $testee "drop-readable")) (with "drop-writable" (func $testee "drop-writable")) + (with "stream-forward" (func $testee "stream-forward")) + (with "future-forward" (func $testee "future-forward")) (with "poll" (func $testee "poll")) (with "await" (func $testee "await")) (with "poll-readable" (func $testee "poll-readable")) @@ -853,6 +895,7 @@ (export "poll-expect" (type $driver "poll-expect")) (export "sub-args" (type $driver "sub-args")) (export "sub-expect" (type $driver "sub-expect")) + (export "forward-args" (type $driver "forward-args")) (export "command" (type $driver "command"))) (export "types" (instance $types)) (alias export $driver "run" (func $run)) @@ -1689,3 +1732,49 @@ (variant.const "expect-code" (s32.const 3)) (variant.const "subtask-drop" (u8.const 1)) (variant.const "mock-bp-dec")))) + +(component instance $i $Tester) + +(assert_return + (invoke "run" + (list.const + (variant.const "stream-new" (u8.const 0)) + (variant.const "stream-new" (u8.const 1)) + (variant.const "future-new" (u8.const 2)) + (variant.const "future-new" (u8.const 3)) + (variant.const "stream-new" (u8.const 4)) + (variant.const "stream-new" (u8.const 5)) + (variant.const "future-new" (u8.const 6)) + (variant.const "future-new" (u8.const 7)) + (variant.const "stream-forward" (record.const (field "src" u8.const 0) (field "dst" u8.const 1))) + (variant.const "future-forward" (record.const (field "src" u8.const 2) (field "dst" u8.const 3))) + (variant.const "stream-forward" (record.const (field "src" u8.const 4) (field "dst" u8.const 5))) + (variant.const "future-forward" (record.const (field "src" u8.const 6) (field "dst" u8.const 7))) + (variant.const "testee-write" (record.const (field "handle" u8.const 0) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "future-write" (u8.const 2)) + (variant.const "expect-code" (s32.const -1)) + (variant.const "testee-write" (record.const (field "handle" u8.const 4) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const -1)) + (variant.const "future-write" (u8.const 6)) + (variant.const "expect-code" (s32.const -1)) + (variant.const "future-read" (u8.const 7)) + (variant.const "expect-code" (s32.const 0)) + (variant.const "testee-read" (record.const (field "handle" u8.const 1) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "future-read" (u8.const 3)) + (variant.const "expect-code" (s32.const 0)) + (variant.const "testee-read" (record.const (field "handle" u8.const 5) (field "bytes" u32.const 4))) + (variant.const "expect-code" (s32.const 0x40)) + (variant.const "poll" (record.const (field "slot" u8.const 0) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 2) (field "event" enum.const "future-write") (field "payload" u32.const 0))) + (variant.const "poll" (record.const (field "slot" u8.const 4) (field "event" enum.const "stream-write") (field "payload" u32.const 0x40))) + (variant.const "poll" (record.const (field "slot" u8.const 6) (field "event" enum.const "future-write") (field "payload" u32.const 0))) + (variant.const "drop-writable" (u8.const 0)) + (variant.const "drop-readable" (u8.const 1)) + (variant.const "future-drop-writable" (u8.const 2)) + (variant.const "future-drop-readable" (u8.const 3)) + (variant.const "drop-writable" (u8.const 4)) + (variant.const "drop-readable" (u8.const 5)) + (variant.const "future-drop-writable" (u8.const 6)) + (variant.const "future-drop-readable" (u8.const 7))))) diff --git a/test/async/cancel-stream.wast b/test/async/cancel-stream.wast index 4bc9a0e9..aa57e449 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)) diff --git a/test/async/forward.wast b/test/async/forward.wast new file mode 100644 index 00000000..cb15ce0e --- /dev/null +++ b/test/async/forward.wast @@ -0,0 +1,1455 @@ +;; Tests the `stream.forward` and `future.forward` built-ins. + +(component definition $Tester + (core module $Memory + (memory (export "mem") 1) + (func (export "realloc") (param i32 i32 i32 i32) (result i32) + unreachable) + ) + (core instance $memory (instantiate $Memory)) + (core module $M + (import "" "mem" (memory 1)) + (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.join" (func $waitable.join (param i32 i32))) + (import "" "stream.new" (func $stream.new (result i64))) + (import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32))) + (import "" "stream.write" (func $stream.write (param i32 i32 i32) (result i32))) + (import "" "stream.cancel-read" (func $stream.cancel-read (param i32) (result i32))) + (import "" "stream.cancel-write" (func $stream.cancel-write (param i32) (result i32))) + (import "" "stream.drop-readable" (func $stream.drop-readable (param i32))) + (import "" "stream.drop-writable" (func $stream.drop-writable (param i32))) + (import "" "stream.forward" (func $stream.forward (param i32 i32))) + (import "" "stream.forward-u32" (func $stream.forward-u32 (param i32 i32))) + (import "" "stream.new-e" (func $stream.new-e (result i64))) + (import "" "stream.read-e" (func $stream.read-e (param i32 i32 i32) (result i32))) + (import "" "stream.write-e" (func $stream.write-e (param i32 i32 i32) (result i32))) + (import "" "stream.forward-e" (func $stream.forward-e (param i32 i32))) + (import "" "stream.new-s" (func $stream.new-s (result i64))) + (import "" "stream.read-s" (func $stream.read-s (param i32 i32 i32) (result i32))) + (import "" "stream.write-s" (func $stream.write-s (param i32 i32 i32) (result i32))) + (import "" "stream.forward-s" (func $stream.forward-s (param i32 i32))) + (import "" "future.new" (func $future.new (result i64))) + (import "" "future.read" (func $future.read (param i32 i32) (result i32))) + (import "" "future.write" (func $future.write (param i32 i32) (result i32))) + (import "" "future.drop-readable" (func $future.drop-readable (param i32))) + (import "" "future.drop-writable" (func $future.drop-writable (param i32))) + (import "" "future.forward" (func $future.forward (param i32 i32))) + (import "" "future.new-s" (func $future.new-s (result i64))) + (import "" "future.read-s" (func $future.read-s (param i32 i32) (result i32))) + (import "" "future.write-s" (func $future.write-s (param i32 i32) (result i32))) + (import "" "future.forward-s" (func $future.forward-s (param i32 i32))) + + ;; The three streams/futures used by the tests below are named A, B and C + ;; and their ends are kept in globals so that each test can stay readable. + ;; A fresh $Tester instance is created before every invoke, so the handle + ;; indices allocated by the $new-* helpers are deterministic: 1,2 for A, + ;; 3,4 for B and 5,6 for C. + (global $rA (mut i32) (i32.const 0)) + (global $wA (mut i32) (i32.const 0)) + (global $rB (mut i32) (i32.const 0)) + (global $wB (mut i32) (i32.const 0)) + (global $rC (mut i32) (i32.const 0)) + (global $wC (mut i32) (i32.const 0)) + + (func $new-a + (local $ret64 i64) + (local.set $ret64 (call $stream.new)) + (global.set $rA (i32.wrap_i64 (local.get $ret64))) + (global.set $wA (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + ) + (func $new-b + (local $ret64 i64) + (local.set $ret64 (call $stream.new)) + (global.set $rB (i32.wrap_i64 (local.get $ret64))) + (global.set $wB (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + ) + (func $new-c + (local $ret64 i64) + (local.set $ret64 (call $stream.new)) + (global.set $rC (i32.wrap_i64 (local.get $ret64))) + (global.set $wC (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + ) + (func $fnew-a + (local $ret64 i64) + (local.set $ret64 (call $future.new)) + (global.set $rA (i32.wrap_i64 (local.get $ret64))) + (global.set $wA (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + ) + (func $fnew-b + (local $ret64 i64) + (local.set $ret64 (call $future.new)) + (global.set $rB (i32.wrap_i64 (local.get $ret64))) + (global.set $wB (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + ) + (func $fnew-c + (local $ret64 i64) + (local.set $ret64 (call $future.new)) + (global.set $rC (i32.wrap_i64 (local.get $ret64))) + (global.set $wC (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + ) + (func $enew-a + (local $ret64 i64) + (local.set $ret64 (call $stream.new-e)) + (global.set $rA (i32.wrap_i64 (local.get $ret64))) + (global.set $wA (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + ) + (func $enew-b + (local $ret64 i64) + (local.set $ret64 (call $stream.new-e)) + (global.set $rB (i32.wrap_i64 (local.get $ret64))) + (global.set $wB (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + ) + (func $snew-a + (local $ret64 i64) + (local.set $ret64 (call $stream.new-s)) + (global.set $rA (i32.wrap_i64 (local.get $ret64))) + (global.set $wA (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + ) + (func $snew-b + (local $ret64 i64) + (local.set $ret64 (call $stream.new-s)) + (global.set $rB (i32.wrap_i64 (local.get $ret64))) + (global.set $wB (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + ) + (func $fsnew-a + (local $ret64 i64) + (local.set $ret64 (call $future.new-s)) + (global.set $rA (i32.wrap_i64 (local.get $ret64))) + (global.set $wA (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + ) + (func $fsnew-b + (local $ret64 i64) + (local.set $ret64 (call $future.new-s)) + (global.set $rB (i32.wrap_i64 (local.get $ret64))) + (global.set $wB (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + ) + + ;; the merged stream is usable in both arrival orders + (func (export "forward-then-write-then-read") (result i32) + (local $ret i32) + (call $new-a) + (call $new-b) + (call $stream.forward (global.get $rA) (global.get $wB)) + (i32.store (i32.const 0) (i32.const 0x04030201)) + (local.set $ret (call $stream.write (global.get $wA) (i32.const 0) (i32.const 4))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $stream.read (global.get $rB) (i32.const 8) (i32.const 4))) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 0x04030201) (i32.load (i32.const 8))) + (then unreachable)) + (i32.const 42) + ) + (func (export "forward-then-read-then-write") (result i32) + (local $ret i32) + (call $new-a) + (call $new-b) + (call $stream.forward (global.get $rA) (global.get $wB)) + (local.set $ret (call $stream.read (global.get $rB) (i32.const 8) (i32.const 4))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (i32.store (i32.const 0) (i32.const 0x04030201)) + (local.set $ret (call $stream.write (global.get $wA) (i32.const 0) (i32.const 4))) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 0x04030201) (i32.load (i32.const 8))) + (then unreachable)) + (i32.const 42) + ) + + ;; a copy that was already pending when the forward happened is carried + ;; over onto the merged stream + (func (export "read-then-forward-then-write") (result i32) + (local $ret i32) (local $ws i32) + (call $new-a) + (call $new-b) + (local.set $ret (call $stream.read (global.get $rB) (i32.const 8) (i32.const 4))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (call $stream.forward (global.get $rA) (global.get $wB)) + (i32.store (i32.const 0) (i32.const 0x04030201)) + (local.set $ret (call $stream.write (global.get $wA) (i32.const 0) (i32.const 4))) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 0x04030201) (i32.load (i32.const 8))) + (then unreachable)) + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (global.get $rB) (local.get $ws)) + (local.set $ret (call $waitable-set.poll (local.get $ws) (i32.const 32))) + (if (i32.ne (i32.const 2 (; STREAM_READ ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (global.get $rB) (i32.load (i32.const 32))) + (then unreachable)) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) (i32.load (i32.const 36))) + (then unreachable)) + (i32.const 42) + ) + + ;; both sides are blocked before the forward, so the forward itself + ;; performs the rendezvous + (func (export "rendezvous-during-forward") (result i32) + (local $ret i32) (local $ws1 i32) (local $ws2 i32) + (call $new-a) + (call $new-b) + (i32.store (i32.const 0) (i32.const 0x04030201)) + (local.set $ret (call $stream.write (global.get $wA) (i32.const 0) (i32.const 4))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $stream.read (global.get $rB) (i32.const 8) (i32.const 4))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (call $stream.forward (global.get $rA) (global.get $wB)) + (if (i32.ne (i32.const 0x04030201) (i32.load (i32.const 8))) + (then unreachable)) + (local.set $ws1 (call $waitable-set.new)) + (local.set $ws2 (call $waitable-set.new)) + (call $waitable.join (global.get $wA) (local.get $ws1)) + (call $waitable.join (global.get $rB) (local.get $ws2)) + (local.set $ret (call $waitable-set.poll (local.get $ws1) (i32.const 32))) + (if (i32.ne (i32.const 3 (; STREAM_WRITE ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (global.get $wA) (i32.load (i32.const 32))) + (then unreachable)) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) (i32.load (i32.const 36))) + (then unreachable)) + (local.set $ret (call $waitable-set.poll (local.get $ws2) (i32.const 40))) + (if (i32.ne (i32.const 2 (; STREAM_READ ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (global.get $rB) (i32.load (i32.const 40))) + (then unreachable)) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) (i32.load (i32.const 44))) + (then unreachable)) + (i32.const 42) + ) + + ;; the destination's readable end may already be in a waitable set when + ;; the forward happens, and the carried-over read must still be reported + ;; through it + (func (export "pending-read-in-set-before-forward") (result i32) + (local $ret i32) (local $ws i32) + (call $new-a) + (call $new-b) + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (global.get $rB) (local.get $ws)) + (local.set $ret (call $stream.read (global.get $rB) (i32.const 8) (i32.const 4))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (call $stream.forward (global.get $rA) (global.get $wB)) + (i32.store (i32.const 0) (i32.const 0x04030201)) + (local.set $ret (call $stream.write (global.get $wA) (i32.const 0) (i32.const 4))) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $waitable-set.poll (local.get $ws) (i32.const 32))) + (if (i32.ne (i32.const 2 (; STREAM_READ ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (global.get $rB) (i32.load (i32.const 32))) + (then unreachable)) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) (i32.load (i32.const 36))) + (then unreachable)) + (if (i32.ne (i32.const 0x04030201) (i32.load (i32.const 8))) + (then unreachable)) + (i32.const 42) + ) + + ;; discarding A's readable end and B's writable end must not look like a + ;; drop to the two ends that survive + (func (export "no-event-from-forward") (result i32) + (local $ret i32) (local $ws i32) + (call $new-a) + (call $new-b) + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (global.get $wA) (local.get $ws)) + (call $waitable.join (global.get $rB) (local.get $ws)) + (call $stream.forward (global.get $rA) (global.get $wB)) + (local.set $ret (call $waitable-set.poll (local.get $ws) (i32.const 32))) + (if (i32.ne (i32.const 0 (; NONE ;)) (local.get $ret)) + (then unreachable)) + (i32.store (i32.const 0) (i32.const 0x04030201)) + (local.set $ret (call $stream.write (global.get $wA) (i32.const 0) (i32.const 4))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $stream.read (global.get $rB) (i32.const 8) (i32.const 4))) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $waitable-set.poll (local.get $ws) (i32.const 32))) + (if (i32.ne (i32.const 3 (; STREAM_WRITE ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (global.get $wA) (i32.load (i32.const 32))) + (then unreachable)) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) (i32.load (i32.const 36))) + (then unreachable)) + (local.set $ret (call $waitable-set.poll (local.get $ws) (i32.const 32))) + (if (i32.ne (i32.const 0 (; NONE ;)) (local.get $ret)) + (then unreachable)) + (i32.const 42) + ) + + ;; a zero-length readiness read or write is carried over just like a real + ;; one and completes with 0 elements once the other end shows up + (func (export "zero-length-read-carried-over") (result i32) + (local $ret i32) (local $ws i32) + (call $new-a) + (call $new-b) + (local.set $ret (call $stream.read (global.get $rB) (i32.const 0) (i32.const 0))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (call $stream.forward (global.get $rA) (global.get $wB)) + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (global.get $rB) (local.get $ws)) + (i32.store (i32.const 0) (i32.const 0x04030201)) + (local.set $ret (call $stream.write (global.get $wA) (i32.const 0) (i32.const 4))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $waitable-set.poll (local.get $ws) (i32.const 32))) + (if (i32.ne (i32.const 2 (; STREAM_READ ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (global.get $rB) (i32.load (i32.const 32))) + (then unreachable)) + (if (i32.ne (i32.const 0x0 (; COMPLETED=0 | (0<<4) ;)) (i32.load (i32.const 36))) + (then unreachable)) + (local.set $ret (call $stream.read (global.get $rB) (i32.const 8) (i32.const 4))) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 0x04030201) (i32.load (i32.const 8))) + (then unreachable)) + (i32.const 42) + ) + (func (export "zero-length-write-then-forward") (result i32) + (local $ret i32) (local $ws i32) + (call $new-a) + (call $new-b) + (local.set $ret (call $stream.write (global.get $wA) (i32.const 0) (i32.const 0))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (call $stream.forward (global.get $rA) (global.get $wB)) + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (global.get $wA) (local.get $ws)) + (local.set $ret (call $stream.read (global.get $rB) (i32.const 8) (i32.const 4))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $waitable-set.poll (local.get $ws) (i32.const 32))) + (if (i32.ne (i32.const 3 (; STREAM_WRITE ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (global.get $wA) (i32.load (i32.const 32))) + (then unreachable)) + (if (i32.ne (i32.const 0x0 (; COMPLETED=0 | (0<<4) ;)) (i32.load (i32.const 36))) + (then unreachable)) + (i32.store (i32.const 0) (i32.const 0x04030201)) + (local.set $ret (call $stream.write (global.get $wA) (i32.const 0) (i32.const 4))) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 0x04030201) (i32.load (i32.const 8))) + (then unreachable)) + (i32.const 42) + ) + + ;; drop propagation in both directions + (func (export "dest-reader-dropped-before-forward") (result i32) + (local $ret i32) + (call $new-a) + (call $new-b) + (call $stream.drop-readable (global.get $rB)) + (call $stream.forward (global.get $rA) (global.get $wB)) + (local.set $ret (call $stream.write (global.get $wA) (i32.const 0) (i32.const 4))) + (if (i32.ne (i32.const 0x1 (; DROPPED=1 | (0<<4) ;)) (local.get $ret)) + (then unreachable)) + (call $stream.drop-writable (global.get $wA)) + (i32.const 42) + ) + (func (export "dest-reader-dropped-before-forward-blocked-writer") (result i32) + (local $ret i32) (local $ws i32) + (call $new-a) + (call $new-b) + (local.set $ret (call $stream.write (global.get $wA) (i32.const 0) (i32.const 4))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (call $stream.drop-readable (global.get $rB)) + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (global.get $wA) (local.get $ws)) + (call $stream.forward (global.get $rA) (global.get $wB)) + (local.set $ret (call $waitable-set.poll (local.get $ws) (i32.const 32))) + (if (i32.ne (i32.const 3 (; STREAM_WRITE ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (global.get $wA) (i32.load (i32.const 32))) + (then unreachable)) + (if (i32.ne (i32.const 0x1 (; DROPPED=1 | (0<<4) ;)) (i32.load (i32.const 36))) + (then unreachable)) + (call $stream.drop-writable (global.get $wA)) + (i32.const 42) + ) + (func (export "dest-reader-dropped-after-forward") (result i32) + (local $ret i32) (local $ws i32) + (call $new-a) + (call $new-b) + (call $stream.forward (global.get $rA) (global.get $wB)) + (local.set $ret (call $stream.write (global.get $wA) (i32.const 0) (i32.const 4))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (global.get $wA) (local.get $ws)) + (call $stream.drop-readable (global.get $rB)) + (local.set $ret (call $waitable-set.poll (local.get $ws) (i32.const 32))) + (if (i32.ne (i32.const 3 (; STREAM_WRITE ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (global.get $wA) (i32.load (i32.const 32))) + (then unreachable)) + (if (i32.ne (i32.const 0x1 (; DROPPED=1 | (0<<4) ;)) (i32.load (i32.const 36))) + (then unreachable)) + (i32.const 42) + ) + (func (export "src-writer-dropped-before-forward") (result i32) + (local $ret i32) + (call $new-a) + (call $new-b) + (call $stream.drop-writable (global.get $wA)) + (call $stream.forward (global.get $rA) (global.get $wB)) + (local.set $ret (call $stream.read (global.get $rB) (i32.const 8) (i32.const 4))) + (if (i32.ne (i32.const 0x1 (; DROPPED=1 | (0<<4) ;)) (local.get $ret)) + (then unreachable)) + (call $stream.drop-readable (global.get $rB)) + (i32.const 42) + ) + (func (export "src-writer-dropped-after-forward") (result i32) + (local $ret i32) (local $ws i32) + (call $new-a) + (call $new-b) + (call $stream.forward (global.get $rA) (global.get $wB)) + (local.set $ret (call $stream.read (global.get $rB) (i32.const 8) (i32.const 4))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (global.get $rB) (local.get $ws)) + (call $stream.drop-writable (global.get $wA)) + (local.set $ret (call $waitable-set.poll (local.get $ws) (i32.const 32))) + (if (i32.ne (i32.const 2 (; STREAM_READ ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (global.get $rB) (i32.load (i32.const 32))) + (then unreachable)) + (if (i32.ne (i32.const 0x1 (; DROPPED=1 | (0<<4) ;)) (i32.load (i32.const 36))) + (then unreachable)) + (i32.const 42) + ) + + ;; cancellation after forward + (func (export "cancel-carried-over-read") (result i32) + (local $ret i32) + (call $new-a) + (call $new-b) + (local.set $ret (call $stream.read (global.get $rB) (i32.const 8) (i32.const 4))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (call $stream.forward (global.get $rA) (global.get $wB)) + (local.set $ret (call $stream.cancel-read (global.get $rB))) + (if (i32.ne (i32.const 0x2 (; CANCELLED=2 | (0<<4) ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $stream.read (global.get $rB) (i32.const 8) (i32.const 4))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (i32.store (i32.const 0) (i32.const 0x04030201)) + (local.set $ret (call $stream.write (global.get $wA) (i32.const 0) (i32.const 4))) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 0x04030201) (i32.load (i32.const 8))) + (then unreachable)) + (i32.const 42) + ) + + ;; self-loop + (func (export "self-forward") (result i32) + (call $new-a) + (if (i32.ne (i32.const 1) (global.get $rA)) + (then unreachable)) + (if (i32.ne (i32.const 2) (global.get $wA)) + (then unreachable)) + (call $stream.forward (global.get $rA) (global.get $wA)) + (call $new-b) + (if (i32.ne (i32.const 2) (global.get $rB)) + (then unreachable)) + (if (i32.ne (i32.const 1) (global.get $wB)) + (then unreachable)) + (i32.const 42) + ) + + ;; A absorbs B, then C absorbs A: one stream from C's writer to B's reader + (func (export "forward-chain") (result i32) + (local $ret i32) + (call $new-a) + (call $new-b) + (call $new-c) + (call $stream.forward (global.get $rA) (global.get $wB)) + (call $stream.forward (global.get $rC) (global.get $wA)) + (i32.store (i32.const 0) (i32.const 0x04030201)) + (local.set $ret (call $stream.write (global.get $wC) (i32.const 0) (i32.const 4))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $stream.read (global.get $rB) (i32.const 8) (i32.const 4))) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 0x04030201) (i32.load (i32.const 8))) + (then unreachable)) + (i32.const 42) + ) + + ;; forwarding A into B and then B's (now-shared) readable end back into + ;; A's writable end degenerates into the self-loop case, so no cycle can + ;; be built and there is nothing left to trap on + (func (export "forward-cycle") (result i32) + (call $new-a) + (call $new-b) + (call $stream.forward (global.get $rA) (global.get $wB)) + (call $stream.forward (global.get $rB) (global.get $wA)) + (call $new-c) + (if (i32.ne (i32.const 2) (global.get $rC)) + (then unreachable)) + (if (i32.ne (i32.const 3) (global.get $wC)) + (then unreachable)) + (i32.const 42) + ) + + ;; future versions of all of the above + (func (export "future-forward-then-write-then-read") (result i32) + (local $ret i32) + (call $fnew-a) + (call $fnew-b) + (call $future.forward (global.get $rA) (global.get $wB)) + (i32.store8 (i32.const 0) (i32.const 0x5a)) + (local.set $ret (call $future.write (global.get $wA) (i32.const 0))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $future.read (global.get $rB) (i32.const 8))) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 0x5a) (i32.load8_u (i32.const 8))) + (then unreachable)) + (i32.const 42) + ) + (func (export "future-forward-read-then-write") (result i32) + (local $ret i32) (local $ws i32) + (call $fnew-a) + (call $fnew-b) + (local.set $ret (call $future.read (global.get $rB) (i32.const 8))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (call $future.forward (global.get $rA) (global.get $wB)) + (i32.store8 (i32.const 0) (i32.const 0x5a)) + (local.set $ret (call $future.write (global.get $wA) (i32.const 0))) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 0x5a) (i32.load8_u (i32.const 8))) + (then unreachable)) + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (global.get $rB) (local.get $ws)) + (local.set $ret (call $waitable-set.poll (local.get $ws) (i32.const 32))) + (if (i32.ne (i32.const 4 (; FUTURE_READ ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (global.get $rB) (i32.load (i32.const 32))) + (then unreachable)) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (i32.load (i32.const 36))) + (then unreachable)) + (i32.const 42) + ) + (func (export "future-rendezvous-during-forward") (result i32) + (local $ret i32) (local $ws1 i32) (local $ws2 i32) + (call $fnew-a) + (call $fnew-b) + (i32.store8 (i32.const 0) (i32.const 0x5a)) + (local.set $ret (call $future.write (global.get $wA) (i32.const 0))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $future.read (global.get $rB) (i32.const 8))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (call $future.forward (global.get $rA) (global.get $wB)) + (if (i32.ne (i32.const 0x5a) (i32.load8_u (i32.const 8))) + (then unreachable)) + (local.set $ws1 (call $waitable-set.new)) + (local.set $ws2 (call $waitable-set.new)) + (call $waitable.join (global.get $wA) (local.get $ws1)) + (call $waitable.join (global.get $rB) (local.get $ws2)) + (local.set $ret (call $waitable-set.poll (local.get $ws1) (i32.const 32))) + (if (i32.ne (i32.const 5 (; FUTURE_WRITE ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (global.get $wA) (i32.load (i32.const 32))) + (then unreachable)) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (i32.load (i32.const 36))) + (then unreachable)) + (local.set $ret (call $waitable-set.poll (local.get $ws2) (i32.const 40))) + (if (i32.ne (i32.const 4 (; FUTURE_READ ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (global.get $rB) (i32.load (i32.const 40))) + (then unreachable)) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (i32.load (i32.const 44))) + (then unreachable)) + (call $future.drop-writable (global.get $wA)) + (i32.const 42) + ) + (func (export "future-forward-dest-reader-dropped") (result i32) + (local $ret i32) + (call $fnew-a) + (call $fnew-b) + (call $future.drop-readable (global.get $rB)) + (call $future.forward (global.get $rA) (global.get $wB)) + (local.set $ret (call $future.write (global.get $wA) (i32.const 0))) + (if (i32.ne (i32.const 1 (; DROPPED ;)) (local.get $ret)) + (then unreachable)) + (call $future.drop-writable (global.get $wA)) + (i32.const 42) + ) + (func (export "future-forward-dest-reader-dropped-blocked-writer") (result i32) + (local $ret i32) (local $ws i32) + (call $fnew-a) + (call $fnew-b) + (i32.store8 (i32.const 0) (i32.const 0x5a)) + (local.set $ret (call $future.write (global.get $wA) (i32.const 0))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (call $future.drop-readable (global.get $rB)) + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (global.get $wA) (local.get $ws)) + (call $future.forward (global.get $rA) (global.get $wB)) + (local.set $ret (call $waitable-set.poll (local.get $ws) (i32.const 32))) + (if (i32.ne (i32.const 5 (; FUTURE_WRITE ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (global.get $wA) (i32.load (i32.const 32))) + (then unreachable)) + (if (i32.ne (i32.const 1 (; DROPPED ;)) (i32.load (i32.const 36))) + (then unreachable)) + (call $future.drop-writable (global.get $wA)) + (i32.const 42) + ) + (func (export "future-forward-discards-unwritten-writer") (result i32) + (local $ret i32) + (call $fnew-a) + (call $fnew-b) + (call $future.forward (global.get $rA) (global.get $wB)) + (i32.store8 (i32.const 0) (i32.const 0x5a)) + (local.set $ret (call $future.write (global.get $wA) (i32.const 0))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $future.read (global.get $rB) (i32.const 8))) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 0x5a) (i32.load8_u (i32.const 8))) + (then unreachable)) + (i32.const 42) + ) + (func (export "future-self-forward") (result i32) + (call $fnew-a) + (if (i32.ne (i32.const 1) (global.get $rA)) + (then unreachable)) + (if (i32.ne (i32.const 2) (global.get $wA)) + (then unreachable)) + (call $future.forward (global.get $rA) (global.get $wA)) + (call $fnew-b) + (if (i32.ne (i32.const 2) (global.get $rB)) + (then unreachable)) + (if (i32.ne (i32.const 1) (global.get $wB)) + (then unreachable)) + (i32.const 42) + ) + (func (export "future-forward-chain") (result i32) + (local $ret i32) + (call $fnew-a) + (call $fnew-b) + (call $fnew-c) + (call $future.forward (global.get $rA) (global.get $wB)) + (call $future.forward (global.get $rC) (global.get $wA)) + (i32.store8 (i32.const 0) (i32.const 0x5a)) + (local.set $ret (call $future.write (global.get $wC) (i32.const 0))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $future.read (global.get $rB) (i32.const 8))) + (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 0x5a) (i32.load8_u (i32.const 8))) + (then unreachable)) + (i32.const 42) + ) + + ;; an empty element type forwards just like any other + (func (export "empty-element-type") (result i32) + (local $ret i32) + (call $enew-a) + (call $enew-b) + (call $stream.forward-e (global.get $rA) (global.get $wB)) + (local.set $ret (call $stream.write-e (global.get $wA) (i32.const 0) (i32.const 4))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $stream.read-e (global.get $rB) (i32.const 0) (i32.const 4))) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) (local.get $ret)) + (then unreachable)) + (i32.const 42) + ) + + ;; trap conditions + + (func (export "trap-swapped-args") + (call $new-a) + (call $new-b) + (call $stream.forward (global.get $wA) (global.get $rB)) + unreachable + ) + (func (export "trap-type-mismatch") + (call $new-a) + (call $new-b) + (call $stream.forward-u32 (global.get $rA) (global.get $wB)) + unreachable + ) + (func (export "trap-same-index") + (call $new-a) + (call $stream.forward (global.get $rA) (global.get $rA)) + unreachable + ) + (func (export "trap-src-reader-copying") + (local $ret i32) + (call $new-a) + (call $new-b) + (local.set $ret (call $stream.read (global.get $rA) (i32.const 8) (i32.const 4))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (call $stream.forward (global.get $rA) (global.get $wB)) + unreachable + ) + (func (export "trap-dest-writer-copying") + (local $ret i32) + (call $new-a) + (call $new-b) + (local.set $ret (call $stream.write (global.get $wB) (i32.const 0) (i32.const 4))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (call $stream.forward (global.get $rA) (global.get $wB)) + unreachable + ) + (func (export "trap-src-reader-in-set") + (call $new-a) + (call $new-b) + (call $waitable.join (global.get $rA) (call $waitable-set.new)) + (call $stream.forward (global.get $rA) (global.get $wB)) + unreachable + ) + (func (export "trap-dest-writer-in-set") + (call $new-a) + (call $new-b) + (call $waitable.join (global.get $wB) (call $waitable-set.new)) + (call $stream.forward (global.get $rA) (global.get $wB)) + unreachable + ) + (func (export "trap-src-reader-done") + (local $ret i32) + (call $new-a) + (call $new-b) + (call $stream.drop-writable (global.get $wA)) + (local.set $ret (call $stream.read (global.get $rA) (i32.const 8) (i32.const 4))) + (if (i32.ne (i32.const 0x1 (; DROPPED=1 | (0<<4) ;)) (local.get $ret)) + (then unreachable)) + (call $stream.forward (global.get $rA) (global.get $wB)) + unreachable + ) + (func (export "trap-dest-writer-done") + (local $ret i32) + (call $new-a) + (call $new-b) + (call $stream.drop-readable (global.get $rB)) + (local.set $ret (call $stream.write (global.get $wB) (i32.const 0) (i32.const 4))) + (if (i32.ne (i32.const 0x1 (; DROPPED=1 | (0<<4) ;)) (local.get $ret)) + (then unreachable)) + (call $stream.forward (global.get $rA) (global.get $wB)) + unreachable + ) + (func (export "trap-use-after-forward") + (call $new-a) + (call $new-b) + (call $stream.forward (global.get $rA) (global.get $wB)) + (call $stream.drop-readable (global.get $rA)) + unreachable + ) + (func (export "trap-stream-forward-of-future-handles") + (call $fnew-a) + (call $fnew-b) + (call $stream.forward (global.get $rA) (global.get $wB)) + unreachable + ) + (func (export "trap-future-swapped-args") + (call $fnew-a) + (call $fnew-b) + (call $future.forward (global.get $wA) (global.get $rB)) + unreachable + ) + (func (export "trap-future-reader-copying") + (local $ret i32) + (call $fnew-a) + (call $fnew-b) + (local.set $ret (call $future.read (global.get $rA) (i32.const 8))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (call $future.forward (global.get $rA) (global.get $wB)) + unreachable + ) + + ;; trap (in the temporary same-instance trap) if forwarding triggers a + ;; same-instance copy of a complex value + (func (export "trap-same-instance-rendezvous") + (local $ret i32) + (call $snew-a) + (call $snew-b) + (i32.store (i32.const 0) (i32.const 64)) ;; the one string's pointer ... + (i32.store (i32.const 4) (i32.const 2)) ;; ... and its length + (i32.store16 (i32.const 64) (i32.const 0x6968 (; "hi" ;))) + (local.set $ret (call $stream.write-s (global.get $wA) (i32.const 0) (i32.const 1))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $stream.read-s (global.get $rB) (i32.const 16) (i32.const 1))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (call $stream.forward-s (global.get $rA) (global.get $wB)) + unreachable + ) + (func (export "trap-future-same-instance-rendezvous") + (local $ret i32) + (call $fsnew-a) + (call $fsnew-b) + (i32.store (i32.const 0) (i32.const 64)) ;; the string's pointer ... + (i32.store (i32.const 4) (i32.const 2)) ;; ... and its length + (i32.store16 (i32.const 64) (i32.const 0x6968 (; "hi" ;))) + (local.set $ret (call $future.write-s (global.get $wA) (i32.const 0))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $future.read-s (global.get $rB) (i32.const 16))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (call $future.forward-s (global.get $rA) (global.get $wB)) + unreachable + ) + ) + (type $ST (stream u8)) + (type $ST32 (stream u32)) + (type $STE (stream)) + (type $STS (stream string)) + (type $FT (future u8)) + (type $FTS (future string)) + (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.join (core func $waitable.join)) + (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 (core func $stream.cancel-read)) + (canon stream.cancel-write $ST (core func $stream.cancel-write)) + (canon stream.drop-readable $ST (core func $stream.drop-readable)) + (canon stream.drop-writable $ST (core func $stream.drop-writable)) + (canon stream.forward $ST (core func $stream.forward)) + (canon stream.forward $ST32 (core func $stream.forward-u32)) + (canon stream.new $STE (core func $stream.new-e)) + (canon stream.read $STE async (core func $stream.read-e)) + (canon stream.write $STE async (core func $stream.write-e)) + (canon stream.forward $STE (core func $stream.forward-e)) + (canon stream.new $STS (core func $stream.new-s)) + (canon stream.read $STS async (memory (core memory $memory "mem")) (realloc (core func $memory "realloc")) (core func $stream.read-s)) + (canon stream.write $STS async (memory (core memory $memory "mem")) (core func $stream.write-s)) + (canon stream.forward $STS (core func $stream.forward-s)) + (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.drop-readable $FT (core func $future.drop-readable)) + (canon future.drop-writable $FT (core func $future.drop-writable)) + (canon future.forward $FT (core func $future.forward)) + (canon future.new $FTS (core func $future.new-s)) + (canon future.read $FTS async (memory (core memory $memory "mem")) (realloc (core func $memory "realloc")) (core func $future.read-s)) + (canon future.write $FTS async (memory (core memory $memory "mem")) (core func $future.write-s)) + (canon future.forward $FTS (core func $future.forward-s)) + (core instance $m (instantiate $M (with "" (instance + (export "mem" (memory $memory "mem")) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.poll" (func $waitable-set.poll)) + (export "waitable.join" (func $waitable.join)) + (export "stream.new" (func $stream.new)) + (export "stream.read" (func $stream.read)) + (export "stream.write" (func $stream.write)) + (export "stream.cancel-read" (func $stream.cancel-read)) + (export "stream.cancel-write" (func $stream.cancel-write)) + (export "stream.drop-readable" (func $stream.drop-readable)) + (export "stream.drop-writable" (func $stream.drop-writable)) + (export "stream.forward" (func $stream.forward)) + (export "stream.forward-u32" (func $stream.forward-u32)) + (export "stream.new-e" (func $stream.new-e)) + (export "stream.read-e" (func $stream.read-e)) + (export "stream.write-e" (func $stream.write-e)) + (export "stream.forward-e" (func $stream.forward-e)) + (export "stream.new-s" (func $stream.new-s)) + (export "stream.read-s" (func $stream.read-s)) + (export "stream.write-s" (func $stream.write-s)) + (export "stream.forward-s" (func $stream.forward-s)) + (export "future.new" (func $future.new)) + (export "future.read" (func $future.read)) + (export "future.write" (func $future.write)) + (export "future.drop-readable" (func $future.drop-readable)) + (export "future.drop-writable" (func $future.drop-writable)) + (export "future.forward" (func $future.forward)) + (export "future.new-s" (func $future.new-s)) + (export "future.read-s" (func $future.read-s)) + (export "future.write-s" (func $future.write-s)) + (export "future.forward-s" (func $future.forward-s)) + )))) + (func (export "forward-then-write-then-read") (result u32) (canon lift (core func $m "forward-then-write-then-read"))) + (func (export "forward-then-read-then-write") (result u32) (canon lift (core func $m "forward-then-read-then-write"))) + (func (export "read-then-forward-then-write") (result u32) (canon lift (core func $m "read-then-forward-then-write"))) + (func (export "rendezvous-during-forward") (result u32) (canon lift (core func $m "rendezvous-during-forward"))) + (func (export "pending-read-in-set-before-forward") (result u32) (canon lift (core func $m "pending-read-in-set-before-forward"))) + (func (export "no-event-from-forward") (result u32) (canon lift (core func $m "no-event-from-forward"))) + (func (export "zero-length-read-carried-over") (result u32) (canon lift (core func $m "zero-length-read-carried-over"))) + (func (export "zero-length-write-then-forward") (result u32) (canon lift (core func $m "zero-length-write-then-forward"))) + (func (export "dest-reader-dropped-before-forward") (result u32) (canon lift (core func $m "dest-reader-dropped-before-forward"))) + (func (export "dest-reader-dropped-before-forward-blocked-writer") (result u32) (canon lift (core func $m "dest-reader-dropped-before-forward-blocked-writer"))) + (func (export "dest-reader-dropped-after-forward") (result u32) (canon lift (core func $m "dest-reader-dropped-after-forward"))) + (func (export "src-writer-dropped-before-forward") (result u32) (canon lift (core func $m "src-writer-dropped-before-forward"))) + (func (export "src-writer-dropped-after-forward") (result u32) (canon lift (core func $m "src-writer-dropped-after-forward"))) + (func (export "cancel-carried-over-read") (result u32) (canon lift (core func $m "cancel-carried-over-read"))) + (func (export "self-forward") (result u32) (canon lift (core func $m "self-forward"))) + (func (export "forward-chain") (result u32) (canon lift (core func $m "forward-chain"))) + (func (export "forward-cycle") (result u32) (canon lift (core func $m "forward-cycle"))) + (func (export "future-forward-then-write-then-read") (result u32) (canon lift (core func $m "future-forward-then-write-then-read"))) + (func (export "future-forward-read-then-write") (result u32) (canon lift (core func $m "future-forward-read-then-write"))) + (func (export "future-rendezvous-during-forward") (result u32) (canon lift (core func $m "future-rendezvous-during-forward"))) + (func (export "future-forward-dest-reader-dropped") (result u32) (canon lift (core func $m "future-forward-dest-reader-dropped"))) + (func (export "future-forward-dest-reader-dropped-blocked-writer") (result u32) (canon lift (core func $m "future-forward-dest-reader-dropped-blocked-writer"))) + (func (export "future-forward-discards-unwritten-writer") (result u32) (canon lift (core func $m "future-forward-discards-unwritten-writer"))) + (func (export "future-self-forward") (result u32) (canon lift (core func $m "future-self-forward"))) + (func (export "future-forward-chain") (result u32) (canon lift (core func $m "future-forward-chain"))) + (func (export "empty-element-type") (result u32) (canon lift (core func $m "empty-element-type"))) + (func (export "trap-swapped-args") (canon lift (core func $m "trap-swapped-args"))) + (func (export "trap-type-mismatch") (canon lift (core func $m "trap-type-mismatch"))) + (func (export "trap-same-index") (canon lift (core func $m "trap-same-index"))) + (func (export "trap-src-reader-copying") (canon lift (core func $m "trap-src-reader-copying"))) + (func (export "trap-dest-writer-copying") (canon lift (core func $m "trap-dest-writer-copying"))) + (func (export "trap-src-reader-in-set") (canon lift (core func $m "trap-src-reader-in-set"))) + (func (export "trap-dest-writer-in-set") (canon lift (core func $m "trap-dest-writer-in-set"))) + (func (export "trap-src-reader-done") (canon lift (core func $m "trap-src-reader-done"))) + (func (export "trap-dest-writer-done") (canon lift (core func $m "trap-dest-writer-done"))) + (func (export "trap-use-after-forward") (canon lift (core func $m "trap-use-after-forward"))) + (func (export "trap-stream-forward-of-future-handles") (canon lift (core func $m "trap-stream-forward-of-future-handles"))) + (func (export "trap-future-swapped-args") (canon lift (core func $m "trap-future-swapped-args"))) + (func (export "trap-future-reader-copying") (canon lift (core func $m "trap-future-reader-copying"))) + (func (export "trap-same-instance-rendezvous") (canon lift (core func $m "trap-same-instance-rendezvous"))) + (func (export "trap-future-same-instance-rendezvous") (canon lift (core func $m "trap-future-same-instance-rendezvous"))) +) + +(component instance $i $Tester) +(assert_return (invoke "forward-then-write-then-read") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "forward-then-read-then-write") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "read-then-forward-then-write") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "rendezvous-during-forward") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "pending-read-in-set-before-forward") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "no-event-from-forward") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "zero-length-read-carried-over") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "zero-length-write-then-forward") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "dest-reader-dropped-before-forward") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "dest-reader-dropped-before-forward-blocked-writer") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "dest-reader-dropped-after-forward") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "src-writer-dropped-before-forward") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "src-writer-dropped-after-forward") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "cancel-carried-over-read") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "self-forward") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "forward-chain") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "forward-cycle") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "future-forward-then-write-then-read") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "future-forward-read-then-write") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "future-rendezvous-during-forward") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "future-forward-dest-reader-dropped") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "future-forward-dest-reader-dropped-blocked-writer") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "future-forward-discards-unwritten-writer") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "future-self-forward") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "future-forward-chain") (u32.const 42)) +(component instance $i $Tester) +(assert_return (invoke "empty-element-type") (u32.const 42)) +(component instance $i $Tester) +(assert_trap (invoke "trap-swapped-args") "handle index 2 used with the wrong type") +(component instance $i $Tester) +(assert_trap (invoke "trap-type-mismatch") "handle index 1 used with the wrong type") +(component instance $i $Tester) +(assert_trap (invoke "trap-same-index") "unknown handle index 1") +(component instance $i $Tester) +(assert_trap (invoke "trap-src-reader-copying") "cannot forward busy stream") +(component instance $i $Tester) +(assert_trap (invoke "trap-dest-writer-copying") "cannot forward busy stream") +(component instance $i $Tester) +(assert_trap (invoke "trap-src-reader-in-set") "cannot forward stream while it's in a waitable set") +(component instance $i $Tester) +(assert_trap (invoke "trap-dest-writer-in-set") "cannot forward stream while it's in a waitable set") +(component instance $i $Tester) +(assert_trap (invoke "trap-src-reader-done") "cannot forward stream after being notified that the writable end dropped") +(component instance $i $Tester) +(assert_trap (invoke "trap-dest-writer-done") "cannot forward stream after being notified that the readable end dropped") +(component instance $i $Tester) +(assert_trap (invoke "trap-use-after-forward") "unknown handle index 1") +(component instance $i $Tester) +(assert_trap (invoke "trap-stream-forward-of-future-handles") "handle index 1 used with the wrong type") +(component instance $i $Tester) +(assert_trap (invoke "trap-future-swapped-args") "handle index 2 used with the wrong type") +(component instance $i $Tester) +(assert_trap (invoke "trap-future-reader-copying") "cannot forward busy future") +(component instance $i $Tester) +(assert_trap (invoke "trap-same-instance-rendezvous") "cannot read from and write to intra-component future/stream with non-numeric payload") +(component instance $i $Tester) +(assert_trap (invoke "trap-future-same-instance-rendezvous") "cannot read from and write to intra-component future/stream with non-numeric payload") + +;; A proxying use case: $P produces a stream, $X splices it into a stream of its +;; own and hands the readable end on, and $D consumes it. Because $X gives up +;; both handles in the same call, the copy from $P to $D runs with $X holding no +;; stream ends at all. +(component definition $ProxyTester + (component $P + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $PM + (import "" "mem" (memory 1)) + (import "" "stream.new" (func $stream.new (result i64))) + (import "" "stream.write" (func $stream.write (param i32 i32 i32) (result i32))) + (global $w (mut i32) (i32.const 0)) + (func (export "start") (result i32) + (local $ret64 i64) + (local.set $ret64 (call $stream.new)) + (global.set $w (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (i32.wrap_i64 (local.get $ret64)) + ) + (func (export "write4") (result i32) + (i32.store (i32.const 0) (i32.const 0x04030201)) + (call $stream.write (global.get $w) (i32.const 0) (i32.const 4)) + ) + ) + (type $ST (stream u8)) + (canon stream.new $ST (core func $stream.new)) + (canon stream.write $ST async (memory (core memory $memory "mem")) (core func $stream.write)) + (core instance $pm (instantiate $PM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "stream.new" (func $stream.new)) + (export "stream.write" (func $stream.write)) + )))) + (func (export "start") (result (stream u8)) (canon lift (core func $pm "start"))) + (func (export "write4") (result u32) (canon lift (core func $pm "write4"))) + ) + (component $X + (core module $XM + (import "" "stream.new" (func $stream.new (result i64))) + (import "" "stream.forward" (func $stream.forward (param i32 i32))) + (func (export "proxy") (param $in i32) (result i32) + (local $ret64 i64) (local $r i32) (local $w i32) + (local.set $ret64 (call $stream.new)) + (local.set $r (i32.wrap_i64 (local.get $ret64))) + (local.set $w (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + ;; splice the incoming stream into the outgoing one, giving up both the + ;; incoming readable end and the outgoing writable end + (call $stream.forward (local.get $in) (local.get $w)) + (local.get $r) + ) + ) + (type $ST (stream u8)) + (canon stream.new $ST (core func $stream.new)) + (canon stream.forward $ST (core func $stream.forward)) + (core instance $xm (instantiate $XM (with "" (instance + (export "stream.new" (func $stream.new)) + (export "stream.forward" (func $stream.forward)) + )))) + (func (export "proxy") (param "in" (stream u8)) (result (stream u8)) + (canon lift (core func $xm "proxy"))) + ) + (component $D + (import "p" (instance $p + (export "start" (func (result (stream u8)))) + (export "write4" (func (result u32))) + )) + (import "x" (instance $x + (export "proxy" (func (param "in" (stream u8)) (result (stream u8)))) + )) + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $DM + (import "" "mem" (memory 1)) + (import "" "start" (func $start (result i32))) + (import "" "write4" (func $write4 (result i32))) + (import "" "proxy" (func $proxy (param i32) (result 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.join" (func $waitable.join (param i32 i32))) + (import "" "stream.read" (func $stream.read (param i32 i32 i32) (result i32))) + (import "" "stream.drop-readable" (func $stream.drop-readable (param i32))) + + (func (export "proxy-read-then-write") (result i32) + (local $ret i32) (local $rq i32) (local $ws i32) + (local.set $rq (call $proxy (call $start))) + (local.set $ret (call $stream.read (local.get $rq) (i32.const 8) (i32.const 4))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + + ;; $P's write rendezvouses with $D's read even though the stream $D is + ;; reading from was created by $X + (local.set $ret (call $write4)) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 0x04030201) (i32.load (i32.const 8))) + (then unreachable)) + + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $rq) (local.get $ws)) + (local.set $ret (call $waitable-set.poll (local.get $ws) (i32.const 32))) + (if (i32.ne (i32.const 2 (; STREAM_READ ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (local.get $rq) (i32.load (i32.const 32))) + (then unreachable)) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) (i32.load (i32.const 36))) + (then unreachable)) + + (i32.const 42) + ) + (func (export "proxy-write-then-read") (result i32) + (local $ret i32) (local $rq i32) + (local.set $rq (call $proxy (call $start))) + (local.set $ret (call $write4)) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (local.set $ret (call $stream.read (local.get $rq) (i32.const 8) (i32.const 4))) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 0x04030201) (i32.load (i32.const 8))) + (then unreachable)) + (i32.const 43) + ) + (func (export "proxy-drop-reader") (result i32) + (local $ret i32) (local $rq i32) + (local.set $rq (call $proxy (call $start))) + ;; dropping the far end of the spliced stream is visible to $P + (call $stream.drop-readable (local.get $rq)) + (local.set $ret (call $write4)) + (if (i32.ne (i32.const 0x1 (; DROPPED=1 | (0<<4) ;)) (local.get $ret)) + (then unreachable)) + (i32.const 44) + ) + ) + (type $ST (stream u8)) + (canon lower (func $p "start") (memory (core memory $memory "mem")) (core func $start)) + (canon lower (func $p "write4") (memory (core memory $memory "mem")) (core func $write4)) + (canon lower (func $x "proxy") (memory (core memory $memory "mem")) (core func $proxy)) + (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.join (core func $waitable.join)) + (canon stream.read $ST async (memory (core memory $memory "mem")) (core func $stream.read)) + (canon stream.drop-readable $ST (core func $stream.drop-readable)) + (core instance $dm (instantiate $DM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "start" (func $start)) + (export "write4" (func $write4)) + (export "proxy" (func $proxy)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.poll" (func $waitable-set.poll)) + (export "waitable.join" (func $waitable.join)) + (export "stream.read" (func $stream.read)) + (export "stream.drop-readable" (func $stream.drop-readable)) + )))) + (func (export "proxy-read-then-write") (result u32) (canon lift (core func $dm "proxy-read-then-write"))) + (func (export "proxy-write-then-read") (result u32) (canon lift (core func $dm "proxy-write-then-read"))) + (func (export "proxy-drop-reader") (result u32) (canon lift (core func $dm "proxy-drop-reader"))) + ) + (instance $p (instantiate $P)) + (instance $x (instantiate $X)) + (instance $d (instantiate $D (with "p" (instance $p)) (with "x" (instance $x)))) + (func (export "proxy-read-then-write") (alias export $d "proxy-read-then-write")) + (func (export "proxy-write-then-read") (alias export $d "proxy-write-then-read")) + (func (export "proxy-drop-reader") (alias export $d "proxy-drop-reader")) +) + +(component instance $i $ProxyTester) +(assert_return (invoke "proxy-read-then-write") (u32.const 42)) +(component instance $i $ProxyTester) +(assert_return (invoke "proxy-write-then-read") (u32.const 43)) +(component instance $i $ProxyTester) +(assert_return (invoke "proxy-drop-reader") (u32.const 44)) + + +;; One stream forwarded five times, ping-ponging between two components: $C and +;; $D take turns splicing the chain into a stream of their own, and after every +;; splice the surviving writer writes and the surviving reader reads a distinct +;; 4-byte pattern all the way through. +(component definition $PingPongTester + (component $C + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $CM + (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.forward" (func $stream.forward (param i32 i32))) + (import "" "stream.drop-writable" (func $stream.drop-writable (param i32))) + + (global $r (mut i32) (i32.const 0)) ;; the chain's readable end, while $C holds it + (global $w (mut i32) (i32.const 0)) ;; the writable end of the stream made by "fresh" + + ;; splice the incoming readable end into a fresh local stream, keeping the + ;; local readable end: $C becomes the reader of the merged stream + (func (export "splice") (param $in i32) + (local $ret64 i64) + (local.set $ret64 (call $stream.new)) + (global.set $r (i32.wrap_i64 (local.get $ret64))) + (call $stream.forward (local.get $in) + (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + ) + (func (export "read4") (param $expected i32) (result i32) + (local $ret i32) + (local.set $ret (call $stream.read (global.get $r) (i32.const 8) (i32.const 4))) + (if (i32.eq (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) (local.get $ret)) + (then + (if (i32.ne (i32.load (i32.const 8)) (local.get $expected)) + (then unreachable)))) + (local.get $ret) + ) + ;; give the chain's readable end back to $D so that it can take the next turn + (func (export "hand-back") (result i32) + (global.get $r) + ) + ;; a brand new stream whose readable end $D can splice in front of the chain + (func (export "fresh") (result i32) + (local $ret64 i64) + (local.set $ret64 (call $stream.new)) + (global.set $w (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + (i32.wrap_i64 (local.get $ret64)) + ) + (func (export "write4") (param $val i32) (result i32) + (i32.store (i32.const 0) (local.get $val)) + (call $stream.write (global.get $w) (i32.const 0) (i32.const 4)) + ) + (func (export "drop-writer") + (call $stream.drop-writable (global.get $w)) + ) + ) + (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.forward $ST (core func $stream.forward)) + (canon stream.drop-writable $ST (core func $stream.drop-writable)) + (core instance $cm (instantiate $CM (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.forward" (func $stream.forward)) + (export "stream.drop-writable" (func $stream.drop-writable)) + )))) + (func (export "splice") (param "in" (stream u8)) (canon lift (core func $cm "splice"))) + (func (export "read4") (param "expected" u32) (result s32) (canon lift (core func $cm "read4"))) + (func (export "hand-back") (result (stream u8)) (canon lift (core func $cm "hand-back"))) + (func (export "fresh") (result (stream u8)) (canon lift (core func $cm "fresh"))) + (func (export "write4") (param "val" u32) (result s32) (canon lift (core func $cm "write4"))) + (func (export "drop-writer") (canon lift (core func $cm "drop-writer"))) + ) + (component $D + (import "c" (instance $c + (export "splice" (func (param "in" (stream u8)))) + (export "read4" (func (param "expected" u32) (result s32))) + (export "hand-back" (func (result (stream u8)))) + (export "fresh" (func (result (stream u8)))) + (export "write4" (func (param "val" u32) (result s32))) + (export "drop-writer" (func)) + )) + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $DM + (import "" "mem" (memory 1)) + (import "" "c-splice" (func $c-splice (param i32))) + (import "" "c-read4" (func $c-read4 (param i32) (result i32))) + (import "" "c-hand-back" (func $c-hand-back (result i32))) + (import "" "c-fresh" (func $c-fresh (result i32))) + (import "" "c-write4" (func $c-write4 (param i32) (result i32))) + (import "" "c-drop-writer" (func $c-drop-writer)) + (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.forward" (func $stream.forward (param i32 i32))) + (import "" "stream.drop-readable" (func $stream.drop-readable (param 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 "" "waitable.join" (func $waitable.join (param i32 i32))) + + (global $r (mut i32) (i32.const 0)) ;; the chain's readable end, while $D holds it + (global $w (mut i32) (i32.const 0)) ;; the chain's writable end, while $D holds it + + ;; the mirror image of $C's "splice": $D becomes the reader + (func $d-splice (param $in i32) + (local $ret64 i64) + (local.set $ret64 (call $stream.new)) + (global.set $r (i32.wrap_i64 (local.get $ret64))) + (call $stream.forward (local.get $in) + (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + ) + (func $d-write4 (param $val i32) + (local $ret i32) + (i32.store (i32.const 0) (local.get $val)) + (local.set $ret (call $stream.write (global.get $w) (i32.const 0) (i32.const 4))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + ) + (func $d-read4 (param $expected i32) + (local $ret i32) + (local.set $ret (call $stream.read (global.get $r) (i32.const 8) (i32.const 4))) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.load (i32.const 8)) (local.get $expected)) + (then unreachable)) + ) + (func $expect-c-read4 (param $expected i32) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) + (call $c-read4 (local.get $expected))) + (then unreachable)) + ) + ;; the blocked write is completed by the far side's read, so its event has + ;; to be taken before the same writable end can be used for the next hop + (func $poll (param $waitable i32) (param $event i32) (param $payload i32) + (local $ws i32) + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $waitable) (local.get $ws)) + (if (i32.ne (local.get $event) (call $waitable-set.poll (local.get $ws) (i32.const 32))) + (then unreachable)) + (if (i32.ne (local.get $waitable) (i32.load (i32.const 32))) + (then unreachable)) + (if (i32.ne (local.get $payload) (i32.load (i32.const 36))) + (then unreachable)) + (call $waitable.join (local.get $waitable) (i32.const 0)) + (call $waitable-set.drop (local.get $ws)) + ) + + (func (export "run") (result i32) + (local $ret i32) (local $ret64 i64) + + ;; $D makes the stream and keeps the writable end for the first four hops + (local.set $ret64 (call $stream.new)) + (global.set $r (i32.wrap_i64 (local.get $ret64))) + (global.set $w (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + + ;; hop 1: $C splices, so the merged stream runs from $D's writer to $C's + ;; reader and the copy crosses a component boundary + (call $c-splice (global.get $r)) + (call $d-write4 (i32.const 0x11111111)) + (call $expect-c-read4 (i32.const 0x11111111)) + (call $poll (global.get $w) (i32.const 3 (; STREAM_WRITE ;)) (i32.const 0x40)) + + ;; hop 2: $C hands the reader back and $D splices, so this time both ends + ;; of the merged stream are $D's own + (call $d-splice (call $c-hand-back)) + (call $d-write4 (i32.const 0x22222222)) + (call $d-read4 (i32.const 0x22222222)) + (call $poll (global.get $w) (i32.const 3 (; STREAM_WRITE ;)) (i32.const 0x40)) + + ;; hop 3: and back over to $C + (call $c-splice (global.get $r)) + (call $d-write4 (i32.const 0x33333333)) + (call $expect-c-read4 (i32.const 0x33333333)) + (call $poll (global.get $w) (i32.const 3 (; STREAM_WRITE ;)) (i32.const 0x40)) + + ;; hop 4: and back to $D + (call $d-splice (call $c-hand-back)) + (call $d-write4 (i32.const 0x44444444)) + (call $d-read4 (i32.const 0x44444444)) + (call $poll (global.get $w) (i32.const 3 (; STREAM_WRITE ;)) (i32.const 0x40)) + + ;; hop 5: $D splices a stream made by $C in front of the chain, which is + ;; the one way the writer can end up in the other component. $D keeps the + ;; reader it already had, so the copy now runs $C -> $D. + (call $stream.forward (call $c-fresh) (global.get $w)) + (local.set $ret (call $stream.read (global.get $r) (i32.const 8) (i32.const 4))) + (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then unreachable)) + (if (i32.ne (i32.const 0x40 (; COMPLETED=0 | (4<<4) ;)) + (call $c-write4 (i32.const 0x55555555))) + (then unreachable)) + (call $poll (global.get $r) (i32.const 2 (; STREAM_READ ;)) (i32.const 0x40)) + (if (i32.ne (i32.load (i32.const 8)) (i32.const 0x55555555)) + (then unreachable)) + + (call $stream.drop-readable (global.get $r)) + (call $c-drop-writer) + (i32.const 42) + ) + ) + (type $ST (stream u8)) + (canon lower (func $c "splice") (memory (core memory $memory "mem")) (core func $c-splice)) + (canon lower (func $c "read4") (memory (core memory $memory "mem")) (core func $c-read4)) + (canon lower (func $c "hand-back") (memory (core memory $memory "mem")) (core func $c-hand-back)) + (canon lower (func $c "fresh") (memory (core memory $memory "mem")) (core func $c-fresh)) + (canon lower (func $c "write4") (memory (core memory $memory "mem")) (core func $c-write4)) + (canon lower (func $c "drop-writer") (core func $c-drop-writer)) + (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.forward $ST (core func $stream.forward)) + (canon stream.drop-readable $ST (core func $stream.drop-readable)) + (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 waitable.join (core func $waitable.join)) + (core instance $dm (instantiate $DM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "c-splice" (func $c-splice)) + (export "c-read4" (func $c-read4)) + (export "c-hand-back" (func $c-hand-back)) + (export "c-fresh" (func $c-fresh)) + (export "c-write4" (func $c-write4)) + (export "c-drop-writer" (func $c-drop-writer)) + (export "stream.new" (func $stream.new)) + (export "stream.read" (func $stream.read)) + (export "stream.write" (func $stream.write)) + (export "stream.forward" (func $stream.forward)) + (export "stream.drop-readable" (func $stream.drop-readable)) + (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 "waitable.join" (func $waitable.join)) + )))) + (func (export "run") (result u32) (canon lift (core func $dm "run"))) + ) + (instance $c (instantiate $C)) + (instance $d (instantiate $D (with "c" (instance $c)))) + (func (export "run") (alias export $d "run")) +) + +(component instance $i $PingPongTester) +(assert_return (invoke "run") (u32.const 42)) + + +(assert_invalid + (component + (type $FT (future u8)) + (canon stream.forward $FT (core func $f)) + ) + "`stream.forward` requires a stream type") +(assert_invalid + (component + (type $ST (stream u8)) + (canon future.forward $ST (core func $f)) + ) + "`future.forward` requires a future type") 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") diff --git a/test/nyi.txt b/test/nyi.txt index 1b7fd69a..1ce5d24f 100644 --- a/test/nyi.txt +++ b/test/nyi.txt @@ -1,4 +1,7 @@ # See README.md +./async/big-interleaving-test.wast +./async/forward.wast +./values/post-return.wast ./validation/max-value-size.wast ./validation/kebab.wast ./binary/binary.wast diff --git a/test/values/post-return.wast b/test/values/post-return.wast index 936dd457..55860ca0 100644 --- a/test/values/post-return.wast +++ b/test/values/post-return.wast @@ -37,6 +37,7 @@ (canon stream.cancel-write $ST (core func $stream.cancel-write)) (canon stream.drop-readable $ST (core func $stream.drop-readable)) (canon stream.drop-writable $ST (core func $stream.drop-writable)) + (canon stream.forward $ST (core func $stream.forward)) (canon future.new $FT (core func $future.new)) (canon future.read $FT (memory (core memory $memory "mem")) (core func $future.read)) (canon future.write $FT (memory (core memory $memory "mem")) (core func $future.write)) @@ -44,6 +45,7 @@ (canon future.cancel-write $FT (core func $future.cancel-write)) (canon future.drop-readable $FT (core func $future.drop-readable)) (canon future.drop-writable $FT (core func $future.drop-writable)) + (canon future.forward $FT (core func $future.forward)) (core module $DM (import "" "mem" (memory 1)) (import "" "import" (func $import)) @@ -67,6 +69,7 @@ (import "" "stream.cancel-write" (func $stream.cancel-write (param i32) (result i32))) (import "" "stream.drop-readable" (func $stream.drop-readable (param i32))) (import "" "stream.drop-writable" (func $stream.drop-writable (param i32))) + (import "" "stream.forward" (func $stream.forward (param i32 i32))) (import "" "future.new" (func $future.new (result i64))) (import "" "future.read" (func $future.read (param i32 i32) (result i32))) (import "" "future.write" (func $future.write (param i32 i32) (result i32))) @@ -74,6 +77,7 @@ (import "" "future.cancel-write" (func $future.cancel-write (param i32) (result i32))) (import "" "future.drop-readable" (func $future.drop-readable (param i32))) (import "" "future.drop-writable" (func $future.drop-writable (param i32))) + (import "" "future.forward" (func $future.forward (param i32 i32))) (func (export "noop")) (func (export "trap-calling-import") (call $import)) @@ -97,6 +101,7 @@ (func (export "trap-calling-stream-cancel-write") (drop (call $stream.cancel-write (i32.const 0)))) (func (export "trap-calling-stream-drop-readable") (call $stream.drop-readable (i32.const 0))) (func (export "trap-calling-stream-drop-writable") (call $stream.drop-writable (i32.const 0))) + (func (export "trap-calling-stream-forward") (call $stream.forward (i32.const 0) (i32.const 0))) (func (export "trap-calling-future-new") (drop (call $future.new))) (func (export "trap-calling-future-read") (drop (call $future.read (i32.const 0) (i32.const 0)))) (func (export "trap-calling-future-write") (drop (call $future.write (i32.const 0) (i32.const 0)))) @@ -104,6 +109,7 @@ (func (export "trap-calling-future-cancel-write") (drop (call $future.cancel-write (i32.const 0)))) (func (export "trap-calling-future-drop-readable") (call $future.drop-readable (i32.const 0))) (func (export "trap-calling-future-drop-writable") (call $future.drop-writable (i32.const 0))) + (func (export "trap-calling-future-forward") (call $future.forward (i32.const 0) (i32.const 0))) ) (canon lower (func $import) (core func $import')) (core instance $dm (instantiate $DM (with "" (instance @@ -129,6 +135,7 @@ (export "stream.cancel-write" (func $stream.cancel-write)) (export "stream.drop-readable" (func $stream.drop-readable)) (export "stream.drop-writable" (func $stream.drop-writable)) + (export "stream.forward" (func $stream.forward)) (export "future.new" (func $future.new)) (export "future.read" (func $future.read)) (export "future.write" (func $future.write)) @@ -136,6 +143,7 @@ (export "future.cancel-write" (func $future.cancel-write)) (export "future.drop-readable" (func $future.drop-readable)) (export "future.drop-writable" (func $future.drop-writable)) + (export "future.forward" (func $future.forward)) )))) (func (export "trap-calling-import") (canon lift (core func $dm "noop") (post-return (core func $dm "trap-calling-import")))) (func (export "trap-calling-resource-new") (canon lift (core func $dm "noop") (post-return (core func $dm "trap-calling-resource-new")))) @@ -158,6 +166,7 @@ (func (export "trap-calling-stream-cancel-write") (canon lift (core func $dm "noop") (post-return (core func $dm "trap-calling-stream-cancel-write")))) (func (export "trap-calling-stream-drop-readable") (canon lift (core func $dm "noop") (post-return (core func $dm "trap-calling-stream-drop-readable")))) (func (export "trap-calling-stream-drop-writable") (canon lift (core func $dm "noop") (post-return (core func $dm "trap-calling-stream-drop-writable")))) + (func (export "trap-calling-stream-forward") (canon lift (core func $dm "noop") (post-return (core func $dm "trap-calling-stream-forward")))) (func (export "trap-calling-future-new") (canon lift (core func $dm "noop") (post-return (core func $dm "trap-calling-future-new")))) (func (export "trap-calling-future-read") (canon lift (core func $dm "noop") (post-return (core func $dm "trap-calling-future-read")))) (func (export "trap-calling-future-write") (canon lift (core func $dm "noop") (post-return (core func $dm "trap-calling-future-write")))) @@ -165,6 +174,7 @@ (func (export "trap-calling-future-cancel-write") (canon lift (core func $dm "noop") (post-return (core func $dm "trap-calling-future-cancel-write")))) (func (export "trap-calling-future-drop-readable") (canon lift (core func $dm "noop") (post-return (core func $dm "trap-calling-future-drop-readable")))) (func (export "trap-calling-future-drop-writable") (canon lift (core func $dm "noop") (post-return (core func $dm "trap-calling-future-drop-writable")))) + (func (export "trap-calling-future-forward") (canon lift (core func $dm "noop") (post-return (core func $dm "trap-calling-future-forward")))) ) (instance $c (instantiate $C)) (instance $d (instantiate $D (with "import" (func $c "import")))) @@ -189,6 +199,7 @@ (func (export "trap-calling-stream-cancel-write") (alias export $d "trap-calling-stream-cancel-write")) (func (export "trap-calling-stream-drop-readable") (alias export $d "trap-calling-stream-drop-readable")) (func (export "trap-calling-stream-drop-writable") (alias export $d "trap-calling-stream-drop-writable")) + (func (export "trap-calling-stream-forward") (alias export $d "trap-calling-stream-forward")) (func (export "trap-calling-future-new") (alias export $d "trap-calling-future-new")) (func (export "trap-calling-future-read") (alias export $d "trap-calling-future-read")) (func (export "trap-calling-future-write") (alias export $d "trap-calling-future-write")) @@ -196,6 +207,7 @@ (func (export "trap-calling-future-cancel-write") (alias export $d "trap-calling-future-cancel-write")) (func (export "trap-calling-future-drop-readable") (alias export $d "trap-calling-future-drop-readable")) (func (export "trap-calling-future-drop-writable") (alias export $d "trap-calling-future-drop-writable")) + (func (export "trap-calling-future-forward") (alias export $d "trap-calling-future-forward")) ) (component instance $i1 $Tester) @@ -214,46 +226,50 @@ (assert_trap (invoke "trap-calling-thread-index") "cannot leave component instance") (component instance $i8 $Tester) (assert_trap (invoke "trap-calling-waitable-set-new") "cannot leave component instance") -(component instance $i8 $Tester) -(assert_trap (invoke "trap-calling-waitable-set-wait") "cannot leave component instance") (component instance $i9 $Tester) -(assert_trap (invoke "trap-calling-waitable-set-poll") "cannot leave component instance") +(assert_trap (invoke "trap-calling-waitable-set-wait") "cannot leave component instance") (component instance $i10 $Tester) -(assert_trap (invoke "trap-calling-waitable-set-drop") "cannot leave component instance") +(assert_trap (invoke "trap-calling-waitable-set-poll") "cannot leave component instance") (component instance $i11 $Tester) -(assert_trap (invoke "trap-calling-waitable-join") "cannot leave component instance") +(assert_trap (invoke "trap-calling-waitable-set-drop") "cannot leave component instance") (component instance $i12 $Tester) -(assert_trap (invoke "trap-calling-subtask-cancel") "cannot leave component instance") +(assert_trap (invoke "trap-calling-waitable-join") "cannot leave component instance") (component instance $i13 $Tester) -(assert_trap (invoke "trap-calling-subtask-drop") "cannot leave component instance") +(assert_trap (invoke "trap-calling-subtask-cancel") "cannot leave component instance") (component instance $i14 $Tester) -(assert_trap (invoke "trap-calling-stream-new") "cannot leave component instance") +(assert_trap (invoke "trap-calling-subtask-drop") "cannot leave component instance") (component instance $i15 $Tester) -(assert_trap (invoke "trap-calling-stream-read") "cannot leave component instance") +(assert_trap (invoke "trap-calling-stream-new") "cannot leave component instance") (component instance $i16 $Tester) -(assert_trap (invoke "trap-calling-stream-write") "cannot leave component instance") +(assert_trap (invoke "trap-calling-stream-read") "cannot leave component instance") (component instance $i17 $Tester) -(assert_trap (invoke "trap-calling-stream-cancel-read") "cannot leave component instance") +(assert_trap (invoke "trap-calling-stream-write") "cannot leave component instance") (component instance $i18 $Tester) -(assert_trap (invoke "trap-calling-stream-cancel-write") "cannot leave component instance") +(assert_trap (invoke "trap-calling-stream-cancel-read") "cannot leave component instance") (component instance $i19 $Tester) -(assert_trap (invoke "trap-calling-stream-drop-readable") "cannot leave component instance") +(assert_trap (invoke "trap-calling-stream-cancel-write") "cannot leave component instance") (component instance $i20 $Tester) -(assert_trap (invoke "trap-calling-stream-drop-writable") "cannot leave component instance") +(assert_trap (invoke "trap-calling-stream-drop-readable") "cannot leave component instance") (component instance $i21 $Tester) -(assert_trap (invoke "trap-calling-future-new") "cannot leave component instance") +(assert_trap (invoke "trap-calling-stream-drop-writable") "cannot leave component instance") (component instance $i22 $Tester) -(assert_trap (invoke "trap-calling-future-read") "cannot leave component instance") +(assert_trap (invoke "trap-calling-stream-forward") "cannot leave component instance") (component instance $i23 $Tester) -(assert_trap (invoke "trap-calling-future-write") "cannot leave component instance") +(assert_trap (invoke "trap-calling-future-new") "cannot leave component instance") (component instance $i24 $Tester) -(assert_trap (invoke "trap-calling-future-cancel-read") "cannot leave component instance") +(assert_trap (invoke "trap-calling-future-read") "cannot leave component instance") (component instance $i25 $Tester) -(assert_trap (invoke "trap-calling-future-cancel-write") "cannot leave component instance") +(assert_trap (invoke "trap-calling-future-write") "cannot leave component instance") (component instance $i26 $Tester) -(assert_trap (invoke "trap-calling-future-drop-readable") "cannot leave component instance") +(assert_trap (invoke "trap-calling-future-cancel-read") "cannot leave component instance") (component instance $i27 $Tester) +(assert_trap (invoke "trap-calling-future-cancel-write") "cannot leave component instance") +(component instance $i28 $Tester) +(assert_trap (invoke "trap-calling-future-drop-readable") "cannot leave component instance") +(component instance $i29 $Tester) (assert_trap (invoke "trap-calling-future-drop-writable") "cannot leave component instance") +(component instance $i30 $Tester) +(assert_trap (invoke "trap-calling-future-forward") "cannot leave component instance") ;; built-ins that don't trap: