Skip to content

Separately gate error-context from async #489

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions design/mvp/Binary.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ primvaltype ::= 0x7f => bool
| 0x75 => f64
| 0x74 => char
| 0x73 => string
| 0x64 => error-context 🔀
| 0x64 => error-context 📝
defvaltype ::= pvt:<primvaltype> => pvt
| 0x72 lt*:vec(<labelvaltype>) => (record (field lt)*) (if |lt*| > 0)
| 0x71 case*:vec(<case>) => (variant case+) (if |case*| > 0)
Expand Down Expand Up @@ -307,9 +307,9 @@ canon ::= 0x00 0x00 f:<core:funcidx> opts:<opts> ft:<typeidx> => (canon lift
| 0x19 t:<typeidx> async?:<async?> => (canon future.cancel-write async? (core func)) 🔀
| 0x1a t:<typeidx> => (canon future.close-readable t (core func)) 🔀
| 0x1b t:<typeidx> => (canon future.close-writable t (core func)) 🔀
| 0x1c opts:<opts> => (canon error-context.new opts (core func)) 🔀
| 0x1d opts:<opts> => (canon error-context.debug-message opts (core func)) 🔀
| 0x1e => (canon error-context.drop (core func)) 🔀
| 0x1c opts:<opts> => (canon error-context.new opts (core func)) 📝
| 0x1d opts:<opts> => (canon error-context.debug-message opts (core func)) 📝
| 0x1e => (canon error-context.drop (core func)) 📝
| 0x1f => (canon waitable-set.new (core func)) 🔀
| 0x20 async?:<async>? m:<core:memidx> => (canon waitable-set.wait async? (memory m) (core func)) 🔀
| 0x21 async?:<async>? m:<core:memidx> => (canon waitable-set.poll async? (memory m) (core func)) 🔀
Expand Down
78 changes: 25 additions & 53 deletions design/mvp/CanonicalABI.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ being specified here.
* [`canon {stream,future}.{read,write}`](#-canon-streamfuturereadwrite) 🔀
* [`canon {stream,future}.cancel-{read,write}`](#-canon-streamfuturecancel-readwrite) 🔀
* [`canon {stream,future}.close-{readable,writable}`](#-canon-streamfutureclose-readablewritable) 🔀
* [`canon error-context.new`](#-canon-error-contextnew) 🔀
* [`canon error-context.debug-message`](#-canon-error-contextdebug-message) 🔀
* [`canon error-context.drop`](#-canon-error-contextdrop) 🔀
* [`canon error-context.new`](#-canon-error-contextnew) 📝
* [`canon error-context.debug-message`](#-canon-error-contextdebug-message) 📝
* [`canon error-context.drop`](#-canon-error-contextdrop) 📝
* [`canon thread.spawn_ref`](#-canon-threadspawn_ref) 🧵
* [`canon thread.spawn_indirect`](#-canon-threadspawn_indirect) 🧵
* [`canon thread.available_parallelism`](#-canon-threadavailable_parallelism) 🧵
Expand Down Expand Up @@ -1062,9 +1062,8 @@ class ReadableStream:
t: ValType
read: Callable[[WritableBuffer, OnPartialCopy, OnCopyDone], Literal['done','blocked']]
cancel: Callable[[], None]
close: Callable[[Optional[ErrorContext]]]
close: Callable[[]]
closed: Callable[[], bool]
closed_with: Callable[[], Optional[ErrorContext]]
```
The key operation is `read` which works as follows:
* `read` is non-blocking, returning `'blocked'` if it would have blocked.
Expand Down Expand Up @@ -1100,7 +1099,6 @@ class in chunks, starting with the fields and initialization:
class ReadableStreamGuestImpl(ReadableStream):
impl: ComponentInstance
closed_: bool
maybe_errctx: Optional[ErrorContext]
pending_buffer: Optional[Buffer]
pending_on_partial_copy: Optional[OnPartialCopy]
pending_on_copy_done: Optional[OnCopyDone]
Expand All @@ -1109,7 +1107,6 @@ class ReadableStreamGuestImpl(ReadableStream):
self.t = t
self.impl = inst
self.closed_ = False
self.maybe_errctx = None
self.reset_pending()

def reset_pending(self):
Expand All @@ -1135,19 +1132,14 @@ been returned:
def cancel(self):
self.reset_and_notify_pending()

def close(self, maybe_errctx):
def close(self):
if not self.closed_:
self.closed_ = True
self.maybe_errctx = maybe_errctx
if self.pending_buffer:
self.reset_and_notify_pending()

def closed(self):
return self.closed_

def closed_with(self):
assert(self.closed_)
return self.maybe_errctx
```
While the abstract `ReadableStream` interface *allows* `cancel` to return
without having returned ownership of the buffer (which, in general, is
Expand Down Expand Up @@ -1212,9 +1204,9 @@ class StreamEnd(Waitable):
self.stream = stream
self.copying = False

def drop(self, maybe_errctx):
def drop(self):
trap_if(self.copying)
self.stream.close(maybe_errctx)
self.stream.close()
Waitable.drop(self)

class ReadableStreamEnd(StreamEnd):
Expand Down Expand Up @@ -1251,11 +1243,11 @@ class FutureEnd(StreamEnd):
assert(buffer.remain() == 1)
def on_copy_done_wrapper():
if buffer.remain() == 0:
self.stream.close(maybe_errctx = None)
self.stream.close()
on_copy_done()
ret = copy_op(buffer, on_partial_copy = None, on_copy_done = on_copy_done_wrapper)
if ret == 'done' and buffer.remain() == 0:
self.stream.close(maybe_errctx = None)
self.stream.close()
return ret

class ReadableFutureEnd(FutureEnd):
Expand All @@ -1266,9 +1258,8 @@ class WritableFutureEnd(FutureEnd):
paired: bool = False
def copy(self, src, on_partial_copy, on_copy_done):
return self.close_after_copy(self.stream.write, src, on_copy_done)
def drop(self, maybe_errctx):
trap_if(not self.stream.closed() and not maybe_errctx)
FutureEnd.drop(self, maybe_errctx)
def drop(self):
FutureEnd.drop(self)
```
The `future.{read,write}` built-ins fix the buffer length to `1`, ensuring the
`assert(buffer.remain() == 1)` holds. Because of this, there are no partial
Expand Down Expand Up @@ -3607,14 +3598,7 @@ def pack_copy_result(task, buffer, e):
assert(not (buffer.progress & CLOSED))
return buffer.progress
else:
if (maybe_errctx := e.stream.closed_with()):
errctxi = task.inst.error_contexts.add(maybe_errctx)
assert(errctxi != 0)
else:
errctxi = 0
assert(errctxi <= Table.MAX_LENGTH < BLOCKED)
assert(not (errctxi & CLOSED))
return errctxi | CLOSED
return CLOSED
```
The order of tests here indicates that, if some progress was made and then the
stream was closed, only the progress is reported and the `CLOSED` status is
Expand Down Expand Up @@ -3705,41 +3689,29 @@ the given index from the current component instance's `waitable` table,
performing the guards and bookkeeping defined by
`{Readable,Writable}{Stream,Future}End.drop()` above.
```python
async def canon_stream_close_readable(t, task, i, errctxi):
return await close(ReadableStreamEnd, t, task, i, errctxi)
async def canon_stream_close_readable(t, task, i):
return await close(ReadableStreamEnd, t, task, i)

async def canon_stream_close_writable(t, task, hi, errctxi):
return await close(WritableStreamEnd, t, task, hi, errctxi)
async def canon_stream_close_writable(t, task, hi):
return await close(WritableStreamEnd, t, task, hi)

async def canon_future_close_readable(t, task, i, errctxi):
return await close(ReadableFutureEnd, t, task, i, errctxi)
async def canon_future_close_readable(t, task, i):
return await close(ReadableFutureEnd, t, task, i)

async def canon_future_close_writable(t, task, hi, errctxi):
return await close(WritableFutureEnd, t, task, hi, errctxi)
async def canon_future_close_writable(t, task, hi):
return await close(WritableFutureEnd, t, task, hi)

async def close(EndT, t, task, hi, errctxi):
async def close(EndT, t, task, hi):
trap_if(not task.inst.may_leave)
e = task.inst.waitables.remove(hi)
if errctxi == 0:
maybe_errctx = None
else:
maybe_errctx = task.inst.error_contexts.get(errctxi)
trap_if(not isinstance(e, EndT))
trap_if(e.stream.t != t)
e.drop(maybe_errctx)
e.drop()
return []
```
Passing a non-zero `errctxi` index indicates that this stream end is being
closed due to an error, with the given `error-context` providing information
that can be printed to aid in debugging. While, as explained above, the
*contents* of the `error-context` value are non-deterministic (and may, e.g.,
be empty), the presence or absence of an `error-context` value is semantically
meaningful for distinguishing between success or failure. Concretely, the
packed `i32` returned by `{stream,future}.{read,write}` operations indicates
success or failure by whether the `error-context` index is `0` or not.


### 🔀 `canon error-context.new`
### 📝 `canon error-context.new`

For a canonical definition:
```wat
Expand Down Expand Up @@ -3780,7 +3752,7 @@ are not checked. (Note that `host_defined_transformation` is not defined by the
Canonical ABI and stands for an arbitrary host-defined function.)


### 🔀 `canon error-context.debug-message`
### 📝 `canon error-context.debug-message`

For a canonical definition:
```wat
Expand Down Expand Up @@ -3808,7 +3780,7 @@ async def canon_error_context_debug_message(opts, task, i, ptr):
Note that `ptr` points to an 8-byte region of memory into which will be stored
the pointer and length of the debug string (allocated via `opts.realloc`).

### 🔀 `canon error-context.drop`
### 📝 `canon error-context.drop`

For a canonical definition:
```wat
Expand Down
41 changes: 18 additions & 23 deletions design/mvp/Explainer.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ implemented, considered stable and included in a future milestone:
* 🚟: using `async` with `canon lift` without `callback` (stackful lift)
* 🧵: threading built-ins
* 🔧: fixed-length lists
* 📝: the `error-context` type

(Based on the previous [scoping and layering] proposal to the WebAssembly CG,
this repo merges and supersedes the [module-linking] and [interface-types]
Expand Down Expand Up @@ -546,7 +547,7 @@ defvaltype ::= bool
| s8 | u8 | s16 | u16 | s32 | u32 | s64 | u64
| f32 | f64
| char | string
| error-context 🔀
| error-context 📝
| (record (field "<label>" <valtype>)+)
| (variant (case "<label>" <valtype>?)+)
| (list <valtype>)
Expand Down Expand Up @@ -605,14 +606,14 @@ sets of abstract values:
| `u8`, `u16`, `u32`, `u64` | integers in the range [0, 2<sup>N</sup>-1] |
| `f32`, `f64` | [IEEE754] floating-point numbers, with a single NaN value |
| `char` | [Unicode Scalar Values] |
| `error-context` | an immutable, non-deterministic, host-defined value meant to aid in debugging |
| `error-context` 📝 | an immutable, non-deterministic, host-defined value meant to aid in debugging |
| `record` | heterogeneous [tuples] of named values |
| `variant` | heterogeneous [tagged unions] of named values |
| `list` | homogeneous, variable- or fixed-length [sequences] of values |
| `own` | a unique, opaque address of a resource that will be destroyed when this value is dropped |
| `borrow` | an opaque address of a resource that must be dropped before the current export call returns |
| `stream` | an asynchronously-passed list of homogeneous values |
| `future` | an asynchronously-passed single value |
| `stream` 🔀 | an asynchronously-passed list of homogeneous values |
| `future` 🔀 | an asynchronously-passed single value |

How these abstract values are produced and consumed from Core WebAssembly
values and linear memory is configured by the component via *canonical lifting
Expand All @@ -637,7 +638,7 @@ a single NaN value. And boolean values in core wasm are usually represented as
`i32`s where operations interpret all-zeros as `false`, while at the
component-level there is a `bool` type with `true` and `false` values.

##### 🔀 Error Context type
##### 📝 Error Context type

Values of `error-context` type are immutable, non-deterministic, host-defined
and meant to be propagated from failure sources to callers in order to aid in
Expand Down Expand Up @@ -1438,9 +1439,9 @@ canon ::= ...
| (canon future.cancel-write <typeidx> async? (core func <id>?)) 🔀
| (canon future.close-readable <typeidx> (core func <id>?)) 🔀
| (canon future.close-writable <typeidx> (core func <id>?)) 🔀
| (canon error-context.new <canonopt>* (core func <id>?))
| (canon error-context.debug-message <canonopt>* (core func <id>?))
| (canon error-context.drop (core func <id>?))
| (canon error-context.new <canonopt>* (core func <id>?)) 📝
| (canon error-context.debug-message <canonopt>* (core func <id>?)) 📝
| (canon error-context.drop (core func <id>?)) 📝
| (canon thread.spawn_ref <typeidx> (core func <id>?)) 🧵
| (canon thread.spawn_indirect <typeidx> <core:tableidx> (core func <id>?)) 🧵
| (canon thread.available_parallelism (core func <id>?)) 🧵
Expand Down Expand Up @@ -1780,7 +1781,7 @@ enum read-status {
blocked,

// The end of the stream has been reached.
closed(option<error-context>),
closed,
}
```

Expand Down Expand Up @@ -1816,8 +1817,6 @@ the Canonical ABI explainer for details.)
`read-status` and `write-status` are lowered in the Canonical ABI as:
- The value `0xffff_ffff` represents `blocked`.
- Otherwise, if the bit `0x8000_0000` is set, the value represents `closed`.
For `read-status`, the remaining bits `0x7fff_ffff` contain the index of an
`error-context` in the instance's `error-context` table.
- Otherwise, the value represents `complete` and contains the number of
element read or written.

Expand Down Expand Up @@ -1883,24 +1882,20 @@ delivered to indicate the completion of the `read` or `write`. (See

| Synopsis | |
| ----------------------------------------------------- | ---------------------------------------------------------------- |
| Approximate WIT signature for `stream.close-readable` | `func<T>(e: readable-stream-end<T>, err: option<error-context>)` |
| Approximate WIT signature for `stream.close-writable` | `func<T>(e: writable-stream-end<T>, err: option<error-context>)` |
| Approximate WIT signature for `future.close-readable` | `func<T>(e: readable-future-end<T>, err: option<error-context>)` |
| Approximate WIT signature for `future.close-writable` | `func<T>(e: writable-future-end<T>, err: option<error-context>)` |
| Approximate WIT signature for `stream.close-readable` | `func<T>(e: readable-stream-end<T>)` |
| Approximate WIT signature for `stream.close-writable` | `func<T>(e: writable-stream-end<T>)` |
| Approximate WIT signature for `future.close-readable` | `func<T>(e: readable-future-end<T>)` |
| Approximate WIT signature for `future.close-writable` | `func<T>(e: writable-future-end<T>)` |
| Canonical ABI signature | `[end:i32 err:i32] -> []` |

The `{stream,future}.close-{readable,writable}` built-ins remove the indicated
[stream or future] from the current component instance's table of [waitables],
trapping if the stream or future has a mismatched direction or type or are in
the middle of a `read` or `write`.

In the Canonical ABI, an `err` value of `0` represents `none`, and a non-zero
value represents `some` of the index of an `error-context` in the instance's
table. (See also [the `close` built-ins] in the Canonical ABI explainer.)

##### 🔀 Error Context built-ins
##### 📝 Error Context built-ins

###### `error-context.new`
###### 📝 `error-context.new`

| Synopsis | |
| -------------------------------- | ---------------------------------------- |
Expand All @@ -1915,7 +1910,7 @@ In the Canonical ABI, the returned value is an index into a
per-component-instance table. (See also [`canon_error_context_new`] in the
Canonical ABI explainer.)

###### `error-context.debug-message`
###### 📝 `error-context.debug-message`

| Synopsis | |
| -------------------------------- | --------------------------------------- |
Expand All @@ -1930,7 +1925,7 @@ In the Canonical ABI, it writes the debug message into `ptr` as an 8-byte
`<canonopt>*` immediates. (See also [`canon_error_context_debug_message`] in
the Canonical ABI explainer.)

###### `error-context.drop`
###### 📝 `error-context.drop`

| Synopsis | |
| -------------------------------- | ----------------------------- |
Expand Down
Loading