Skip to content

Commit 996bed8

Browse files
committed
Extend number of flat parameters in async lower from 1 to 4
Closes #434
1 parent e1b9cb0 commit 996bed8

12 files changed

+126
-77
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
@@ -792,7 +802,7 @@ with `...` to focus on the overall flow of function calls:
792802
(core module $Main
793803
(import "libc" "mem" (memory 1))
794804
(import "libc" "realloc" (func (param i32 i32 i32 i32) (result i32)))
795-
(import "" "fetch" (func $fetch (param i32 i32) (result i32)))
805+
(import "" "fetch" (func $fetch (param i32 i32 i32) (result i32)))
796806
(import "" "waitable-set.new" (func $new_waitable_set (result i32)))
797807
(import "" "waitable-set.wait" (func $wait (param i32 i32) (result i32)))
798808
(import "" "waitable.join" (func $join (param i32 i32)))
@@ -806,7 +816,7 @@ with `...` to focus on the overall flow of function calls:
806816
...
807817
loop
808818
...
809-
call $fetch ;; pass a pointer-to-string and pointer-to-list-of-bytes outparam
819+
call $fetch ;; pass a string pointer, string length and pointer-to-list-of-bytes outparam
810820
... ;; ... and receive the index of a new async subtask
811821
global.get $wsi
812822
call $join ;; ... and add it to the waitable set
@@ -884,7 +894,7 @@ not externally-visible behavior.
884894
(core module $Main
885895
(import "libc" "mem" (memory 1))
886896
(import "libc" "realloc" (func (param i32 i32 i32 i32) (result i32)))
887-
(import "" "fetch" (func $fetch (param i32 i32) (result i32)))
897+
(import "" "fetch" (func $fetch (param i32 i32 i32) (result i32)))
888898
(import "" "waitable-set.new" (func $new_waitable_set (result i32)))
889899
(import "" "waitable.join" (func $join (param i32 i32)))
890900
(import "" "task.return" (func $task_return (param i32 i32)))
@@ -897,7 +907,7 @@ not externally-visible behavior.
897907
...
898908
loop
899909
...
900-
call $fetch ;; pass a pointer-to-string and pointer-to-list-of-bytes outparam
910+
call $fetch ;; pass a string pointer, string length and pointer-to-list-of-bytes outparam
901911
... ;; ... and receive the index of a new async subtask
902912
global.get $wsi
903913
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

test/async/async-calls-sync.wast

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,8 @@
119119
(import "" "waitable.join" (func $waitable.join (param i32 i32)))
120120
(import "" "waitable-set.new" (func $waitable-set.new (result i32)))
121121
(import "" "unblock" (func $unblock))
122-
(import "" "sync-func1" (func $sync-func1 (param i32 i32) (result i32)))
123-
(import "" "sync-func2" (func $sync-func2 (param i32 i32) (result i32)))
122+
(import "" "sync-func1" (func $sync-func1 (param i32) (result i32)))
123+
(import "" "sync-func2" (func $sync-func2 (param i32) (result i32)))
124124

125125
(global $ws (mut i32) (i32.const 0))
126126
(func $start (global.set $ws (call $waitable-set.new)))
@@ -134,22 +134,22 @@
134134
;; call 'sync-func1' and 'sync-func2' asynchronously, both of which will block
135135
;; (on $AsyncInner.blocking-call). because 'sync-func1/2' are in different instances,
136136
;; both calls will reach the STARTED state.
137-
(local.set $ret (call $sync-func1 (i32.const 0xdeadbeef) (i32.const 8)))
137+
(local.set $ret (call $sync-func1 (i32.const 8)))
138138
(if (i32.ne (i32.const 0x21 (; STARTED=1 | (subtask=2 << 4) ;)) (local.get $ret))
139139
(then unreachable))
140140
(call $waitable.join (i32.const 2) (global.get $ws))
141-
(local.set $ret (call $sync-func2 (i32.const 0xdeadbeef) (i32.const 12)))
141+
(local.set $ret (call $sync-func2 (i32.const 12)))
142142
(if (i32.ne (i32.const 0x31 (; STARTED=1 | (subtask=3 << 4) ;)) (local.get $ret))
143143
(then unreachable))
144144
(call $waitable.join (i32.const 3) (global.get $ws))
145145

146146
;; now start another pair of 'sync-func1/2' calls, both of which should see auto
147147
;; backpressure and get stuck in the STARTING state.
148-
(local.set $ret (call $sync-func1 (i32.const 0xdeadbeef) (i32.const 16)))
148+
(local.set $ret (call $sync-func1 (i32.const 16)))
149149
(if (i32.ne (i32.const 0x40 (; STARTING=0 | (subtask=4 << 4) ;)) (local.get $ret))
150150
(then unreachable))
151151
(call $waitable.join (i32.const 4) (global.get $ws))
152-
(local.set $ret (call $sync-func2 (i32.const 0xdeadbeef) (i32.const 20)))
152+
(local.set $ret (call $sync-func2 (i32.const 20)))
153153
(if (i32.ne (i32.const 0x50 (; STARTING=0 | (subtask=5 << 4) ;)) (local.get $ret))
154154
(then unreachable))
155155
(call $waitable.join (i32.const 5) (global.get $ws))

test/async/cancel-subtask.wast

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
(import "" "mem" (memory 1))
6060
(import "" "subtask.cancel" (func $subtask.cancel (param i32) (result i32)))
6161
(import "" "subtask.drop" (func $subtask.drop (param i32)))
62-
(import "" "f" (func $f (param i32 i32) (result i32)))
62+
(import "" "f" (func $f (param i32) (result i32)))
6363

6464
(func $run (export "run") (result i32)
6565
(local $ret i32) (local $retp i32)
@@ -69,7 +69,7 @@
6969
;; call 'f'; it should block
7070
(local.set $retp (i32.const 4))
7171
(i32.store (local.get $retp) (i32.const 0xbad0bad0))
72-
(local.set $ret (call $f (i32.const 0xdeadbeef) (local.get $retp)))
72+
(local.set $ret (call $f (local.get $retp)))
7373
(if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf)))
7474
(then unreachable))
7575
(local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4)))

test/async/deadlock.wast

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,11 @@
4040
(import "" "waitable.join" (func $waitable.join (param i32 i32)))
4141
(import "" "waitable-set.new" (func $waitable-set.new (result i32)))
4242
(import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32)))
43-
(import "" "f" (func $f (param i32 i32) (result i32)))
43+
(import "" "f" (func $f (param i32) (result i32)))
4444

4545
(func (export "g") (result i32)
4646
(local $ws i32) (local $ret i32) (local $subtaski i32)
47-
(local.set $ret (call $f (i32.const 0) (i32.const 0)))
47+
(local.set $ret (call $f (i32.const 0)))
4848
(local.set $subtaski (i32.shr_u (local.get $ret) (i32.const 4)))
4949
(local.set $ws (call $waitable-set.new))
5050
(call $waitable.join (local.get $subtaski) (local.get $ws))

test/async/drop-subtask.wast

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,14 +62,14 @@
6262
(import "" "waitable.join" (func $waitable.join (param i32 i32)))
6363
(import "" "waitable-set.new" (func $waitable-set.new (result i32)))
6464
(import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32)))
65-
(import "" "loop" (func $loop (param i32 i32) (result i32)))
65+
(import "" "loop" (func $loop (result i32)))
6666
(import "" "return" (func $return))
6767

6868
(func $drop-after-return (export "drop-after-return") (result i32)
6969
(local $ret i32) (local $ws i32) (local $subtask i32)
7070

7171
;; start 'loop'
72-
(local.set $ret (call $loop (i32.const 0xdead) (i32.const 0xbeef)))
72+
(local.set $ret (call $loop))
7373
(if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf)))
7474
(then unreachable))
7575
(local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4)))

test/async/drop-waitable-set.wast

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,14 @@
5151
(core instance $memory (instantiate $Memory))
5252
(core module $Core
5353
(import "" "mem" (memory 1))
54-
(import "" "wait-on-set" (func $wait-on-set (param i32 i32) (result i32)))
54+
(import "" "wait-on-set" (func $wait-on-set (result i32)))
5555
(import "" "drop-while-waiting" (func $drop-while-waiting))
5656
(func $run (export "run") (result i32)
5757
(local $ret i32)
5858

5959
;; start an async call to 'wait-on-set' which blocks, waiting on a
6060
;; waitable-set.
61-
(local.set $ret (call $wait-on-set (i32.const 0xdeadbeef) (i32.const 0xdeadbeef)))
61+
(local.set $ret (call $wait-on-set))
6262
(if (i32.ne (i32.const 0x11) (local.get $ret))
6363
(then unreachable))
6464

test/async/empty-wait.wast

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,8 @@
126126
(import "" "waitable-set.new" (func $waitable-set.new (result i32)))
127127
(import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32)))
128128
(import "" "subtask.drop" (func $subtask.drop (param i32)))
129-
(import "" "blocker" (func $blocker (param i32 i32) (result i32)))
130-
(import "" "unblocker" (func $unblocker (param i32 i32) (result i32)))
129+
(import "" "blocker" (func $blocker (param i32) (result i32)))
130+
(import "" "unblocker" (func $unblocker (param i32) (result i32)))
131131

132132
(global $ws (mut i32) (i32.const 0))
133133
(func $start (global.set $ws (call $waitable-set.new)))
@@ -140,7 +140,7 @@
140140

141141
;; call 'blocker'; it should block
142142
(local.set $retp1 (i32.const 4))
143-
(local.set $ret (call $blocker (i32.const 0xdeadbeef) (local.get $retp1)))
143+
(local.set $ret (call $blocker (local.get $retp1)))
144144
(if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf)))
145145
(then unreachable))
146146
(local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4)))
@@ -149,7 +149,7 @@
149149

150150
;; call 'unblocker' to unblock 'blocker'; it should complete eagerly
151151
(local.set $retp2 (i32.const 8))
152-
(local.set $ret (call $unblocker (i32.const 0xbeefdead) (local.get $retp2)))
152+
(local.set $ret (call $unblocker (local.get $retp2)))
153153
(if (i32.ne (i32.const 2 (; RETURNED ;)) (local.get $ret))
154154
(then unreachable))
155155
(if (i32.ne (i32.const 43) (i32.load (local.get $retp2)))

0 commit comments

Comments
 (0)