Skip to content

Commit 7479890

Browse files
authored
Make 'async' part of the function type, not a hint (#578)
* Make 'async' part of the function type, not a hint * Add and update tests * Remove yielding behavior of waitable-set.poll to allow it to be called from synchronous functions * Also prohibit blocking during the start function
1 parent 0352151 commit 7479890

22 files changed

+1277
-548
lines changed

design/mvp/Binary.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ valtype ::= i:<typeidx> => i
216216
resourcetype ::= 0x3f 0x7f f?:<funcidx>? => (resource (rep i32) (dtor f)?)
217217
| 0x3e 0x7f f:<funcidx> cb?:<funcidx>? => (resource (rep i32) (dtor async f (callback cb)?)) 🚝
218218
functype ::= 0x40 ps:<paramlist> rs:<resultlist> => (func ps rs)
219+
| 0x43 ps:<paramlist> rs:<resultlist> => (func async ps rs)
219220
paramlist ::= lt*:vec(<labelvaltype>) => (param lt)*
220221
resultlist ::= 0x00 t:<valtype> => (result t)
221222
| 0x01 0x00 =>
@@ -288,7 +289,6 @@ canon ::= 0x00 0x00 f:<core:funcidx> opts:<opts> ft:<typeidx> => (canon lift
288289
| 0x01 0x00 f:<funcidx> opts:<opts> => (canon lower f opts (core func))
289290
| 0x02 rt:<typeidx> => (canon resource.new rt (core func))
290291
| 0x03 rt:<typeidx> => (canon resource.drop rt (core func))
291-
| 0x07 rt:<typeidx> => (canon resource.drop rt async (core func)) 🚝
292292
| 0x04 rt:<typeidx> => (canon resource.rep rt (core func))
293293
| 0x08 => (canon backpressure.set (core func)) 🔀✕
294294
| 0x24 => (canon backpressure.inc (core func)) 🔀
@@ -515,7 +515,8 @@ named once.
515515

516516
* The opcodes (for types, canon built-ins, etc) should be re-sorted
517517
* The two `depname` cases should be merged into one (`dep=<...>`)
518-
* The two `list` type codes should be merged into one with an optional immediate.
518+
* The two `list` type codes should be merged into one with an optional immediate
519+
and similarly for `func`.
519520
* The `0x00` variant of `importname'` and `exportname'` will be removed. Any
520521
remaining variant(s) will be renumbered or the prefix byte will be removed or
521522
repurposed.

design/mvp/CanonicalABI.md

Lines changed: 139 additions & 84 deletions
Large diffs are not rendered by default.

design/mvp/Concurrency.md

Lines changed: 262 additions & 192 deletions
Large diffs are not rendered by default.

design/mvp/Explainer.md

Lines changed: 69 additions & 80 deletions
Large diffs are not rendered by default.

design/mvp/WIT.md

Lines changed: 7 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1450,16 +1450,11 @@ named-type-list ::= ϵ
14501450
named-type ::= id ':' ty
14511451
```
14521452

1453-
The optional `async` hint in a WIT function type indicates that the callee
1454-
is expected to block and thus the caller should emit whatever asynchronous
1455-
language bindings are appropriate (e.g., in JS, Python, C# or Rust, an `async`
1456-
WIT function would emit an `async` JS/Python/C#/Rust function). Because `async`
1457-
is just a hint and not enforced by the runtime, it is technically possible for
1458-
a non-`async` callee to block. In that case, though, it is the *callee's* fault
1459-
for any resultant loss of concurrency, not the caller's. Thus, `async` is
1460-
primarily intended to document expectations in a way that can be taken
1461-
advantage of by bindings generators. (For more details, see the [concurrency
1462-
explainer](Concurrency.md).)
1453+
The optional `async` prefix in a WIT function type indicates that the callee
1454+
may block and thus the caller should use the async ABI and asynchronous
1455+
source-language bindings (e.g., `async` functions in JS, Python, C# or Rust) if
1456+
concurrency execution is desired. For more details, see the [concurrency
1457+
explainer](Concurrency.md#summary).
14631458

14641459

14651460
## Item: `use`
@@ -1689,8 +1684,8 @@ resource-method ::= func-item
16891684
| 'constructor' param-list ';'
16901685
```
16911686

1692-
The optional `async` hint on `static` functions has the same meaning as
1693-
in a non-`static` `func-item`.
1687+
The optional `async` on `static` functions has the same meaning as in a
1688+
non-`static` `func-item`.
16941689

16951690
The syntax for handle types is presented [below](#handles).
16961691

design/mvp/canonical-abi/definitions.py

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ class InstanceType(ExternType):
8888
class FuncType(ExternType):
8989
params: list[tuple[str,ValType]]
9090
result: list[ValType|tuple[str,ValType]]
91+
async_: bool = False
9192
def param_types(self):
9293
return self.extract_types(self.params)
9394
def result_type(self):
@@ -402,6 +403,7 @@ def resume(self, suspend_result = SuspendResult.NOT_CANCELLED):
402403
assert(not self.running())
403404

404405
def suspend(self, cancellable) -> SuspendResult:
406+
assert(self.task.may_block())
405407
assert(self.running() and not self.cancellable and self.suspend_result is None)
406408
self.cancellable = cancellable
407409
self.parent_lock.release()
@@ -420,6 +422,7 @@ def resume_later(self):
420422
self.task.inst.store.pending.append(self)
421423

422424
def suspend_until(self, ready_func, cancellable = False) -> SuspendResult:
425+
assert(self.task.may_block())
423426
assert(self.running())
424427
if ready_func() and not DETERMINISTIC_PROFILE and random.randint(0,1):
425428
return SuspendResult.NOT_CANCELLED
@@ -566,8 +569,13 @@ def trap_if_on_the_stack(self, inst):
566569
def needs_exclusive(self):
567570
return not self.opts.async_ or self.opts.callback
568571

572+
def may_block(self):
573+
return self.ft.async_ or self.state == Task.State.RESOLVED
574+
569575
def enter(self, thread):
570576
assert(thread in self.threads and thread.task is self)
577+
if not self.ft.async_:
578+
return True
571579
def has_backpressure():
572580
return self.inst.backpressure > 0 or (self.needs_exclusive() and self.inst.exclusive)
573581
if has_backpressure() or self.inst.num_waiting_to_enter > 0:
@@ -584,6 +592,8 @@ def has_backpressure():
584592

585593
def exit(self):
586594
assert(len(self.threads) > 0)
595+
if not self.ft.async_:
596+
return
587597
if self.needs_exclusive():
588598
assert(self.inst.exclusive)
589599
self.inst.exclusive = False
@@ -641,20 +651,6 @@ def ready_and_has_event():
641651
wset.num_waiting -= 1
642652
return event
643653

644-
def poll_until(self, ready_func, thread, wset, cancellable) -> Optional[EventTuple]:
645-
assert(thread in self.threads and thread.task is self)
646-
wset.num_waiting += 1
647-
match self.suspend_until(ready_func, thread, cancellable):
648-
case SuspendResult.CANCELLED:
649-
event = (EventCode.TASK_CANCELLED, 0, 0)
650-
case SuspendResult.NOT_CANCELLED:
651-
if wset.has_pending_event():
652-
event = wset.get_pending_event()
653-
else:
654-
event = (EventCode.NONE, 0, 0)
655-
wset.num_waiting -= 1
656-
return event
657-
658654
def yield_until(self, ready_func, thread, cancellable) -> EventTuple:
659655
assert(thread in self.threads and thread.task is self)
660656
match self.suspend_until(ready_func, thread, cancellable):
@@ -2028,15 +2024,17 @@ def thread_func(thread):
20282024
inst.exclusive = False
20292025
match code:
20302026
case CallbackCode.YIELD:
2031-
event = task.yield_until(lambda: not inst.exclusive, thread, cancellable = True)
2027+
if task.may_block():
2028+
event = task.yield_until(lambda: not inst.exclusive, thread, cancellable = True)
2029+
else:
2030+
event = (EventCode.NONE, 0, 0)
20322031
case CallbackCode.WAIT:
2032+
trap_if(not task.may_block())
20332033
wset = inst.table.get(si)
20342034
trap_if(not isinstance(wset, WaitableSet))
20352035
event = task.wait_until(lambda: not inst.exclusive, thread, wset, cancellable = True)
2036-
case CallbackCode.POLL:
2037-
wset = inst.table.get(si)
2038-
trap_if(not isinstance(wset, WaitableSet))
2039-
event = task.poll_until(lambda: not inst.exclusive, thread, wset, cancellable = True)
2036+
case _:
2037+
trap()
20402038
thread.in_event_loop = False
20412039
inst.exclusive = True
20422040
event_code, p1, p2 = event
@@ -2053,8 +2051,7 @@ class CallbackCode(IntEnum):
20532051
EXIT = 0
20542052
YIELD = 1
20552053
WAIT = 2
2056-
POLL = 3
2057-
MAX = 3
2054+
MAX = 2
20582055

20592056
def unpack_callback_result(packed):
20602057
code = packed & 0xf
@@ -2074,6 +2071,8 @@ def call_and_trap_on_throw(callee, thread, args):
20742071

20752072
def canon_lower(opts, ft, callee: FuncInst, thread, flat_args):
20762073
trap_if(not thread.task.inst.may_leave)
2074+
trap_if(not thread.task.may_block() and ft.async_ and not opts.async_)
2075+
20772076
subtask = Subtask()
20782077
cx = LiftLowerContext(opts, thread.task.inst, subtask)
20792078

@@ -2113,6 +2112,7 @@ def on_resolve(result):
21132112
flat_results = lower_flat_values(cx, max_flat_results, result, ft.result_type(), flat_args)
21142113

21152114
subtask.callee = callee(thread.task, on_start, on_resolve)
2115+
assert(ft.async_ or subtask.state == Subtask.State.RETURNED)
21162116

21172117
if not opts.async_:
21182118
if not subtask.resolved():
@@ -2147,31 +2147,30 @@ def canon_resource_new(rt, thread, rep):
21472147

21482148
### `canon resource.drop`
21492149

2150-
def canon_resource_drop(rt, async_, thread, i):
2150+
def canon_resource_drop(rt, thread, i):
21512151
trap_if(not thread.task.inst.may_leave)
21522152
inst = thread.task.inst
21532153
h = inst.table.remove(i)
21542154
trap_if(not isinstance(h, ResourceHandle))
21552155
trap_if(h.rt is not rt)
21562156
trap_if(h.num_lends != 0)
2157-
flat_results = [] if not async_ else [0]
21582157
if h.own:
21592158
assert(h.borrow_scope is None)
21602159
if inst is rt.impl:
21612160
if rt.dtor:
21622161
rt.dtor(h.rep)
21632162
else:
21642163
if rt.dtor:
2165-
caller_opts = CanonicalOptions(async_ = async_)
2164+
caller_opts = CanonicalOptions(async_ = False)
21662165
callee_opts = CanonicalOptions(async_ = rt.dtor_async, callback = rt.dtor_callback)
2167-
ft = FuncType([U32Type()],[])
2166+
ft = FuncType([U32Type()],[], async_ = False)
21682167
callee = partial(canon_lift, callee_opts, rt.impl, ft, rt.dtor)
2169-
flat_results = canon_lower(caller_opts, ft, callee, thread, [h.rep])
2168+
[] = canon_lower(caller_opts, ft, callee, thread, [h.rep])
21702169
else:
21712170
thread.task.trap_if_on_the_stack(rt.impl)
21722171
else:
21732172
h.borrow_scope.num_borrows -= 1
2174-
return flat_results
2173+
return []
21752174

21762175
### `canon resource.rep`
21772176

@@ -2249,6 +2248,7 @@ def canon_waitable_set_new(thread):
22492248

22502249
def canon_waitable_set_wait(cancellable, mem, thread, si, ptr):
22512250
trap_if(not thread.task.inst.may_leave)
2251+
trap_if(not thread.task.may_block())
22522252
wset = thread.task.inst.table.get(si)
22532253
trap_if(not isinstance(wset, WaitableSet))
22542254
event = thread.task.wait_until(lambda: True, thread, wset, cancellable)
@@ -2267,7 +2267,12 @@ def canon_waitable_set_poll(cancellable, mem, thread, si, ptr):
22672267
trap_if(not thread.task.inst.may_leave)
22682268
wset = thread.task.inst.table.get(si)
22692269
trap_if(not isinstance(wset, WaitableSet))
2270-
event = thread.task.poll_until(lambda: True, thread, wset, cancellable)
2270+
if thread.task.deliver_pending_cancel(cancellable):
2271+
event = (EventCode.TASK_CANCELLED, 0, 0)
2272+
elif not wset.has_pending_event():
2273+
event = (EventCode.NONE, 0, 0)
2274+
else:
2275+
event = wset.get_pending_event()
22712276
return unpack_event(mem, thread, ptr, event)
22722277

22732278
### 🔀 `canon waitable-set.drop`
@@ -2299,6 +2304,7 @@ def canon_waitable_join(thread, wi, si):
22992304

23002305
def canon_subtask_cancel(async_, thread, i):
23012306
trap_if(not thread.task.inst.may_leave)
2307+
trap_if(not thread.task.may_block() and not async_)
23022308
subtask = thread.task.inst.table.get(i)
23032309
trap_if(not isinstance(subtask, Subtask))
23042310
trap_if(subtask.resolve_delivered())
@@ -2355,6 +2361,8 @@ def canon_stream_write(stream_t, opts, thread, i, ptr, n):
23552361

23562362
def stream_copy(EndT, BufferT, event_code, stream_t, opts, thread, i, ptr, n):
23572363
trap_if(not thread.task.inst.may_leave)
2364+
trap_if(not thread.task.may_block() and not opts.async_)
2365+
23582366
e = thread.task.inst.table.get(i)
23592367
trap_if(not isinstance(e, EndT))
23602368
trap_if(e.shared.t != stream_t.t)
@@ -2406,6 +2414,8 @@ def canon_future_write(future_t, opts, thread, i, ptr):
24062414

24072415
def future_copy(EndT, BufferT, event_code, future_t, opts, thread, i, ptr):
24082416
trap_if(not thread.task.inst.may_leave)
2417+
trap_if(not thread.task.may_block() and not opts.async_)
2418+
24092419
e = thread.task.inst.table.get(i)
24102420
trap_if(not isinstance(e, EndT))
24112421
trap_if(e.shared.t != future_t.t)
@@ -2456,6 +2466,7 @@ def canon_future_cancel_write(future_t, async_, thread, i):
24562466

24572467
def cancel_copy(EndT, event_code, stream_or_future_t, async_, thread, i):
24582468
trap_if(not thread.task.inst.may_leave)
2469+
trap_if(not thread.task.may_block() and not async_)
24592470
e = thread.task.inst.table.get(i)
24602471
trap_if(not isinstance(e, EndT))
24612472
trap_if(e.shared.t != stream_or_future_t.t)
@@ -2532,6 +2543,7 @@ def canon_thread_switch_to(cancellable, thread, i):
25322543

25332544
def canon_thread_suspend(cancellable, thread):
25342545
trap_if(not thread.task.inst.may_leave)
2546+
trap_if(not thread.task.may_block())
25352547
suspend_result = thread.task.suspend(thread, cancellable)
25362548
return [suspend_result]
25372549

@@ -2559,6 +2571,8 @@ def canon_thread_yield_to(cancellable, thread, i):
25592571

25602572
def canon_thread_yield(cancellable, thread):
25612573
trap_if(not thread.task.inst.may_leave)
2574+
if not thread.task.may_block():
2575+
return [SuspendResult.NOT_CANCELLED]
25622576
event_code,_,_ = thread.task.yield_until(lambda: True, thread, cancellable)
25632577
match event_code:
25642578
case EventCode.NONE:

0 commit comments

Comments
 (0)