Skip to content

Commit 7d61ffa

Browse files
committed
Extend number of flat parameters in async lower from 1 to 4
Closes #434
1 parent deb0b0a commit 7d61ffa

File tree

4 files changed

+102
-51
lines changed

4 files changed

+102
-51
lines changed

design/mvp/Async.md

Lines changed: 57 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -620,58 +620,68 @@ which to use by default.
620620

621621
### Async Import ABI
622622

623-
Given an imported WIT function:
623+
Given these imported WIT functions (using the fixed-length-list feature 🔧):
624624
```wit
625625
world w {
626-
import foo: func(s: string) -> string;
626+
import foo: func(s: string) -> u32;
627+
import bar: func(s: string) -> string;
628+
import baz: func(t: tuple<u64; 5>) -> string;
629+
import quux: func(t: tuple<u32; 17>) -> string;
627630
}
628631
```
629-
the default sync import function signature is:
632+
the default/synchronous lowered import function signatures are:
630633
```wat
631634
;; sync
632-
(func (param $s-ptr i32) (param $s-len i32) (param $out i32))
635+
(func $foo (param $s-ptr i32) (param $s-len i32) (result i32))
636+
(func $bar (param $s-ptr i32) (param $s-len i32) (param $out-ptr i32))
637+
(func $baz (param i64 i64 i64 i64 i64) (param $out-ptr i32))
638+
(func $quux (param $in-ptr i32) (param $out-ptr i32))
633639
```
634-
where `$out` must be a 4-byte-aligned pointer into linear memory into which the
635-
8-byte (pointer, length) of the returned string will be stored.
636-
637-
The new async import function signature is:
640+
Here: `foo`, `bar` and `baz` pass their parameters as "flattened" core value
641+
types while `quux` passes its parameters via the `$in-ptr` linear memory
642+
pointer (due to the Canonical ABI limitation of 16 maximum flattened
643+
parameters). Similarly, `foo` returns its result as a single core value while
644+
`bar`, `baz` and `quux` return their results via the `$out-ptr` linear memory
645+
pointer (due to the current Canonical ABI limitation of 1 maximum flattened
646+
result).
647+
648+
The corresponding asynchronous lowered import function signatures are:
638649
```wat
639650
;; async
640-
(func (param $in i32) (param $out i32) (result i32))
651+
(func $foo (param $s-ptr i32) (param $s-len i32) (param $out-ptr i32) (result i32))
652+
(func $bar (param $s-ptr i32) (param $s-len i32) (param $out-ptr i32) (result i32))
653+
(func $baz (param $in-ptr i32) (param $out-ptr i32) (result i32))
654+
(func $quux (param $in-ptr i32) (param $out-ptr i32) (result i32))
641655
```
642-
where `$in` must be a 4-byte-aligned pointer into linear memory from which the
643-
8-byte (pointer, length) of the string argument will be loaded and `$out` works
644-
the same as in the synchronous case. What's different, however, is *when* `$in`
645-
and `$out` are read or written. In a synchronous call, they are always read or
646-
written before the call returns. In an asynchronous call, there is a set of
647-
possibilities indicated by the `(result i32)` value:
648-
* If the returned `i32` is `2`, then the call returned eagerly without
649-
blocking and so `$in` has been read and `$out` has been written.
650-
* Otherwise, the high 28 bits of the `i32` are the index of a new `Subtask`
651-
in the current component instance's table. The low 4 bits indicate how far
652-
the callee made it before blocking:
653-
* If `1`, the callee didn't even start (due to backpressure), and thus
654-
neither `$in` nor `$out` have been accessed yet.
655-
* If `2`, the callee started by reading `$in`, but blocked before writing
656-
`$out`.
657-
658-
The async signature `(func (param i32 i32) (result i32))` is the same for
659-
almost all WIT function types since the ABI stores everything in linear memory.
660-
However, there are three special cases:
661-
* If the WIT parameter list is empty, `$in` is removed.
662-
* If the WIT parameter list flattens to exactly 1 core value type (`i32` or
663-
otherwise), `$in` uses that core value type and the argument is passed
664-
by value.
665-
* If the WIT result is empty, `$out` is removed.
666-
667-
For example:
656+
Comparing signatures, the differences are:
657+
* Async-lowered functions have a maximum of 4 flat parameters (not 16).
658+
* Async-lowered functions always return their value via linear memory pointer.
659+
* Async-lowered functions always have a single `i32` "status" code.
660+
661+
Additionally, *when* the parameter and result pointers are read/written depends
662+
on the status code:
663+
* If the low 4 bits of the status are `0`, the call didn't even start and so
664+
`$in-ptr` hasn't been read and `$out-ptr` hasn't been written and the high
665+
28 bits are the index of a new async subtask to wait on.
666+
* If the low 4 bits of the status are `1`, the call started, `$in-ptr` was
667+
read, but `$out-ptr` hasn't been written and the high 28 bits are the index
668+
of a new async subtask to wait on.
669+
* If the low 4 bits of the status are `2`, the call returned and so `$in-ptr`
670+
and `$out-ptr` have been read/written and the high 28 bits are `0` because
671+
there is no async subtask to wait on.
672+
673+
When a parameter/result pointer hasn't yet been read/written, the async caller
674+
must take care to keep the region of memory allocated to the call until
675+
receiving an event indicating that the async subtask has started/returned.
676+
677+
Other example asynchronous lowered signatures:
678+
668679
| WIT function type | Async ABI |
669680
| ----------------------------------------- | --------------------- |
670681
| `func()` | `(func (result i32))` |
671-
| `func() -> string` | `(func (param $out i32) (result i32))` |
672-
| `func(s: string)` | `(func (param $in i32) (result i32))` |
673-
| `func(x: f32) -> f32` | `(func (param $in f32) (param $out i32) (result i32))` |
674-
| `func(x: list<list<u8>>) -> list<string>` | `(func (param $in i32) (param $out i32) (result i32))` |
682+
| `func() -> string` | `(func (param $out-ptr i32) (result i32))` |
683+
| `func(x: f32) -> f32` | `(func (param $x f32) (param $out-ptr i32) (result i32))` |
684+
| `func(s: string, t: string)` | `(func (param $s-ptr i32) (param $s-len i32) (result $t-ptr i32) (param $t-len i32) (result i32))` |
675685

676686
`future` and `stream` can appear anywhere in the parameter or result types. For example:
677687
```wit
@@ -689,11 +699,11 @@ the synchronous ABI has signature:
689699
```
690700
and the asynchronous ABI has the signature:
691701
```wat
692-
(func (param $in i32) (param $out i32) (result i32))
702+
(func (param $f i32) (param $out-ptr i32) (result i32))
693703
```
694-
where, according to the above rules, `$in` is the index of a future in the
695-
current component instance's table (not a pointer to one) while `$out` is a
696-
pointer to a linear memory location that will receive an `i32` index.
704+
where `$f` is the index of a future (not a pointer to one) while while
705+
`$out-ptr` is a pointer to a linear memory location that will receive an `i32`
706+
index.
697707

698708
For the runtime semantics of this `i32` index, see `lift_stream`,
699709
`lift_future`, `lower_stream` and `lower_future` in the [Canonical ABI
@@ -786,7 +796,7 @@ replaced with `...` to focus on the overall flow of function calls.
786796
(core module $Main
787797
(import "libc" "mem" (memory 1))
788798
(import "libc" "realloc" (func (param i32 i32 i32 i32) (result i32)))
789-
(import "" "fetch" (func $fetch (param i32 i32) (result i32)))
799+
(import "" "fetch" (func $fetch (param i32 i32 i32) (result i32)))
790800
(import "" "waitable-set.new" (func $new_waitable_set (result i32)))
791801
(import "" "waitable-set.wait" (func $wait (param i32 i32) (result i32)))
792802
(import "" "waitable.join" (func $join (param i32 i32)))
@@ -800,7 +810,7 @@ replaced with `...` to focus on the overall flow of function calls.
800810
...
801811
loop
802812
...
803-
call $fetch ;; pass a pointer-to-string and pointer-to-list-of-bytes outparam
813+
call $fetch ;; pass a string pointer, string length and pointer-to-list-of-bytes outparam
804814
... ;; ... and receive the index of a new async subtask
805815
global.get $wsi
806816
call $join ;; ... and add it to the waitable set
@@ -878,7 +888,7 @@ not externally-visible behavior.
878888
(core module $Main
879889
(import "libc" "mem" (memory 1))
880890
(import "libc" "realloc" (func (param i32 i32 i32 i32) (result i32)))
881-
(import "" "fetch" (func $fetch (param i32 i32) (result i32)))
891+
(import "" "fetch" (func $fetch (param i32 i32 i32) (result i32)))
882892
(import "" "waitable-set.new" (func $new_waitable_set (result i32)))
883893
(import "" "waitable.join" (func $join (param i32 i32)))
884894
(import "" "task.return" (func $task_return (param i32 i32)))
@@ -891,7 +901,7 @@ not externally-visible behavior.
891901
...
892902
loop
893903
...
894-
call $fetch ;; pass a pointer-to-string and pointer-to-list-of-bytes outparam
904+
call $fetch ;; pass a string pointer, string length and pointer-to-list-of-bytes outparam
895905
... ;; ... and receive the index of a new async subtask
896906
global.get $wsi
897907
call $join ;; ... and add it to the waitable set

design/mvp/CanonicalABI.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2446,6 +2446,7 @@ stack), passing in an `i32` pointer as an parameter instead of returning an
24462446
Given all this, the top-level definition of `flatten_functype` is:
24472447
```python
24482448
MAX_FLAT_PARAMS = 16
2449+
MAX_FLAT_ASYNC_PARAMS = 4
24492450
MAX_FLAT_RESULTS = 1
24502451

24512452
def flatten_functype(opts, ft, context):
@@ -2470,7 +2471,7 @@ def flatten_functype(opts, ft, context):
24702471
else:
24712472
flat_results = []
24722473
case 'lower':
2473-
if len(flat_params) > 1:
2474+
if len(flat_params) > MAX_FLAT_ASYNC_PARAMS:
24742475
flat_params = ['i32']
24752476
if len(flat_results) > 0:
24762477
flat_params += ['i32']
@@ -3124,7 +3125,7 @@ always returns control flow back to the caller without blocking:
31243125
```python
31253126
def on_start():
31263127
on_progress()
3127-
return lift_flat_values(cx, 1, flat_args, ft.param_types())
3128+
return lift_flat_values(cx, MAX_FLAT_ASYNC_PARAMS, flat_args, ft.param_types())
31283129

31293130
def on_resolve(results):
31303131
on_progress()

design/mvp/canonical-abi/definitions.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1521,6 +1521,7 @@ def lower_async_value(ReadableEndT, cx, v, t):
15211521
### Flattening
15221522

15231523
MAX_FLAT_PARAMS = 16
1524+
MAX_FLAT_ASYNC_PARAMS = 4
15241525
MAX_FLAT_RESULTS = 1
15251526

15261527
def flatten_functype(opts, ft, context):
@@ -1545,7 +1546,7 @@ def flatten_functype(opts, ft, context):
15451546
else:
15461547
flat_results = []
15471548
case 'lower':
1548-
if len(flat_params) > 1:
1549+
if len(flat_params) > MAX_FLAT_ASYNC_PARAMS:
15491550
flat_params = ['i32']
15501551
if len(flat_results) > 0:
15511552
flat_params += ['i32']
@@ -1932,7 +1933,7 @@ def on_resolve(results):
19321933

19331934
def on_start():
19341935
on_progress()
1935-
return lift_flat_values(cx, 1, flat_args, ft.param_types())
1936+
return lift_flat_values(cx, MAX_FLAT_ASYNC_PARAMS, flat_args, ft.param_types())
19361937

19371938
def on_resolve(results):
19381939
on_progress()

design/mvp/canonical-abi/run_tests.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2231,6 +2231,44 @@ async def core_func(task, args):
22312231

22322232
await canon_lift(sync_opts, inst, ft, core_func, None, lambda:[], lambda _:(), host_on_block)
22332233

2234+
async def test_async_flat_params():
2235+
heap = Heap(1000)
2236+
opts = mk_opts(heap.memory, 'utf8', heap.realloc, sync = False)
2237+
inst = ComponentInstance()
2238+
caller = Task(opts, inst, FuncType([],[]), None, None, None)
2239+
2240+
ft1 = FuncType([F32Type(), F64Type(), U32Type(), S64Type()],[])
2241+
async def f1(task, on_start, on_resolve, on_block):
2242+
args = on_start()
2243+
assert(len(args) == 4)
2244+
assert(args[0] == 1.1)
2245+
assert(args[1] == 2.2)
2246+
assert(args[2] == 3)
2247+
assert(args[3] == 4)
2248+
on_resolve([])
2249+
[ret] = await canon_lower(opts, ft1, f1, caller, [1.1, 2.2, 3, 4])
2250+
assert(ret == Subtask.State.RETURNED)
2251+
2252+
ft2 = FuncType([U32Type(),U8Type(),U8Type(),U8Type()],[])
2253+
async def f2(task, on_start, on_resolve, on_block):
2254+
args = on_start()
2255+
assert(len(args) == 4)
2256+
assert(args == [1,2,3,4])
2257+
on_resolve([])
2258+
[ret] = await canon_lower(opts, ft2, f2, caller, [1,2,3,4])
2259+
assert(ret == Subtask.State.RETURNED)
2260+
2261+
ft3 = FuncType([U32Type(),U8Type(),U8Type(),U8Type(),U8Type()],[])
2262+
async def f3(task, on_start, on_resolve, on_block):
2263+
args = on_start()
2264+
assert(len(args) == 5)
2265+
assert(args == [1,2,3,4,5])
2266+
on_resolve([])
2267+
heap.memory[12:20] = b'\x01\x00\x00\x00\x02\x03\x04\x05'
2268+
[ret] = await canon_lower(opts, ft3, f3, caller, [12])
2269+
assert(ret == Subtask.State.RETURNED)
2270+
2271+
22342272
async def run_async_tests():
22352273
await test_roundtrips()
22362274
await test_handles()
@@ -2250,6 +2288,7 @@ async def run_async_tests():
22502288
await test_futures()
22512289
await test_cancel_subtask()
22522290
await test_self_empty()
2291+
await test_async_flat_params()
22532292

22542293
asyncio.run(run_async_tests())
22552294

0 commit comments

Comments
 (0)