Skip to content

Commit 41f9bf0

Browse files
committed
Remove yielding behavior of waitable-set.poll to allow it to be called from synchronous functions
1 parent c0482c3 commit 41f9bf0

File tree

4 files changed

+54
-84
lines changed

4 files changed

+54
-84
lines changed

design/mvp/CanonicalABI.md

Lines changed: 21 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,31 +1042,10 @@ trap if another task tries to drop the waitable set being used.
10421042
return event
10431043
```
10441044

1045-
The `Task.poll_until` method is called by `canon_waitable_set_poll` and from
1046-
the event loop in `canon_lift` when `CallbackCode.POLL` is returned. Unlike
1047-
`wait_until`, `poll_until` does not wait for the given waitable set to have a
1048-
pending event, returning `EventCode.NONE` if there is none already. However,
1049-
`poll_until` *does* call `suspsend_until` to allow the runtime to
1050-
nondeterministically switch to another task (or not).
1051-
```python
1052-
def poll_until(self, ready_func, thread, wset, cancellable) -> Optional[EventTuple]:
1053-
assert(thread in self.threads and thread.task is self)
1054-
wset.num_waiting += 1
1055-
match self.suspend_until(ready_func, thread, cancellable):
1056-
case SuspendResult.CANCELLED:
1057-
event = (EventCode.TASK_CANCELLED, 0, 0)
1058-
case SuspendResult.NOT_CANCELLED:
1059-
if wset.has_pending_event():
1060-
event = wset.get_pending_event()
1061-
else:
1062-
event = (EventCode.NONE, 0, 0)
1063-
wset.num_waiting -= 1
1064-
return event
1065-
```
1066-
10671045
The `Task.yield_until` method is called by `canon_thread_yield` and from
1068-
the event loop in `canon_lift` when `CallbackCode.YIELD` is returned.
1069-
`yield_until` works like `poll_until` if given a fresh empty waitable set.
1046+
the event loop in `canon_lift` when `CallbackCode.YIELD` is returned and
1047+
calls `suspend_until` to allow the runtime to nondeterministically switch to
1048+
another task (or not).
10701049
```python
10711050
def yield_until(self, ready_func, thread, cancellable) -> EventTuple:
10721051
assert(thread in self.threads and thread.task is self)
@@ -3277,11 +3256,8 @@ function (specified as a `funcidx` immediate in `canon lift`) until the
32773256
wset = inst.table.get(si)
32783257
trap_if(not isinstance(wset, WaitableSet))
32793258
event = task.wait_until(lambda: not inst.exclusive, thread, wset, cancellable = True)
3280-
case CallbackCode.POLL:
3281-
trap_if(not task.may_block())
3282-
wset = inst.table.get(si)
3283-
trap_if(not isinstance(wset, WaitableSet))
3284-
event = task.poll_until(lambda: not inst.exclusive, thread, wset, cancellable = True)
3259+
case _:
3260+
trap()
32853261
thread.in_event_loop = False
32863262
inst.exclusive = True
32873263
event_code, p1, p2 = event
@@ -3290,11 +3266,11 @@ function (specified as a `funcidx` immediate in `canon lift`) until the
32903266
task.exit()
32913267
return
32923268
```
3293-
The `Task.{wait,poll,yield}_until` methods called by the event loop are the
3294-
same methods called by the `yield`, `waitable-set.wait` and `waitable-set.poll`
3295-
built-ins. Thus, the main difference between stackful and stackless async is
3296-
whether these suspending operations are performed from an empty or non-empty
3297-
core wasm callstack (with the former allowing additional engine optimization).
3269+
The `Task.{wait,yield}_until` methods called by the event loop are the same
3270+
methods called by the `yield` and `waitable-set.wait` built-ins. Thus, the
3271+
main difference between stackful and stackless async is whether these
3272+
suspending operations are performed from an empty or non-empty core wasm
3273+
callstack (with the former allowing additional engine optimization).
32983274

32993275
If a `Task` is not allowed to block (because it was created for a non-`async`-
33003276
typed function call and has not yet returned a value), `YIELD` is always a
@@ -3336,8 +3312,7 @@ class CallbackCode(IntEnum):
33363312
EXIT = 0
33373313
YIELD = 1
33383314
WAIT = 2
3339-
POLL = 3
3340-
MAX = 3
3315+
MAX = 2
33413316

33423317
def unpack_callback_result(packed):
33433318
code = packed & 0xf
@@ -3347,7 +3322,7 @@ def unpack_callback_result(packed):
33473322
waitable_set_index = packed >> 4
33483323
return (CallbackCode(code), waitable_set_index)
33493324
```
3350-
The ability to asynchronously wait, poll, yield and exit is thus available to
3325+
The ability to asynchronously wait, yield and exit is thus available to
33513326
both the `callback` and non-`callback` cases, making `callback` just an
33523327
optimization to avoid allocating stacks for async languages that have avoided
33533328
the need for stackful coroutines by design (e.g., `async`/`await` in JS,
@@ -3878,26 +3853,22 @@ validation specifies:
38783853
* `$f` is given type `(func (param $si i32) (param $ptr i32) (result i32))`
38793854
* 🚟 - `cancellable` is allowed (otherwise it must be absent)
38803855

3881-
Calling `$f` invokes the following function, which returns `NONE` (`0`) instead
3882-
of blocking if there is no event available, and otherwise returns the event the
3883-
same way as `wait`.
3856+
Calling `$f` invokes the following function, which either returns an event that
3857+
was pending on one of the waitables in the given waitable set (the same way as
3858+
`waitable-set.wait`) or, if there is none, returns `0`.
38843859
```python
38853860
def canon_waitable_set_poll(cancellable, mem, thread, si, ptr):
38863861
trap_if(not thread.task.inst.may_leave)
3887-
trap_if(not thread.task.may_block())
38883862
wset = thread.task.inst.table.get(si)
38893863
trap_if(not isinstance(wset, WaitableSet))
3890-
event = thread.task.poll_until(lambda: True, thread, wset, cancellable)
3864+
if thread.task.deliver_pending_cancel(cancellable):
3865+
event = (EventCode.TASK_CANCELLED, 0, 0)
3866+
elif not wset.has_pending_event():
3867+
event = (EventCode.NONE, 0, 0)
3868+
else:
3869+
event = wset.get_pending_event()
38913870
return unpack_event(mem, thread, ptr, event)
38923871
```
3893-
Even though `waitable-set.poll` doesn't block until the given waitable set has
3894-
a pending event, `poll_until` does transitively perform a `Thread.suspend`
3895-
which allows the embedder to nondeterministically switch to executing another
3896-
task (like `thread.yield`). To avoid encouraging spin-waiting and to support
3897-
hosts like browsers that require returning to the event loop for async I/O to
3898-
resolve, a non-`async`-typed function export that has not yet returned a value
3899-
unconditionally traps if it transitively attempts to call `poll`.
3900-
39013872
If `cancellable` is set, then `waitable-set.poll` will return whether the
39023873
supertask has already or concurrently requested cancellation.
39033874
`waitable-set.poll` (and other cancellable operations) will only indicate

design/mvp/canonical-abi/definitions.py

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -651,20 +651,6 @@ def ready_and_has_event():
651651
wset.num_waiting -= 1
652652
return event
653653

654-
def poll_until(self, ready_func, thread, wset, cancellable) -> Optional[EventTuple]:
655-
assert(thread in self.threads and thread.task is self)
656-
wset.num_waiting += 1
657-
match self.suspend_until(ready_func, thread, cancellable):
658-
case SuspendResult.CANCELLED:
659-
event = (EventCode.TASK_CANCELLED, 0, 0)
660-
case SuspendResult.NOT_CANCELLED:
661-
if wset.has_pending_event():
662-
event = wset.get_pending_event()
663-
else:
664-
event = (EventCode.NONE, 0, 0)
665-
wset.num_waiting -= 1
666-
return event
667-
668654
def yield_until(self, ready_func, thread, cancellable) -> EventTuple:
669655
assert(thread in self.threads and thread.task is self)
670656
match self.suspend_until(ready_func, thread, cancellable):
@@ -2042,11 +2028,8 @@ def thread_func(thread):
20422028
wset = inst.table.get(si)
20432029
trap_if(not isinstance(wset, WaitableSet))
20442030
event = task.wait_until(lambda: not inst.exclusive, thread, wset, cancellable = True)
2045-
case CallbackCode.POLL:
2046-
trap_if(not task.may_block())
2047-
wset = inst.table.get(si)
2048-
trap_if(not isinstance(wset, WaitableSet))
2049-
event = task.poll_until(lambda: not inst.exclusive, thread, wset, cancellable = True)
2031+
case _:
2032+
trap()
20502033
thread.in_event_loop = False
20512034
inst.exclusive = True
20522035
event_code, p1, p2 = event
@@ -2063,8 +2046,7 @@ class CallbackCode(IntEnum):
20632046
EXIT = 0
20642047
YIELD = 1
20652048
WAIT = 2
2066-
POLL = 3
2067-
MAX = 3
2049+
MAX = 2
20682050

20692051
def unpack_callback_result(packed):
20702052
code = packed & 0xf
@@ -2278,10 +2260,14 @@ def unpack_event(mem, thread, ptr, e: EventTuple):
22782260

22792261
def canon_waitable_set_poll(cancellable, mem, thread, si, ptr):
22802262
trap_if(not thread.task.inst.may_leave)
2281-
trap_if(not thread.task.may_block())
22822263
wset = thread.task.inst.table.get(si)
22832264
trap_if(not isinstance(wset, WaitableSet))
2284-
event = thread.task.poll_until(lambda: True, thread, wset, cancellable)
2265+
if thread.task.deliver_pending_cancel(cancellable):
2266+
event = (EventCode.TASK_CANCELLED, 0, 0)
2267+
elif not wset.has_pending_event():
2268+
event = (EventCode.NONE, 0, 0)
2269+
else:
2270+
event = wset.get_pending_event()
22852271
return unpack_event(mem, thread, ptr, event)
22862272

22872273
### 🔀 `canon waitable-set.drop`

design/mvp/canonical-abi/run_tests.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,6 +1031,8 @@ def consumer(thread, args):
10311031

10321032
remain = [subi1, subi2]
10331033
while remain:
1034+
[ret] = canon_thread_yield(True, thread)
1035+
assert(ret == 0)
10341036
retp = 8
10351037
[event] = canon_waitable_set_poll(True, consumer_heap.memory, thread, seti, retp)
10361038
if event == EventCode.NONE:

test/async/trap-if-block-and-sync.wast

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -184,13 +184,20 @@
184184
(i32.const 2 (; WAIT ;))
185185
(i32.shl (call $waitable-set.new) (i32.const 4)))
186186
)
187-
(func (export "trap-if-poll")
188-
(call $waitable-set.poll (call $waitable-set.new) (i32.const 0xdeadbeef))
189-
unreachable
187+
(func (export "poll-is-fine") (result i32)
188+
(local $ret i32)
189+
(local.set $ret (call $waitable-set.poll (call $waitable-set.new) (i32.const 0)))
190+
(if (i32.ne (i32.const 0 (; NONE ;)) (local.get $ret))
191+
(then unreachable))
192+
(if (i32.ne (i32.const 0) (i32.load (i32.const 0)))
193+
(then unreachable))
194+
(if (i32.ne (i32.const 0) (i32.load (i32.const 4)))
195+
(then unreachable))
196+
(i32.const 42)
190197
)
191-
(func (export "trap-if-poll-cb") (result i32)
198+
(func (export "trap-if-invalid-callback-code") (param $invalid-code i32) (result i32)
192199
(i32.or
193-
(i32.const 3 (; POLL ;))
200+
(local.get $invalid-code)
194201
(i32.shl (call $waitable-set.new) (i32.const 4)))
195202
)
196203
(func (export "yield-is-fine") (result i32)
@@ -288,8 +295,8 @@
288295
(func (export "trap-if-suspend") (canon lift (core func $core "trap-if-suspend")))
289296
(func (export "trap-if-wait") (canon lift (core func $core "trap-if-wait")))
290297
(func (export "trap-if-wait-cb") (canon lift (core func $core "trap-if-wait-cb") async (callback (func $core "unreachable-cb"))))
291-
(func (export "trap-if-poll") (canon lift (core func $core "trap-if-poll")))
292-
(func (export "trap-if-poll-cb") (canon lift (core func $core "trap-if-poll-cb") async (callback (func $core "unreachable-cb"))))
298+
(func (export "poll-is-fine") (result u32) (canon lift (core func $core "poll-is-fine")))
299+
(func (export "trap-if-invalid-callback-code") (param "invalid-code" u32) (canon lift (core func $core "trap-if-invalid-callback-code") async (callback (func $core "unreachable-cb"))))
293300
(func (export "yield-is-fine") (result u32) (canon lift (core func $core "yield-is-fine")))
294301
(func (export "yield-is-fine-cb") (result u32) (canon lift (core func $core "yield-is-fine-cb") async (callback (func $core "return-42-cb"))))
295302
(func (export "trap-if-sync-call-async1") (canon lift (core func $core "trap-if-sync-call-async1")))
@@ -312,8 +319,8 @@
312319
(func (export "trap-if-suspend") (alias export $d "trap-if-suspend"))
313320
(func (export "trap-if-wait") (alias export $d "trap-if-wait"))
314321
(func (export "trap-if-wait-cb") (alias export $d "trap-if-wait-cb"))
315-
(func (export "trap-if-poll") (alias export $d "trap-if-poll"))
316-
(func (export "trap-if-poll-cb") (alias export $d "trap-if-poll-cb"))
322+
(func (export "poll-is-fine") (alias export $d "poll-is-fine"))
323+
(func (export "trap-if-invalid-callback-code") (alias export $d "trap-if-invalid-callback-code"))
317324
(func (export "yield-is-fine") (alias export $d "yield-is-fine"))
318325
(func (export "yield-is-fine-cb") (alias export $d "yield-is-fine-cb"))
319326
(func (export "trap-if-sync-cancel") (alias export $d "trap-if-sync-cancel"))
@@ -341,9 +348,13 @@
341348
(component instance $i $Tester)
342349
(assert_trap (invoke "trap-if-wait-cb") "cannot block a synchronous task before returning")
343350
(component instance $i $Tester)
344-
(assert_trap (invoke "trap-if-poll") "cannot block a synchronous task before returning")
351+
(assert_return (invoke "poll-is-fine") (u32.const 42))
352+
(component instance $i $Tester)
353+
(assert_trap (invoke "trap-if-invalid-callback-code" (u32.const 3)) "unsupported callback code: 3")
354+
(component instance $i $Tester)
355+
(assert_trap (invoke "trap-if-invalid-callback-code" (u32.const 4)) "unsupported callback code: 4")
345356
(component instance $i $Tester)
346-
(assert_trap (invoke "trap-if-poll-cb") "cannot block a synchronous task before returning")
357+
(assert_trap (invoke "trap-if-invalid-callback-code" (u32.const 15)) "unsupported callback code: 15")
347358
(component instance $i $Tester)
348359
(assert_return (invoke "yield-is-fine") (u32.const 42))
349360
(component instance $i $Tester)

0 commit comments

Comments
 (0)