Skip to content

Commit b6be91e

Browse files
authored
Add some wast tests for async (#519)
1 parent deb0b0a commit b6be91e

16 files changed

+2158
-3
lines changed

design/mvp/Async.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -772,9 +772,15 @@ Canonical ABI Explainer.
772772

773773
## Examples
774774

775-
With that background, we can sketch the shape of an async component that lifts
776-
and lowers its imports and exports with `async`. The meat of this component is
777-
replaced with `...` to focus on the overall flow of function calls.
775+
For a list of working examples expressed as executable WebAssembly Test (WAST)
776+
files, see [this directory](../../test/async).
777+
778+
This rest of this section sketches the shape of a component that uses `async`
779+
to lift and lower its imports and exports with both the stackful and stackless
780+
ABI options.
781+
782+
Starting with the stackless ABI, the meat of this example component is replaced
783+
with `...` to focus on the overall flow of function calls:
778784
```wat
779785
(component
780786
(import "fetch" (func $fetch (param "url" string) (result (list u8))))

test/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Reference Tests
2+
3+
This directory contains Component Model reference tests, grouped by functionality.
4+
5+
## Running in Wasmtime
6+
7+
A single `.wast` test can be run via:
8+
```
9+
wasmtime wast -W component-model-async=y the-test.wast
10+
```

test/async/async-calls-sync.wast

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
;; This test contains 3 components, $AsyncInner, $SyncMiddle and $AsyncOuter,
2+
;; where there are two instances of $SyncMiddle that import a single instance
3+
;; of $AsyncInner, and $AsyncOuter imports all 3 preceding instances.
4+
;;
5+
;; $AsyncOuter.run asynchronously calls $SyncMiddle.sync-func twice concurrently
6+
;; in each instance (4 total calls), hitting the synchronous backpressure case
7+
;; in 2 of the 4 calls.
8+
;;
9+
;; $SyncMiddle.sync-func makes a blocking call to $AsyncInner.blocking-call
10+
;; which is used to emulate a host call that blocks until $AsyncOuter.run
11+
;; calls $AsyncInner.unblock to unblock all the 'blocking-call' calls.
12+
(component
13+
(component $AsyncInner
14+
(core module $CoreAsyncInner
15+
(import "" "context.set" (func $context.set (param i32)))
16+
(import "" "context.get" (func $context.get (result i32)))
17+
(import "" "task.return0" (func $task.return0))
18+
(import "" "task.return1" (func $task.return1 (param i32)))
19+
20+
(memory 1)
21+
(global $blocked (mut i32) (i32.const 1))
22+
(global $counter (mut i32) (i32.const 2))
23+
24+
;; 'blocking-call' cooperatively "spin-waits" until $blocked is 0.
25+
(func $blocking-call (export "blocking-call") (result i32)
26+
(call $context.set (global.get $counter))
27+
(global.set $counter (i32.add (i32.const 1) (global.get $counter)))
28+
(i32.const 1 (; YIELD ;))
29+
)
30+
(func $blocking-call-cb (export "blocking-call-cb") (param i32 i32 i32) (result i32)
31+
(if (i32.eqz (global.get $blocked)) (then
32+
(call $task.return1 (call $context.get))
33+
(return (i32.const 0 (; EXIT ;)))
34+
))
35+
(i32.const 1 (; YIELD ;))
36+
)
37+
(func $unblock (export "unblock") (result i32)
38+
(global.set $blocked (i32.const 0))
39+
(call $task.return0)
40+
(i32.const 0 (; EXIT ;))
41+
)
42+
(func $unblock-cb (export "unblock-cb") (param i32 i32 i32) (result i32)
43+
unreachable
44+
)
45+
)
46+
(canon task.return (core func $task.return0))
47+
(canon task.return (result u32) (core func $task.return1))
48+
(canon context.set i32 0 (core func $context.set))
49+
(canon context.get i32 0 (core func $context.get))
50+
(core instance $core_async_inner (instantiate $CoreAsyncInner (with "" (instance
51+
(export "task.return0" (func $task.return0))
52+
(export "task.return1" (func $task.return1))
53+
(export "context.set" (func $context.set))
54+
(export "context.get" (func $context.get))
55+
))))
56+
(func (export "blocking-call") (result u32) (canon lift
57+
(core func $core_async_inner "blocking-call")
58+
async (callback (func $core_async_inner "blocking-call-cb"))
59+
))
60+
(func (export "unblock") (canon lift
61+
(core func $core_async_inner "unblock")
62+
async (callback (func $core_async_inner "unblock-cb"))
63+
))
64+
)
65+
66+
(component $SyncMiddle
67+
(import "blocking-call" (func $blocking-call (result u32)))
68+
(core module $CoreSyncMiddle
69+
(import "" "blocking-call" (func $blocking-call (result i32)))
70+
(func $sync-func (export "sync-func") (result i32)
71+
(call $blocking-call)
72+
)
73+
)
74+
(canon lower (func $blocking-call) (core func $blocking-call'))
75+
(core instance $core_sync_middle (instantiate $CoreSyncMiddle (with "" (instance
76+
(export "blocking-call" (func $blocking-call'))
77+
))))
78+
(func (export "sync-func") (result u32) (canon lift
79+
(core func $core_sync_middle "sync-func")
80+
))
81+
)
82+
83+
(component $AsyncMiddle
84+
(import "blocking-call" (func $blocking-call (result u32)))
85+
(core module $CoreSyncMiddle
86+
(import "" "task.return" (func $task.return (param i32)))
87+
(import "" "blocking-call" (func $blocking-call (result i32)))
88+
(func $sync-func (export "sync-func") (result i32)
89+
(call $task.return (call $blocking-call))
90+
(i32.const 0 (; EXIT ;))
91+
)
92+
(func $sync-func-cb (export "sync-func-cb") (param i32 i32 i32) (result i32)
93+
unreachable
94+
)
95+
)
96+
(canon task.return (result u32) (core func $task.return))
97+
(canon lower (func $blocking-call) (core func $blocking-call'))
98+
(core instance $core_sync_middle (instantiate $CoreSyncMiddle (with "" (instance
99+
(export "task.return" (func $task.return))
100+
(export "blocking-call" (func $blocking-call'))
101+
))))
102+
(func (export "sync-func") (result u32) (canon lift
103+
(core func $core_sync_middle "sync-func")
104+
async (callback (func $core_sync_middle "sync-func-cb"))
105+
))
106+
)
107+
108+
(component $AsyncOuter
109+
(import "unblock" (func $unblock))
110+
(import "sync-func1" (func $sync-func1 (result u32)))
111+
(import "sync-func2" (func $sync-func2 (result u32)))
112+
113+
(core module $Memory (memory (export "mem") 1))
114+
(core instance $memory (instantiate $Memory))
115+
(core module $CoreAsyncOuter
116+
(import "" "mem" (memory 1))
117+
(import "" "task.return" (func $task.return (param i32)))
118+
(import "" "subtask.drop" (func $subtask.drop (param i32)))
119+
(import "" "waitable.join" (func $waitable.join (param i32 i32)))
120+
(import "" "waitable-set.new" (func $waitable-set.new (result i32)))
121+
(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)))
124+
125+
(global $ws (mut i32) (i32.const 0))
126+
(func $start (global.set $ws (call $waitable-set.new)))
127+
(start $start)
128+
129+
(global $remain (mut i32) (i32.const -1))
130+
131+
(func $run (export "run") (result i32)
132+
(local $ret i32)
133+
134+
;; call 'sync-func1' and 'sync-func2' asynchronously, both of which will block
135+
;; (on $AsyncInner.blocking-call). because 'sync-func1/2' are in different instances,
136+
;; both calls will reach the STARTED state.
137+
(local.set $ret (call $sync-func1 (i32.const 0xdeadbeef) (i32.const 8)))
138+
(if (i32.ne (i32.const 0x21 (; STARTED=1 | (subtask=2 << 4) ;)) (local.get $ret))
139+
(then unreachable))
140+
(call $waitable.join (i32.const 2) (global.get $ws))
141+
(local.set $ret (call $sync-func2 (i32.const 0xdeadbeef) (i32.const 12)))
142+
(if (i32.ne (i32.const 0x31 (; STARTED=1 | (subtask=3 << 4) ;)) (local.get $ret))
143+
(then unreachable))
144+
(call $waitable.join (i32.const 3) (global.get $ws))
145+
146+
;; now start another pair of 'sync-func1/2' calls, both of which should see auto
147+
;; backpressure and get stuck in the STARTING state.
148+
(local.set $ret (call $sync-func1 (i32.const 0xdeadbeef) (i32.const 16)))
149+
(if (i32.ne (i32.const 0x40 (; STARTING=0 | (subtask=4 << 4) ;)) (local.get $ret))
150+
(then unreachable))
151+
(call $waitable.join (i32.const 4) (global.get $ws))
152+
(local.set $ret (call $sync-func2 (i32.const 0xdeadbeef) (i32.const 20)))
153+
(if (i32.ne (i32.const 0x50 (; STARTING=0 | (subtask=5 << 4) ;)) (local.get $ret))
154+
(then unreachable))
155+
(call $waitable.join (i32.const 5) (global.get $ws))
156+
157+
;; this POLL should return that nothing is ready
158+
(i32.or (i32.const 3 (; POLL ;)) (i32.shl (global.get $ws) (i32.const 4)))
159+
)
160+
(func $run-cb (export "run-cb") (param $event_code i32) (param $index i32) (param $payload i32) (result i32)
161+
(local $ret i32)
162+
163+
;; $remain is initially -1, so confirm that POLL found nothing was ready and then
164+
;; unblock all the subtasks and set $remain to 4 to count how many to wait for.
165+
(if (i32.eq (global.get $remain) (i32.const -1)) (then
166+
(if (i32.ne (local.get $event_code) (i32.const 0 (; NONE ;)))
167+
(then unreachable))
168+
(call $unblock)
169+
(global.set $remain (i32.const 4))
170+
(return (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (global.get $ws) (i32.const 4))))
171+
))
172+
173+
;; confirm we only receive SUBTASK events after the first NONE event.
174+
(if (i32.ne (local.get $event_code) (i32.const 1 (; SUBTASK ;)))
175+
(then unreachable))
176+
177+
;; if we receive a SUBTASK STARTED event, it should only be for the 3rd or
178+
;; 4th subtask (at indices 4/5, resp), so keep waiting for completion
179+
(if (i32.eq (local.get $payload) (i32.const 1 (; STARTED ;))) (then
180+
(if (i32.and
181+
(i32.ne (local.get $index) (i32.const 4))
182+
(i32.ne (local.get $index) (i32.const 5)))
183+
(then unreachable))
184+
(return (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (global.get $ws) (i32.const 4))))
185+
))
186+
187+
;; when we receive a SUBTASK RETURNED event, check the return value is equal to the
188+
;; subtask index (which we've ensured by having $AsyncInner.$counter start at 2, the
189+
;; first subtask index. The address of the return buffer is the index*4.
190+
(if (i32.ne (local.get $payload) (i32.const 2 (; RETURNED ;)))
191+
(then unreachable))
192+
(if (i32.ne (local.get $index) (i32.load (i32.mul (local.get $index) (i32.const 4))))
193+
(then unreachable))
194+
195+
;; decrement $remain and exit if 0
196+
(call $subtask.drop (local.get $index))
197+
(global.set $remain (i32.sub (global.get $remain) (i32.const 1)))
198+
(if (i32.gt_u (global.get $remain) (i32.const 0)) (then
199+
(return (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (global.get $ws) (i32.const 4))))
200+
))
201+
(call $task.return (i32.const 42))
202+
(i32.const 0 (; EXIT ;))
203+
)
204+
)
205+
(canon task.return (result u32) (core func $task.return))
206+
(canon subtask.drop (core func $subtask.drop))
207+
(canon waitable.join (core func $waitable.join))
208+
(canon waitable-set.new (core func $waitable-set.new))
209+
(canon lower (func $unblock) (core func $unblock))
210+
(canon lower (func $sync-func1) async (memory $memory "mem") (core func $sync-func1'))
211+
(canon lower (func $sync-func2) async (memory $memory "mem") (core func $sync-func2'))
212+
(core instance $em (instantiate $CoreAsyncOuter (with "" (instance
213+
(export "mem" (memory $memory "mem"))
214+
(export "task.return" (func $task.return))
215+
(export "subtask.drop" (func $subtask.drop))
216+
(export "waitable.join" (func $waitable.join))
217+
(export "waitable-set.new" (func $waitable-set.new))
218+
(export "unblock" (func $unblock))
219+
(export "sync-func1" (func $sync-func1'))
220+
(export "sync-func2" (func $sync-func2'))
221+
))))
222+
(func (export "run") (result u32) (canon lift
223+
(core func $em "run")
224+
async (callback (func $em "run-cb"))
225+
))
226+
)
227+
228+
;; run1 uses $SyncMiddle
229+
(instance $async_inner1 (instantiate $AsyncInner))
230+
(instance $sync_middle11 (instantiate $SyncMiddle
231+
(with "blocking-call" (func $async_inner1 "blocking-call"))
232+
))
233+
(instance $sync_middle12 (instantiate $SyncMiddle
234+
(with "blocking-call" (func $async_inner1 "blocking-call"))
235+
))
236+
(instance $async_outer1 (instantiate $AsyncOuter
237+
(with "unblock" (func $async_inner1 "unblock"))
238+
(with "sync-func1" (func $sync_middle11 "sync-func"))
239+
(with "sync-func2" (func $sync_middle12 "sync-func"))
240+
))
241+
(func (export "run1") (alias export $async_outer1 "run"))
242+
243+
;; run2 uses $AsyncMiddle
244+
(instance $async_inner2 (instantiate $AsyncInner))
245+
(instance $sync_middle21 (instantiate $SyncMiddle
246+
(with "blocking-call" (func $async_inner2 "blocking-call"))
247+
))
248+
(instance $sync_middle22 (instantiate $AsyncMiddle
249+
(with "blocking-call" (func $async_inner2 "blocking-call"))
250+
))
251+
(instance $async_outer2 (instantiate $AsyncOuter
252+
(with "unblock" (func $async_inner2 "unblock"))
253+
(with "sync-func1" (func $sync_middle21 "sync-func"))
254+
(with "sync-func2" (func $sync_middle22 "sync-func"))
255+
))
256+
(func (export "run2") (alias export $async_outer2 "run"))
257+
)
258+
(assert_return (invoke "run1") (u32.const 42))
259+
(assert_return (invoke "run2") (u32.const 42))

0 commit comments

Comments
 (0)