From 7559f7fd08f6a749ebcb664974757649ae3eae61 Mon Sep 17 00:00:00 2001 From: Luke Wagner Date: Tue, 11 Aug 2026 13:37:14 -0500 Subject: [PATCH] Fix CABI definition of thread promotability and add tests --- design/mvp/CanonicalABI.md | 75 ++- design/mvp/Concurrency.md | 9 +- design/mvp/Explainer.md | 26 +- design/mvp/canonical-abi/definitions.py | 14 +- design/mvp/canonical-abi/run_tests.py | 101 ++++ .../during-sync-call-no-exclusive-resume.wast | 2 +- .../during-sync-call-no-sibling-resume.wast | 2 +- test/async/promotable-candidates.wast | 496 ++++++++++++++++++ test/nyi.txt | 1 + 9 files changed, 677 insertions(+), 49 deletions(-) create mode 100644 test/async/promotable-candidates.wast diff --git a/design/mvp/CanonicalABI.md b/design/mvp/CanonicalABI.md index 621296b5..c731f642 100644 --- a/design/mvp/CanonicalABI.md +++ b/design/mvp/CanonicalABI.md @@ -695,15 +695,21 @@ 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", which means: +* the thread is in the `ready` state (not in the `suspended` state; this allows + one thread to give another thread in an unknown state a scheduling "boost" + with `pthread_join` being an example use case); and +* the thread does not assume exclusive use of a global linear memory shadow + stack (as defined below as part of `Task`). + +If the `other` thread is indeed promotable, 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* promotable, then these operations fall back +to plain `suspend` or `yield_` behavior, resp. ```python def suspend_then_promote(self, cancellable, other: Thread) -> Cancelled: assert(self.running()) - if other.ready(): + if other.promotable(): other.stop_waiting_internal(cancelled = False) return self.suspend_then_resume(cancellable, other) else: @@ -711,11 +717,14 @@ 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 other.promotable(): other.stop_waiting_internal(cancelled = False) return self.yield_then_resume(cancellable, other) else: return self.yield_(cancellable) + + def promotable(self): + return self.ready() and not Task.thread_needs_exclusive(self) ``` @@ -808,6 +817,19 @@ holding the lock. return not self.opts.async_ or self.opts.callback ``` +Building on this *task*-level definition, the following function defines when +a particular *thread* needs exclusive access to a global linear memory shadow +stack, which isn't simply `thread.task.needs_exclusive()` since, according to +[Component Invariant] #3, explicit threads can be assumed to have their own +stack and non-`async`-typed functions can be assumed to execute in a LIFO +fashion (on top of any existing stack): +```python + def thread_needs_exclusive(thread): + return (thread is thread.task.implicit_thread + and thread.task.ft.async_ + and 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 @@ -3752,32 +3774,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 `Thread.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 t.promotable() } 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 +5093,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 `Thread.promotable`, leaving the [current thread] +suspended in any case. ```python def canon_thread_suspend_then_promote(cancellable, i): thread = current_thread() @@ -5103,9 +5122,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 `Thread.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..9db67f55 100644 --- a/design/mvp/Concurrency.md +++ b/design/mvp/Concurrency.md @@ -406,10 +406,11 @@ 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 -[`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 +also switch to threads that are in a "ready to run" state and have no need +for exclusive access to a global shadow stack ([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 thread *may* know that the target thread is ready to run (e.g., because the target thread is known to have yielded or to be waiting on a future/stream operation that the calling thread just completed). However, in general, diff --git a/design/mvp/Explainer.md b/design/mvp/Explainer.md index 5ae72e9f..9ecc2571 100644 --- a/design/mvp/Explainer.md +++ b/design/mvp/Explainer.md @@ -2271,10 +2271,11 @@ 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` -returns whether the current task was [cancelled] by the caller; otherwise, -`thread.suspend-then-promote` always returns `false`. +thread `t` if `t` is "ready" and has no requirements of exclusive access to a +global linear memory shadow stack ([Component Invariant] #3), in any case +leaving the current thread in a "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`. For details, see [Thread Built-ins] in the concurrency explainer and [`canon_thread_suspend_then_promote`] in the Canonical ABI explainer. @@ -2287,10 +2288,11 @@ 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.yield-then-promote` always returns `false`. +thread `t` if `t` is "ready" and has no requirements of exclusive access to a +global linear memory shadow stack ([Component Invariant] #3), 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.yield-then-promote` always returns `false`. For details, see [Thread Built-ins] in the concurrency explainer and [`canon_thread_yield_then_promote`] in the Canonical ABI explainer. @@ -3005,10 +3007,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 @@ -3336,6 +3337,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..05229310 100644 --- a/design/mvp/canonical-abi/definitions.py +++ b/design/mvp/canonical-abi/definitions.py @@ -420,7 +420,7 @@ 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 other.promotable(): other.stop_waiting_internal(cancelled = False) return self.suspend_then_resume(cancellable, other) else: @@ -428,12 +428,15 @@ 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 other.promotable(): other.stop_waiting_internal(cancelled = False) return self.yield_then_resume(cancellable, other) else: return self.yield_(cancellable) + def promotable(self): + return self.ready() and not Task.thread_needs_exclusive(self) + ### Tasks OnStart = Callable[[], list[any]] @@ -474,6 +477,11 @@ def needs_exclusive(self): assert(self.ft.async_) return not self.opts.async_ or self.opts.callback + def thread_needs_exclusive(thread): + return (thread is thread.task.implicit_thread + and thread.task.ft.async_ + and thread.task.needs_exclusive()) + def enter_implicit_thread(self): assert(self.state == Task.State.INITIAL) self.implicit_thread = current_thread() @@ -2212,7 +2220,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 t.promotable() } 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..dbbf58fb 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) @@ -3046,6 +3146,7 @@ def core_consumer(args): test_async_flat_params() test_threads() test_sync_threads() +test_promotable() test_thread_cancel_callback() 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