@@ -570,6 +570,7 @@ complementarily using `parent_lock` and `fiber_lock` as follows:
570570 assert (not self .running())
571571
572572 def suspend (self , cancellable ) -> SuspendResult:
573+ assert (self .task.may_suspend())
573574 assert (self .running() and not self .cancellable and self .suspend_result is None )
574575 self .cancellable = cancellable
575576 self .parent_lock.release()
@@ -877,15 +878,24 @@ synchronously or with `async callback`. This predicate is used by the other
877878``` python
878879 def needs_exclusive (self ):
879880 return not self .opts.async_ or self .opts.callback
881+ ```
880882
883+ The ` Task.may_suspend ` predicate returns whether the type of the function for
884+ which this ` Task ` was spawned allows current execution to suspend.
885+ Specifically, synchronous functions that have not yet returned a value to their
886+ caller may not suspend.
887+ ``` python
888+ def may_suspend (self ):
889+ return self .ft.async_ or self .state == Task.State.RESOLVED
881890```
882891
883- The ` Task.enter ` method implements [ backpressure] between when a caller makes a
884- call to an imported callee and when the callee's core wasm entry point is
885- executed. This interstitial placement allows an overloaded component instance
886- to avoid the need to otherwise-endlessly allocate guest memory for blocked
887- async calls until OOM. When backpressure is enabled, ` enter ` will block until
888- backpressure is disabled. There are three sources of backpressure:
892+ The ` Task.enter ` method implements [ backpressure] between when the caller of an
893+ ` async ` -typed function initiates the call and when the callee's core wasm entry
894+ point is executed. This interstitial placement allows a component instance that
895+ has been overloaded with concurrent function invocations to avoid OOM while
896+ allocating core wasm memory for each concurrent task. When backpressure is
897+ enabled, ` enter ` will block new ` async ` -typed calls until backpressure is
898+ disabled. There are three sources of backpressure:
889899 1 . * Explicit backpressure* is triggered by core wasm calling
890900 ` backpressure.{inc,dec} ` which modify the ` ComponentInstance.backpressure `
891901 counter.
@@ -896,9 +906,18 @@ backpressure is disabled. There are three sources of backpressure:
896906 ` enter ` that need to be given the chance to start without getting starved
897907 by new tasks.
898908
909+ Note that, because non-` async ` -typed functions ignore backpressure entirely,
910+ they may reenter core wasm while a previous ` async ` -typed call is suspended
911+ at a cooperative yield point (where an ` async ` -typed function would have been
912+ blocked by implicit backpressure). Thus, export bindings generators must be
913+ careful to handle this kind of reentrance (e.g., while maintaining the
914+ linear-memory shadow stack) for components with mixed ` async ` - and non-` async ` -
915+ typed exports.
899916``` python
900917 def enter (self , thread ):
901918 assert (thread in self .threads and thread.task is self )
919+ if not self .ft.async_:
920+ return True
902921 def has_backpressure ():
903922 return self .inst.backpressure > 0 or (self .needs_exclusive() and self .inst.exclusive)
904923 if has_backpressure() or self .inst.num_waiting_to_enter > 0 :
@@ -931,6 +950,8 @@ returns to clear the `exclusive` flag set by `Task.enter`, allowing other
931950``` python
932951 def exit (self ):
933952 assert (len (self .threads) > 0 )
953+ if not self .ft.async_:
954+ return
934955 if self .needs_exclusive():
935956 assert (self .inst.exclusive)
936957 self .inst.exclusive = False
@@ -3249,12 +3270,17 @@ function (specified as a `funcidx` immediate in `canon lift`) until the
32493270 inst.exclusive = False
32503271 match code:
32513272 case CallbackCode.YIELD :
3252- event = task.yield_until(lambda : not inst.exclusive, thread, cancellable = True )
3273+ if thread.task.may_suspend():
3274+ event = task.yield_until(lambda : not inst.exclusive, thread, cancellable = True )
3275+ else :
3276+ event = (EventCode.NONE , 0 , 0 )
32533277 case CallbackCode.WAIT :
3278+ trap_if(not thread.task.may_suspend())
32543279 wset = inst.table.get(si)
32553280 trap_if(not isinstance (wset, WaitableSet))
32563281 event = task.wait_until(lambda : not inst.exclusive, thread, wset, cancellable = True )
32573282 case CallbackCode.POLL :
3283+ trap_if(not thread.task.may_suspend())
32583284 wset = inst.table.get(si)
32593285 trap_if(not isinstance (wset, WaitableSet))
32603286 event = task.poll_until(lambda : not inst.exclusive, thread, wset, cancellable = True )
@@ -3272,6 +3298,12 @@ built-ins. Thus, the main difference between stackful and stackless async is
32723298whether these suspending operations are performed from an empty or non-empty
32733299core wasm callstack (with the former allowing additional engine optimization).
32743300
3301+ If a ` Task ` is not allowed to suspend because it was created for a non-` async ` -
3302+ typed function call and has not yet returned a value, ` YIELD ` is always a no-op
3303+ and ` WAIT ` and ` POLL ` always trap. Thus, it * is* possible for a component to
3304+ implement a non-` async ` -typed function with the ` async callback ` ABI, but the
3305+ component * must* call ` task.return ` * before* ` WAIT ` ing or ` POLL ` ing.
3306+
32753307The event loop also releases ` ComponentInstance.exclusive ` (which was acquired
32763308by ` Task.enter ` and will be released by ` Task.exit ` ) before potentially
32773309suspending the thread to allow other synchronous and ` async callback ` tasks to
@@ -3363,14 +3395,23 @@ Based on this, `canon_lower` is defined in chunks as follows:
33633395``` python
33643396def canon_lower (opts , ft , callee : FuncInst, thread , flat_args ):
33653397 trap_if(not thread.task.inst.may_leave)
3366- subtask = Subtask()
3367- cx = LiftLowerContext(opts, thread.task.inst, subtask)
3398+ trap_if(not thread.task.may_suspend() and ft.async_ and not opts.async_)
33683399```
3400+ A non-` async ` -typed function export unconditionally traps if it transitively
3401+ attempts to make a synchronous call to an ` async ` -typed function import (even
3402+ if the callee wouldn't have actually suspended at runtime). It is however
3403+ always fine to ` async ` -lowered call an ` async ` -typed function import, since
3404+ this * never* suspends.
3405+
33693406Each call to ` canon_lower ` creates a new ` Subtask ` . However, this ` Subtask ` is
33703407only added to the current component instance's table (below) if ` async ` is
33713408specified * and* ` callee ` blocks. In any case, this ` Subtask ` is used as the
33723409` LiftLowerContext.borrow_scope ` for ` borrow ` arguments, ensuring that owned
33733410handles are not dropped before ` Subtask.deliver_return ` is called (below).
3411+ ``` python
3412+ subtask = Subtask()
3413+ cx = LiftLowerContext(opts, thread.task.inst, subtask)
3414+ ```
33743415
33753416The next chunk makes the call to ` callee ` (which has type ` FuncInst ` , as
33763417defined in the [ Embedding] ( #embedding ) interface). The [ current task] serves as
@@ -3415,6 +3456,7 @@ above).
34153456 flat_results = lower_flat_values(cx, max_flat_results, result, ft.result_type(), flat_args)
34163457
34173458 subtask.callee = callee(thread.task, on_start, on_resolve)
3459+ assert (ft.async_ or subtask.state == Subtask.State.RETURNED )
34183460```
34193461The ` Subtask.state ` field is updated by the callbacks to keep track of the
34203462call progres. The ` on_progress ` variable starts as a no-op, but is used by the
@@ -3423,7 +3465,9 @@ call progres. The `on_progress` variable starts as a no-op, but is used by the
34233465According to the ` FuncInst ` calling contract, the call to ` callee ` should never
34243466"block" (i.e., wait on I/O). If the ` callee ` * would* block, it will instead
34253467return a ` Call ` object which is stored in the ` Subtask ` (so that it can be used
3426- to ` request_cancellation ` in the future).
3468+ to ` request_cancellation ` in the future). Furthermore, if the function type
3469+ does not have the ` async ` attribute, the function * must* have returned a value
3470+ by the time is blocks or returns.
34273471
34283472In the synchronous case (when the ` async ` ` canonopt ` is not set), if the
34293473` callee ` blocked before calling ` on_resolve ` , the synchronous caller's thread
@@ -3518,45 +3562,45 @@ For a canonical definition:
35183562validation specifies:
35193563* ` $rt ` must refer to resource type
35203564* ` $f ` is given type ` (func (param i32)) `
3521- * 🔀+🚝 - ` async ` is allowed (otherwise it is not allowed)
35223565
35233566Calling ` $f ` invokes the following function, which removes the handle from the
35243567current component instance's table and, if the handle was owning, calls the
35253568resource's destructor.
35263569``` python
3527- def canon_resource_drop (rt , async_ , thread , i ):
3570+ def canon_resource_drop (rt , thread , i ):
35283571 trap_if(not thread.task.inst.may_leave)
35293572 inst = thread.task.inst
35303573 h = inst.table.remove(i)
35313574 trap_if(not isinstance (h, ResourceHandle))
35323575 trap_if(h.rt is not rt)
35333576 trap_if(h.num_lends != 0 )
3534- flat_results = [] if not async_ else [0 ]
35353577 if h.own:
35363578 assert (h.borrow_scope is None )
35373579 if inst is rt.impl:
35383580 if rt.dtor:
35393581 rt.dtor(h.rep)
35403582 else :
35413583 if rt.dtor:
3542- caller_opts = CanonicalOptions(async_ = async_ )
3584+ caller_opts = CanonicalOptions(async_ = False )
35433585 callee_opts = CanonicalOptions(async_ = rt.dtor_async, callback = rt.dtor_callback)
3544- ft = FuncType([U32Type()],[])
3586+ ft = FuncType([U32Type()],[], async_ = False )
35453587 callee = partial(canon_lift, callee_opts, rt.impl, ft, rt.dtor)
3546- flat_results = canon_lower(caller_opts, ft, callee, thread, [h.rep])
3588+ [] = canon_lower(caller_opts, ft, callee, thread, [h.rep])
35473589 else :
35483590 thread.task.trap_if_on_the_stack(rt.impl)
35493591 else :
35503592 h.borrow_scope.num_borrows -= 1
3551- return flat_results
3552- ```
3553- In general, the call to a resource's destructor is treated like a
3554- cross-component call (as-if the destructor was exported by the component
3555- defining the resource type). This means that cross-component destructor calls
3556- follow the same concurrency rules as normal exports. However, since there are
3557- valid reasons to call ` resource.drop ` in the same component instance that
3558- defined the resource, which would otherwise trap at the reentrance guard of
3559- ` Task.enter ` , an exception is made when the resource type's
3593+ return []
3594+ ```
3595+ The call to a resource's destructor is defined as a non-` async ` -lowered,
3596+ non-` async ` -typed function call to a possibly-` async ` -lifted callee, passing
3597+ the private ` i32 ` representation as a parameter with an empty return. Thus,
3598+ destructors * may* block on I/O, but only after they ` task.return ` , ensuring
3599+ that ` resource.drop ` never blocks.
3600+
3601+ Since there are valid reasons to call ` resource.drop ` in the same component
3602+ instance that defined the resource, which would otherwise trap at the
3603+ reentrance guard of ` Task.enter ` , an exception is made when the resource type's
35603604implementation-instance is the same as the current instance (which is
35613605statically known for any given ` canon resource.drop ` ).
35623606
@@ -3798,6 +3842,7 @@ returning its `EventCode` and writing the payload values into linear memory:
37983842``` python
37993843def canon_waitable_set_wait (cancellable , mem , thread , si , ptr ):
38003844 trap_if(not thread.task.inst.may_leave)
3845+ trap_if(not thread.task.may_suspend())
38013846 wset = thread.task.inst.table.get(si)
38023847 trap_if(not isinstance (wset, WaitableSet))
38033848 event = thread.task.wait_until(lambda : True , thread, wset, cancellable)
@@ -3810,6 +3855,10 @@ def unpack_event(mem, thread, ptr, e: EventTuple):
38103855 store(cx, p2, U32Type(), ptr + 4 )
38113856 return [event]
38123857```
3858+ A non-` async ` -typed function export unconditionally traps if it transitively
3859+ attempts to call to ` waitable-set.wait ` (regardless of whether there are any
3860+ waitables with pending events).
3861+
38133862The ` lambda: True ` passed to ` wait_until ` means that ` wait_until ` will only
38143863wait for the given ` wset ` to have a pending event with no extra conditions.
38153864
@@ -3837,6 +3886,7 @@ same way as `wait`.
38373886``` python
38383887def canon_waitable_set_poll (cancellable , mem , thread , si , ptr ):
38393888 trap_if(not thread.task.inst.may_leave)
3889+ trap_if(not thread.task.may_suspend())
38403890 wset = thread.task.inst.table.get(si)
38413891 trap_if(not isinstance (wset, WaitableSet))
38423892 event = thread.task.poll_until(lambda : True , thread, wset, cancellable)
@@ -3845,7 +3895,8 @@ def canon_waitable_set_poll(cancellable, mem, thread, si, ptr):
38453895Even though ` waitable-set.poll ` doesn't block until the given waitable set has
38463896a pending event, ` poll_until ` does transitively perform a ` Thread.suspend `
38473897which allows the embedder to nondeterministically switch to executing another
3848- task (like ` thread.yield ` ).
3898+ task (like ` thread.yield ` ). Thus, a non-` async ` -typed function export
3899+ unconditionally traps if it transitively attempts to call ` waitable-set.poll ` .
38493900
38503901If ` cancellable ` is set, then ` waitable-set.poll ` will return whether the
38513902supertask has already or concurrently requested cancellation.
@@ -3944,6 +3995,7 @@ BLOCKED = 0xffff_ffff
39443995
39453996def canon_subtask_cancel (async_ , thread , i ):
39463997 trap_if(not thread.task.inst.may_leave)
3998+ trap_if(not thread.task.may_suspend() and not async_)
39473999 subtask = thread.task.inst.table.get(i)
39484000 trap_if(not isinstance (subtask, Subtask))
39494001 trap_if(subtask.resolve_delivered())
@@ -3963,9 +4015,13 @@ def canon_subtask_cancel(async_, thread, i):
39634015 assert (subtask.resolve_delivered())
39644016 return [subtask.state]
39654017```
3966- The initial trapping conditions disallow calling ` subtask.cancel ` twice for the
3967- same subtask or after the supertask has already been notified that the subtask
3968- has returned.
4018+ A non-` async ` -typed function export unconditionally traps if it transitively
4019+ attempts to make a synchronous call to ` subtask.cancel ` (regardless of whether
4020+ the cancellation would have succeeded without suspending).
4021+
4022+ The following trapping conditions disallow calling ` subtask.cancel ` twice for
4023+ the same subtask or after the supertask has already been notified that the
4024+ subtask has returned.
39694025
39704026A race condition handled by the above code is that it's possible for a subtask
39714027to have already resolved (by calling ` task.return ` or ` task.cancel ` ) and
@@ -4060,13 +4116,20 @@ def canon_stream_write(stream_t, opts, thread, i, ptr, n):
40604116 stream_t, opts, thread, i, ptr, n)
40614117```
40624118
4063- Introducing the ` stream_copy ` function in chunks, ` stream_copy ` first checks
4064- that the element at index ` i ` is of the right type and allowed to start a new
4065- copy. (In the future, the "trap if not ` IDLE ` " condition could be relaxed to
4066- allow multiple pipelined reads or writes.)
4119+ Introducing the ` stream_copy ` function in chunks, a non- ` async ` -typed function
4120+ export unconditionally traps if it transitively attempts to make a synchronous
4121+ call to ` stream.{read,write} ` (regardless of whether the operation would have
4122+ succeeded eagerly without suspending).
40674123``` python
40684124def stream_copy (EndT , BufferT , event_code , stream_t , opts , thread , i , ptr , n ):
40694125 trap_if(not thread.task.inst.may_leave)
4126+ trap_if(not thread.task.may_suspend() and not opts.async_)
4127+ ```
4128+
4129+ Next, ` stream_copy ` checks that the element at index ` i ` is of the right type
4130+ and allowed to start a new copy. (In the future, the "trap if not ` IDLE ` "
4131+ condition could be relaxed to allow multiple pipelined reads or writes.)
4132+ ``` python
40704133 e = thread.task.inst.table.get(i)
40714134 trap_if(not isinstance (e, EndT))
40724135 trap_if(e.shared.t != stream_t.t)
@@ -4166,11 +4229,14 @@ def canon_future_write(future_t, opts, thread, i, ptr):
41664229```
41674230
41684231Introducing the ` future_copy ` function in chunks, ` future_copy ` starts with the
4169- same set of guards as ` stream_copy ` for parameters ` i ` and ` ptr ` . The only
4170- difference is that, with futures, the ` Buffer ` length is fixed to ` 1 ` .
4232+ same set of guards as ` stream_copy ` regarding whether suspension is allowed and
4233+ parameters ` i ` and ` ptr ` . The only difference is that, with futures, the
4234+ ` Buffer ` length is fixed to ` 1 ` .
41714235``` python
41724236def future_copy (EndT , BufferT , event_code , future_t , opts , thread , i , ptr ):
41734237 trap_if(not thread.task.inst.may_leave)
4238+ trap_if(not thread.task.may_suspend() and not opts.async_)
4239+
41744240 e = thread.task.inst.table.get(i)
41754241 trap_if(not isinstance (e, EndT))
41764242 trap_if(e.shared.t != future_t.t)
@@ -4254,6 +4320,7 @@ def canon_future_cancel_write(future_t, async_, thread, i):
42544320
42554321def cancel_copy (EndT , event_code , stream_or_future_t , async_ , thread , i ):
42564322 trap_if(not thread.task.inst.may_leave)
4323+ trap_if(not thread.task.may_suspend() and not async_)
42574324 e = thread.task.inst.table.get(i)
42584325 trap_if(not isinstance (e, EndT))
42594326 trap_if(e.shared.t != stream_or_future_t.t)
@@ -4269,8 +4336,12 @@ def cancel_copy(EndT, event_code, stream_or_future_t, async_, thread, i):
42694336 assert (not e.copying() and code == event_code and index == i)
42704337 return [payload]
42714338```
4272- Cancellation traps if there is not currently an async copy in progress (sync
4273- copies do not expect or check for cancellation and thus cannot be cancelled).
4339+ A non-` async ` -typed function export unconditionally traps if it transitively
4340+ attempts to make a synchronous call to ` {stream,future}.cancel-{read,write} `
4341+ (regardless of whether the cancellation would have completed without
4342+ suspending). There is also a trap if there is not currently an async copy in
4343+ progress (sync copies do not expect or check for cancellation and thus cannot
4344+ be cancelled).
42744345
42754346The * first* check for ` e.has_pending_event() ` catches the case where the copy has
42764347already racily finished, in which case we must * not* call ` cancel() ` . Calling
@@ -4435,9 +4506,13 @@ calling component.
44354506``` python
44364507def canon_thread_suspend (cancellable , thread ):
44374508 trap_if(not thread.task.inst.may_leave)
4509+ trap_if(not thread.task.may_suspend())
44384510 suspend_result = thread.task.suspend(thread, cancellable)
44394511 return [suspend_result]
44404512```
4513+ A non-` async ` -typed function export traps if it transitively attempts to call
4514+ ` thread.suspend ` .
4515+
44414516If ` cancellable ` is set, then ` thread.suspend ` will return a ` SuspendResult `
44424517value to indicate whether the supertask has already or concurrently requested
44434518cancellation. ` thread.suspend ` (and other cancellable operations) will only
@@ -4521,13 +4596,21 @@ other threads in a cooperative setting.
45214596``` python
45224597def canon_thread_yield (cancellable , thread ):
45234598 trap_if(not thread.task.inst.may_leave)
4599+ if not thread.task.may_suspend():
4600+ return [SuspendResult.NOT_CANCELLED ]
45244601 event_code,_,_ = thread.task.yield_until(lambda : True , thread, cancellable)
45254602 match event_code:
45264603 case EventCode.NONE :
45274604 return [SuspendResult.NOT_CANCELLED ]
45284605 case EventCode.TASK_CANCELLED :
45294606 return [SuspendResult.CANCELLED ]
45304607```
4608+ If a non-` async ` -typed function export transitively calls ` thread.yield ` , the
4609+ operation is a no-op (instead of trapping, as with other possibly-suspending
4610+ operations like ` waitable-set.poll ` ). This is because, unlike other built-ins,
4611+ ` thread.yield ` may be scattered liberally throughout code that might show up in
4612+ the transitive call tree of a synchronous function call.
4613+
45314614Even though ` yield_until ` passes ` lambda: True ` as the condition it is waiting
45324615for, ` yield_until ` does transitively peform a ` Thread.suspend ` which allows
45334616the embedder to nondeterministically switch to executing another thread.
0 commit comments