Skip to content

Commit 3be121a

Browse files
Add {stream,future}.forward built-in
Co-authored-by: Roman Volosatovs <rvolosatovs@riseup.net>
1 parent 205ac74 commit 3be121a

10 files changed

Lines changed: 1830 additions & 30 deletions

File tree

design/mvp/Binary.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,13 +316,15 @@ canon ::= 0x00 0x00 f:<core:funcidx> opts:<opts> ft:<typeidx> => (canon lift
316316
| 0x12 t:<typeidx> async?:<async?> => (canon stream.cancel-write t async? (core func)) 🔀
317317
| 0x13 t:<typeidx> => (canon stream.drop-readable t (core func)) 🔀
318318
| 0x14 t:<typeidx> => (canon stream.drop-writable t (core func)) 🔀
319+
| 0x2e t:<typeidx> => (canon stream.forward t (core func)) ➡️
319320
| 0x15 t:<typeidx> => (canon future.new t (core func)) 🔀
320321
| 0x16 t:<typeidx> opts:<opts> => (canon future.read t opts (core func)) 🔀
321322
| 0x17 t:<typeidx> opts:<opts> => (canon future.write t opts (core func)) 🔀
322323
| 0x18 t:<typeidx> async?:<async?> => (canon future.cancel-read t async? (core func)) 🔀
323324
| 0x19 t:<typeidx> async?:<async?> => (canon future.cancel-write t async? (core func)) 🔀
324325
| 0x1a t:<typeidx> => (canon future.drop-readable t (core func)) 🔀
325326
| 0x1b t:<typeidx> => (canon future.drop-writable t (core func)) 🔀
327+
| 0x2f t:<typeidx> => (canon future.forward t (core func)) ➡️
326328
| 0x1c opts:<opts> => (canon error-context.new opts (core func)) 📝
327329
| 0x1d opts:<opts> => (canon error-context.debug-message opts (core func)) 📝
328330
| 0x1e => (canon error-context.drop (core func)) 📝

design/mvp/CanonicalABI.md

Lines changed: 76 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ specified here.
5656
* [`canon {stream,future}.{read,write}`](#-canon-streamfuturereadwrite) 🔀
5757
* [`canon {stream,future}.cancel-{read,write}`](#-canon-streamfuturecancel-readwrite) 🔀
5858
* [`canon {stream,future}.drop-{readable,writable}`](#-canon-streamfuturedrop-readablewritable) 🔀
59+
* [`canon {stream,future}.forward`](#-canon-streamfutureforward) ➡️
5960
* [`canon thread.index`](#-canon-threadindex) 🧵
6061
* [`canon thread.new-indirect`](#-canon-threadnew-indirect) 🧵
6162
* [`canon thread.resume-later`](#-canon-threadresume-later) 🧵
@@ -1719,9 +1720,10 @@ cancellation. In the future, guest components may be given the same capability.
17191720
self.notify(End.Result.CANCELLED)
17201721
```
17211722

1722-
The `End.drop` method is called by `{stream,future}.drop-{readable,writable}` to
1723-
update the `other` end's state and possibly set a pending notification for the
1724-
other end, if doing so wouldn't clobber an already-pending notification.
1723+
The `End.drop` method is called by `{stream,future}.drop-{readable,writable}`
1724+
and `End.forward` to update the `other` end's state and possibly set a pending
1725+
notification for the other end, if doing so wouldn't clobber an already-pending
1726+
notification.
17251727
```python
17261728
def drop(self):
17271729
assert(not self.copying_or_cancelling())
@@ -1737,6 +1739,37 @@ other end, if doing so wouldn't clobber an already-pending notification.
17371739
return self.state in { End.State.COPYING, End.State.CANCELLING_COPY }
17381740
```
17391741

1742+
The `End.forward` function is called by `{stream,future}.forward` to efficiently
1743+
forward all the values from a given readable end into a given writable end,
1744+
propagating `DROPPED` results in both directions. This causes the given readable
1745+
end and writable end to disappear, leaving only the `other` writable and
1746+
readable ends, which are now linked together directly to form a single stream or
1747+
future. If *both* of these formerly-separate ends have pending copy operations
1748+
with pending `buffer`s, the forwarding operation performs a synchronous copy,
1749+
leaving at most one pending buffer (the bigger of the two) and notifying one or
1750+
both sides of the progress made. In the corner case where a stream or future's
1751+
readable end is forwarded to its own writable end (creating a trivial recursive
1752+
loop), there is no trap since the whole thing simply disappears.
1753+
```python
1754+
def forward(src: End, dst: End):
1755+
if src.other is dst or src.other is None or dst.other is None:
1756+
src.drop()
1757+
dst.drop()
1758+
else:
1759+
writable_end = src.other
1760+
readable_end = dst.other
1761+
writable_end.other = readable_end
1762+
readable_end.other = writable_end
1763+
if writable_end.buffer is not None and readable_end.buffer is not None:
1764+
if readable_end.buffer.remain() > writable_end.buffer.remain():
1765+
bigger_end, smaller_end = readable_end, writable_end
1766+
else:
1767+
bigger_end, smaller_end = writable_end, readable_end
1768+
buffer = smaller_end.buffer
1769+
smaller_end.buffer = None
1770+
smaller_end.copy(buffer)
1771+
```
1772+
17401773
Next, the intermediate `{Stream,Future}End` base classes are defined to implement
17411774
the `{Stream,Future}End.notify` methods that are called by the
17421775
`End.{copy,cancel,drop}` methods above. `notify`'s behavior does not exhibit the
@@ -4451,6 +4484,46 @@ def drop(EndT, stream_or_future_t, i):
44514484
```
44524485

44534486

4487+
### ➡️ `canon {stream,future}.forward`
4488+
4489+
For canonical definitions:
4490+
```wat
4491+
(canon stream.forward $stream_t (core func $forward))
4492+
(canon future.forward $future_t (core func $forward))
4493+
```
4494+
validation specifies:
4495+
* `$forward` is given type `(func (param $ri i32) (param $wi i32))`
4496+
* `$stream_t`/`$future_t` must be a type of the form `(stream $t?)`/`(future $t?)`
4497+
4498+
Calling `$forward` removes the readable and writable ends at the given indices,
4499+
after checking that all the types match, the ends are in the `IDLE` state, and
4500+
the ends are not currently part of a waitable set. Then the readable end is
4501+
forwarded into the writable end as defined by `End.forward` above.
4502+
```python
4503+
def canon_stream_forward(stream_t, ri, wi):
4504+
return forward(ReadableStreamEnd, WritableStreamEnd, stream_t, ri, wi)
4505+
4506+
def canon_future_forward(future_t, ri, wi):
4507+
return forward(ReadableFutureEnd, WritableFutureEnd, future_t, ri, wi)
4508+
4509+
def forward(ReadableEndT, WritableEndT, stream_or_future_t, ri, wi):
4510+
inst = current_instance()
4511+
trap_if(not inst.may_leave)
4512+
readable_end = inst.handles.remove(ri)
4513+
trap_if(not isinstance(readable_end, ReadableEndT))
4514+
trap_if(readable_end.t != stream_or_future_t.t)
4515+
trap_if(readable_end.state != End.State.IDLE)
4516+
trap_if(readable_end.in_waitable_set())
4517+
writable_end = inst.handles.remove(wi)
4518+
trap_if(not isinstance(writable_end, WritableEndT))
4519+
trap_if(writable_end.t != stream_or_future_t.t)
4520+
trap_if(writable_end.state != End.State.IDLE)
4521+
trap_if(writable_end.in_waitable_set())
4522+
End.forward(readable_end, writable_end)
4523+
return []
4524+
```
4525+
4526+
44544527
### 🧵 `canon thread.index`
44554528

44564529
For a canonical definition:

design/mvp/Concurrency.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -577,7 +577,7 @@ or `future`. When *producing* a `stream` or `future` value as a parameter (of
577577
an import call) or result (of an export call), the producer can *transfer
578578
ownership* of a readable end that it has either been given by the outside world
579579
or freshly created via `{stream,future}.new` (which also return a fresh paired
580-
writable end that is permanently owned by the calling component instance).
580+
writable end).
581581

582582
Based on this, `stream<T>` and `future<T>` values can be passed between
583583
functions as if they were synchronous `list<T>` and `T` values, resp. For
@@ -619,6 +619,13 @@ without requiring an explicit `future` return type. Thus, a function like
619619
which point the caller receives the readable end of a `future` that, when
620620
successfully read, conveys the completion of a second event.
621621

622+
Given the readable end of one stream/future and the writable end of another, the
623+
`{stream,future}.forward` built-ins can be called to efficiently forward all
624+
remaining values from the readable end into the writable end, avoiding any
625+
intermediate copies. Doing so relinquishes ownership of both handles, allowing
626+
the calling component instance to be eagerly torn down while the forwarding is
627+
in progress.
628+
622629
The [Stream and Future State] section describes the runtime state maintained for
623630
streams and futures by the Canonical ABI.
624631

@@ -1478,7 +1485,6 @@ specified, the following features are being considered for addition to complete
14781485
the concurrency story:
14791486
* remove the temporary trap mentioned above that occurs when a `read` and
14801487
`write` of a stream/future happen from within the same component instance
1481-
* zero-copy forwarding/splicing
14821488
* allow `async` functions using the stackful ABI to be notified of
14831489
cancellation
14841490
* allow the `stream<char>` type to validate; make it use `string-encoding`

design/mvp/Explainer.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ shipped as part of a future WASI Developer Preview release:
7575
* 📝: the `error-context` type
7676
* 🔗: canonical interface names
7777
* 🐘: [memory64]
78+
* ➡️: `stream.forward` and `future.forward` built-ins
7879

7980

8081
## Grammar
@@ -1573,13 +1574,15 @@ canon ::= ...
15731574
| (canon stream.cancel-write <typeidx> async? (core func <id>?)) 🔀
15741575
| (canon stream.drop-readable <typeidx> (core func <id>?)) 🔀
15751576
| (canon stream.drop-writable <typeidx> (core func <id>?)) 🔀
1577+
| (canon stream.forward <typeidx> (core func <id>?)) ➡️
15761578
| (canon future.new <typeidx> (core func <id>?)) 🔀
15771579
| (canon future.read <typeidx> <canonopt>* (core func <id>?)) 🔀
15781580
| (canon future.write <typeidx> <canonopt>* (core func <id>?)) 🔀
15791581
| (canon future.cancel-read <typeidx> async? (core func <id>?)) 🔀
15801582
| (canon future.cancel-write <typeidx> async? (core func <id>?)) 🔀
15811583
| (canon future.drop-readable <typeidx> (core func <id>?)) 🔀
15821584
| (canon future.drop-writable <typeidx> (core func <id>?)) 🔀
1585+
| (canon future.forward <typeidx> (core func <id>?)) ➡️
15831586
| (canon thread.index (core func <id>?)) 🧵
15841587
| (canon thread.new-indirect core-prefix(<core:typeidx>) core-prefix(<core:tableidx>) (core func <id>?)) 🧵
15851588
| (canon thread.resume-later (core func <id>?)) 🧵
@@ -2145,6 +2148,25 @@ already been dropped.
21452148
For details, see [Streams and Futures] in the concurrency explainer and
21462149
[`canon_stream_drop_readable`] in the Canonical ABI explainer.
21472150

2151+
###### ➡️ `stream.forward` and `future.forward`
2152+
2153+
| Synopsis | |
2154+
| ---------------------------------------------- | -------------------------------------------------------------------------- |
2155+
| Approximate WIT signature for `stream.forward` | `func<stream<T?>>(r: readable-stream-end<T?>, w: writable-stream-end<T?>)` |
2156+
| Approximate WIT signature for `future.forward` | `func<future<T?>>(r: readable-future-end<T?>, w: writable-future-end<T?>)` |
2157+
| Canonical ABI signature | `[ri:i32 wi:i32] -> []` |
2158+
2159+
The `{stream,future}.forward` built-ins remove the given readable and writable
2160+
ends from the caller's handle table and logically forward everything from the
2161+
readable end into the writable end (propagating drops in both directions), but
2162+
do so without an intermediate copy. The call traps if either end has a
2163+
mismatched direction or element type, is in the middle of a read or write, is a
2164+
member of a waitable set, or has received its final `dropped` or, for futures,
2165+
`completed` result.
2166+
2167+
For details, see [Streams and Futures] in the concurrency explainer and
2168+
[`canon_stream_forward`] in the Canonical ABI explainer.
2169+
21482170
###### 🧵 `thread.index`
21492171

21502172
| Synopsis | |
@@ -3377,6 +3399,7 @@ For some use-case-focused, worked examples, see:
33773399
[`canon_future_read`]: CanonicalABI.md#-canon-streamfuturereadwrite
33783400
[`canon_stream_cancel_read`]: CanonicalABI.md#-canon-streamfuturecancel-readwrite
33793401
[`canon_stream_drop_readable`]: CanonicalABI.md#-canon-streamfuturedrop-readablewritable
3402+
[`canon_stream_forward`]: CanonicalABI.md#-canon-streamfutureforward
33803403
[`canon_subtask_cancel`]: CanonicalABI.md#-canon-subtaskcancel
33813404
[`canon_subtask_drop`]: CanonicalABI.md#-canon-subtaskdrop
33823405
[`canon_resource_new`]: CanonicalABI.md#canon-resourcenew

design/mvp/canonical-abi/definitions.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@
44

55
### Boilerplate
66

7-
from __future__ import annotations
87
from dataclasses import dataclass
9-
from typing import Any, Optional, Callable, TypeVar, Generic, Literal
8+
from typing import Optional, Callable, Literal
109
from enum import Enum, IntEnum
1110
import math
1211
import struct
@@ -988,6 +987,24 @@ def drop(self):
988987
def copying_or_cancelling(self):
989988
return self.state in { End.State.COPYING, End.State.CANCELLING_COPY }
990989

990+
def forward(src: End, dst: End):
991+
if src.other is dst or src.other is None or dst.other is None:
992+
src.drop()
993+
dst.drop()
994+
else:
995+
writable_end = src.other
996+
readable_end = dst.other
997+
writable_end.other = readable_end
998+
readable_end.other = writable_end
999+
if writable_end.buffer is not None and readable_end.buffer is not None:
1000+
if readable_end.buffer.remain() > writable_end.buffer.remain():
1001+
bigger_end, smaller_end = readable_end, writable_end
1002+
else:
1003+
bigger_end, smaller_end = writable_end, readable_end
1004+
buffer = smaller_end.buffer
1005+
smaller_end.buffer = None
1006+
smaller_end.copy(buffer)
1007+
9911008
class StreamEnd(End):
9921009
def notify(self, result: End.Result, progress = 0):
9931010
def stream_event():
@@ -2498,6 +2515,30 @@ def drop(EndT, stream_or_future_t, i):
24982515
end.drop()
24992516
return []
25002517

2518+
### ➡️ `canon {stream,future}.forward`
2519+
2520+
def canon_stream_forward(stream_t, ri, wi):
2521+
return forward(ReadableStreamEnd, WritableStreamEnd, stream_t, ri, wi)
2522+
2523+
def canon_future_forward(future_t, ri, wi):
2524+
return forward(ReadableFutureEnd, WritableFutureEnd, future_t, ri, wi)
2525+
2526+
def forward(ReadableEndT, WritableEndT, stream_or_future_t, ri, wi):
2527+
inst = current_instance()
2528+
trap_if(not inst.may_leave)
2529+
readable_end = inst.handles.remove(ri)
2530+
trap_if(not isinstance(readable_end, ReadableEndT))
2531+
trap_if(readable_end.t != stream_or_future_t.t)
2532+
trap_if(readable_end.state != End.State.IDLE)
2533+
trap_if(readable_end.in_waitable_set())
2534+
writable_end = inst.handles.remove(wi)
2535+
trap_if(not isinstance(writable_end, WritableEndT))
2536+
trap_if(writable_end.t != stream_or_future_t.t)
2537+
trap_if(writable_end.state != End.State.IDLE)
2538+
trap_if(writable_end.in_waitable_set())
2539+
End.forward(readable_end, writable_end)
2540+
return []
2541+
25012542
### 🧵 `canon thread.index`
25022543

25032544
def canon_thread_index():

design/mvp/canonical-abi/run_tests.py

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1651,7 +1651,7 @@ def core_func(args):
16511651
assert(host_reader2.received == [11,12,13,14,15,16,17,18])
16521652

16531653

1654-
def test_stream_forward():
1654+
def test_transfer_readable_end():
16551655
host_writer = HostWriter(U8Type(), [1,2,3,4], chunk=4)
16561656
def on_start():
16571657
return [host_writer.readable_end]
@@ -1674,6 +1674,97 @@ def core_func(args):
16741674
assert(host_writer.readable_end is return_value)
16751675

16761676

1677+
def test_forward():
1678+
store = Store()
1679+
mem = bytearray(32)
1680+
opts = mk_opts(memory=MemInst(mem, 'i32'), async_=True)
1681+
inst = ComponentInstance(store)
1682+
st = StreamType(U8Type())
1683+
ft = FutureType(U8Type())
1684+
1685+
def core_func(args):
1686+
def new_stream(t = st):
1687+
[packed] = canon_stream_new(t)
1688+
return unpack_new_ends(packed)
1689+
def new_future():
1690+
[packed] = canon_future_new(ft)
1691+
return unpack_new_ends(packed)
1692+
1693+
rsi,wsi = new_stream()
1694+
[] = canon_stream_forward(st, rsi, wsi)
1695+
1696+
rsi1,wsi1 = new_stream()
1697+
rsi2,wsi2 = new_stream()
1698+
[] = canon_stream_drop_readable(st, rsi2)
1699+
[] = canon_stream_forward(st, rsi1, wsi2)
1700+
[ret] = canon_stream_write(st, opts, wsi1, 0, 4)
1701+
result,n = unpack_result(ret)
1702+
assert(n == 0 and result == End.Result.DROPPED)
1703+
[] = canon_stream_drop_writable(st, wsi1)
1704+
1705+
rsi1,wsi1 = new_stream()
1706+
rsi2,wsi2 = new_stream()
1707+
[] = canon_stream_forward(st, rsi1, wsi2)
1708+
mem[0:4] = b'\x01\x02\x03\x04'
1709+
[ret] = canon_stream_write(st, opts, wsi1, 0, 4)
1710+
assert(ret == definitions.BLOCKED)
1711+
[ret] = canon_stream_read(st, opts, rsi2, 8, 4)
1712+
result,n = unpack_result(ret)
1713+
assert(n == 4 and result == End.Result.COMPLETED)
1714+
assert(mem[8:12] == b'\x01\x02\x03\x04')
1715+
1716+
rsi1,wsi1 = new_stream()
1717+
rsi2,wsi2 = new_stream()
1718+
[ret] = canon_stream_read(st, opts, rsi2, 8, 4)
1719+
assert(ret == definitions.BLOCKED)
1720+
[] = canon_stream_forward(st, rsi1, wsi2)
1721+
mem[0:4] = b'\x05\x06\x07\x08'
1722+
[ret] = canon_stream_write(st, opts, wsi1, 0, 4)
1723+
result,n = unpack_result(ret)
1724+
assert(n == 4 and result == End.Result.COMPLETED)
1725+
assert(mem[8:12] == b'\x05\x06\x07\x08')
1726+
[seti] = canon_waitable_set_new()
1727+
[] = canon_waitable_join(rsi2, seti)
1728+
[event] = canon_waitable_set_wait(MemInst(mem, 'i32'), seti, 16)
1729+
assert(event == EventCode.STREAM_READ)
1730+
assert(mem[16] == rsi2)
1731+
result,n = unpack_result(mem[20])
1732+
assert(n == 4 and result == End.Result.COMPLETED)
1733+
1734+
rfi1,wfi1 = new_future()
1735+
rfi2,wfi2 = new_future()
1736+
[ret] = canon_future_read(ft, opts, rfi2, 8)
1737+
assert(ret == definitions.BLOCKED)
1738+
[] = canon_future_forward(ft, rfi1, wfi2)
1739+
mem[0] = 42
1740+
[ret] = canon_future_write(ft, opts, wfi1, 0)
1741+
assert(ret == End.Result.COMPLETED)
1742+
assert(mem[8] == 42)
1743+
1744+
rfi1,wfi1 = new_future()
1745+
rfi2,wfi2 = new_future()
1746+
mem[0] = 43
1747+
[ret] = canon_future_write(ft, opts, wfi1, 0)
1748+
assert(ret == definitions.BLOCKED)
1749+
[ret] = canon_future_read(ft, opts, rfi2, 8)
1750+
assert(ret == definitions.BLOCKED)
1751+
[] = canon_future_forward(ft, rfi1, wfi2)
1752+
assert(mem[8] == 43)
1753+
1754+
rfi1,wfi1 = new_future()
1755+
rfi2,wfi2 = new_future()
1756+
[] = canon_future_drop_readable(ft, rfi2)
1757+
[] = canon_future_forward(ft, rfi1, wfi2)
1758+
[ret] = canon_future_write(ft, opts, wfi1, 0)
1759+
assert(ret == End.Result.DROPPED)
1760+
[] = canon_future_drop_writable(ft, wfi1)
1761+
1762+
return []
1763+
1764+
caller_ft = FuncType([], [], async_ = True)
1765+
lift_and_run(mk_opts(), inst, caller_ft, core_func, lambda:[], lambda _:())
1766+
1767+
16771768
def test_receive_own_stream():
16781769
store = Store()
16791770
inst = ComponentInstance(store)
@@ -2974,7 +3065,8 @@ def on_resolve(v):
29743065
test_sync_using_wait()
29753066
test_eager_stream_completion()
29763067
test_async_stream_ops()
2977-
test_stream_forward()
3068+
test_transfer_readable_end()
3069+
test_forward()
29783070
test_receive_own_stream()
29793071
test_host_partial_reads_writes()
29803072
test_wasm_to_wasm_stream()

0 commit comments

Comments
 (0)