Skip to content
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

Add 'stream' and 'future' types #405

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open

Add 'stream' and 'future' types #405

wants to merge 1 commit into from

Conversation

lukewagner
Copy link
Member

This PR builds on #363 to add future and stream type constructors along with the associated canon built-ins ({stream,future}.new, {stream,future}.{read,write}, {stream,future}.cancel-{read,write}). It also generalizes the "async_subtasks" table to also hold futures and streams, renaming it to be the "waitables" table and renaming subtask.drop to waitable.drop. Lastly, a new canonopt called always-task-return is added so that sync-lifted exports can use task.return to return their value, which you need if you want to return a stream or future from a synchronous function.

I'd suggest reading the new text in Explainer.md/Binary to get a short overview of the concrete syntactic/binary additions, then Async.md to get the high-level summary, then CanonicalABI.md to get the full details.

a simple per-[`Task`] `num_async_subtasks` counter that traps if not zero when
the `Task` finishes.
The Canonical ABI's Python code enforces Structured Concurrency by incrementing
a per-[`Task`] counter when a `Subtask` is created, decrementing when a
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't literally have to be a counter, right? An implementation might keep track of the subtasks of a given task as a list or hash table, so we just need to make sure that list or hash table is empty before exiting the task.

an import call) or result (of an export call), the producer can either
*transfer ownership* of a readable end it has already received or it can
create a fresh writable end (via `stream.new` or `future.new`) and lift this
writable end (maintaining ownership of the writable end, but creating a fresh
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
writable end (maintaining ownership of the writable end, but creating a fresh
writable end into a readable end (maintaining ownership of the writable end, but creating a fresh

Not sure what the best way to word this is, but let's make it clear that lifting a writable end produces a readable end in the receiving component or host.

| 0x0d => (canon subtask.drop (core func)) 🔀
| 0x0d => (canon waitable.drop (core func)) 🔀
| 0x0e t:<typeidx> => (canon stream.new t (core func)) 🔀
| 0x0f => (canon stream.read (core func)) 🔀
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have expected all the stream.* and future.* functions to take a t:<typeidx> parameter. In my prototype, I use that parameter to generate efficient code during pre-instantiation, fast-pathing read and write calls for "flat" payloads (i.e. no pointers or handles). I don't think that would be feasible if we had to discover the type at runtime.

take an index to the matching [readable or writable end](Async.md#streams-and-futures)
of a future as the first parameter and a pointer linear memory as the second
parameter. The return value is either `1` if the future value was eagerly
read or written to the pointer or the sentinel "`BLOCKED`" value otherwise.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
read or written to the pointer or the sentinel "`BLOCKED`" value otherwise.
read from or written to the pointer or the sentinel "`BLOCKED`" value otherwise.

cancellation finished eagerly, the return value is the number of elements read
or written into the given buffer (`0` or `1` for a `future`). If cancellation
blocks, the return value is the sentinel "`BLOCKED`" value and the caller must
`task.wait` for a `{STREAM,FUTURE}_{READ,WRITE}` event to indicate the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is task.wait the only option here, or can it receive the event via its callback if it was invoked via an async-with-callback export?

Python code represents `events` as a list of closures, an optimizing
implementation should be able to avoid dynamically allocating this list and
instead represent `events` as a linked list embedded in the elements of the
`waitables` table (noting that, by design, any given `watiables` element can be
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`waitables` table (noting that, by design, any given `watiables` element can be
`waitables` table (noting that, by design, any given `waitables` element can be

| 0x0d => (canon subtask.drop (core func)) 🔀
| 0x0d => (canon waitable.drop (core func)) 🔀
| 0x0e t:<typeidx> => (canon stream.new t (core func)) 🔀
| 0x0f => (canon stream.read (core func)) 🔀
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, would it be too out-of-scope to add {stream|future}.write-error to this list just to reserve the byte codes, with the semantics left for a later PR?

| 0x13 t:<typeidx> => (canon future.new t (core func)) 🔀
| 0x14 => (canon future.read (core func)) 🔀
| 0x15 => (canon future.write (core func)) 🔀
| 0x16 async?:<async?> => (canon future.cancel-read async? (core func)) 🔀
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we talked about this earlier and I forgot, but could we consider leaving the cancel functions as post-WASIp3 (or nice-to-have-for-WASIp3) features?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants