diff --git a/design/mvp/CanonicalABI.md b/design/mvp/CanonicalABI.md index 621296b5..34279de7 100644 --- a/design/mvp/CanonicalABI.md +++ b/design/mvp/CanonicalABI.md @@ -126,6 +126,7 @@ class ComponentInstance: threads: Table[Thread] may_enter: bool may_leave: bool + sync_depth: int backpressure: int num_waiting_to_enter: int exclusive_thread: Optional[Thread] @@ -138,6 +139,7 @@ class ComponentInstance: self.threads = Table() self.may_enter = True self.may_leave = True + self.sync_depth = 0 self.backpressure = 0 self.num_waiting_to_enter = 0 self.exclusive_thread = None @@ -695,15 +697,17 @@ report any pending cancellation if the caller is `cancellable`. Lastly, the `Thread.suspend_then_promote` and `Thread.yield_then_promote` methods *attempt* to immediately resume execution of some `other` thread in the -same component instance *if* the `other` thread is in a `ready` `waiting` state. -If so, control flow is transferred directly and the current thread is left -`suspended` or in a `ready` `waiting` state, resp. If the `other` thread is -*not* ready to run, then these operations fall back to plain `suspend` or -`yield_` behavior, resp. +same component instance *if* the `other` thread is "promotable" (as defined in +the next section by `Task.promotable`); otherwise these operations fall back to +plain `suspend` or `yield_` behavior, resp. This allows one thread to give +another thread in an unknown state a scheduling "boost" (with `pthread_join` +being an example use case). ```python def suspend_then_promote(self, cancellable, other: Thread) -> Cancelled: assert(self.running()) - if other.ready(): + if self.task.deliver_pending_cancel(cancellable): + return Cancelled.TRUE + if current_task().promotable(other): other.stop_waiting_internal(cancelled = False) return self.suspend_then_resume(cancellable, other) else: @@ -711,7 +715,9 @@ If so, control flow is transferred directly and the current thread is left def yield_then_promote(self, cancellable, other: Thread) -> Cancelled: assert(self.running()) - if other.ready(): + if self.task.deliver_pending_cancel(cancellable): + return Cancelled.TRUE + if current_task().promotable(other): other.stop_waiting_internal(cancelled = False) return self.yield_then_resume(cancellable, other) else: @@ -808,6 +814,21 @@ holding the lock. return not self.opts.async_ or self.opts.callback ``` +Building on this, the `Task.promotable` predicate defines when a `ready` thread +can be resumed without violating [Component Invariant] #3 via either the +`Thread.{suspend,yield}_then_promote` methods or the synchronous thread +scheduling performed in `canon_lift` below. In particular, explicit threads, the +implicit thread of the current non-`async`-typed task (passed as `self`), and +the implicit threads of stackful `async`-typed tasks are all "promotable" if +they are in the `ready` state. +```python + def promotable(self, thread): + return (thread.ready() + and (thread is not thread.task.implicit_thread + or (not thread.task.ft.async_ and thread.task is self) + or (thread.task.ft.async_ and not thread.task.needs_exclusive()))) +``` + The `Task.enter_implicit_thread` method implements [backpressure] between when the caller of an `async`-typed function initiates the call and when the callee's core wasm entry point is executed. This interstitial placement allows a @@ -819,7 +840,10 @@ of backpressure: `backpressure.{inc,dec}` which modify the `ComponentInstance.backpressure` counter. 2. *Implicit backpressure* triggered when `Task.needs_exclusive()` is true and - the `ComponentInstance.exclusive_thread` lock is already held. + either the `ComponentInstance.exclusive_thread` lock is already held *or*, + in a [donut wrapping] scenario, a parent's `async` function is being called + by a child component's import while the parent has a non-`async` call + already on the stack. 3. *Residual backpressure* triggered by explicit or implicit backpressure having been enabled then disabled, but there still being tasks waiting to enter that need to be given the chance to start without getting starved @@ -837,8 +861,10 @@ exports. self.implicit_thread = current_thread() if self.ft.async_: def has_backpressure(): - return (self.inst.backpressure > 0 or - (self.needs_exclusive() and self.inst.exclusive_thread is not None)) + return (self.inst.backpressure > 0 + or (self.needs_exclusive() + and (self.inst.exclusive_thread is not None + or self.inst.sync_depth > 0))) if has_backpressure() or self.inst.num_waiting_to_enter > 0: self.inst.num_waiting_to_enter += 1 cancelled = self.implicit_thread.wait_until(lambda: not has_backpressure(), cancellable = True) @@ -849,6 +875,8 @@ exports. if self.needs_exclusive(): assert(self.inst.exclusive_thread is None) self.inst.exclusive_thread = self.implicit_thread + else: + self.inst.sync_depth += 1 self.register_thread(self.implicit_thread) return True @@ -884,9 +912,12 @@ returned a value to its caller. def exit_implicit_thread(self): assert(current_thread() is self.implicit_thread) self.unregister_thread(self.implicit_thread) - if self.ft.async_ and self.needs_exclusive(): - assert(self.inst.exclusive_thread is self.implicit_thread) - self.inst.exclusive_thread = None + if self.ft.async_: + if self.needs_exclusive(): + assert(self.inst.exclusive_thread is self.implicit_thread) + self.inst.exclusive_thread = None + else: + self.inst.sync_depth -= 1 def unregister_thread(self, thread): assert(thread in self.threads and thread.task is self) @@ -916,9 +947,12 @@ multiple), giving the thread the chance to handle cancellation promptly so that self.implicit_thread.resume(Cancelled.TRUE) else: assert(self.state == Task.State.STARTED) - candidates = { t for t in self.threads if t.cancellable } - if self.needs_exclusive() and self.inst.exclusive_thread not in { None, self.implicit_thread }: - candidates.discard(self.implicit_thread) + def exclusive_conflict(thread): + return (self.needs_exclusive() + and thread is self.implicit_thread + and (self.inst.exclusive_thread not in { None, self.implicit_thread } + or self.inst.sync_depth > 0)) + candidates = { t for t in self.threads if t.cancellable and not exclusive_conflict(t) } if candidates and self.inst.may_enter_from(caller): self.state = Task.State.CANCEL_DELIVERED self.inst.enter_from(caller) @@ -932,7 +966,8 @@ thread when doing so would violate [Component Invariant] #2 or #3. In particular, invariant #2 requires not resuming any thread while the task's containing component instance may not be reentered and invariant #3 requires not resuming a `needs_exclusive` task's implicit thread while another task's -implicit thread is running exclusively. +`needs_exclusive` implicit thread is holding the `exclusive_thread` lock *or* +there's a non-`async` call on the stack (which must execute in a LIFO manner). If cancellation cannot be immediately delivered by `Task.request_cancellation`, the request is remembered in `Task.state` and delivered at the next opportunity @@ -3752,32 +3787,29 @@ calls `Thread.resume` on the new thread to synchronously transfer control flow to it (jumping to the top of `thread_func` above). The new thread executes until it either returns from `thread_func` or [blocks] by (transitively) calling `Thread.block_internal`. If a non-`async`-typed call blocks before the implicit -thread has returned a value and there are no other `ready` threads in the same -component instance, `canon_lift` traps, since non-`async`-typed calls may not -block. Otherwise, `canon_lift` switches to a thread (nondeterministically, if -multiple are `ready`), as if the guest code had done so itself using a built-in -like `thread.suspend-then-promote`. This allows fully-synchronous components to -still use cooperative pthreads that interleave via threading built-ins (e.g., -`thread.yield`) and *even perform blocking I/O* as long as the blocking I/O does -not transitively block returning a value to the caller (as would also be -expressible with a CPS transform like [Asyncify]). Lastly, `canon_lift` returns -`Task.request_cancellation`, bound to the call's new task, as the `OnCancel` -return value of `FuncInst`. +thread has returned a value and there are no "promotable" threads in the +component instance (with `Task.promotable` as defined for the +`thread.{suspend,yield}-then-promote` built-ins above), `canon_lift` traps, +since non-`async`-typed calls may not block. Otherwise, `canon_lift` switches to +a promotable thread (nondeterministically, if there are multiple), as if the +guest code had done so itself using `thread.{suspend,yield}-then-promote`. This +allows fully-synchronous components to still use cooperative pthreads that +interleave via threading built-ins (e.g., `thread.yield`) and *even perform +blocking I/O* as long as the blocking I/O does not transitively block returning +a value to the caller (as would also be expressible with a CPS transform like +[Asyncify]). Lastly, `canon_lift` returns `Task.request_cancellation`, bound to +the call's new task, as the `OnCancel` return value of `FuncInst`. ```python task = Task(ft, opts, inst, on_start, on_resolve) thread = Thread(task, thread_func) thread.resume() if not ft.async_: while task.state != Task.State.RESOLVED: - candidates = { t for t in inst.threads if t.ready() and t is not inst.exclusive_thread } + candidates = { t for t in inst.threads if task.promotable(t) } trap_if(not candidates) random.choice(list(candidates)).resume() return task.request_cancellation ``` -The special case that excludes any thread (created by a previous blocked `async` -call) holding the instance's `exclusive_thread` lock is necessary to preserve -[Component Invariant] #3, which might otherwise be violated if the current -synchronous call is using the single global linear memory shadow stack. Note that, because non-`async`-typed functions can't block, they do not actually require a separate thread/fiber/stack to implement the above specified behavior @@ -5074,8 +5106,8 @@ validation specifies: Calling `$suspend-then-promote` invokes the following function which loads a thread at index `$i` from the current component instance's `threads` table and -then calls `Thread.suspend_then_resume` to resume the `other_thread` if it's -`ready` and, in any case, leave the [current thread] suspended. +resumes that thread if it's `Task.promotable`, leaving the [current thread] +suspended in any case. ```python def canon_thread_suspend_then_promote(cancellable, i): thread = current_thread() @@ -5103,9 +5135,9 @@ validation specifies: Calling `$yield-then-promote` invokes the following function which loads a thread at index `$i` from the current component instance's `threads` table and -then calls `Thread.yield_then_resume` to resume the `other_thread` if it's -`ready` and, in any case, leave the [current thread] ready to run at some -nondeterministic point in the future chosen by the embedder. +resumes that thread if it's `Task.promotable`, leaving the [current thread] +ready to run at some nondeterministic point in the future chosen by the +embedder in any case. ```python def canon_thread_yield_then_promote(cancellable, i): thread = current_thread() diff --git a/design/mvp/Concurrency.md b/design/mvp/Concurrency.md index 44cb269d..a8980168 100644 --- a/design/mvp/Concurrency.md +++ b/design/mvp/Concurrency.md @@ -406,7 +406,8 @@ useful if a thread has a long-running computation without I/O but still needs to allow other cooperative threads to make progress concurrently. Lastly, in addition to being able to switch to "suspended" threads, threads can -also switch to threads that are in a "ready to run" state by calling the +also switch to threads that are in a "ready to run" state (when doing so would +not otherwise violate [Component Invariant] #3) by calling the [`thread.suspend-then-promote`] and [`thread.yield-then-promote`] built-ins which, like the `thread.{suspend,yield}-then-resume` built-ins, leave the calling thread in a "suspended" or "ready to run" state, resp. The calling @@ -711,7 +712,11 @@ the event loop after every event (instead of once at the end of the task), stackless async exports release the lock between every event, allowing a higher degree of concurrency than synchronous exports. Stackful async exports ignore the lock entirely and thus achieve the highest degree of (cooperative) -concurrency. +concurrency. Another source of implicit backpressure arises when, in a [donut +wrapping] scenario, a recursive `async` call into the parent that requires the +exclusive lock is attempted while the parent is actively executing a +non-`async`-typed call (as this would otherwise allow non-LIFO execution that +would break invariant #3). Since non-`async` functions are not allowed to block (including due to backpressure) and also don't pile up like `async` functions, non-`async` diff --git a/design/mvp/Explainer.md b/design/mvp/Explainer.md index 28d51969..a396e9c3 100644 --- a/design/mvp/Explainer.md +++ b/design/mvp/Explainer.md @@ -2277,8 +2277,9 @@ For details, see [Thread Built-ins] in the concurrency explainer and | Canonical ABI signature | `[t:i32] -> [i32]` | The `thread.suspend-then-promote` built-in immediately resumes execution of the -thread `t` if `t` is in a "ready" state, in any case leaving the current thread -in a "suspended" state. If `cancellable` is set, `thread.suspend-then-promote` +thread `t` if `t` is "ready" and doing so would not otherwise violate +[Component Invariant] #3. In any case, the current thread is left in the +"suspended" state. If `cancellable` is set, `thread.suspend-then-promote` returns whether the current task was [cancelled] by the caller; otherwise, `thread.suspend-then-promote` always returns `false`. @@ -2293,9 +2294,10 @@ For details, see [Thread Built-ins] in the concurrency explainer and | Canonical ABI signature | `[t:i32] -> [i32]` | The `thread.yield-then-promote` built-in immediately resumes execution of the -thread `t` if `t` is in a "ready" state, in any case leaving the current thread -in a "ready" state. If `cancellable` is set, `thread.yield-then-promote` returns -whether the current task was [cancelled] by the caller; otherwise, +thread `t` if `t` is "ready" and doing so would not otherwise violate +[Component Invariant] #3. In any case, the current thread is left in the "ready" +state. If `cancellable` is set, `thread.yield-then-promote` returns whether the +current task was [cancelled] by the caller; otherwise, `thread.yield-then-promote` always returns `false`. For details, see [Thread Built-ins] in the concurrency explainer and @@ -3011,10 +3013,9 @@ In particular, the Component Model maintains the following invariants: restriction in an explicit opt-in manner.) 3. To ease adoption, unless a component opts in (via "stackful" lift ๐ŸšŸ or - cooperative threads ๐Ÿงต), all core wasm execution inside a component instance - is locally serialized (via automatic backpressure applied at export calls) so - that producer toolchains can continue to use a single global linear memory - shadow stack that is pushed and popped in LIFO order. + cooperative threads ๐Ÿงต), all core wasm inside a component instance executes + in a LIFO manner so that producer toolchains can continue to use a single + global linear memory shadow stack that is pushed and popped in LIFO order. ## JavaScript Embedding @@ -3342,6 +3343,7 @@ For some use-case-focused, worked examples, see: [GC ABI Option]: https://github.com/WebAssembly/component-model/issues/525 [Strongly-unique]: #name-uniqueness +[Component Invariant]: #component-invariants [Donut Wrapped]: Linking.md#higher-order-shared-nothing-linking-aka-donut-wrapping [Adapter Functions]: FutureFeatures.md#custom-abis-via-adapter-functions diff --git a/design/mvp/canonical-abi/definitions.py b/design/mvp/canonical-abi/definitions.py index 8f04f093..5923f5ba 100644 --- a/design/mvp/canonical-abi/definitions.py +++ b/design/mvp/canonical-abi/definitions.py @@ -195,6 +195,7 @@ class ComponentInstance: threads: Table[Thread] may_enter: bool may_leave: bool + sync_depth: int backpressure: int num_waiting_to_enter: int exclusive_thread: Optional[Thread] @@ -207,6 +208,7 @@ def __init__(self, store, parent = None): self.threads = Table() self.may_enter = True self.may_leave = True + self.sync_depth = 0 self.backpressure = 0 self.num_waiting_to_enter = 0 self.exclusive_thread = None @@ -420,7 +422,9 @@ def yield_then_resume(self, cancellable, other: Thread) -> Cancelled: def suspend_then_promote(self, cancellable, other: Thread) -> Cancelled: assert(self.running()) - if other.ready(): + if self.task.deliver_pending_cancel(cancellable): + return Cancelled.TRUE + if current_task().promotable(other): other.stop_waiting_internal(cancelled = False) return self.suspend_then_resume(cancellable, other) else: @@ -428,7 +432,9 @@ def suspend_then_promote(self, cancellable, other: Thread) -> Cancelled: def yield_then_promote(self, cancellable, other: Thread) -> Cancelled: assert(self.running()) - if other.ready(): + if self.task.deliver_pending_cancel(cancellable): + return Cancelled.TRUE + if current_task().promotable(other): other.stop_waiting_internal(cancelled = False) return self.yield_then_resume(cancellable, other) else: @@ -474,13 +480,21 @@ def needs_exclusive(self): assert(self.ft.async_) return not self.opts.async_ or self.opts.callback + def promotable(self, thread): + return (thread.ready() + and (thread is not thread.task.implicit_thread + or (not thread.task.ft.async_ and thread.task is self) + or (thread.task.ft.async_ and not thread.task.needs_exclusive()))) + def enter_implicit_thread(self): assert(self.state == Task.State.INITIAL) self.implicit_thread = current_thread() if self.ft.async_: def has_backpressure(): - return (self.inst.backpressure > 0 or - (self.needs_exclusive() and self.inst.exclusive_thread is not None)) + return (self.inst.backpressure > 0 + or (self.needs_exclusive() + and (self.inst.exclusive_thread is not None + or self.inst.sync_depth > 0))) if has_backpressure() or self.inst.num_waiting_to_enter > 0: self.inst.num_waiting_to_enter += 1 cancelled = self.implicit_thread.wait_until(lambda: not has_backpressure(), cancellable = True) @@ -491,6 +505,8 @@ def has_backpressure(): if self.needs_exclusive(): assert(self.inst.exclusive_thread is None) self.inst.exclusive_thread = self.implicit_thread + else: + self.inst.sync_depth += 1 self.register_thread(self.implicit_thread) return True @@ -503,9 +519,12 @@ def register_thread(self, thread): def exit_implicit_thread(self): assert(current_thread() is self.implicit_thread) self.unregister_thread(self.implicit_thread) - if self.ft.async_ and self.needs_exclusive(): - assert(self.inst.exclusive_thread is self.implicit_thread) - self.inst.exclusive_thread = None + if self.ft.async_: + if self.needs_exclusive(): + assert(self.inst.exclusive_thread is self.implicit_thread) + self.inst.exclusive_thread = None + else: + self.inst.sync_depth -= 1 def unregister_thread(self, thread): assert(thread in self.threads and thread.task is self) @@ -522,9 +541,12 @@ def request_cancellation(self, caller: Optional[ComponentInstance]): self.implicit_thread.resume(Cancelled.TRUE) else: assert(self.state == Task.State.STARTED) - candidates = { t for t in self.threads if t.cancellable } - if self.needs_exclusive() and self.inst.exclusive_thread not in { None, self.implicit_thread }: - candidates.discard(self.implicit_thread) + def exclusive_conflict(thread): + return (self.needs_exclusive() + and thread is self.implicit_thread + and (self.inst.exclusive_thread not in { None, self.implicit_thread } + or self.inst.sync_depth > 0)) + candidates = { t for t in self.threads if t.cancellable and not exclusive_conflict(t) } if candidates and self.inst.may_enter_from(caller): self.state = Task.State.CANCEL_DELIVERED self.inst.enter_from(caller) @@ -2212,7 +2234,7 @@ def thread_func(): thread.resume() if not ft.async_: while task.state != Task.State.RESOLVED: - candidates = { t for t in inst.threads if t.ready() and t is not inst.exclusive_thread } + candidates = { t for t in inst.threads if task.promotable(t) } trap_if(not candidates) random.choice(list(candidates)).resume() return task.request_cancellation diff --git a/design/mvp/canonical-abi/run_tests.py b/design/mvp/canonical-abi/run_tests.py index 619e5f42..a6c15eb6 100644 --- a/design/mvp/canonical-abi/run_tests.py +++ b/design/mvp/canonical-abi/run_tests.py @@ -2949,6 +2949,106 @@ def on_resolve(v): assert(result == 42) assert(other_result == 43) +def test_promotable(): + store = Store() + inst = ComponentInstance(store) + + run_done = False + cb_threadi = None + cb_exited = False + cb_ft = FuncType([], [U32Type()], async_ = True) + cb_opts = mk_opts(async_ = True) + def core_cb(args): + assert(not args) + nonlocal cb_threadi + [cb_threadi] = canon_thread_index() + [] = canon_task_return([U32Type()], cb_opts, [1]) + return [CallbackCode.YIELD] + def core_cb_callback(args): + [event,payload1,payload2] = args + assert(event == EventCode.NONE and payload1 == 0 and payload2 == 0) + assert(run_done) + nonlocal cb_exited + cb_exited = True + return [CallbackCode.EXIT] + cb_opts.callback = core_cb_callback + cb_result = None + def on_cb_resolve(v): + nonlocal cb_result + [cb_result] = v + _ = store.invoke(store.lift(core_cb, cb_ft, cb_opts, inst), lambda:[], on_cb_resolve) + assert(cb_result == 1) + + sf_ft = FuncType([], [U32Type()], async_ = True) + sf_opts = mk_opts(async_ = True) + + sf1_done = False + def core_sf1(args): + assert(not args) + [] = canon_task_return([U32Type()], sf_opts, [2]) + [ret] = canon_thread_yield(False) + assert(ret == Cancelled.FALSE) + nonlocal sf1_done + sf1_done = True + return [] + sf1_result = None + def on_sf1_resolve(v): + nonlocal sf1_result + [sf1_result] = v + _ = store.invoke(store.lift(core_sf1, sf_ft, sf_opts, inst), lambda:[], on_sf1_resolve) + assert(sf1_result == 2) + assert(not sf1_done) + + sf2_threadi = None + sf2_done = False + def core_sf2(args): + assert(not args) + nonlocal sf2_threadi + [sf2_threadi] = canon_thread_index() + [] = canon_task_return([U32Type()], sf_opts, [3]) + [ret] = canon_thread_suspend(False) + assert(ret == Cancelled.FALSE) + nonlocal sf2_done + sf2_done = True + return [] + sf2_result = None + def on_sf2_resolve(v): + nonlocal sf2_result + [sf2_result] = v + _ = store.invoke(store.lift(core_sf2, sf_ft, sf_opts, inst), lambda:[], on_sf2_resolve) + assert(sf2_result == 3) + assert(not sf2_done) + + def core_run(args): + assert(not args) + + while not sf1_done: + [ret] = canon_thread_yield(False) + assert(ret == Cancelled.FALSE) + + [] = canon_thread_resume_later(sf2_threadi) + assert(not sf2_done) + [ret] = canon_thread_yield_then_promote(False, sf2_threadi) + assert(ret == Cancelled.FALSE) + assert(sf2_done) + + for _ in range(10): + [ret] = canon_thread_yield_then_promote(False, cb_threadi) + assert(ret == Cancelled.FALSE) + + return [42] + + run_result = None + def on_run_resolve(v): + nonlocal run_result, run_done + [run_result] = v + run_done = True + + caller_ft = FuncType([], [U8Type()]) + lift_and_run(mk_opts(), inst, caller_ft, core_run, lambda:[], on_run_resolve) + assert(run_result == 42) + assert(cb_exited) + def test_thread_cancel_callback(): store = Store() root_inst = ComponentInstance(store) @@ -3020,6 +3120,134 @@ def core_consumer(args): lift_and_run(mk_opts(), consumer_inst, consumer_ft, core_consumer, lambda:[], lambda _:()) +def test_donut_sync_defers_async_enter(): + store = Store() + parent_inst = ComponentInstance(store) + child_inst = ComponentInstance(store, parent_inst) + + h_ran = RacyBool(False) + h_ft = FuncType([],[], async_ = True) + def h_core(args): + assert(len(args) == 0) + h_ran.set() + return [] + h = store.lift(h_core, h_ft, mk_opts(), parent_inst) + + child_mem = bytearray(16) + child_opts = mk_opts(memory = MemInst(child_mem, 'i32'), async_ = True) + subi = None + g_ft = FuncType([],[]) + def g_core(args): + nonlocal subi + assert(parent_inst.sync_depth == 1) + [ret] = store.lower(h, h_ft, child_opts, child_inst)([]) + state,subi = unpack_result(ret) + assert(state == Subtask.State.STARTING) + assert(h_ran.is_clear()) + return [] + g = store.lift(g_core, g_ft, mk_opts(), child_inst) + + f_ft = FuncType([],[]) + def f_core(args): + assert(len(args) == 0) + [] = store.lower(g, g_ft, mk_opts(), parent_inst)([]) + assert(h_ran.is_clear()) + return [] + + lift_and_run(mk_opts(), parent_inst, f_ft, f_core, lambda:[], lambda _:()) + assert(h_ran.is_set()) + + def finish_core(args): + [seti] = canon_waitable_set_new() + [] = canon_waitable_join(subi, seti) + retp = 8 + [event] = canon_waitable_set_wait(True, MemInst(child_mem, 'i32'), seti, retp) + assert(event == EventCode.SUBTASK) + assert(child_mem[retp+0] == subi) + assert(child_mem[retp+4] == Subtask.State.RETURNED) + [] = canon_subtask_drop(subi) + [] = canon_waitable_set_drop(seti) + return [] + lift_and_run(mk_opts(), child_inst, FuncType([],[]), finish_core, lambda:[], lambda _:()) + +def test_donut_sync_defers_cancellation(): + store = Store() + parent_inst = ComponentInstance(store) + child_inst = ComponentInstance(store, parent_inst) + + parent_mem = bytearray(16) + f_done = RacyBool(False) + h_ft = FuncType([FutureType(None)],[], async_ = True) + def h_core(args): + [rfut] = args + [ret] = canon_future_read(FutureType(None), mk_opts(MemInst(parent_mem, 'i32'), async_ = True), rfut, 0xdeadbeef) + assert(ret == definitions.BLOCKED) + [seti] = canon_waitable_set_new() + [] = canon_waitable_join(rfut, seti) + retp = 8 + [event] = canon_waitable_set_wait(True, MemInst(parent_mem, 'i32'), seti, retp) + assert(event == EventCode.FUTURE_READ) + assert(parent_mem[retp+0] == rfut) + assert(parent_mem[retp+4] == CopyResult.COMPLETED) + assert(f_done.is_set()) + [cancelled] = canon_thread_yield(True) + assert(cancelled == Cancelled.TRUE) + [] = canon_future_drop_readable(FutureType(None), rfut) + [] = canon_waitable_set_drop(seti) + return [] + h = store.lift(h_core, h_ft, mk_opts(), parent_inst) + + child_mem = bytearray(24) + child_async_opts = mk_opts(memory = MemInst(child_mem, 'i32'), async_ = True) + subi = None + wfut = None + setup_ft = FuncType([],[]) + def setup_core(args): + nonlocal subi, wfut + [packed] = canon_future_new(FutureType(None)) + rfut,wfut = unpack_new_ends(packed) + [ret] = store.lower(h, h_ft, child_async_opts, child_inst)([rfut]) + state,subi = unpack_result(ret) + assert(state == Subtask.State.STARTED) + return [] + _ = store.invoke(store.lift(setup_core, setup_ft, mk_opts(), child_inst), lambda:[], lambda _:()) + assert(parent_inst.exclusive_thread is not None) + + do_cancel_ft = FuncType([],[]) + def do_cancel_core(args): + assert(parent_inst.sync_depth == 1) + [ret] = canon_subtask_cancel(True, subi) + assert(ret == definitions.BLOCKED) + [ret] = canon_future_write(FutureType(None), mk_opts(MemInst(child_mem, 'i32')), wfut, 0xdeadbeef) + assert(ret == CopyResult.COMPLETED) + [] = canon_future_drop_writable(FutureType(None), wfut) + return [] + do_cancel = store.lift(do_cancel_core, do_cancel_ft, mk_opts(), child_inst) + + f_ft = FuncType([],[]) + def f_core(args): + assert(len(args) == 0) + [] = store.lower(do_cancel, do_cancel_ft, mk_opts(), parent_inst)([]) + f_done.set() + return [] + _ = store.invoke(store.lift(f_core, f_ft, mk_opts(), parent_inst), lambda:[], lambda _:()) + + while store.waiting: + store.tick() + + def finish_core(args): + [seti] = canon_waitable_set_new() + [] = canon_waitable_join(subi, seti) + retp = 8 + [event] = canon_waitable_set_wait(True, MemInst(child_mem, 'i32'), seti, retp) + assert(event == EventCode.SUBTASK) + assert(child_mem[retp+0] == subi) + assert(child_mem[retp+4] == Subtask.State.RETURNED) + [] = canon_subtask_drop(subi) + [] = canon_waitable_set_drop(seti) + return [] + lift_and_run(mk_opts(), child_inst, FuncType([],[]), finish_core, lambda:[], lambda _:()) + test_roundtrips() test_cross_component_realloc() test_handles() @@ -3046,6 +3274,9 @@ def core_consumer(args): test_async_flat_params() test_threads() test_sync_threads() +test_promotable() test_thread_cancel_callback() +test_donut_sync_defers_async_enter() +test_donut_sync_defers_cancellation() print("All tests passed") diff --git a/test/async/during-sync-call-no-exclusive-resume.wast b/test/async/during-sync-call-no-exclusive-resume.wast index 1298fe8e..baa552ae 100644 --- a/test/async/during-sync-call-no-exclusive-resume.wast +++ b/test/async/during-sync-call-no-exclusive-resume.wast @@ -160,4 +160,4 @@ (canon lift (core func $core "sync-block"))) ) (assert_return (invoke "arm") (u32.const 1)) -(assert_trap (invoke "sync-block") "deadlock detected: event loop cannot make further progress") +(assert_trap (invoke "sync-block") "cannot block a synchronous task before returning") diff --git a/test/async/during-sync-call-no-sibling-resume.wast b/test/async/during-sync-call-no-sibling-resume.wast index d3ff9e05..63d5e892 100644 --- a/test/async/during-sync-call-no-sibling-resume.wast +++ b/test/async/during-sync-call-no-sibling-resume.wast @@ -212,4 +212,4 @@ (func (export "sync-block") (alias export $inner "sync-block")) ) (assert_return (invoke "arm")) -(assert_trap (invoke "sync-block") "deadlock detected: event loop cannot make further progress") +(assert_trap (invoke "sync-block") "cannot block a synchronous task before returning") diff --git a/test/async/promotable-candidates.wast b/test/async/promotable-candidates.wast new file mode 100644 index 00000000..1a78812b --- /dev/null +++ b/test/async/promotable-candidates.wast @@ -0,0 +1,496 @@ +;; Test the thread "promotion" predicate, which is used in two cases: +;; - the thread.{suspend,yield}-then-promote built-ins +;; - the implicit promotion that happens during a sync-typed function +;; when the implicit thread blocks but other threads (which may unblock +;; the implicit thread) can be resumed +;; +;; The following tests test the various cases of which threads are allowed or +;; disallowed to be promoted. Each component below tests one such case in both +;; contexts, side by side: 'run' blocks with plain thread.yield, so promotion +;; happens implicitly as part of sync-call scheduling, while 'run-promote' +;; targets the thread in question explicitly with thread.yield-then-promote. +;; For allowed threads, both must (eventually) promote the target thread; for +;; excluded threads, the target thread must never run during the +;; non-async-typed call, with the promote built-ins falling back to plain +;; thread.{suspend,yield} behavior. + +;; Allowed: any explicit thread (including of the sync task itself) +(component + (core module $Table (table (export "__indirect_function_table") 1 funcref)) + (core instance $table (instantiate $Table)) + (core module $Core + (import "" "thread.new-indirect" (func $thread.new-indirect (param i32 i32) (result i32))) + (import "" "thread.resume-later" (func $thread.resume-later (param i32))) + (import "" "thread.yield" (func $thread.yield (result i32))) + (import "" "thread.yield-then-promote" (func $thread.yield-then-promote (param i32) (result i32))) + (import "" "__indirect_function_table" (table $tbl 1 funcref)) + + (global $worker-ran (mut i32) (i32.const 0)) + (global $worker-thread (mut i32) (i32.const 0)) + + (func $worker (param i32) + (global.set $worker-ran (i32.const 1))) + (elem (table $tbl) (i32.const 0) func $worker) + + (func $spawn-worker + (global.set $worker-ran (i32.const 0)) + (global.set $worker-thread (call $thread.new-indirect (i32.const 0) (i32.const 0))) + (call $thread.resume-later (global.get $worker-thread))) + + (func (export "run") (result i32) + (call $spawn-worker) + (loop $again + (drop (call $thread.yield)) + (br_if $again (i32.eqz (global.get $worker-ran)))) + (i32.const 42)) + + (func (export "run-promote") (result i32) + (call $spawn-worker) + (loop $again + (drop (call $thread.yield-then-promote (global.get $worker-thread))) + (br_if $again (i32.eqz (global.get $worker-ran)))) + (i32.const 42)) + ) + (core type $start-func-ty (func (param i32))) + (alias core export $table "__indirect_function_table" (core table $indirect-function-table)) + (core func $thread.new-indirect + (canon thread.new-indirect $start-func-ty (core table $indirect-function-table))) + (canon thread.resume-later (core func $thread.resume-later)) + (canon thread.yield (core func $thread.yield)) + (canon thread.yield-then-promote (core func $thread.yield-then-promote)) + (core instance $core (instantiate $Core (with "" (instance + (export "thread.new-indirect" (func $thread.new-indirect)) + (export "thread.resume-later" (func $thread.resume-later)) + (export "thread.yield" (func $thread.yield)) + (export "thread.yield-then-promote" (func $thread.yield-then-promote)) + (export "__indirect_function_table" (table $indirect-function-table)) + )))) + (func (export "run") (result u32) + (canon lift (core func $core "run"))) + (func (export "run-promote") (result u32) + (canon lift (core func $core "run-promote"))) +) +(assert_return (invoke "run") (u32.const 42)) +(assert_return (invoke "run-promote") (u32.const 42)) + +;; Allowed: implicit thread of a stackful async task +(component + (core module $Core + (import "" "task.return" (func $task.return (param i32))) + (import "" "thread.index" (func $thread.index (result i32))) + (import "" "thread.yield" (func $thread.yield (result i32))) + (import "" "thread.yield-then-promote" (func $thread.yield-then-promote (param i32) (result i32))) + + (global $finished (mut i32) (i32.const 0)) + (global $setup-thread (mut i32) (i32.const 0)) + + (func (export "setup") + (global.set $finished (i32.const 0)) + (global.set $setup-thread (call $thread.index)) + (call $task.return (i32.const 1)) + (drop (call $thread.yield)) + (global.set $finished (i32.const 1))) + + (func (export "run") (result i32) + (loop $again + (drop (call $thread.yield)) + (br_if $again (i32.eqz (global.get $finished)))) + (i32.const 42)) + + (func (export "run-promote") (result i32) + ;; check $finished before promoting: setup's yield may + ;; nondeterministically complete without suspending, in which case + ;; setup's thread index is already gone + (block $done + (loop $again + (br_if $done (global.get $finished)) + (drop (call $thread.yield-then-promote (global.get $setup-thread))) + (br $again))) + (i32.const 42)) + ) + (canon task.return (result u32) (core func $task.return)) + (canon thread.index (core func $thread.index)) + (canon thread.yield (core func $thread.yield)) + (canon thread.yield-then-promote (core func $thread.yield-then-promote)) + (core instance $core (instantiate $Core (with "" (instance + (export "task.return" (func $task.return)) + (export "thread.index" (func $thread.index)) + (export "thread.yield" (func $thread.yield)) + (export "thread.yield-then-promote" (func $thread.yield-then-promote)) + )))) + (func (export "setup") async (result u32) + (canon lift (core func $core "setup") async)) + (func (export "run") (result u32) + (canon lift (core func $core "run"))) + (func (export "run-promote") (result u32) + (canon lift (core func $core "run-promote"))) +) +(assert_return (invoke "setup") (u32.const 1)) +(assert_return (invoke "run") (u32.const 42)) +(assert_return (invoke "setup") (u32.const 1)) +(assert_return (invoke "run-promote") (u32.const 42)) + +;; Excluded: implicit thread of an async callback task waiting in an event loop +(component + (component $C + (core module $Core + (import "" "task.return" (func $task.return (param i32))) + (import "" "thread.index" (func $thread.index (result i32))) + (import "" "thread.yield" (func $thread.yield (result i32))) + (import "" "thread.yield-then-promote" (func $thread.yield-then-promote (param i32) (result i32))) + (import "" "thread.suspend-then-promote" (func $thread.suspend-then-promote (param i32) (result i32))) + + (global $setup-thread (mut i32) (i32.const 0)) + (global $in-sync-call (mut i32) (i32.const 0)) + + (func (export "setup") (result i32) + (global.set $setup-thread (call $thread.index)) + (call $task.return (i32.const 1)) + (i32.const 1 (; YIELD ;))) + + ;; Since setup's YIELD may nondeterministically complete without + ;; suspending, 'setup-cb' may be called (with a NONE event) while no + ;; non-async-typed call is in progress and parks the thread again; it + ;; must never be called during 'run*'. + (func (export "setup-cb") (param i32 i32 i32) (result i32) + (if (global.get $in-sync-call) + (then unreachable)) + (i32.const 1 (; YIELD ;))) + + (func (export "run") (result i32) + (local $i i32) + (global.set $in-sync-call (i32.const 1)) + (loop $again + (drop (call $thread.yield)) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br_if $again (i32.lt_u (local.get $i) (i32.const 50)))) + (global.set $in-sync-call (i32.const 0)) + (i32.const 42)) + + (func (export "run-promote") (result i32) + (local $i i32) + (global.set $in-sync-call (i32.const 1)) + (loop $again + (drop (call $thread.yield-then-promote (global.get $setup-thread))) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br_if $again (i32.lt_u (local.get $i) (i32.const 50)))) + (global.set $in-sync-call (i32.const 0)) + (i32.const 42)) + + (func (export "run-suspend-promote") + (global.set $in-sync-call (i32.const 1)) + (drop (call $thread.suspend-then-promote (global.get $setup-thread))) + unreachable) + ) + (canon task.return (result u32) (core func $task.return)) + (canon thread.index (core func $thread.index)) + (canon thread.yield (core func $thread.yield)) + (canon thread.yield-then-promote (core func $thread.yield-then-promote)) + (canon thread.suspend-then-promote (core func $thread.suspend-then-promote)) + (core instance $core (instantiate $Core (with "" (instance + (export "task.return" (func $task.return)) + (export "thread.index" (func $thread.index)) + (export "thread.yield" (func $thread.yield)) + (export "thread.yield-then-promote" (func $thread.yield-then-promote)) + (export "thread.suspend-then-promote" (func $thread.suspend-then-promote)) + )))) + (func (export "setup") async (result u32) + (canon lift (core func $core "setup") async (callback (core func $core "setup-cb")))) + (func (export "run") (result u32) + (canon lift (core func $core "run"))) + (func (export "run-promote") (result u32) + (canon lift (core func $core "run-promote"))) + (func (export "run-suspend-promote") + (canon lift (core func $core "run-suspend-promote"))) + ) + (component $D + (import "run" (func $run (result u32))) + (core module $Core + (import "" "task.return" (func $task.return (param i32))) + (import "" "run" (func $run (result i32))) + + (func (export "driver") (result i32) + (call $task.return (call $run)) + (i32.const 0 (; EXIT ;))) + + (func (export "driver-cb") (param i32 i32 i32) (result i32) + unreachable) + ) + (canon task.return (result u32) (core func $task.return)) + (canon lower (func $run) (core func $run')) + (core instance $core (instantiate $Core (with "" (instance + (export "task.return" (func $task.return)) + (export "run" (func $run')) + )))) + (func (export "driver") async (result u32) + (canon lift (core func $core "driver") async (callback (core func $core "driver-cb")))) + ) + (instance $c (instantiate $C)) + (instance $d (instantiate $D (with "run" (func $c "run")))) + (func (export "setup") (alias export $c "setup")) + (func (export "run") (alias export $c "run")) + (func (export "run-promote") (alias export $c "run-promote")) + (func (export "run-suspend-promote") (alias export $c "run-suspend-promote")) + (func (export "driver") (alias export $d "driver")) +) +(assert_return (invoke "setup") (u32.const 1)) +(assert_return (invoke "run") (u32.const 42)) +(assert_return (invoke "driver") (u32.const 42)) +(assert_return (invoke "run-promote") (u32.const 42)) +(assert_trap (invoke "run-suspend-promote") "cannot block a synchronous task before returning") + +;; Excluded: implicit thread of an async callback task blocked not in the event +;; loop. Since 'run' and 'run-promote' each make setup's suspended thread ready +;; themselves, each runs in a fresh instance. +(component definition $BlockedCallbackTester + (core module $Core + (import "" "task.return" (func $task.return (param i32))) + (import "" "thread.index" (func $thread.index (result i32))) + (import "" "thread.suspend" (func $thread.suspend (result i32))) + (import "" "thread.resume-later" (func $thread.resume-later (param i32))) + (import "" "thread.yield" (func $thread.yield (result i32))) + (import "" "thread.yield-then-promote" (func $thread.yield-then-promote (param i32) (result i32))) + + (global $setup-thread (mut i32) (i32.const 0)) + + (func (export "setup") (result i32) + (global.set $setup-thread (call $thread.index)) + (call $task.return (i32.const 1)) + (drop (call $thread.suspend)) + unreachable) + + (func (export "setup-cb") (param i32 i32 i32) (result i32) + unreachable) + + (func (export "run") (result i32) + (local $i i32) + (call $thread.resume-later (global.get $setup-thread)) + (loop $again + (drop (call $thread.yield)) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br_if $again (i32.lt_u (local.get $i) (i32.const 50)))) + (i32.const 42)) + + (func (export "run-promote") (result i32) + (local $i i32) + (call $thread.resume-later (global.get $setup-thread)) + (loop $again + (drop (call $thread.yield-then-promote (global.get $setup-thread))) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br_if $again (i32.lt_u (local.get $i) (i32.const 50)))) + (i32.const 42)) + ) + (canon task.return (result u32) (core func $task.return)) + (canon thread.index (core func $thread.index)) + (canon thread.suspend (core func $thread.suspend)) + (canon thread.resume-later (core func $thread.resume-later)) + (canon thread.yield (core func $thread.yield)) + (canon thread.yield-then-promote (core func $thread.yield-then-promote)) + (core instance $core (instantiate $Core (with "" (instance + (export "task.return" (func $task.return)) + (export "thread.index" (func $thread.index)) + (export "thread.suspend" (func $thread.suspend)) + (export "thread.resume-later" (func $thread.resume-later)) + (export "thread.yield" (func $thread.yield)) + (export "thread.yield-then-promote" (func $thread.yield-then-promote)) + )))) + (func (export "setup") async (result u32) + (canon lift (core func $core "setup") async (callback (core func $core "setup-cb")))) + (func (export "run") (result u32) + (canon lift (core func $core "run"))) + (func (export "run-promote") (result u32) + (canon lift (core func $core "run-promote"))) +) + +(component instance $i $BlockedCallbackTester) +(assert_return (invoke "setup") (u32.const 1)) +(assert_return (invoke "run") (u32.const 42)) + +(component instance $i $BlockedCallbackTester) +(assert_return (invoke "setup") (u32.const 1)) +(assert_return (invoke "run-promote") (u32.const 42)) + +;; Excluded: implicit thread of a synchronously-lifted async-typed function. +(component definition $SyncLiftedTester + (component $I + (core module $Core + (import "" "thread.index" (func $thread.index (result i32))) + (import "" "thread.suspend" (func $thread.suspend (result i32))) + (import "" "thread.resume-later" (func $thread.resume-later (param i32))) + (import "" "thread.yield" (func $thread.yield (result i32))) + (import "" "thread.yield-then-promote" (func $thread.yield-then-promote (param i32) (result i32))) + + (global $f-started (mut i32) (i32.const 0)) + (global $f-thread (mut i32) (i32.const 0)) + + (func (export "f") + (global.set $f-started (i32.const 1)) + (global.set $f-thread (call $thread.index)) + (drop (call $thread.suspend)) + unreachable) + + (func (export "run") (result i32) + (local $i i32) + (if (i32.eqz (global.get $f-started)) + (then unreachable)) + (call $thread.resume-later (global.get $f-thread)) + (loop $again + (drop (call $thread.yield)) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br_if $again (i32.lt_u (local.get $i) (i32.const 50)))) + (i32.const 42)) + + (func (export "run-promote") (result i32) + (local $i i32) + (if (i32.eqz (global.get $f-started)) + (then unreachable)) + (call $thread.resume-later (global.get $f-thread)) + (loop $again + (drop (call $thread.yield-then-promote (global.get $f-thread))) + (local.set $i (i32.add (local.get $i) (i32.const 1))) + (br_if $again (i32.lt_u (local.get $i) (i32.const 50)))) + (i32.const 42)) + ) + (canon thread.index (core func $thread.index)) + (canon thread.suspend (core func $thread.suspend)) + (canon thread.resume-later (core func $thread.resume-later)) + (canon thread.yield (core func $thread.yield)) + (canon thread.yield-then-promote (core func $thread.yield-then-promote)) + (core instance $core (instantiate $Core (with "" (instance + (export "thread.index" (func $thread.index)) + (export "thread.suspend" (func $thread.suspend)) + (export "thread.resume-later" (func $thread.resume-later)) + (export "thread.yield" (func $thread.yield)) + (export "thread.yield-then-promote" (func $thread.yield-then-promote)) + )))) + (func (export "f") async + (canon lift (core func $core "f"))) + (func (export "run") (result u32) + (canon lift (core func $core "run"))) + (func (export "run-promote") (result u32) + (canon lift (core func $core "run-promote"))) + ) + (component $D + (import "f" (func $f async)) + (core module $Core + (import "" "task.return" (func $task.return (param i32))) + (import "" "f" (func $f (result i32))) + + (func (export "setup") (result i32) + ;; The async-lowered call must come back blocked in the STARTED state. + (if (i32.ne (i32.and (call $f) (i32.const 0xf)) (i32.const 1 (; STARTED ;))) + (then unreachable)) + (call $task.return (i32.const 1)) + (i32.const 0 (; EXIT ;))) + + (func (export "setup-cb") (param i32 i32 i32) (result i32) + unreachable) + ) + (canon task.return (result u32) (core func $task.return)) + (canon lower (func $f) async (core func $f')) + (core instance $core (instantiate $Core (with "" (instance + (export "task.return" (func $task.return)) + (export "f" (func $f')) + )))) + (func (export "setup") async (result u32) + (canon lift (core func $core "setup") async (callback (core func $core "setup-cb")))) + ) + (instance $i (instantiate $I)) + (instance $d (instantiate $D (with "f" (func $i "f")))) + (func (export "setup") (alias export $d "setup")) + (func (export "run") (alias export $i "run")) + (func (export "run-promote") (alias export $i "run-promote")) +) + +(component instance $i $SyncLiftedTester) +(assert_return (invoke "setup") (u32.const 1)) +(assert_return (invoke "run") (u32.const 42)) + +(component instance $i $SyncLiftedTester) +(assert_return (invoke "setup") (u32.const 1)) +(assert_return (invoke "run-promote") (u32.const 42)) + +;; Allowed and excluded together: explicit threads may be promoted even when +;; their task is an async callback task whose own implicit thread may not. +(component + (core module $Table (table (export "__indirect_function_table") 2 funcref)) + (core instance $table (instantiate $Table)) + (core module $Core + (import "" "task.return" (func $task.return (param i32))) + (import "" "thread.new-indirect" (func $thread.new-indirect (param i32 i32) (result i32))) + (import "" "thread.resume-later" (func $thread.resume-later (param i32))) + (import "" "thread.yield" (func $thread.yield (result i32))) + (import "" "thread.yield-then-promote" (func $thread.yield-then-promote (param i32) (result i32))) + (import "" "__indirect_function_table" (table $tbl 2 funcref)) + + (global $worker0-ran (mut i32) (i32.const 0)) + (global $worker0-thread (mut i32) (i32.const 0)) + (global $worker1-ran (mut i32) (i32.const 0)) + (global $worker1-thread (mut i32) (i32.const 0)) + (global $in-sync-call (mut i32) (i32.const 0)) + + (func $worker0 (param i32) + (global.set $worker0-ran (i32.const 1))) + (func $worker1 (param i32) + (global.set $worker1-ran (i32.const 1))) + (elem (table $tbl) (i32.const 0) func $worker0 $worker1) + + (func (export "setup") (result i32) + (global.set $worker0-thread (call $thread.new-indirect (i32.const 0) (i32.const 0))) + (global.set $worker1-thread (call $thread.new-indirect (i32.const 1) (i32.const 0))) + (call $task.return (i32.const 1)) + (i32.const 1 (; YIELD ;))) + + ;; Since setup's YIELD may nondeterministically complete without + ;; suspending, 'setup-cb' may be called (with a NONE event) while no + ;; non-async-typed call is in progress and parks the thread again; it + ;; must never be called during 'run*'. + (func (export "setup-cb") (param i32 i32 i32) (result i32) + (if (global.get $in-sync-call) + (then unreachable)) + (i32.const 1 (; YIELD ;))) + + (func (export "run") (result i32) + (global.set $in-sync-call (i32.const 1)) + (call $thread.resume-later (global.get $worker0-thread)) + (loop $again + (drop (call $thread.yield)) + (br_if $again (i32.eqz (global.get $worker0-ran)))) + (global.set $in-sync-call (i32.const 0)) + (i32.const 42)) + + (func (export "run-promote") (result i32) + (global.set $in-sync-call (i32.const 1)) + (call $thread.resume-later (global.get $worker1-thread)) + (loop $again + (drop (call $thread.yield-then-promote (global.get $worker1-thread))) + (br_if $again (i32.eqz (global.get $worker1-ran)))) + (global.set $in-sync-call (i32.const 0)) + (i32.const 42)) + ) + (core type $start-func-ty (func (param i32))) + (alias core export $table "__indirect_function_table" (core table $indirect-function-table)) + (core func $thread.new-indirect + (canon thread.new-indirect $start-func-ty (core table $indirect-function-table))) + (canon task.return (result u32) (core func $task.return)) + (canon thread.resume-later (core func $thread.resume-later)) + (canon thread.yield (core func $thread.yield)) + (canon thread.yield-then-promote (core func $thread.yield-then-promote)) + (core instance $core (instantiate $Core (with "" (instance + (export "task.return" (func $task.return)) + (export "thread.new-indirect" (func $thread.new-indirect)) + (export "thread.resume-later" (func $thread.resume-later)) + (export "thread.yield" (func $thread.yield)) + (export "thread.yield-then-promote" (func $thread.yield-then-promote)) + (export "__indirect_function_table" (table $indirect-function-table)) + )))) + (func (export "setup") async (result u32) + (canon lift (core func $core "setup") async (callback (core func $core "setup-cb")))) + (func (export "run") (result u32) + (canon lift (core func $core "run"))) + (func (export "run-promote") (result u32) + (canon lift (core func $core "run-promote"))) +) +(assert_return (invoke "setup") (u32.const 1)) +(assert_return (invoke "run") (u32.const 42)) +(assert_return (invoke "run-promote") (u32.const 42)) diff --git a/test/nyi.txt b/test/nyi.txt index ffb2d182..4778de28 100644 --- a/test/nyi.txt +++ b/test/nyi.txt @@ -2,3 +2,4 @@ ./async/during-sync-call-may-block-if-other-ready-threads.wast ./async/during-sync-call-no-exclusive-resume.wast ./async/during-sync-call-no-sibling-resume.wast +./async/promotable-candidates.wast