From 1fbe350e8126f294441965465eac9be7221fb49a Mon Sep 17 00:00:00 2001 From: Luke Wagner Date: Thu, 27 Aug 2026 12:19:22 -0500 Subject: [PATCH] Remove 'cancellable' immediate from built-ins --- design/mvp/Binary.md | 20 +- design/mvp/CanonicalABI.md | 672 ++++++++++------------ design/mvp/Concurrency.md | 68 +-- design/mvp/Explainer.md | 128 ++--- design/mvp/canonical-abi/definitions.py | 278 ++++----- design/mvp/canonical-abi/run_tests.py | 545 +++++++++--------- test/async/big-interleaving-test.wast | 57 +- test/async/cancel-and-exclusive-lock.wast | 8 +- test/async/cancel-delivery.wast | 278 +++++++++ test/async/cancel-subtask.wast | 22 +- test/async/cancellable.wast | 404 ------------- test/async/reentrance.wast | 119 +++- test/binary/binary.wast | 106 +++- test/nyi.txt | 1 + 14 files changed, 1313 insertions(+), 1393 deletions(-) create mode 100644 test/async/cancel-delivery.wast delete mode 100644 test/async/cancellable.wast diff --git a/design/mvp/Binary.md b/design/mvp/Binary.md index 3915bb75..46c7f81c 100644 --- a/design/mvp/Binary.md +++ b/design/mvp/Binary.md @@ -327,26 +327,24 @@ canon ::= 0x00 0x00 f: opts: ft: => (canon lift | 0x1d opts: => (canon error-context.debug-message opts (core func)) ๐Ÿ“ | 0x1e => (canon error-context.drop (core func)) ๐Ÿ“ | 0x1f => (canon waitable-set.new (core func)) ๐Ÿ”€ - | 0x20 cancel?: m: => (canon waitable-set.wait cancel? (memory m) (core func)) ๐Ÿ”€ - | 0x21 cancel?: m: => (canon waitable-set.poll cancel? (memory m) (core func)) ๐Ÿ”€ + | 0x20 0x00 m: => (canon waitable-set.wait (memory m) (core func)) ๐Ÿ”€ + | 0x21 0x00 m: => (canon waitable-set.poll (memory m) (core func)) ๐Ÿ”€ | 0x22 => (canon waitable-set.drop (core func)) ๐Ÿ”€ | 0x23 => (canon waitable.join (core func)) ๐Ÿ”€ | 0x26 => (canon thread.index (core func)) ๐Ÿงต | 0x27 ft: tbl: => (canon thread.new-indirect ft tbl (core func)) ๐Ÿงต | 0x28 => (canon thread.resume-later (core func)) ๐Ÿงต - | 0x29 cancel?: => (canon thread.suspend cancel? (core func)) ๐Ÿงต - | 0x0c cancel?: => (canon thread.yield cancel? (core func)) ๐Ÿ”€ - | 0x2a cancel?: => (canon thread.suspend-then-resume cancel? (core func)) ๐Ÿงต - | 0x2b cancel?: => (canon thread.yield-then-resume cancel? (core func)) ๐Ÿงต - | 0x2c cancel?: => (canon thread.suspend-then-promote cancel? (core func)) ๐Ÿงต - | 0x2d cancel?: => (canon thread.yield-then-promote cancel? (core func)) ๐Ÿงต + | 0x29 0x00 => (canon thread.suspend (core func)) ๐Ÿงต + | 0x0c 0x00 => (canon thread.yield (core func)) ๐Ÿ”€ + | 0x2a 0x00 => (canon thread.suspend-then-resume (core func)) ๐Ÿงต + | 0x2b 0x00 => (canon thread.yield-then-resume (core func)) ๐Ÿงต + | 0x2c 0x00 => (canon thread.suspend-then-promote (core func)) ๐Ÿงต + | 0x2d 0x00 => (canon thread.yield-then-promote (core func)) ๐Ÿงต | 0x40 shared?: ft: => (canon thread.spawn-ref shared? ft (core func)) ๐Ÿงตโ‘ก | 0x41 shared?: ft: tbl: => (canon thread.spawn-indirect shared? ft tbl (core func)) ๐Ÿงตโ‘ก | 0x42 shared?: => (canon thread.available-parallelism shared? (core func)) ๐Ÿงตโ‘ก async? ::= 0x00 => | 0x01 => async -cancel? ::= 0x00 => - | 0x01 => cancellable sh? ::= 0x00 => | 0x01 => shared ๐Ÿงตโ‘ก opts ::= opt*:vec() => opt* @@ -539,6 +537,8 @@ named once. * The two `list` type codes should be merged into one with an optional immediate and similarly for `func`. * The redundant `0x00` and `0x01` opcodes of `nameattributes` will be merged. +* The vestigial `0x00` in `thread.*` and `waitable-set.*` built-ins may be + removed. * Most built-ins should have a `*` immediate instead of an ad hoc subset of `canonopt`s. * Add optional `shared` immediate to all canonical definitions (explicitly or diff --git a/design/mvp/CanonicalABI.md b/design/mvp/CanonicalABI.md index 2c5c1431..24e2ef98 100644 --- a/design/mvp/CanonicalABI.md +++ b/design/mvp/CanonicalABI.md @@ -182,18 +182,21 @@ stack-switching, only that subset is implemented, which significantly simplifies things. In particular, the Component Model uses stack-switching in the following restricted manner: -First, there are only two global [control tags] used with `suspend`: +First, there are only three global [control tags] used with `suspend`: ```wat -(tag $block (param $switch-to (ref null $Thread)) (result $cancelled bool)) +(tag $block) +(tag $switch-to (param (ref $Thread))) (tag $current-thread (result (ref $Thread))) ``` -Consequently, instead of having a single generic Python `suspend()` function, -there are `block()` and `current_thread()` Python functions that implement -`suspend $block` and `suspend $current-thread`, resp. +Consequently, instead of having a single generic Python `suspend` function, +there are `block`, `switch_to` and `current_thread` Python functions defined +below that implement `suspend $block`, `suspend $switch-to` and `suspend +$current-thread`, resp. -The `$block` tag is used to suspend a [thread] until some future event. The -parameters and results will be described in the next section, where they are -used to define `Thread`. +The `$block` tag is used to suspend execution until some future event. + +The `$switch-to` tag is used to suspend and then immediately resume the given +[thread]. (The next section will define what `Thread` is.) The `$current-thread` tag is used to retrieve the [current thread], which is semantically stored in the `resume` handler's local state (although an @@ -202,14 +205,16 @@ execution context (or a special Core WebAssembly `global`) so that it could be cheaply loaded and/or kept in register state). Second, there is only a single type of continuation passed to `resume` that -corresponds to the `$block` tag (`$current-thread` continuations are -immediately resumed and never "escape"): +corresponds to the merging of the `$block` and `$switch-to` tags, with `$block` +passing a null thread reference: ```wat -(type $ct (cont (func (param bool) (result (ref null $Thread))))) +(type $ct (cont (func (result (ref null $Thread))))) ``` +`$current-thread` continuations are immediately resumed and so never "escape" +and thus do not need a first-class continuation reference. -Third, every `resume` performed by the Canonical ABI always handles both -`$block` and `$current-thread` and every Canonical ABI `suspend` is, by +Third, every `resume` performed by the Canonical ABI always handles `$block`, +`$switch-to` and `$current-thread` and every Canonical ABI `suspend` is, by construction, always scoped by a Canonical ABI `resume`. Thus, every Canonical ABI `suspend` unconditionally transfers control flow directly to the innermost enclosing Canonical ABI `resume` without a general handler/tag search. @@ -227,20 +232,15 @@ expected to use (a pool of) fibers. Starting with `cont.new`, the monomorphized version takes a function type matching `$ct`, as defined above: ```python -class Cancelled(IntEnum): - FALSE = 0 - TRUE = 1 - class Continuation: lock: threading.Lock handler: Handler - block_result: Cancelled class Handler: lock: threading.Lock current_thread: Thread cont: Optional[Continuation] - block_arg: Optional[Thread] + switch_to: Optional[Thread] thread_local_handler = threading.local() @@ -249,66 +249,69 @@ def new_already_acquired_lock() -> threading.Lock: lock.acquire() return lock -def cont_new(f: Callable[[Cancelled], Optional[Thread]]) -> Continuation: +def cont_new(f: Callable[[], Optional[Thread]]) -> Continuation: cont = Continuation() cont.lock = new_already_acquired_lock() def thread_base(): cont.lock.acquire() thread_local_handler.value = cont.handler - block_arg = f(cont.block_result) + switch_to = f() handler = thread_local_handler.value handler.cont = None - handler.block_arg = block_arg + handler.switch_to = switch_to handler.lock.release() threading.Thread(target = thread_base).start() return cont ``` -`Continuation.block_result` and `Continuation.handler` are set by `resume` right -before `resume` calls `Continuation.lock.release()` to transfer control flow to -the continuation. After resuming the continuation, `resume` calls -`Handler.lock.acquire()` to wait until the continuation signals suspension or -return by calling `Handler.lock.release()`. The `Handler` is stored in -`thread_local_handler.value` to implement the dynamic scoping that is required -for `suspend`. Because the thread created by `cont_new` can be suspended and -resumed many times (each time with a new `Continuation` and `Handler`, resp.), -`Handler` must be re-loaded from `thread_local_handler.value` after `f` returns -since it may have changed since the initial `resume`. +`Continuation.handler` is set by `resume` right before `resume` calls +`Continuation.lock.release()` to transfer control flow to the continuation. +After resuming the continuation, `resume` calls `Handler.lock.acquire()` to wait +until the continuation signals suspension or return by calling +`Handler.lock.release()`. The `Handler` is stored in `thread_local_handler.value` +to implement the dynamic scoping that is required for `suspend`. Because the +thread created by `cont_new` can be suspended and resumed many times (each time +with a new `Continuation` and `Handler`, resp.), `Handler` must be re-loaded +from `thread_local_handler.value` after `f` returns since it may have changed +since the initial `resume`. Next, `resume` is monomorphized to take a continuation of type `$ct`, the argument to pass to the continuation, and the `Thread` to use to implement the -`(on $current-thread)` handler. The remaining `(on $block)` and "returned" cases -join to produce a single return value, with the `(on $block)` case returning a -`Continuation` + argument passed to `suspend $block` and the "returned" case -returning `None` + the continuation function's return value. +`(on $current-thread)` handler. The remaining `(on $block)`, `(on $switch-to)` +and "returned" cases join to produce a single return value, with the `(on *)` +cases returning a `Continuation` and the "returned" case returning `None`. ```python -def resume(cont: Continuation, block_result: Cancelled, current_thread: Thread) -> \ +def resume(cont: Continuation, current_thread: Thread) -> \ tuple[Optional[Continuation], Optional[Thread]]: handler = Handler() handler.lock = new_already_acquired_lock() handler.current_thread = current_thread cont.handler = handler - cont.block_result = block_result cont.lock.release() handler.lock.acquire() - return (handler.cont, handler.block_arg) + return (handler.cont, handler.switch_to) ``` -Next, the `block` function implements `suspend $block`, taking its signature -from the `$block` tag defined above. Following the locking scheme already -established by `cont_new` and `resume`, the implementation passes control flow -and event arguments back to the parent `resume` and then waits to be unblocked -by a future `resume` that provides the event results. +Next, the `block` and `switch_to` functions implement `suspend $block` and +`suspend $switch-to` in terms of a common `suspend` function. Following the +locking scheme already established by `cont_new` and `resume`, the +implementation passes control flow and event arguments back to the parent +`resume` and then waits to be unblocked by a future `resume`. ```python -def block(switch_to: Optional[Thread]) -> Cancelled: +def block(): + suspend(switch_to = None) + +def switch_to(other: Thread): + suspend(switch_to = other) + +def suspend(switch_to: Optional[Thread]): cont = Continuation() cont.lock = new_already_acquired_lock() handler = thread_local_handler.value handler.cont = cont - handler.block_arg = switch_to + handler.switch_to = switch_to handler.lock.release() cont.lock.acquire() thread_local_handler.value = cont.handler - return cont.block_result ``` Lastly, the `current_thread` function implements `suspend $current-thread`, @@ -330,14 +333,14 @@ def current_instance() -> ComponentInstance: return current_task().inst ``` -Once Core WebAssembly gets stack-switching, the Component Model's `$block` and -`$current-thread` tags would *not* be exposed to Core WebAssembly. Thus, an -optimizing implementation would continue to be able to implement `block()` as a -direct control flow transfer and `current_thread()` with implicit execution -context, both without a general handler/tag search. In particular, this avoids -the pathological O(N2) behavior which would otherwise arise if -Component Model cooperative threads were used in conjunction with deeply-nested -Core WebAssembly handlers. +Once Core WebAssembly gets stack-switching, the Component Model's `$block`, +`$switch-to` and `$current-thread` tags would *not* be exposed to Core +WebAssembly. Thus, an optimizing implementation would continue to be able to +implement `block()` and `switch_to()` as a direct control flow transfer and +`current_thread()` with implicit execution context, both without a general +handler/tag search. In particular, this avoids the pathological O(N2) +behavior which would otherwise arise if Component Model cooperative threads were +used in conjunction with deeply-nested Core WebAssembly handlers. Additionally, once Core WebAssembly has stack switching, any unhandled events that originate in Core WebAssembly would turn into traps if they reach a @@ -389,7 +392,6 @@ class Thread: cont: Optional[Continuation] ready_func: Optional[Callable[[], bool]] task: Task - cancellable: Callable[[], bool] index: Optional[int] storage: tuple[int,int] @@ -412,14 +414,13 @@ expected by `cont_new`) and leaving the thread initially in the `suspended` state. ```python def __init__(self, task, thread_func): - def cont_func(cancelled): - assert(self.running() and not cancelled) + def cont_func(): + assert(self.running()) thread_func() return None self.cont = cont_new(cont_func) self.ready_func = None self.task = task - self.cancellable = lambda: False self.index = None self.storage = [0,0] assert(self.suspended()) @@ -430,14 +431,13 @@ and remove a thread to the `Store.waiting` list at the same time as setting and clearing, resp., the readiness function that `Store.tick` (defined below) will test repeatedly to determine when the thread is ready to be resumed. ```python - def start_waiting_internal(self, ready_func): + def start_waiting(self, ready_func): assert(not self.waiting() and not self.ready_func) self.ready_func = ready_func self.task.inst.store.waiting.append(self) - def stop_waiting_internal(self, cancelled): + def stop_waiting(self): assert(self.waiting() and self.ready_func) - assert(cancelled or self.ready()) self.ready_func = None self.task.inst.store.waiting.remove(self) ``` @@ -449,133 +449,83 @@ switch execution to the thread but instead transitions the thread to the `ready` ```python def resume_later(self): assert(self.suspended()) - self.start_waiting_internal(lambda: True) + self.start_waiting(ready_func = lambda: True) assert(self.ready()) ``` -Once it's time to execute a `suspended` or `waiting` thread, `Thread.resume` -is called on that thread. This method transitions the thread to the -`running` state by clearing and then `resume`ing the `Thread`'s stored -continuation. If the `resume`d continuation suspends with a `Thread` to -`switch_to`, `Thread.resume` will `resume` *that* `Thread`'s continuation, and -so on, repeatedly, until the continuation either returns or suspends with no -thread to `switch_to`. +Once it's time to execute a `suspended` or `waiting` thread, `Thread.resume` is +called on that thread. This method transitions the thread to the `running` state +by clearing and then `resume`ing the `Thread`'s stored continuation. If the +`resume`d continuation suspends via `switch_to`, `Thread.resume` will `resume` +*that* `Thread`'s continuation, and so on, repeatedly, until the continuation +either returns or suspends with no thread to `switch_to`. ```python - def resume(self, cancelled = Cancelled.FALSE): - assert(not self.running() and (self.cancellable() or not cancelled)) + def resume(self): + assert(not self.running()) if self.waiting(): - self.stop_waiting_internal(cancelled) + self.stop_waiting() thread = self while thread is not None: cont = thread.cont thread.cont = None - (thread.cont, switch_to) = resume(cont, cancelled, thread) + (thread.cont, switch_to) = resume(cont, thread) thread = switch_to - cancelled = Cancelled.FALSE ``` -The `Thread.resume` method passes cancellation requests (from -`Task.request_cancellation` defined below) to the continuation being resumed, -allowing a thread that opted-in to being `cancellable` to be `resume`d even if -it's not `ready`. - Note that the `while` loop shown above is effectively implementing the `switch` instruction of the [stack-switching] proposal since `switch` is just an optimization of `suspend` followed by `resume`. The non-optimized version is used here to simplify storing of the new `Continuation` into `Thread.cont`. However, an optimized implementation could do the direct switch. -The next two `Thread` methods are only called by `Thread` methods below to -suspend with the `block` effect (defined in the preceding section). -`Thread.block_internal` passes no thread to `switch_to` and so causes -`Thread.resume` to actually [block]. In contrast, `Thread.switch_to_internal` -passes a thread to `switch_to`, causing the loop in `Thread.resume` to directly -switch to that thread without blocking. -```python - def block_internal(self, cancellable): - self.cancellable = cancellable - cancelled = block(switch_to = None) - assert(self.running() and (cancellable() or not cancelled)) - return cancelled - - def switch_to_internal(self, cancellable, other): - self.cancellable = cancellable - cancelled = block(switch_to = other) - assert(self.running() and (cancellable() or not cancelled)) - return cancelled -``` -The `cancellable` parameters in these methods indicate whether the caller is -prepared to handle cancellation. If `cancellable()` is false for all of a task's -threads, the cancellation request will be stored in `Task.state` and delivered -the next time `Task.deliver_pending_cancel()` is called when `cancellable()` -is true. - Once a thread is `Thread.resume()`ed and starts executing, it can suspend its execution by calling the `thread.suspend` built-in which calls `Thread.suspend` -here. `Thread.suspend` first attempts to deliver any pending cancellation -requests and then otherwise simply [blocks]. +here which simply [blocks]. ```python - def suspend(self, cancellable) -> Cancelled: + def suspend(self): + assert(self.running()) + block() assert(self.running()) - if self.task.deliver_pending_cancel(cancellable): - return Cancelled.TRUE - return self.block_internal(cancellable) ``` -The `Thread.wait_until` method is used by all the synchronous blocking -built-ins, as well as auto-backpressure and the `callback` event loop, to wait -until a particular readiness condition is met. Given `wait_until`, "yielding" -can simply be defined as waiting on a readiness condition that is already met. +While running, a thread can also cooperatively yield execution by calling the +`thread.yield` built-in. This calls the `Thread.yield_` method which itself +calls the `Thread.wait_until` with a readiness condition that is already met. +`Thread.wait_until` is also used below to define various synchronous blocking +built-ins as well as auto-backpressure and the `async callback` event loop. ```python - def wait_until(self, ready_func, cancellable = lambda: False) -> Cancelled: + def yield_(self): + return self.wait_until(ready_func = lambda: True) + + def wait_until(self, ready_func): assert(self.running()) - if self.task.deliver_pending_cancel(cancellable): - return Cancelled.TRUE if ready_func() and not DETERMINISTIC_PROFILE and random.randint(0,1): - return Cancelled.FALSE - def ready_or_cancelled(): - return ready_func() or (cancellable() and self.task.has_pending_cancel()) - self.start_waiting_internal(ready_or_cancelled) - cancelled = self.block_internal(cancellable) - if self.task.deliver_pending_cancel(cancellable): - return Cancelled.TRUE - return cancelled - - def yield_(self, cancellable) -> Cancelled: - return self.wait_until(lambda: True, cancellable) -``` -As with `Thread.suspend`, before anything else, `wait_until` reports any pending -cancellation requests if the caller is `cancellable`. The `randint` conjunct -on the early return if `ready_func()` is already `True` means that, at any -potential suspension point, the embedder can nondeterministically decide whether -to switch to another thread or keep running the current one. In particular, when -a caller makes an `async` call to a callee which `wait_until`s a condition -that's already met (e.g. in the case of `yield`), the embedder can use -scheduling heuristics to decide whether or not to block the current thread. - -The `ready_or_cancelled` predicate in `Thread.wait_until` allows a `waiting` -thread to become `ready` if it was not `cancellable` when a pending cancellation -request was set, but has since become `cancellable`. If this `ready` thread is -then scheduled, the second call to `deliver_pending_cancel` is needed to produce -the actual `Cancelled.TRUE` return value. + return + self.start_waiting(ready_func) + block() + assert(self.running()) +``` +If `ready_func()` is already true on entry, the host can nondeterministically +decide whether to switch to another thread or keep running the current one. In +particular, when a caller makes an `async`-lowered call to a callee which +`wait_until`s a condition that's already met (including `thread.yield`), the +embedder can use scheduling heuristics to decide whether or not to [block] the +current thread and return control flow back to the caller. The `Thread.suspend_then_resume` and `Thread.yield_then_resume` methods immediately resume execution of some `other` `suspended` thread in the same component instance, leaving the original thread in either a `suspended` or -`ready` `waiting` state, resp. Like other `Thread` methods, these methods first -report any pending cancellation if the caller is `cancellable`. +`ready` `waiting` state, resp. ```python - def suspend_then_resume(self, cancellable, other: Thread) -> Cancelled: + def suspend_then_resume(self, other: Thread): assert(self.running() and other.suspended()) - if self.task.deliver_pending_cancel(cancellable): - return Cancelled.TRUE - return self.switch_to_internal(cancellable, other) + switch_to(other) + assert(self.running()) - def yield_then_resume(self, cancellable, other: Thread) -> Cancelled: + def yield_then_resume(self, other: Thread): assert(self.running() and other.suspended()) - if self.task.deliver_pending_cancel(cancellable): - return Cancelled.TRUE - self.start_waiting_internal(lambda: True) - return self.switch_to_internal(cancellable, other) + self.start_waiting(ready_func = lambda: True) + switch_to(other) + assert(self.running()) ``` Lastly, the `Thread.suspend_then_promote` and `Thread.yield_then_promote` @@ -586,25 +536,21 @@ If so, control flow is transferred directly and the current thread is left *not* ready to run, then these operations fall back to plain `suspend` or `yield_` behavior, resp. ```python - def suspend_then_promote(self, cancellable, other: Thread) -> Cancelled: + def suspend_then_promote(self, other: Thread): assert(self.running()) - if self.task.deliver_pending_cancel(cancellable): - return Cancelled.TRUE if other.ready(): - other.stop_waiting_internal(cancelled = False) - return self.suspend_then_resume(cancellable, other) + other.stop_waiting() + self.suspend_then_resume(other) else: - return self.suspend(cancellable) + self.suspend() - def yield_then_promote(self, cancellable, other: Thread) -> Cancelled: + def yield_then_promote(self, other: Thread): assert(self.running()) - if self.task.deliver_pending_cancel(cancellable): - return Cancelled.TRUE if other.ready(): - other.stop_waiting_internal(cancelled = False) - return self.yield_then_resume(cancellable, other) + other.stop_waiting() + self.yield_then_resume(other) else: - return self.yield_(cancellable) + self.yield_() ``` @@ -728,10 +674,9 @@ exports. (self.needs_exclusive() and self.inst.exclusive_thread is not None)) 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 = lambda: True) + self.implicit_thread.wait_until(lambda: not has_backpressure()) self.inst.num_waiting_to_enter -= 1 - if cancelled: + if self.deliver_pending_cancel(): self.cancel() return False if self.needs_exclusive(): @@ -786,50 +731,32 @@ returned a value to its caller. self.inst.threads.remove(thread.index) ``` -The `Task.request_cancellation` method is called by the host or wasm caller to -signal that they don't need the return value and that the callee should hurry up -and call the `OnResolve` callback. If a task's implicit thread is waiting to -start (in `Task.enter_implicit_thread`) due to backpressure, then it is -immediately cancelled without running any guest code. Otherwise, if any of a -cancelled task's threads are expecting cancellation (e.g., when an `async -callback` export returns to the event loop or when `waitable-set.wait` or a -`thread.*` built-in is called with `cancellable` set), `request_cancellation` -considers resuming that thread (picking one nondeterministically if there are -multiple), giving the thread the chance to handle cancellation promptly so that -`subtask.cancel` completes without blocking. +The `Task.request_cancellation` method implements the `OnCancel` callback +described above that allows a caller to indicate that they are no longer +interested in the return value. If a task's implicit thread is waiting to start +(in `Task.enter_implicit_thread`, defined above) due to backpressure, then it is +immediately cancelled without running any guest code. Otherwise, the pending +request is stored in the task state so that it can be picked up from the +`async callback` event loop via `Task.deliver_pending_cancel`. ```python def request_cancellation(self): if self.state == Task.State.INITIAL: - self.state = Task.State.CANCEL_DELIVERED - self.implicit_thread.resume(Cancelled.TRUE) + self.state = Task.State.PENDING_CANCEL + self.implicit_thread.resume() + assert(self.state == Task.State.RESOLVED) else: assert(self.state == Task.State.STARTED) - candidates = { t for t in self.threads if t.cancellable() } - if candidates: - self.state = Task.State.CANCEL_DELIVERED - random.choice(list(candidates)).resume(Cancelled.TRUE) - else: - self.state = Task.State.PENDING_CANCEL -``` -Note that `Thread.cancellable` is a first-class function, instead of a boolean -flag, so that whether a `Thread` is cancellable or not can vary dynamically. -Concretely, `cancellable` only varies dynamically in one situation: in -`canon_lift`, when a `callback`-lifted `async` function is waiting in its event -loop and must not be resumed if some other thread holds the `exclusive_thread` -lock (to preserve [Component Invariant] #2). + self.state = Task.State.PENDING_CANCEL -If cancellation cannot be immediately delivered by `Task.request_cancellation`, -the request is remembered in `Task.state` and delivered at the next opportunity -by `Task.deliver_pending_cancel`, which is checked at all cancellation points: -```python def has_pending_cancel(self): return self.state == Task.State.PENDING_CANCEL - def deliver_pending_cancel(self, cancellable) -> bool: - if cancellable() and self.has_pending_cancel(): + def deliver_pending_cancel(self) -> bool: + if self.has_pending_cancel(): self.state = Task.State.CANCEL_DELIVERED return True - return False + else: + return False ``` The `Task.start` method is called by `canon_lift` to get the list of @@ -841,7 +768,6 @@ guest code runs, the definition can assert that it is called correctly. assert(self.state == Task.State.INITIAL) self.state = Task.State.STARTED return self.on_start() - ``` The `Task.return_` method is called by `canon_task_return` and `canon_lift` to @@ -865,9 +791,8 @@ enforces the same `num_borrows` condition as `return_`, ensuring that when the caller's `OnResolve` callback is called, the caller knows all borrows have been returned. The initial `trap_if` only allows cancellation after cancellation has been *delivered* to core wasm. In particular, if -`request_cancellation` cannot synchronously deliver cancellation and sets -`Task.state` to `PENDING_CANCEL`, core wasm will still trap if it tries to -call `task.cancel`. +`request_cancellation` sets `Task.state` to `PENDING_CANCEL`, core wasm will +still trap if it tries to call `task.cancel`. ```python def cancel(self): trap_if(self.state != Task.State.CANCEL_DELIVERED) @@ -1293,7 +1218,8 @@ the closure fields directly in the component instance table. A waitable can belong to at most one "waitable set" (defined next) which is referred to by the `wset` field. A `Waitable`'s `pending_event` is delivered (via `get_pending_event`) when core wasm code waits on its waitable set (via -`waitable-set.wait` or, when using `callback`, by returning to the event loop). +`waitable-set.wait` or, when using the `async callback` ABI, by returning to the +event loop). Lastly, a waitable cannot be waited on *both* by a waitable set and one of `subtask.cancel` or a synchronous `{stream,future}.{,cancel-}{read,write}` since @@ -1324,7 +1250,7 @@ class Waitable: def wait_for_pending_event(self): assert(not self.in_waitable_set() and not self.has_sync_waiter) self.has_sync_waiter = True - current_thread().wait_until(self.has_pending_event, cancellable = lambda: False) + current_thread().wait_until(self.has_pending_event) self.has_sync_waiter = False def get_pending_event(self) -> EventTuple: @@ -1353,7 +1279,10 @@ with an O(n) search, because a waitable can be associated with at most one set and can contain at most one pending event, a real implementation could instead store a list of waitables-with-pending-events as a linked list embedded directly in the component instance's table element to avoid the separate -allocation while providing O(1) polling. +allocation while providing O(1) polling. The `random.shuffle` in +`get_pending_event` gives embedders the semantic freedom to schedule delivery of +events nondeterministically (e.g., taking into account priorities); runtimes do +not have to literally randomize event delivery. ```python class WaitableSet: elems: list[Waitable] @@ -1373,46 +1302,56 @@ class WaitableSet: assert(self is w.wset) if w.has_pending_event(): return w.get_pending_event() +``` - def wait_for_event_and(self, ready_func, cancellable) -> EventTuple: - def ready_and_has_event(): - return ready_func() and self.has_pending_event() +Given the above helper methods, the `waitable-set.wait` and `waitable-set.poll` +built-ins can wait on or poll `has_pending_event`, resp. The `num_waiting` count +is used to trap in `WaitableSet.drop`, below, if guest code attempts to destroy +a waitable set that is in use. +```python + def wait(self) -> EventTuple: self.num_waiting += 1 - cancelled = current_thread().wait_until(ready_and_has_event, cancellable) - if cancelled: - event = (EventCode.TASK_CANCELLED, 0, 0) - else: - event = self.get_pending_event() + current_thread().wait_until(self.has_pending_event) self.num_waiting -= 1 - return event + return self.get_pending_event() - def wait_for_event(self, cancellable) -> EventTuple: - return self.wait_for_event_and(lambda: True, cancellable) + def poll(self) -> EventTuple: + if not self.has_pending_event(): + return (EventCode.NONE, 0, 0) + else: + return self.get_pending_event() +``` - def poll(self, cancellable) -> EventTuple: - if current_task().deliver_pending_cancel(cancellable): +When the `async callback` ABI is used and core wasm code returns `WAIT`, there +are two extra wrinkles beyond the plain `WaitableSet.wait` shown above. First, +to ensure the run-to-completion semantics required by [Component Invariant] #2, +the readiness condition passed to `Thread.wait_until` requires that there is no +`needs_exclusive` thread on the stack (holding the `exclusive_thread` lock). +Second, in addition to delivering normal waitable events, pending cancellation +on the current task is also delivered as a `TASK_CANCELLED` event. +```python + def wait_from_callback(self) -> EventTuple: + thread = current_thread() + def ready(): + return (thread.task.inst.exclusive_thread is None + and (self.has_pending_event() or thread.task.has_pending_cancel())) + self.num_waiting += 1 + thread.wait_until(ready) + self.num_waiting -= 1 + if thread.task.deliver_pending_cancel(): return (EventCode.TASK_CANCELLED, 0, 0) - elif not self.has_pending_event(): - return (EventCode.NONE, 0, 0) else: return self.get_pending_event() +``` +The `WaitableSet.drop` method traps if dropped while it still contains elements +(whose `Waitable.wset` field would become dangling) or if it is being +waited-upon by another thread (as indicated by a non-zero `num_waiting`). +```python def drop(self): trap_if(len(self.elems) > 0) trap_if(self.num_waiting > 0) ``` -The `random.shuffle` in `get_pending_event` give embedders the semantic freedom -to schedule delivery of events nondeterministically (e.g., taking into account -priorities); runtimes do not have to literally randomize event delivery. - -The `ready_func` passed to `WaitableSet.wait_for_event_and` allows the caller to -stipulate extra conditions that have to be met, *in addition* to there being an -event ready for delivery. In particular, this is used by the `async callback` -event loop to avoid overlapping callback execution. - -The `WaitableSet.drop` method traps if dropped while it still contains elements -(whose `Waitable.wset` field would become dangling) or if it is being -waited-upon by another `Task` (as indicated by a non-zero `num_waiting`). ### Subtask State @@ -3566,65 +3505,69 @@ function (specified as a `funcidx` immediate in `canon lift`) until the [packed] = call_and_trap_on_throw(callee, flat_args) code,si = unpack_callback_result(packed) while code != CallbackCode.EXIT: - assert(task.needs_exclusive() and inst.exclusive_thread is task.implicit_thread) - inst.exclusive_thread = None - def lock_available(): - return inst.exclusive_thread is None - match code: - case CallbackCode.YIELD: - cancelled = thread.wait_until(lock_available, cancellable = lock_available) - if cancelled: - event = (EventCode.TASK_CANCELLED, 0, 0) - else: - event = (EventCode.NONE, 0, 0) - case CallbackCode.WAIT: - wset = inst.handles.get(si) - trap_if(not isinstance(wset, WaitableSet)) - event = wset.wait_for_event_and(lock_available, cancellable = lock_available) - case _: - trap() - assert(inst.exclusive_thread is None) - inst.exclusive_thread = task.implicit_thread + if thread.task.deliver_pending_cancel(): + event = (EventCode.TASK_CANCELLED, 0, 0) + else: + assert(inst.exclusive_thread is task.implicit_thread) + inst.exclusive_thread = None + match code: + case CallbackCode.YIELD: + thread.wait_until(lambda: inst.exclusive_thread is None) + if thread.task.deliver_pending_cancel(): + event = (EventCode.TASK_CANCELLED, 0, 0) + else: + event = (EventCode.NONE, 0, 0) + case CallbackCode.WAIT: + wset = inst.handles.get(si) + trap_if(not isinstance(wset, WaitableSet)) + event = wset.wait_from_callback() + case _: + trap() + assert(inst.exclusive_thread is None) + inst.exclusive_thread = task.implicit_thread event_code, p1, p2 = event [packed] = call_and_trap_on_throw(opts.callback, [event_code, p1, p2]) code,si = unpack_callback_result(packed) task.exit_implicit_thread() return ``` -The `Thread.wait_until` and `WaitableSet.wait_for_event_and` methods called by -the event loop are the same methods called by the `thread.yield` and -`waitable-set.wait` built-ins. The main difference (other than whether they are -called from an empty vs. non-empty guest call stack, which allows an engine -to reuse the stack in the former case) is the `lock_available` predicate used -to additionally gate readiness and cancellability in order to ensure -[Component Invariant] #3. - The event loop releases `ComponentInstance.exclusive_thread` (which was acquired by `Task.enter_implicit_thread`) before potentially blocking the thread to allow -other `needs_exclusive` tasks to execute in the interim. However, the -`exclusive_thread` lock is held throughout each core wasm invocation from the -event loop to maintain [Component Invariant] #2. Thus, `async callback`-lifted -tasks allow *more* concurrency than synchronously-lifted tasks (which only -release the `exclusive_thread` lock after they've returned) but *less* -concurrency than (stackful) non-`callback` `async`-lifted tasks, which entirely -ignore `exclusive_thread`. +other `needs_exclusive` tasks to execute in the interim. To ensure the +run-to-completion semantics required by [Component Invariant] #2, the +`exclusive_thread` is continuously held for the duration of each turn of the +event loop. Thus, `async callback`-lifted tasks allow *more* concurrency than +synchronously-lifted tasks (which only release the `exclusive_thread` lock after +they've returned) but *less* concurrency than (stackful) non-`callback` +`async`-lifted tasks, which entirely ignore `exclusive_thread`. + +Because `YIELD` waits for `exclusive_thread` to be `None`, which is trivially +true when `Thread.wait_until` is called, the host is *allowed*, but not +*required* to schedule another thread. If another thread is scheduled and that +thread acquires `exclusive_thread`, the `YIELD`ed thread cannot run until the +`exclusive_thread` is released. `WaitableSet.wait_from_callback` similarly waits +until `exclusive_thread` is not held, thereby ensuring [Component Invariant] #2 +in either case. + +Another important property of the event loop as defined above is that it +delivers pending cancellation requests as soon as possible: before waiting, +after waiting, and in `wait_from_callback`, *while* waiting. The end of `canon_lift` creates a new task/thread pair for the call and then 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`. +it either returns from `thread_func` or [blocks]. 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`. ```python task = Task(ft, opts, inst, on_start, on_resolve) thread = Thread(task, thread_func) @@ -4113,7 +4056,7 @@ def canon_waitable_set_new(): For a canonical definition: ```wat -(canon waitable-set.wait $cancellable? (memory $mem) (core func $f)) +(canon waitable-set.wait (memory $mem) (core func $f)) ``` validation specifies: * `$f` is given type `(func (param $si i32) (param $ptr T) (result i32))` where @@ -4124,12 +4067,12 @@ Calling `$f` invokes the following function which waits for progress to be made on a `Waitable` in the given waitable set (indicated by index `$si`) and then returning its `EventCode` and writing the payload values into linear memory: ```python -def canon_waitable_set_wait(cancellable, mem, si, ptr): +def canon_waitable_set_wait(mem, si, ptr): inst = current_instance() trap_if(not inst.may_leave) wset = inst.handles.get(si) trap_if(not isinstance(wset, WaitableSet)) - event = wset.wait_for_event(lambda: cancellable) + event = wset.wait() return unpack_event(mem, inst, ptr, event) def unpack_event(mem, inst, ptr, e: EventTuple): @@ -4139,19 +4082,13 @@ def unpack_event(mem, inst, ptr, e: EventTuple): store(cx, p2, U32Type(), ptr + 4) return [event] ``` -If `cancellable` is set, then `waitable-set.wait` will return whether the -supertask has already or concurrently requested cancellation. -`waitable-set.wait` (and other cancellable operations) will only indicate -cancellation once and thus, if a caller is not prepared to propagate -cancellation, they can omit `cancellable` so that cancellation is instead -delivered at a later `cancellable` call. ### ๐Ÿ”€ `canon waitable-set.poll` For a canonical definition: ```wat -(canon waitable-set.poll $cancellable? (memory $mem) (core func $f)) +(canon waitable-set.poll (memory $mem) (core func $f)) ``` validation specifies: * `$f` is given type `(func (param $si i32) (param $ptr T) (result i32))` where @@ -4162,20 +4099,14 @@ Calling `$f` invokes the following function, which either returns an event that was pending on one of the waitables in the given waitable set (the same way as `waitable-set.wait`) or, if there is none, returns `0`. ```python -def canon_waitable_set_poll(cancellable, mem, si, ptr): +def canon_waitable_set_poll(mem, si, ptr): inst = current_instance() trap_if(not inst.may_leave) wset = inst.handles.get(si) trap_if(not isinstance(wset, WaitableSet)) - event = wset.poll(lambda: cancellable) + event = wset.poll() return unpack_event(mem, inst, ptr, event) ``` -If `cancellable` is set, then `waitable-set.poll` will return whether the -supertask has already or concurrently requested cancellation. -`waitable-set.poll` (and other cancellable operations) will only indicate -cancellation once and thus, if a caller is not prepared to propagate -cancellation, they can omit `cancellable` so that cancellation is instead -delivered at a later `cancellable` call. ### ๐Ÿ”€ `canon waitable-set.drop` @@ -4284,8 +4215,11 @@ def canon_subtask_cancel(async_, i): subtask.cancellation_requested = True subtask.has_sync_waiter = True subtask.on_cancel() - if not subtask.resolved() and not async_: - thread.wait_until(subtask.resolved) + if not subtask.resolved(): + if not async_: + thread.wait_until(subtask.resolved) + else: + thread.yield_() subtask.has_sync_waiter = False if not subtask.resolved(): return [BLOCKED] @@ -4298,19 +4232,35 @@ def canon_subtask_cancel(async_, i): the supertask has already been notified that the subtask has returned or if the subtask is already in a waitable set. -A race condition handled by the above code is that it's possible for a subtask -to have already resolved (by calling `task.return` or `task.cancel`) and -updated the `state` stored in the `Subtask` (such that `Subtask.resolved()` is -`True`) but this fact has not yet been *delivered* to the supertask by the -supertask calling `get_pending_event` on the `Subtask` in its table. This -distinction is captured by `Subtask.resolved` vs. `Subtask.resolve_delivered`. - -Another race condition guarded by the above code is that during the `on_cancel` -and `wait_until` calls, arbitrary code may run which can reenter the caller's -component instance. By setting `has_sync_waiter` to true for the duration of -these calls, `subtask.cancel` prevents other threads in the same component -instance from "stealing" an event or otherwise invalidating the conditions -guarded at the beginning of `subtask.cancel`. +A race condition handled by the first `if not subtask.resolved()` test is that +it's possible for a subtask to have already resolved and updated the `state` +stored in the `Subtask` (such that `Subtask.resolved()` is true) but this fact +has not yet been *delivered* to the caller. This distinction is captured by +`Subtask.resolved` vs. `Subtask.resolve_delivered` with only the *latter* +trapping on entry to `subtask.cancel`. + +Otherwise, if the subtask is *not* already resolved, the subtask's `OnCancel` +callback is called to officially request cancellation. If the callee is another +component, `OnCancel` is implemented by `Task.request_cancellation` which, as +defined above, will immediately resolve a task that is blocked waiting to enter +due to backpressure. Otherwise, the second `if not subtask.resolved()` handles +the general case where core wasm code needs to run in the callee to receive and +explicitly resolve the task (by calling `task.cancel` or `task.return`). + +If `subtask.cancel` is called synchronously, the call blocks until the subtask +resolves. If `subtask.cancel` is called asynchronously, rather than *blocking*, +`Thread.yield_` is called to allow the host to nondeterministically schedule +whatever thread it heuristically chooses in the hopes that a subtask thread will +get to execute and quickly resolve the subtask, allowing `subtask.cancel` to +complete eagerly. Otherwise, `subtask.cancel` returns `BLOCKED` (`-1`) and the +caller must wait for a `SUBTASK` event using a waitable set. + +Lastly, a subtle race condition guarded by the above code is that during the +calls to `on_cancel`, `yield_` and `wait_until`, arbitrary code may run which +can reenter the caller's component instance. By setting `has_sync_waiter` to +true for the duration of these calls, `subtask.cancel` prevents other threads in +the same component instance from "stealing" an event or otherwise invalidating +the conditions guarded at the beginning of `subtask.cancel`. ### ๐Ÿ”€ `canon subtask.drop` @@ -4794,15 +4744,13 @@ def canon_thread_resume_later(i): other_thread.resume_later() return [] ``` -`thread.resume-later` never suspends the [current thread] and so there is no -possibility of cancellation and thus no `cancellable` immediate. ### ๐Ÿงต `canon thread.suspend` For a canonical definition: ```wat -(canon thread.suspend $cancellable? (core func $suspend)) +(canon thread.suspend (core func $suspend)) ``` validation specifies: * `$suspend` is given type `(func (result i32))` @@ -4811,25 +4759,19 @@ Calling `$suspend` invokes the following function which suspends the [current thread], immediately returning control flow to any transitive `async`-lowered calling component. ```python -def canon_thread_suspend(cancellable): +def canon_thread_suspend(): thread = current_thread() trap_if(not thread.task.inst.may_leave) - cancelled = thread.suspend(lambda: cancellable) - return [cancelled] + thread.suspend() + return [0] ``` -If `cancellable` is set, then `thread.suspend` will return a `Cancelled` -value to indicate whether the supertask has already or concurrently requested -cancellation. `thread.suspend` (and other cancellable operations) will only -indicate cancellation once and thus, if a caller is not prepared to propagate -cancellation, they can omit `cancellable` so that cancellation is instead -delivered at a later `cancellable` call. ### ๐Ÿ”€ `canon thread.yield` For a canonical definition: ```wat -(canon thread.yield $cancellable? (core func $yield)) +(canon thread.yield (core func $yield)) ``` validation specifies: * `$yield` is given type `(func (result i32))` @@ -4840,25 +4782,19 @@ nondeterministic point in the future chosen by the embedder. This allows a long-running computation that is not otherwise performing I/O to avoid starving other threads in a cooperative setting. ```python -def canon_thread_yield(cancellable): +def canon_thread_yield(): thread = current_thread() trap_if(not thread.task.inst.may_leave) - cancelled = thread.yield_(lambda: cancellable) - return [cancelled] + thread.yield_() + return [0] ``` -If `cancellable` is set, then `thread.yield` will return a `Cancelled` -value indicating whether the supertask has already or concurrently requested -cancellation. `thread.yield` (and other cancellable operations) will only -indicate cancellation once and thus, if a caller is not prepared to propagate -cancellation, they can omit `cancellable` so that cancellation is instead -delivered at a later `cancellable` call. ### ๐Ÿงต `canon thread.suspend-then-resume` For a canonical definition: ```wat -(canon thread.suspend-then-resume $cancellable? (core func $suspend-then-resume)) +(canon thread.suspend-then-resume (core func $suspend-then-resume)) ``` validation specifies: * `$suspend-then-resume` is given type `(func (param $i i32) (result i32))` @@ -4868,27 +4804,21 @@ thread at index `$i` from the current component instance's `threads` table, traps if it's not [suspended], and then switches to that thread, leaving the [current thread] suspended. ```python -def canon_thread_suspend_then_resume(cancellable, i): +def canon_thread_suspend_then_resume(i): thread = current_thread() trap_if(not thread.task.inst.may_leave) other_thread = thread.task.inst.threads.get(i) trap_if(not other_thread.suspended()) - cancelled = thread.suspend_then_resume(lambda: cancellable, other_thread) - return [cancelled] + thread.suspend_then_resume(other_thread) + return [0] ``` -If `cancellable` is set, then `thread.suspend-then-resume` will return a -`Cancelled` value to indicate whether the supertask has already or concurrently -requested cancellation. `thread.suspend-then-resume` (and other cancellable -operations) will only indicate cancellation once and thus, if a caller is not -prepared to propagate cancellation, they can omit `cancellable` so that -cancellation is instead delivered at a later `cancellable` call. ### ๐Ÿงต `canon thread.yield-then-resume` For a canonical definition: ```wat -(canon thread.yield-then-resume $cancellable? (core func $yield-then-resume)) +(canon thread.yield-then-resume (core func $yield-then-resume)) ``` validation specifies: * `$yield-then-resume` is given type `(func (param $i i32) (result i32))` @@ -4899,27 +4829,21 @@ it's not [suspended], and then switches to that thread, leaving the [current thread] ready to run at some nondeterministic point in the future chosen by the embedder. ```python -def canon_thread_yield_then_resume(cancellable, i): +def canon_thread_yield_then_resume(i): thread = current_thread() trap_if(not thread.task.inst.may_leave) other_thread = thread.task.inst.threads.get(i) trap_if(not other_thread.suspended()) - cancelled = thread.yield_then_resume(lambda: cancellable, other_thread) - return [cancelled] + thread.yield_then_resume(other_thread) + return [0] ``` -If `cancellable` is set, then `thread.yield-then-resume` will return a -`Cancelled` value indicating whether the supertask has already or concurrently -requested cancellation. `thread.yield-then-resume` (and other cancellable -operations) will only indicate cancellation once and thus, if a caller is not -prepared to propagate cancellation, they can omit `cancellable` so that -cancellation is instead delivered at a later `cancellable` call. ### ๐Ÿงต `canon thread.suspend-then-promote` For a canonical definition: ```wat -(canon thread.suspend-then-promote $cancellable? (core func $suspend-then-promote)) +(canon thread.suspend-then-promote (core func $suspend-then-promote)) ``` validation specifies: * `$suspend-then-promote` is given type `(func (param $i i32) (result i32))` @@ -4929,26 +4853,20 @@ 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. ```python -def canon_thread_suspend_then_promote(cancellable, i): +def canon_thread_suspend_then_promote(i): thread = current_thread() trap_if(not thread.task.inst.may_leave) other_thread = thread.task.inst.threads.get(i) - cancelled = thread.suspend_then_promote(lambda: cancellable, other_thread) - return [cancelled] + thread.suspend_then_promote(other_thread) + return [0] ``` -If `cancellable` is set, then `thread.suspend-then-promote` will return a -`Cancelled` value indicating whether the supertask has already or concurrently -requested cancellation. `thread.suspend-then-promote` (and other cancellable -operations) will only indicate cancellation once and thus, if a caller is not -prepared to propagate cancellation, they can omit `cancellable` so that -cancellation is instead delivered at a later `cancellable` call. ### ๐Ÿงต `canon thread.yield-then-promote` For a canonical definition: ```wat -(canon thread.yield-then-promote $cancellable? (core func $yield-then-promote)) +(canon thread.yield-then-promote (core func $yield-then-promote)) ``` validation specifies: * `$yield-then-promote` is given type `(func (param $i i32) (result i32))` @@ -4959,19 +4877,13 @@ 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. ```python -def canon_thread_yield_then_promote(cancellable, i): +def canon_thread_yield_then_promote(i): thread = current_thread() trap_if(not thread.task.inst.may_leave) other_thread = thread.task.inst.threads.get(i) - cancelled = thread.yield_then_promote(lambda: cancellable, other_thread) - return [cancelled] -``` -If `cancellable` is set, then `thread.yield-then-promote` will return a -`Cancelled` value indicating whether the supertask has already or concurrently -requested cancellation. `thread.yield-then-promote` (and other cancellable -operations) will only indicate cancellation once and thus, if a caller is not -prepared to propagate cancellation, they can omit `cancellable` so that -cancellation is instead delivered at a later `cancellable` call. + thread.yield_then_promote(other_thread) + return [0] +``` ### ๐Ÿ“ `canon error-context.new` diff --git a/design/mvp/Concurrency.md b/design/mvp/Concurrency.md index 2ba31159..b6e69663 100644 --- a/design/mvp/Concurrency.md +++ b/design/mvp/Concurrency.md @@ -790,37 +790,37 @@ cancelled-before-started and cancelled-before-returned. A subtask in one of these terminal states is said to be **resolved**. A resolved subtask has always dropped all the borrowed handles that it was lent during the call. -Cancellation is *cooperative*, delivering the request for cancellation to one -of the subtask's threads and then allowing the subtask to continue executing -for an arbitrary amount of time (calling imports, performing I/O and everything -else) until the subtask decides to call [`task.cancel`] to confirm the -cancellation or, for whatever reason, call `task.return` as-if there had been -no cancellation. `task.cancel` enforces the same "all borrowed handles dropped" -rule as `task.return`, so that once a subtask is resolved, the caller knows its -lent handles have been returned. If the subtask was waiting to start due to -backpressure, the subtask is immediately aborted without running the callee at -all. - -When `subtask.cancel` is called, it will attempt to immediately resume one of -the subtask's threads which is in a cancellable state, passing it a sentinel -"cancelled" value. A thread is in a "cancellable" state if it calls one of the -[blocking](#blocking) built-ins with the `cancellable` immediate set (indicating -that the caller expects and propagates cancellation appropriately) or, if using -a `callback`, returns to the event loop and no other thread is holding the -"exclusive" lock. If a subtask has no cancellable threads, no thread is resumed -and the request for cancellation is remembered in the task state, to be -delivered at the next cancellable opportunity. In the worst case, though, a -component may *never* wait cancellably and thus cancellation may be silently -ignored. +Cancellation is *cooperative*, delivering a *request* for cancellation and then +allowing the subtask to continue executing for an arbitrary amount of time +(calling imports, performing I/O and everything else) until the subtask decides +to call [`task.cancel`] to confirm the cancellation or, for whatever reason, +call `task.return` as-if there had been no cancellation. `task.cancel` enforces +the same "all borrowed handles dropped" rule as `task.return`, so that once a +subtask is resolved, the caller knows its lent handles have been returned. If +the subtask was waiting to start due to backpressure, the subtask is immediately +aborted without running the callee at all. + +When `subtask.cancel` is called, it first records a "pending cancellation +request" on the callee's task. If the callee's task was waiting to start due to +[backpressure](#backpressure), `subtask.cancel` returns "cancelled before +started" immediately. Otherwise, `subtask.cancel` performs a nonblocking, +cooperative `thread.yield` that allows the host to nondeterministically schedule +any cooperative threads it wants in the hopes of delivering the cancellation +request to the callee. Currently, cancellation requests can only be delivered +to `async` functions using the stackless `callback` ABI. In this case, a pending +cancellation request is delivered as a "task cancelled" event to the `callback` +function, following the standard run-to-completion rules mentioned above. In +the [future](#TODO), other cancellation delivery mechanisms will be added for +the `async` functions using the stackful ABI. `subtask.cancel` can be called synchronously or asynchronously. If called -synchronously, `subtask.cancel` blocks until the subtask reaches a resolved -state and returns which state was reached. If called asynchronously, then if a -cancellable subtask thread is resumed *and* the subtask reaches a resolved -state before blocking for whatever reason `subtask.cancel` will return -which state was reached. Otherwise, `subtask.cancel` will return a "blocked" -sentinel value and the caller must [wait](#waitables-and-waitable-sets) via -waitable set until the subtask reaches a resolved state. +synchronously, `subtask.cancel` will block until the subtask has resolved (i.e., +the subtask returns a value or calls `task.cancel`), returning whether the +callee "returned" or "cancelled before returning". If called *asynchronously* +and the callee has *still* not resolved after the host decides the cooperative +yield has completed, `subtask.cancel` will return "blocked" instead and the +caller must [wait](#waitables-and-waitable-sets) via waitable set until the +subtask reaches a resolved state. The Component Model does not provide a mechanism to force prompt termination of threads as this can lead to leaks and corrupt state in a still-live component @@ -869,16 +869,14 @@ defined by the Component Model: * If multiple tasks are blocked by backpressure and the backpressure is disabled, the order in which these pending tasks start, along with how they interleave with new tasks, is nondeterministic. -* If a task containing multiple threads is cancelled, the choice of which - thread receives the request for cancellation is nondeterministic. +* When `subtask.cancel` is called for a task that has started (i.e., passed the + backpressure gate), the host may nondeterministically resume as many or as few + threads as it wants before `subtask.cancel` returns. Despite the above, the following scenarios do behave deterministically: * If a component `a` asynchronously calls the export of another component `b`, control flow deterministically transfers to `b` and then back to `a` when `b` returns or blocks. -* If a component `a` asynchronously cancels a subtask in another component `b`, - control flow deterministically transfers to `b` and then back to `a` when `b` - resolves or blocks. * If a component `a` asynchronously cancels a subtask in another component `b` that was blocked before starting due to backpressure, cancellation completes deterministically and immediately. @@ -1481,6 +1479,8 @@ the concurrency story: * remove the temporary trap mentioned above that occurs when a `read` and `write` of a stream/future happen from within the same component instance * zero-copy forwarding/splicing +* allow `async` functions using the stackful ABI to be notified of + cancellation * allow the `stream` type to validate; make it use `string-encoding` and not split code points * add built-ins providing guest code more control over its containing diff --git a/design/mvp/Explainer.md b/design/mvp/Explainer.md index 5d9c937d..5fa46c39 100644 --- a/design/mvp/Explainer.md +++ b/design/mvp/Explainer.md @@ -1560,8 +1560,8 @@ canon ::= ... | (canon task.return (result )? * (core func ?)) ๐Ÿ”€ | (canon task.cancel (core func ?)) ๐Ÿ”€ | (canon waitable-set.new (core func ?)) ๐Ÿ”€ - | (canon waitable-set.wait cancellable? (memory core-prefix()) (core func ?)) ๐Ÿ”€ - | (canon waitable-set.poll cancellable? (memory core-prefix()) (core func ?)) ๐Ÿ”€ + | (canon waitable-set.wait (memory core-prefix()) (core func ?)) ๐Ÿ”€ + | (canon waitable-set.poll (memory core-prefix()) (core func ?)) ๐Ÿ”€ | (canon waitable-set.drop (core func ?)) ๐Ÿ”€ | (canon waitable.join (core func ?)) ๐Ÿ”€ | (canon subtask.cancel async? (core func ?)) ๐Ÿ”€ @@ -1583,12 +1583,12 @@ canon ::= ... | (canon thread.index (core func ?)) ๐Ÿงต | (canon thread.new-indirect core-prefix() core-prefix() (core func ?)) ๐Ÿงต | (canon thread.resume-later (core func ?)) ๐Ÿงต - | (canon thread.suspend cancellable? (core func ?)) ๐Ÿงต - | (canon thread.yield cancellable? (core func ?)) ๐Ÿ”€ - | (canon thread.suspend-then-resume cancellable? (core func ?)) ๐Ÿงต - | (canon thread.yield-then-resume cancellable? (core func ?)) ๐Ÿงต - | (canon thread.suspend-then-promote cancellable? (core func ?)) ๐Ÿงต - | (canon thread.yield-then-promote cancellable? (core func ?)) ๐Ÿงต + | (canon thread.suspend (core func ?)) ๐Ÿงต + | (canon thread.yield (core func ?)) ๐Ÿ”€ + | (canon thread.suspend-then-resume (core func ?)) ๐Ÿงต + | (canon thread.yield-then-resume (core func ?)) ๐Ÿงต + | (canon thread.suspend-then-promote (core func ?)) ๐Ÿงต + | (canon thread.yield-then-promote (core func ?)) ๐Ÿงต | (canon error-context.new * (core func ?)) ๐Ÿ“ | (canon error-context.debug-message * (core func ?)) ๐Ÿ“ | (canon error-context.drop (core func ?)) ๐Ÿ“ @@ -1764,11 +1764,9 @@ For details, see [Returning] in the concurrency explainer and The `task.cancel` built-in indicates that the [current task] is now [resolved] and has dropped all borrowed handles lent to it during the call (trapping if -otherwise). `task.cancel` can only be called after the `task-cancelled` event -has been received (via `callback`, `waitable-set.{wait,poll}` or `thread.*`) -to indicate that the supertask has requested cancellation and thus is not -expecting a return value. Once this request is received, any of the task's -threads can call `task.cancel` or `task.return`. +otherwise). `task.cancel` can only be called after a request for cancellation +has been [delivered][Cancellation]. Once this request is received, any of the +task's threads may call `task.cancel` or `task.return`. For details, see [Cancellation] in the concurrency explainer and [`canon_task_cancel`] in the Canonical ABI explainer. @@ -1792,7 +1790,7 @@ For details, see [Waitables and Waitable Sets] in the concurrency explainer and | Synopsis | | | -------------------------- | ---------------------------------------------------------- | -| Approximate WIT signature | `func(s: waitable-set) -> event` | +| Approximate WIT signature | `func(s: waitable-set) -> event` | | Canonical ABI signature | `[s:i32 payload-addr:memory.addrtype] -> [event-code:i32]` | where `event` is defined in WIT as: @@ -1825,12 +1823,6 @@ by `waitable-set.poll` and never returned by `waitable-set.wait`.) Waitable sets may be `wait`ed upon when empty, in which case the caller will necessarily block until another thread adds a waitable to the set. -If `cancellable` is set, `waitable-set.wait` may return `task-cancelled` -(`6`) if the caller requests [cancellation] of the [current task]. If -`cancellable` is not set, `task-cancelled` is never returned. -`task-cancelled` is returned at most once for a given task and thus must be -propagated once received. - A `subtask` event notifies the supertask that its subtask is now in the given state (the meanings of which are described by the [concurrency explainer]). @@ -1838,6 +1830,10 @@ The meanings of the `{stream,future}-{read,write}` events/payloads are given as part [`stream.read` and `stream.write`](#-streamread-and-streamwrite) and [`future.read` and `future.write`](#-futureread-and-futurewrite) below. +Lastly, the `task-cancelled` event is never returned by `waitable-set.wait` or +`waitable-set.poll`; it may only be delivered as an event code to an `async` +`callback` function to indicate [cancellation]. + In the Canonical ABI, the `event-code` return value provides the `event` discriminant and the case payloads are stored as two contiguous `i32`s at the 8-byte-aligned address `payload-addr`. @@ -1849,7 +1845,7 @@ For details, see [Waitables and Waitable Sets] in the concurrency explainer and | Synopsis | | | -------------------------- | ---------------------------------------------------------- | -| Approximate WIT signature | `func(s: waitable-set) -> event` | +| Approximate WIT signature | `func(s: waitable-set) -> event` | | Canonical ABI signature | `[s:i32 payload-addr:memory.addrtype] -> [event-code:i32]` | where `event` is defined as in [`waitable-set.wait`](#-waitable-setwait). @@ -1857,12 +1853,6 @@ where `event` is defined as in [`waitable-set.wait`](#-waitable-setwait). The `waitable-set.poll` built-in returns either an event from one of the waitables in `s` or, if there is none, the `none` `event`. -If `cancellable` is set, `waitable-set.poll` may return `task-cancelled` -(`6`) if the caller requests [cancellation] of the [current task]. If -`cancellable` is not set, `task-cancelled` is never returned. -`task-cancelled` is returned at most once for a given task and thus must be -propagated once received. - The Canonical ABI of `waitable-set.poll` is the same as `waitable-set.wait` (with the `none` case indicated by returning `0`). @@ -1912,7 +1902,10 @@ For details, see [Waitables and Waitable Sets] in the concurrency explainer and | Approximate WIT signature | `func(subtask: subtask) -> option` | | Canonical ABI signature | `[subtask:i32] -> [i32]` | -The `subtask.cancel` built-in requests [cancellation] of the indicated subtask. +The `subtask.cancel` built-in requests [cancellation] of the indicated subtask, +trapping if the caller was already notified of resolution, cancellation has +already been requested, or the subtask is already in a waitable set. + If the `async` is present, `none` is returned (represented as `-1` in the Canonical ABI) to indicate that the subtask blocked before it was [resolved]. Otherwise, `subtask.cancel` returns the `subtask-state` that the subtask @@ -2212,98 +2205,92 @@ For details, see [Thread Built-ins] in the concurrency explainer and ###### ๐Ÿงต `thread.suspend` -| Synopsis | | -| -------------------------- | ------------------------------ | -| Approximate WIT signature | `func() -> bool` | -| Canonical ABI signature | `[] -> [i32]` | +| Synopsis | | +| -------------------------- | ------------- | +| Approximate WIT signature | `func()` | +| Canonical ABI signature | `[] -> [i32]` | The `thread.suspend` built-in suspends the [current thread] until it is explicitly resumed by some other thread calling a built-in such as -`thread.resume-later`. If `cancellable` is set, `thread.suspend` returns whether -the current task was [cancelled] by the caller; otherwise, `thread.suspend` -always returns `false`. +`thread.resume-later`. The returned `i32` is always `0` and may be removed +in a future ABI revision. For details, see [Thread Built-ins] in the concurrency explainer and [`canon_thread_suspend`] in the Canonical ABI explainer. ###### ๐Ÿ”€ `thread.yield` -| Synopsis | | -| -------------------------- | ------------------------------ | -| Approximate WIT signature | `func() -> bool` | -| Canonical ABI signature | `[] -> [i32]` | +| Synopsis | | +| -------------------------- | ------------- | +| Approximate WIT signature | `func()` | +| Canonical ABI signature | `[] -> [i32]` | The `thread.yield` built-in allows the runtime to potentially switch to any other thread in the "ready" state, enabling a long-running computation to cooperatively interleave execution without specifically requesting another -thread to be resumed (as with `thread.yield-then-resume`). If `cancellable` is -set, `thread.yield` returns whether the current task was [cancelled] by the -caller; otherwise, `thread.yield` always returns `false`. +thread to be resumed (as with `thread.yield-then-resume`). The returned `i32` is +always `0` and may be removed in a future ABI revision. For details, see [Thread Built-ins] in the concurrency explainer and [`canon_thread_yield`] in the Canonical ABI explainer. ###### ๐Ÿงต `thread.suspend-then-resume` -| Synopsis | | -| -------------------------- | --------------------------------------- | -| Approximate WIT signature | `func(t: thread) -> bool` | -| Canonical ABI signature | `[t:i32] -> [i32]` | +| Synopsis | | +| -------------------------- | ------------------ | +| Approximate WIT signature | `func(t: thread)` | +| Canonical ABI signature | `[t:i32] -> [i32]` | The `thread.suspend-then-resume` built-in suspends the [current thread] and immediately resumes execution of the thread `t`, trapping if `t` is not in a -"suspended" state. If `cancellable` is set, `thread.suspend-then-resume` returns -whether the current task was [cancelled] by the caller; otherwise, -`thread.suspend-then-resume` always returns `false`. +"suspended" state. The returned `i32` is always `0` and may be removed in a +future ABI revision. For details, see [Thread Built-ins] in the concurrency explainer and [`canon_thread_suspend_then_resume`] in the Canonical ABI explainer. ###### ๐Ÿงต `thread.yield-then-resume` -| Synopsis | | -| -------------------------- | --------------------------------------- | -| Approximate WIT signature | `func(t: thread) -> bool` | -| Canonical ABI signature | `[t:i32] -> [i32]` | +| Synopsis | | +| -------------------------- | ------------------ | +| Approximate WIT signature | `func(t: thread)` | +| Canonical ABI signature | `[t:i32] -> [i32]` | The `thread.yield-then-resume` built-in immediately resumes execution of the thread `t` (trapping if `t` is not in a "suspended" state), leaving the [current thread] in a "ready" state so that the runtime can nondeterministically resume -the current thread at some point in the future. If `cancellable` is set, -`thread.yield-then-resume` returns whether the current task was [cancelled] by -the caller; otherwise, `thread.yield-then-resume` always returns `false`. +the current thread at some point in the future. The returned `i32` is always `0` +and may be removed in a future ABI revision. For details, see [Thread Built-ins] in the concurrency explainer and [`canon_thread_yield_then_resume`] in the Canonical ABI explainer. ###### ๐Ÿงต `thread.suspend-then-promote` -| Synopsis | | -| -------------------------- | --------------------------------------- | -| Approximate WIT signature | `func(t: thread) -> bool` | -| Canonical ABI signature | `[t:i32] -> [i32]` | +| Synopsis | | +| -------------------------- | ------------------ | +| Approximate WIT signature | `func(t: thread)` | +| 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`. +in a "suspended" state. The returned `i32` is always `0` and may be removed in a +future ABI revision. For details, see [Thread Built-ins] in the concurrency explainer and [`canon_thread_suspend_then_promote`] in the Canonical ABI explainer. ###### ๐Ÿงต `thread.yield-then-promote` -| Synopsis | | -| -------------------------- | --------------------------------------- | -| Approximate WIT signature | `func(t: thread) -> bool` | -| Canonical ABI signature | `[t:i32] -> [i32]` | +| Synopsis | | +| -------------------------- | ------------------ | +| Approximate WIT signature | `func(t: thread)` | +| 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`. +in a "ready" state. The returned `i32` is always `0` and may be removed in a +future ABI revision. For details, see [Thread Built-ins] in the concurrency explainer and [`canon_thread_yield_then_promote`] in the Canonical ABI explainer. @@ -3407,7 +3394,6 @@ For some use-case-focused, worked examples, see: [Returning]: Concurrency.md#returning [Resolved]: Concurrency.md#cancellation [Cancellation]: Concurrency.md#cancellation -[Cancelled]: Concurrency.md#cancellation [Block]: Concurrency.md#blocking [Component Model Documentation]: https://component-model.bytecodealliance.org diff --git a/design/mvp/canonical-abi/definitions.py b/design/mvp/canonical-abi/definitions.py index d5b490c7..81f1f527 100644 --- a/design/mvp/canonical-abi/definitions.py +++ b/design/mvp/canonical-abi/definitions.py @@ -210,20 +210,15 @@ def __init__(self, store): ### Stack Switching -class Cancelled(IntEnum): - FALSE = 0 - TRUE = 1 - class Continuation: lock: threading.Lock handler: Handler - block_result: Cancelled class Handler: lock: threading.Lock current_thread: Thread cont: Optional[Continuation] - block_arg: Optional[Thread] + switch_to: Optional[Thread] thread_local_handler = threading.local() @@ -232,41 +227,45 @@ def new_already_acquired_lock() -> threading.Lock: lock.acquire() return lock -def cont_new(f: Callable[[Cancelled], Optional[Thread]]) -> Continuation: +def cont_new(f: Callable[[], Optional[Thread]]) -> Continuation: cont = Continuation() cont.lock = new_already_acquired_lock() def thread_base(): cont.lock.acquire() thread_local_handler.value = cont.handler - block_arg = f(cont.block_result) + switch_to = f() handler = thread_local_handler.value handler.cont = None - handler.block_arg = block_arg + handler.switch_to = switch_to handler.lock.release() threading.Thread(target = thread_base).start() return cont -def resume(cont: Continuation, block_result: Cancelled, current_thread: Thread) -> \ +def resume(cont: Continuation, current_thread: Thread) -> \ tuple[Optional[Continuation], Optional[Thread]]: handler = Handler() handler.lock = new_already_acquired_lock() handler.current_thread = current_thread cont.handler = handler - cont.block_result = block_result cont.lock.release() handler.lock.acquire() - return (handler.cont, handler.block_arg) + return (handler.cont, handler.switch_to) + +def block(): + suspend(switch_to = None) -def block(switch_to: Optional[Thread]) -> Cancelled: +def switch_to(other: Thread): + suspend(switch_to = other) + +def suspend(switch_to: Optional[Thread]): cont = Continuation() cont.lock = new_already_acquired_lock() handler = thread_local_handler.value handler.cont = cont - handler.block_arg = switch_to + handler.switch_to = switch_to handler.lock.release() cont.lock.acquire() thread_local_handler.value = cont.handler - return cont.block_result def current_thread() -> Thread: return thread_local_handler.value.current_thread @@ -283,7 +282,6 @@ class Thread: cont: Optional[Continuation] ready_func: Optional[Callable[[], bool]] task: Task - cancellable: Callable[[], bool] index: Optional[int] storage: tuple[int,int] @@ -300,113 +298,85 @@ def ready(self): return self.waiting() and self.ready_func() def __init__(self, task, thread_func): - def cont_func(cancelled): - assert(self.running() and not cancelled) + def cont_func(): + assert(self.running()) thread_func() return None self.cont = cont_new(cont_func) self.ready_func = None self.task = task - self.cancellable = lambda: False self.index = None self.storage = [0,0] assert(self.suspended()) - def start_waiting_internal(self, ready_func): + def start_waiting(self, ready_func): assert(not self.waiting() and not self.ready_func) self.ready_func = ready_func self.task.inst.store.waiting.append(self) - def stop_waiting_internal(self, cancelled): + def stop_waiting(self): assert(self.waiting() and self.ready_func) - assert(cancelled or self.ready()) self.ready_func = None self.task.inst.store.waiting.remove(self) def resume_later(self): assert(self.suspended()) - self.start_waiting_internal(lambda: True) + self.start_waiting(ready_func = lambda: True) assert(self.ready()) - def resume(self, cancelled = Cancelled.FALSE): - assert(not self.running() and (self.cancellable() or not cancelled)) + def resume(self): + assert(not self.running()) if self.waiting(): - self.stop_waiting_internal(cancelled) + self.stop_waiting() thread = self while thread is not None: cont = thread.cont thread.cont = None - (thread.cont, switch_to) = resume(cont, cancelled, thread) + (thread.cont, switch_to) = resume(cont, thread) thread = switch_to - cancelled = Cancelled.FALSE - - def block_internal(self, cancellable): - self.cancellable = cancellable - cancelled = block(switch_to = None) - assert(self.running() and (cancellable() or not cancelled)) - return cancelled - def switch_to_internal(self, cancellable, other): - self.cancellable = cancellable - cancelled = block(switch_to = other) - assert(self.running() and (cancellable() or not cancelled)) - return cancelled - - def suspend(self, cancellable) -> Cancelled: + def suspend(self): + assert(self.running()) + block() assert(self.running()) - if self.task.deliver_pending_cancel(cancellable): - return Cancelled.TRUE - return self.block_internal(cancellable) - def wait_until(self, ready_func, cancellable = lambda: False) -> Cancelled: + def yield_(self): + return self.wait_until(ready_func = lambda: True) + + def wait_until(self, ready_func): assert(self.running()) - if self.task.deliver_pending_cancel(cancellable): - return Cancelled.TRUE if ready_func() and not DETERMINISTIC_PROFILE and random.randint(0,1): - return Cancelled.FALSE - def ready_or_cancelled(): - return ready_func() or (cancellable() and self.task.has_pending_cancel()) - self.start_waiting_internal(ready_or_cancelled) - cancelled = self.block_internal(cancellable) - if self.task.deliver_pending_cancel(cancellable): - return Cancelled.TRUE - return cancelled - - def yield_(self, cancellable) -> Cancelled: - return self.wait_until(lambda: True, cancellable) - - def suspend_then_resume(self, cancellable, other: Thread) -> Cancelled: + return + self.start_waiting(ready_func) + block() + assert(self.running()) + + def suspend_then_resume(self, other: Thread): assert(self.running() and other.suspended()) - if self.task.deliver_pending_cancel(cancellable): - return Cancelled.TRUE - return self.switch_to_internal(cancellable, other) + switch_to(other) + assert(self.running()) - def yield_then_resume(self, cancellable, other: Thread) -> Cancelled: + def yield_then_resume(self, other: Thread): assert(self.running() and other.suspended()) - if self.task.deliver_pending_cancel(cancellable): - return Cancelled.TRUE - self.start_waiting_internal(lambda: True) - return self.switch_to_internal(cancellable, other) + self.start_waiting(ready_func = lambda: True) + switch_to(other) + assert(self.running()) - def suspend_then_promote(self, cancellable, other: Thread) -> Cancelled: + def suspend_then_promote(self, other: Thread): assert(self.running()) - if self.task.deliver_pending_cancel(cancellable): - return Cancelled.TRUE if other.ready(): - other.stop_waiting_internal(cancelled = False) - return self.suspend_then_resume(cancellable, other) + other.stop_waiting() + self.suspend_then_resume(other) else: - return self.suspend(cancellable) + self.suspend() - def yield_then_promote(self, cancellable, other: Thread) -> Cancelled: + def yield_then_promote(self, other: Thread): assert(self.running()) - if self.task.deliver_pending_cancel(cancellable): - return Cancelled.TRUE if other.ready(): - other.stop_waiting_internal(cancelled = False) - return self.yield_then_resume(cancellable, other) + other.stop_waiting() + self.yield_then_resume(other) else: - return self.yield_(cancellable) + self.yield_() ### Tasks @@ -457,10 +427,9 @@ def has_backpressure(): (self.needs_exclusive() and self.inst.exclusive_thread is not None)) 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 = lambda: True) + self.implicit_thread.wait_until(lambda: not has_backpressure()) self.inst.num_waiting_to_enter -= 1 - if cancelled: + if self.deliver_pending_cancel(): self.cancel() return False if self.needs_exclusive(): @@ -493,25 +462,22 @@ def unregister_thread(self, thread): def request_cancellation(self): if self.state == Task.State.INITIAL: - self.state = Task.State.CANCEL_DELIVERED - self.implicit_thread.resume(Cancelled.TRUE) + self.state = Task.State.PENDING_CANCEL + self.implicit_thread.resume() + assert(self.state == Task.State.RESOLVED) else: assert(self.state == Task.State.STARTED) - candidates = { t for t in self.threads if t.cancellable() } - if candidates: - self.state = Task.State.CANCEL_DELIVERED - random.choice(list(candidates)).resume(Cancelled.TRUE) - else: - self.state = Task.State.PENDING_CANCEL + self.state = Task.State.PENDING_CANCEL def has_pending_cancel(self): return self.state == Task.State.PENDING_CANCEL - def deliver_pending_cancel(self, cancellable) -> bool: - if cancellable() and self.has_pending_cancel(): + def deliver_pending_cancel(self) -> bool: + if self.has_pending_cancel(): self.state = Task.State.CANCEL_DELIVERED return True - return False + else: + return False def start(self) -> list[any]: assert(self.state == Task.State.INITIAL) @@ -760,7 +726,7 @@ def in_waitable_set(self): def wait_for_pending_event(self): assert(not self.in_waitable_set() and not self.has_sync_waiter) self.has_sync_waiter = True - current_thread().wait_until(self.has_pending_event, cancellable = lambda: False) + current_thread().wait_until(self.has_pending_event) self.has_sync_waiter = False def get_pending_event(self) -> EventTuple: @@ -800,26 +766,28 @@ def get_pending_event(self) -> EventTuple: if w.has_pending_event(): return w.get_pending_event() - def wait_for_event_and(self, ready_func, cancellable) -> EventTuple: - def ready_and_has_event(): - return ready_func() and self.has_pending_event() + def wait(self) -> EventTuple: self.num_waiting += 1 - cancelled = current_thread().wait_until(ready_and_has_event, cancellable) - if cancelled: - event = (EventCode.TASK_CANCELLED, 0, 0) - else: - event = self.get_pending_event() + current_thread().wait_until(self.has_pending_event) self.num_waiting -= 1 - return event + return self.get_pending_event() - def wait_for_event(self, cancellable) -> EventTuple: - return self.wait_for_event_and(lambda: True, cancellable) + def poll(self) -> EventTuple: + if not self.has_pending_event(): + return (EventCode.NONE, 0, 0) + else: + return self.get_pending_event() - def poll(self, cancellable) -> EventTuple: - if current_task().deliver_pending_cancel(cancellable): + def wait_from_callback(self) -> EventTuple: + thread = current_thread() + def ready(): + return (thread.task.inst.exclusive_thread is None + and (self.has_pending_event() or thread.task.has_pending_cancel())) + self.num_waiting += 1 + thread.wait_until(ready) + self.num_waiting -= 1 + if thread.task.deliver_pending_cancel(): return (EventCode.TASK_CANCELLED, 0, 0) - elif not self.has_pending_event(): - return (EventCode.NONE, 0, 0) else: return self.get_pending_event() @@ -2158,25 +2126,26 @@ def thread_func(): [packed] = call_and_trap_on_throw(callee, flat_args) code,si = unpack_callback_result(packed) while code != CallbackCode.EXIT: - assert(task.needs_exclusive() and inst.exclusive_thread is task.implicit_thread) - inst.exclusive_thread = None - def lock_available(): - return inst.exclusive_thread is None - match code: - case CallbackCode.YIELD: - cancelled = thread.wait_until(lock_available, cancellable = lock_available) - if cancelled: - event = (EventCode.TASK_CANCELLED, 0, 0) - else: - event = (EventCode.NONE, 0, 0) - case CallbackCode.WAIT: - wset = inst.handles.get(si) - trap_if(not isinstance(wset, WaitableSet)) - event = wset.wait_for_event_and(lock_available, cancellable = lock_available) - case _: - trap() - assert(inst.exclusive_thread is None) - inst.exclusive_thread = task.implicit_thread + if thread.task.deliver_pending_cancel(): + event = (EventCode.TASK_CANCELLED, 0, 0) + else: + assert(inst.exclusive_thread is task.implicit_thread) + inst.exclusive_thread = None + match code: + case CallbackCode.YIELD: + thread.wait_until(lambda: inst.exclusive_thread is None) + if thread.task.deliver_pending_cancel(): + event = (EventCode.TASK_CANCELLED, 0, 0) + else: + event = (EventCode.NONE, 0, 0) + case CallbackCode.WAIT: + wset = inst.handles.get(si) + trap_if(not isinstance(wset, WaitableSet)) + event = wset.wait_from_callback() + case _: + trap() + assert(inst.exclusive_thread is None) + inst.exclusive_thread = task.implicit_thread event_code, p1, p2 = event [packed] = call_and_trap_on_throw(opts.callback, [event_code, p1, p2]) code,si = unpack_callback_result(packed) @@ -2387,12 +2356,12 @@ def canon_waitable_set_new(): ### ๐Ÿ”€ `canon waitable-set.wait` -def canon_waitable_set_wait(cancellable, mem, si, ptr): +def canon_waitable_set_wait(mem, si, ptr): inst = current_instance() trap_if(not inst.may_leave) wset = inst.handles.get(si) trap_if(not isinstance(wset, WaitableSet)) - event = wset.wait_for_event(lambda: cancellable) + event = wset.wait() return unpack_event(mem, inst, ptr, event) def unpack_event(mem, inst, ptr, e: EventTuple): @@ -2404,12 +2373,12 @@ def unpack_event(mem, inst, ptr, e: EventTuple): ### ๐Ÿ”€ `canon waitable-set.poll` -def canon_waitable_set_poll(cancellable, mem, si, ptr): +def canon_waitable_set_poll(mem, si, ptr): inst = current_instance() trap_if(not inst.may_leave) wset = inst.handles.get(si) trap_if(not isinstance(wset, WaitableSet)) - event = wset.poll(lambda: cancellable) + event = wset.poll() return unpack_event(mem, inst, ptr, event) ### ๐Ÿ”€ `canon waitable-set.drop` @@ -2454,8 +2423,11 @@ def canon_subtask_cancel(async_, i): subtask.cancellation_requested = True subtask.has_sync_waiter = True subtask.on_cancel() - if not subtask.resolved() and not async_: - thread.wait_until(subtask.resolved) + if not subtask.resolved(): + if not async_: + thread.wait_until(subtask.resolved) + else: + thread.yield_() subtask.has_sync_waiter = False if not subtask.resolved(): return [BLOCKED] @@ -2692,57 +2664,57 @@ def canon_thread_resume_later(i): ### ๐Ÿงต `canon thread.suspend` -def canon_thread_suspend(cancellable): +def canon_thread_suspend(): thread = current_thread() trap_if(not thread.task.inst.may_leave) - cancelled = thread.suspend(lambda: cancellable) - return [cancelled] + thread.suspend() + return [0] ### ๐Ÿงต `canon thread.yield` -def canon_thread_yield(cancellable): +def canon_thread_yield(): thread = current_thread() trap_if(not thread.task.inst.may_leave) - cancelled = thread.yield_(lambda: cancellable) - return [cancelled] + thread.yield_() + return [0] ### ๐Ÿงต `canon thread.suspend-then-resume` -def canon_thread_suspend_then_resume(cancellable, i): +def canon_thread_suspend_then_resume(i): thread = current_thread() trap_if(not thread.task.inst.may_leave) other_thread = thread.task.inst.threads.get(i) trap_if(not other_thread.suspended()) - cancelled = thread.suspend_then_resume(lambda: cancellable, other_thread) - return [cancelled] + thread.suspend_then_resume(other_thread) + return [0] ### ๐Ÿงต `canon thread.yield-then-resume` -def canon_thread_yield_then_resume(cancellable, i): +def canon_thread_yield_then_resume(i): thread = current_thread() trap_if(not thread.task.inst.may_leave) other_thread = thread.task.inst.threads.get(i) trap_if(not other_thread.suspended()) - cancelled = thread.yield_then_resume(lambda: cancellable, other_thread) - return [cancelled] + thread.yield_then_resume(other_thread) + return [0] ### ๐Ÿงต `canon thread.suspend-then-promote` -def canon_thread_suspend_then_promote(cancellable, i): +def canon_thread_suspend_then_promote(i): thread = current_thread() trap_if(not thread.task.inst.may_leave) other_thread = thread.task.inst.threads.get(i) - cancelled = thread.suspend_then_promote(lambda: cancellable, other_thread) - return [cancelled] + thread.suspend_then_promote(other_thread) + return [0] ### ๐Ÿงต `canon thread.yield-then-promote` -def canon_thread_yield_then_promote(cancellable, i): +def canon_thread_yield_then_promote(i): thread = current_thread() trap_if(not thread.task.inst.may_leave) other_thread = thread.task.inst.threads.get(i) - cancelled = thread.yield_then_promote(lambda: cancellable, other_thread) - return [cancelled] + thread.yield_then_promote(other_thread) + return [0] ### ๐Ÿ“ `canon error-context.new` diff --git a/design/mvp/canonical-abi/run_tests.py b/design/mvp/canonical-abi/run_tests.py index 8128760a..90dc344e 100644 --- a/design/mvp/canonical-abi/run_tests.py +++ b/design/mvp/canonical-abi/run_tests.py @@ -1,4 +1,3 @@ -import time import definitions from definitions import * @@ -61,14 +60,14 @@ def lift_and_run(opts, inst, ft, callee, on_start, on_resolve): def mk_host_func(store, host_func, ft): def func_inst(on_start, on_resume) -> OnCancel: def thread_func(): - wait_until = lambda rf: host_thread.wait_until(rf, cancellable = lambda: True) + wait_until = lambda rf: host_thread.wait_until(rf) host_func(on_start, on_resume, wait_until) inst = ComponentInstance(store) task = Task(ft, CanonicalOptions(), inst, on_start, on_resume) host_thread = Thread(task, thread_func) host_thread.resume() def on_cancel(): - host_thread.resume(Cancelled.TRUE) + pass return on_cancel return func_inst @@ -718,21 +717,21 @@ def consumer(args): fut1_1.set() [waitretp] = consumer_heap.realloc([0, 0, 8, 4]) - [event] = canon_waitable_set_wait(True, MemInst(consumer_heap.memory, 'i32'), seti, waitretp) + [event] = canon_waitable_set_wait(MemInst(consumer_heap.memory, 'i32'), seti, waitretp) assert(event == EventCode.SUBTASK) assert(consumer_heap.memory[waitretp] == subi1) assert(consumer_heap.memory[waitretp+4] == Subtask.State.RETURNED) [] = canon_subtask_drop(subi1) fut1_2.set() - [event] = canon_waitable_set_wait(True, MemInst(consumer_heap.memory, 'i32'), seti, waitretp) + [event] = canon_waitable_set_wait(MemInst(consumer_heap.memory, 'i32'), seti, waitretp) assert(event == EventCode.SUBTASK) assert(consumer_heap.memory[waitretp] == subi2) assert(consumer_heap.memory[waitretp+4] == Subtask.State.STARTED) assert(consumer_heap.memory[retp] == 13) fut2.set() - [event] = canon_waitable_set_wait(True, MemInst(consumer_heap.memory, 'i32'), seti, waitretp) + [event] = canon_waitable_set_wait(MemInst(consumer_heap.memory, 'i32'), seti, waitretp) assert(event == EventCode.SUBTASK) assert(consumer_heap.memory[waitretp] == subi2) assert(consumer_heap.memory[waitretp+4] == Subtask.State.RETURNED) @@ -953,7 +952,7 @@ def core_consumer(args): assert(ret == CopyResult.COMPLETED) retp = 0 - [event] = canon_waitable_set_wait(True, MemInst(consumer_mem, 'i32'), seti, retp) + [event] = canon_waitable_set_wait(MemInst(consumer_mem, 'i32'), seti, retp) assert(event == EventCode.SUBTASK) assert(consumer_mem[retp+0] == subi2) assert(consumer_mem[retp+4] == Subtask.State.STARTED) @@ -962,17 +961,16 @@ def core_consumer(args): assert(ret == CopyResult.COMPLETED) for i in range(10): - [ret] = canon_thread_yield(True) - assert(ret == 0) + canon_thread_yield() retp = 0 - [ret] = canon_waitable_set_poll(True, MemInst(consumer_mem, 'i32'), seti, retp) + [ret] = canon_waitable_set_poll(MemInst(consumer_mem, 'i32'), seti, retp) assert(ret == EventCode.NONE) [ret] = canon_future_write(FutureType(None), consumer_opts, wfut21, 0xdeadbeef) assert(ret == CopyResult.COMPLETED) retp = 0 - [event] = canon_waitable_set_wait(True, MemInst(consumer_mem, 'i32'), seti, retp) + [event] = canon_waitable_set_wait(MemInst(consumer_mem, 'i32'), seti, retp) assert(event == EventCode.SUBTASK) assert(consumer_mem[retp+0] == subi1) assert(consumer_mem[retp+4] == Subtask.State.RETURNED) @@ -983,17 +981,16 @@ def core_consumer(args): assert(ret == CopyResult.COMPLETED) for i in range(10): - [ret] = canon_thread_yield(True) - assert(ret == 0) + canon_thread_yield() retp = 0 - [ret] = canon_waitable_set_poll(True, MemInst(consumer_mem, 'i32'), seti, retp) + [ret] = canon_waitable_set_poll(MemInst(consumer_mem, 'i32'), seti, retp) assert(ret == EventCode.NONE) [ret] = canon_future_write(FutureType(None), consumer_opts, wfut13, 0xdeadbeef) assert(ret == CopyResult.COMPLETED) retp = 0 - [event] = canon_waitable_set_wait(True, MemInst(consumer_mem, 'i32'), seti, retp) + [event] = canon_waitable_set_wait(MemInst(consumer_mem, 'i32'), seti, retp) assert(event == EventCode.SUBTASK) assert(consumer_mem[retp+0] == subi2) assert(consumer_mem[retp+4] == Subtask.State.RETURNED) @@ -1010,7 +1007,7 @@ def core_consumer(args): assert(ret == CopyResult.COMPLETED) retp = 0 - [event] = canon_waitable_set_wait(True, MemInst(consumer_mem, 'i32'), seti, retp) + [event] = canon_waitable_set_wait(MemInst(consumer_mem, 'i32'), seti, retp) assert(event == EventCode.SUBTASK) assert(consumer_mem[retp+0] == subi3) assert(consumer_mem[retp+4] == Subtask.State.RETURNED) @@ -1068,7 +1065,7 @@ def core_caller(args): [seti] = canon_waitable_set_new() [] = canon_waitable_join(subi, seti) retp3 = 12 - [event] = canon_waitable_set_wait(True, MemInst(caller_mem, 'i32'), seti, retp3) + [event] = canon_waitable_set_wait(MemInst(caller_mem, 'i32'), seti, retp3) assert(event == EventCode.SUBTASK) assert(caller_mem[retp3+0] == subi) assert(caller_mem[retp3+4] == Subtask.State.RETURNED) @@ -1132,10 +1129,9 @@ def consumer(args): remain = [subi1, subi2] while remain: - [ret] = canon_thread_yield(True) - assert(ret == 0) + canon_thread_yield() retp = 8 - [event] = canon_waitable_set_poll(True, MemInst(consumer_heap.memory, 'i32'), seti, retp) + [event] = canon_waitable_set_poll(MemInst(consumer_heap.memory, 'i32'), seti, retp) if event == EventCode.NONE: continue assert(event == EventCode.SUBTASK) @@ -1220,7 +1216,7 @@ def consumer(args): remain = [subi1, subi2] while remain: retp = 8 - [event] = canon_waitable_set_wait(True, MemInst(consumer_heap.memory, 'i32'), seti, retp) + [event] = canon_waitable_set_wait(MemInst(consumer_heap.memory, 'i32'), seti, retp) assert(event == EventCode.SUBTASK) assert(consumer_heap.memory[retp+4] == Subtask.State.RETURNED) subi = consumer_heap.memory[retp] @@ -1286,14 +1282,14 @@ def core_func(args): fut1.set() [retp] = consumer_heap.realloc([0,0,8,4]) - [event] = canon_waitable_set_wait(True, MemInst(consumer_heap.memory, 'i32'), seti, retp) + [event] = canon_waitable_set_wait(MemInst(consumer_heap.memory, 'i32'), seti, retp) assert(event == EventCode.SUBTASK) assert(consumer_heap.memory[retp] == subi1) assert(consumer_heap.memory[retp+4] == Subtask.State.RETURNED) fut2.set() - [event] = canon_waitable_set_wait(True, MemInst(consumer_heap.memory, 'i32'), seti, retp) + [event] = canon_waitable_set_wait(MemInst(consumer_heap.memory, 'i32'), seti, retp) assert(event == EventCode.SUBTASK) assert(consumer_heap.memory[retp] == subi2) assert(consumer_heap.memory[retp+4] == Subtask.State.RETURNED) @@ -1607,7 +1603,7 @@ def core_func(args): [seti] = canon_waitable_set_new() [] = canon_waitable_join(rsi1, seti) definitions.throw_it = True - [event] = canon_waitable_set_wait(True, MemInst(mem, 'i32'), seti, retp) ## + [event] = canon_waitable_set_wait(MemInst(mem, 'i32'), seti, retp) ## assert(event == EventCode.STREAM_READ) assert(mem[retp+0] == rsi1) result,n = unpack_result(mem[retp+4]) @@ -1623,7 +1619,7 @@ def core_func(args): assert(ret == definitions.BLOCKED) host_import_incoming.set_remain(100) [] = canon_waitable_join(wsi3, seti) - [event] = canon_waitable_set_wait(True, MemInst(mem, 'i32'), seti, retp) + [event] = canon_waitable_set_wait(MemInst(mem, 'i32'), seti, retp) assert(event == EventCode.STREAM_WRITE) assert(mem[retp+0] == wsi3) result,n = unpack_result(mem[retp+4]) @@ -1635,7 +1631,7 @@ def core_func(args): assert(ret == definitions.BLOCKED) dst_stream.set_remain(100) [] = canon_waitable_join(wsi2, seti) - [event] = canon_waitable_set_wait(True, MemInst(mem, 'i32'), seti, retp) + [event] = canon_waitable_set_wait(MemInst(mem, 'i32'), seti, retp) assert(event == EventCode.STREAM_WRITE) assert(mem[retp+0] == wsi2) result,n = unpack_result(mem[retp+4]) @@ -1654,7 +1650,7 @@ def core_func(args): [ret] = canon_stream_read(StreamType(U8Type()), opts, rsi4, 0, 4) assert(ret == definitions.BLOCKED) [] = canon_waitable_join(rsi4, seti) - [event] = canon_waitable_set_wait(True, MemInst(mem, 'i32'), seti, retp) + [event] = canon_waitable_set_wait(MemInst(mem, 'i32'), seti, retp) assert(event == EventCode.STREAM_READ) assert(mem[retp+0] == rsi4) [] = canon_waitable_join(rsi4, 0) @@ -1780,7 +1776,7 @@ def core_func(args): [seti] = canon_waitable_set_new() [] = canon_waitable_join(rsi, seti) - [event] = canon_waitable_set_wait(True, MemInst(mem, 'i32'), seti, retp) + [event] = canon_waitable_set_wait(MemInst(mem, 'i32'), seti, retp) assert(event == EventCode.STREAM_READ) assert(mem[retp+0] == rsi) result,n = unpack_result(mem[retp+4]) @@ -1801,7 +1797,7 @@ def core_func(args): assert(ret == definitions.BLOCKED) dst.set_remain(4) [] = canon_waitable_join(wsi, seti) - [event] = canon_waitable_set_wait(True, MemInst(mem, 'i32'), seti, retp) + [event] = canon_waitable_set_wait(MemInst(mem, 'i32'), seti, retp) assert(event == EventCode.STREAM_WRITE) assert(mem[retp+0] == wsi) result,n = unpack_result(mem[retp+4]) @@ -1862,7 +1858,7 @@ def core_func1(args): retp = 16 [seti] = canon_waitable_set_new() [] = canon_waitable_join(wsi, seti) - [event] = canon_waitable_set_wait(True, MemInst(mem1, 'i32'), seti, retp) + [event] = canon_waitable_set_wait(MemInst(mem1, 'i32'), seti, retp) assert(event == EventCode.STREAM_WRITE) assert(mem1[retp+0] == wsi) result,n = unpack_result(mem1[retp+4]) @@ -1873,7 +1869,7 @@ def core_func1(args): fut4.set() - [event] = canon_waitable_set_wait(True, MemInst(mem1, 'i32'), seti, retp) + [event] = canon_waitable_set_wait(MemInst(mem1, 'i32'), seti, retp) assert(event == EventCode.STREAM_WRITE) assert(mem1[retp+0] == wsi) assert(mem1[retp+4] == 0) @@ -1912,7 +1908,7 @@ def core_func2(args): [seti] = canon_waitable_set_new() [] = canon_waitable_join(rsi, seti) - [event] = canon_waitable_set_wait(True, MemInst(mem2, 'i32'), seti, retp) + [event] = canon_waitable_set_wait(MemInst(mem2, 'i32'), seti, retp) assert(event == EventCode.STREAM_READ) assert(mem2[retp+0] == rsi) result,n = unpack_result(mem2[retp+4]) @@ -1940,7 +1936,7 @@ def core_func2(args): [ret] = canon_stream_read(StreamType(U8Type()), opts2, rsi, 12345, 0) assert(ret == definitions.BLOCKED) - [event] = canon_waitable_set_wait(True, MemInst(mem2, 'i32'), seti, retp) + [event] = canon_waitable_set_wait(MemInst(mem2, 'i32'), seti, retp) assert(event == EventCode.STREAM_READ) assert(mem2[retp+0] == rsi) p2 = int.from_bytes(mem2[retp+4 : retp+8], 'little', signed=False) @@ -1987,7 +1983,7 @@ def core_func1(args): retp = 16 [seti] = canon_waitable_set_new() [] = canon_waitable_join(wsi, seti) - [event] = canon_waitable_set_wait(True, MemInst(mem1, 'i32'), seti, retp) + [event] = canon_waitable_set_wait(MemInst(mem1, 'i32'), seti, retp) assert(event == EventCode.STREAM_WRITE) assert(mem1[retp+0] == wsi) result,n = unpack_result(mem1[retp+4]) @@ -2025,7 +2021,7 @@ def core_func2(args): [seti] = canon_waitable_set_new() [] = canon_waitable_join(rsi, seti) - [event] = canon_waitable_set_wait(True, MemInst(mem2, 'i32'), seti, retp) + [event] = canon_waitable_set_wait(MemInst(mem2, 'i32'), seti, retp) assert(event == EventCode.STREAM_READ) assert(mem2[retp+0] == rsi) result,n = unpack_result(mem2[retp+4]) @@ -2142,7 +2138,7 @@ def core_func(args): host_source.unblock_cancel() [seti] = canon_waitable_set_new() [] = canon_waitable_join(rsi, seti) - [event] = canon_waitable_set_wait(True, MemInst(mem, 'i32'), seti, retp) + [event] = canon_waitable_set_wait(MemInst(mem, 'i32'), seti, retp) assert(event == EventCode.STREAM_READ) assert(mem[retp+0] == rsi) result,n = unpack_result(mem[retp+4]) @@ -2247,7 +2243,7 @@ def core_func(args): [seti] = canon_waitable_set_new() [] = canon_waitable_join(rfi, seti) - [event] = canon_waitable_set_wait(True, MemInst(mem, 'i32'), seti, retp) + [event] = canon_waitable_set_wait(MemInst(mem, 'i32'), seti, retp) assert(event == EventCode.FUTURE_READ) assert(mem[retp+0] == rfi) assert(mem[retp+4] == CopyResult.COMPLETED) @@ -2273,7 +2269,7 @@ def core_func(args): assert(ret == CopyResult.COMPLETED) while not thread.task.inst.handles.get(rfi).has_pending_event(): - canon_thread_yield(True) + canon_thread_yield() [ret] = canon_future_cancel_read(FutureType(U8Type()), False, rfi) assert(ret == CopyResult.COMPLETED) @@ -2320,7 +2316,7 @@ def core_func(args): retp = 16 [seti] = canon_waitable_set_new() [] = canon_waitable_join(wfi, seti) - [event] = canon_waitable_set_wait(True, MemInst(mem, 'i32'), seti, retp) + [event] = canon_waitable_set_wait(MemInst(mem, 'i32'), seti, retp) assert(event == EventCode.FUTURE_WRITE) assert(mem[retp+0] == wfi) assert(mem[retp+4] == CopyResult.DROPPED) @@ -2338,140 +2334,142 @@ def test_cancel_subtask(): ft = FuncType([U8Type()], [U8Type()], async_ = True) callee_heap = Heap(10) - callee_opts = mk_opts(MemInst(callee_heap.memory, 'i32'), async_ = True) - sync_callee_opts = mk_opts(MemInst(callee_heap.memory, 'i32'), async_ = False) + callee_mem = MemInst(callee_heap.memory, 'i32') callee_inst = ComponentInstance(store) - def core_callee1(args): + never_started_opts = mk_opts(callee_mem, async_ = True) + def core_never_started(args): assert(False) - callee1 = store.lift(core_callee1, ft, callee_opts, callee_inst) + callee_never_started = store.lift(core_never_started, ft, never_started_opts, callee_inst) - def core_callee2(args): + # Waits in the event loop and, when cancelled, either resolves immediately or + # goes back to the event loop once more (via `YIELD`) before resolving. + wait_opts = mk_opts(callee_mem, async_ = True) + def core_wait(args): [x] = args + [] = canon_context_set('i32', 0, x) [si] = canon_waitable_set_new() - [ret] = canon_waitable_set_wait(True, MemInst(callee_heap.memory, 'i32'), si, 0) - assert(ret == EventCode.TASK_CANCELLED) - match x: - case 1: - [] = canon_task_return([U8Type()], callee_opts, [42]) - case 2: - [] = canon_task_cancel() - case 3: - [_] = canon_thread_yield(True) - [] = canon_task_return([U8Type()], callee_opts, [43]) - case 4: - [_] = canon_thread_yield(True) - [] = canon_task_cancel() - case _: - assert(False) - return [] - callee2 = store.lift(core_callee2, ft, callee_opts, callee_inst) + return [CallbackCode.WAIT | (si << 4)] + def core_wait_callback(args): + [event,p1,p2] = args + assert(p1 == 0 and p2 == 0) + [x] = canon_context_get('i32', 0) + if event == EventCode.TASK_CANCELLED: + match x: + case 1: + [] = canon_task_return([U8Type()], wait_opts, [42]) + case 2: + [] = canon_task_cancel() + case 3 | 4: + return [CallbackCode.YIELD] + case _: + assert(False) + else: + assert(event == EventCode.NONE) + match x: + case 3: + [] = canon_task_return([U8Type()], wait_opts, [43]) + case 4: + [] = canon_task_cancel() + case _: + assert(False) + return [CallbackCode.EXIT] + wait_opts.callback = core_wait_callback + callee_wait = store.lift(core_wait, ft, wait_opts, callee_inst) - def core_callee3(args): + # Yields to the event loop and then returns `x`, unless it is cancelled first. + yield_opts = mk_opts(callee_mem, async_ = True) + def core_yield(args): [x] = args - [cancelled] = canon_thread_yield(True) - if cancelled: + [] = canon_context_set('i32', 0, x) + return [CallbackCode.YIELD] + def core_yield_callback(args): + [event,p1,p2] = args + assert(p1 == 0 and p2 == 0) + [x] = canon_context_get('i32', 0) + if event == EventCode.TASK_CANCELLED: [] = canon_task_cancel() else: - [] = canon_task_return([U8Type()], callee_opts, [83]) - return [] - callee3 = store.lift(core_callee3, ft, callee_opts, callee_inst) + assert(event == EventCode.NONE) + [] = canon_task_return([U8Type()], yield_opts, [x]) + return [CallbackCode.EXIT] + yield_opts.callback = core_yield_callback + callee_yield = store.lift(core_yield, ft, yield_opts, callee_inst) + + # Holds the "exclusive" lock of `callee_inst` while suspended, thereby + # preventing any other task of `callee_inst` from being cancelled. + lock_hog_opts = mk_opts(callee_mem, async_ = True) + def core_lock_hog(args): + [x] = args + canon_thread_yield() + [] = canon_task_return([U8Type()], lock_hog_opts, [x]) + return [CallbackCode.EXIT] + def core_lock_hog_callback(args): + assert(False) + lock_hog_opts.callback = core_lock_hog_callback + callee_lock_hog = store.lift(core_lock_hog, ft, lock_hog_opts, callee_inst) - host_fut4 = RacyBool(False) - def host_func4(on_start, on_resolve, wait_until): + # Blocks in a synchronous import call (during which it is not cancellable) + # and only then returns to the event loop to receive the cancellation. + host_fut1 = RacyBool(False) + def host_func1(on_start, on_resolve, wait_until): args = on_start() - assert(len(args) == 1) - assert(args[0] == 42) - wait_until(host_fut4.is_set) + assert(len(args) == 1 and args[0] == 42) + wait_until(host_fut1.is_set) on_resolve([43]) - host_func4_inst = mk_host_func(store, host_func4, ft) - def core_callee4(args): + host_func1_inst = mk_host_func(store, host_func1, ft) + sync_lower_opts = mk_opts(callee_mem, async_ = False) + sync_import_opts = mk_opts(callee_mem, async_ = True) + def core_sync_import(args): [x] = args - [result] = store.lower(host_func4_inst, ft, sync_callee_opts, callee_inst)([42]) + [result] = store.lower(host_func1_inst, ft, sync_lower_opts, callee_inst)([42]) assert(result == 43) try: [] = canon_task_cancel() assert(False) except Trap: pass - [seti] = canon_waitable_set_new() - [result] = canon_waitable_set_wait(True, MemInst(callee_heap.memory, 'i32'), seti, 0) - assert(result == EventCode.TASK_CANCELLED) - [result] = canon_waitable_set_poll(True, MemInst(callee_heap.memory, 'i32'), seti, 0) - assert(result == EventCode.NONE) + [si] = canon_waitable_set_new() + [] = canon_context_set('i32', 0, si) + return [CallbackCode.WAIT | (si << 4)] + def core_sync_import_callback(args): + [event,p1,p2] = args + assert(event == EventCode.TASK_CANCELLED and p1 == 0 and p2 == 0) + [si] = canon_context_get('i32', 0) + [event] = canon_waitable_set_poll(callee_mem, si, 0) + assert(event == EventCode.NONE) [] = canon_task_cancel() - return [] - callee4 = store.lift(core_callee4, ft, callee_opts, callee_inst) + return [CallbackCode.EXIT] + sync_import_opts.callback = core_sync_import_callback + callee_sync_import = store.lift(core_sync_import, ft, sync_import_opts, callee_inst) - host_fut5 = RacyBool(False) - def host_func5(on_start, on_resolve, wait_until): + # Uses the stackful ABI and thus never becomes cancellable: the cancellation + # request is silently ignored and the task returns normally. + host_fut2 = RacyBool(False) + def host_func2(on_start, on_resolve, wait_until): args = on_start() - assert(len(args) == 1) - assert(args[0] == 42) - wait_until(host_fut5.is_set) - wait_until(host_fut5.is_set) + assert(len(args) == 1 and args[0] == 42) + wait_until(host_fut2.is_set) + wait_until(host_fut2.is_set) on_resolve([43]) - host_func5_inst = mk_host_func(store, host_func5, ft) - def core_callee5(args): + host_func2_inst = mk_host_func(store, host_func2, ft) + ignores_cancel_opts = mk_opts(callee_mem, async_ = True) + def core_ignores_cancel(args): [x] = args assert(x == 13) - [ret] = store.lower(host_func5_inst, ft, callee_opts, callee_inst)([42, 0]) + [ret] = store.lower(host_func2_inst, ft, ignores_cancel_opts, callee_inst)([42, 0]) state,subi = unpack_result(ret) assert(state == Subtask.State.STARTED) [ret] = canon_subtask_cancel(False, subi) assert(ret == Subtask.State.RETURNED) - [] = canon_task_return([U8Type()], callee_opts, [44]) - return [] - callee5 = store.lift(core_callee5, ft, callee_opts, callee_inst) - - core_ftbl = Table() - core_ft = CoreFuncType(['i32'], []) - def thread_func(cancellable, args): - [mainthreadi] = args - if cancellable: - [ret] = canon_thread_suspend_then_resume(True, mainthreadi) - assert(ret == Cancelled.TRUE) - [ret] = canon_thread_suspend_then_resume(True, mainthreadi) - assert(ret == Cancelled.FALSE) - [] = canon_task_return([U8Type()], callee_opts, [45]) - else: - [ret] = canon_thread_suspend_then_resume(False, mainthreadi) - assert(ret == Cancelled.FALSE) - return [] - cthread_func = partial(thread_func, True) - ncthread_func = partial(thread_func, False) - cfi = core_ftbl.add(CoreFuncRef(core_ft, cthread_func)) - ncfi = core_ftbl.add(CoreFuncRef(core_ft, ncthread_func)) - - def core_callee6(args): - [x] = args - assert(x == 14) - - [mainthreadi] = canon_thread_index() - - [threadi1] = canon_thread_new_indirect(core_ft, core_ftbl, ncfi, mainthreadi) - [ret] = canon_thread_suspend_then_resume(True, threadi1) - assert(ret == Cancelled.FALSE) - - [threadi2] = canon_thread_new_indirect(core_ft, core_ftbl, cfi, mainthreadi) - [ret] = canon_thread_suspend_then_resume(True, threadi2) - assert(ret == Cancelled.FALSE) - - [threadi3] = canon_thread_new_indirect(core_ft, core_ftbl, ncfi, mainthreadi) - [ret] = canon_thread_suspend_then_resume(True, threadi3) - assert(ret == Cancelled.FALSE) - - [ret] = canon_thread_suspend(False) - assert(ret == Cancelled.FALSE) - - [] = canon_thread_resume_later(threadi1) - [] = canon_thread_resume_later(threadi2) - [] = canon_thread_resume_later(threadi3) + [] = canon_subtask_drop(subi) + [] = canon_task_return([U8Type()], ignores_cancel_opts, [44]) return [] - callee6 = store.lift(core_callee6, ft, callee_opts, callee_inst) + callee_ignores_cancel = store.lift(core_ignores_cancel, ft, ignores_cancel_opts, callee_inst) caller_heap = Heap(20) - caller_opts = mk_opts(MemInst(caller_heap.memory, 'i32'), async_ = True) + caller_mem = MemInst(caller_heap.memory, 'i32') + caller_opts = mk_opts(caller_mem, async_ = True) caller_inst = ComponentInstance(store) def core_caller(args): @@ -2479,12 +2477,15 @@ def core_caller(args): assert(x == 1) [seti] = canon_waitable_set_new() + retp = 8 + # A task blocked on backpressure is cancelled without ever running the + # callee, both when cancelling synchronously and asynchronously. callee_inst.backpressure = True - [ret] = store.lower(callee1, ft, caller_opts, caller_inst)([13, 0]) + [ret] = store.lower(callee_never_started, ft, caller_opts, caller_inst)([13, 0]) state,subi1 = unpack_result(ret) assert(state == Subtask.State.STARTING) - [ret] = store.lower(callee1, ft, caller_opts, caller_inst)([13, 0]) + [ret] = store.lower(callee_never_started, ft, caller_opts, caller_inst)([13, 0]) state,subi2 = unpack_result(ret) assert(state == Subtask.State.STARTING) [ret] = canon_subtask_cancel(False, subi2) @@ -2493,22 +2494,25 @@ def core_caller(args): assert(ret == Subtask.State.CANCELLED_BEFORE_STARTED) callee_inst.backpressure = False - [ret] = store.lower(callee2, ft, caller_opts, caller_inst)([1, 0]) + # A `callback` task waiting in the event loop is immediately resumed with a + # TASK_CANCELLED and can then `task.return`, `task.cancel` or go back to the + # event loop first. + [ret] = store.lower(callee_wait, ft, caller_opts, caller_inst)([1, 0]) state,subi1 = unpack_result(ret) assert(state == Subtask.State.STARTED) - [ret] = store.lower(callee2, ft, caller_opts, caller_inst)([2, 0]) + [ret] = store.lower(callee_wait, ft, caller_opts, caller_inst)([2, 0]) state,subi2 = unpack_result(ret) assert(state == Subtask.State.STARTED) - [ret] = store.lower(callee2, ft, caller_opts, caller_inst)([3, 0]) + [ret] = store.lower(callee_wait, ft, caller_opts, caller_inst)([3, 0]) state,subi3 = unpack_result(ret) assert(state == Subtask.State.STARTED) - [ret] = store.lower(callee2, ft, caller_opts, caller_inst)([3, 0]) + [ret] = store.lower(callee_wait, ft, caller_opts, caller_inst)([3, 0]) state,subi3_2 = unpack_result(ret) assert(state == Subtask.State.STARTED) - [ret] = store.lower(callee2, ft, caller_opts, caller_inst)([4, 0]) + [ret] = store.lower(callee_wait, ft, caller_opts, caller_inst)([4, 0]) state,subi4 = unpack_result(ret) assert(state == Subtask.State.STARTED) - [ret] = store.lower(callee2, ft, caller_opts, caller_inst)([4, 0]) + [ret] = store.lower(callee_wait, ft, caller_opts, caller_inst)([4, 0]) state,subi4_2 = unpack_result(ret) assert(state == Subtask.State.STARTED) @@ -2520,20 +2524,28 @@ def core_caller(args): caller_heap.memory[0] = 13 [ret] = canon_subtask_cancel(True, subi2) - assert(ret == Subtask.State.CANCELLED_BEFORE_RETURNED) + if ret == definitions.BLOCKED: + canon_waitable_join(subi2, seti) + [ret] = canon_waitable_set_wait(caller_mem, seti, retp) + assert(ret == EventCode.SUBTASK) + assert(caller_heap.memory[retp+0] == subi2) + assert(caller_heap.memory[retp+4] == Subtask.State.CANCELLED_BEFORE_RETURNED) + else: + assert(ret == Subtask.State.CANCELLED_BEFORE_RETURNED) assert(caller_heap.memory[0] == 13) [] = canon_subtask_drop(subi2) caller_heap.memory[0] = 13 [ret] = canon_subtask_cancel(True, subi3) - assert(ret == definitions.BLOCKED) - assert(caller_heap.memory[0] == 13) - [] = canon_waitable_join(subi3, seti) - retp = 8 - [ret] = canon_waitable_set_wait(True, MemInst(caller_heap.memory, 'i32'), seti, retp) - assert(ret == EventCode.SUBTASK) - assert(caller_heap.memory[retp+0] == subi3) - assert(caller_heap.memory[retp+4] == Subtask.State.RETURNED) + if ret == definitions.BLOCKED: + assert(caller_heap.memory[0] == 13) + [] = canon_waitable_join(subi3, seti) + [ret] = canon_waitable_set_wait(caller_mem, seti, retp) + assert(ret == EventCode.SUBTASK) + assert(caller_heap.memory[retp+0] == subi3) + assert(caller_heap.memory[retp+4] == Subtask.State.RETURNED) + else: + assert(ret == Subtask.State.RETURNED) assert(caller_heap.memory[0] == 43) [] = canon_subtask_drop(subi3) @@ -2545,14 +2557,15 @@ def core_caller(args): caller_heap.memory[0] = 13 [ret] = canon_subtask_cancel(True, subi4) - assert(ret == definitions.BLOCKED) assert(caller_heap.memory[0] == 13) - [] = canon_waitable_join(subi4, seti) - retp = 8 - [ret] = canon_waitable_set_wait(True, MemInst(caller_heap.memory, 'i32'), seti, retp) - assert(ret == EventCode.SUBTASK) - assert(caller_heap.memory[retp+0] == subi4) - assert(caller_heap.memory[retp+4] == Subtask.State.CANCELLED_BEFORE_RETURNED) + if ret == definitions.BLOCKED: + [] = canon_waitable_join(subi4, seti) + [ret] = canon_waitable_set_wait(caller_mem, seti, retp) + assert(ret == EventCode.SUBTASK) + assert(caller_heap.memory[retp+0] == subi4) + assert(caller_heap.memory[retp+4] == Subtask.State.CANCELLED_BEFORE_RETURNED) + else: + assert(ret == Subtask.State.CANCELLED_BEFORE_RETURNED) [] = canon_subtask_drop(subi4) caller_heap.memory[0] = 13 @@ -2561,74 +2574,107 @@ def core_caller(args): assert(caller_heap.memory[0] == 13) [] = canon_subtask_drop(subi4_2) + # Cancelling a subtask that has already returned simply reports the + # already-resolved state. caller_heap.memory[0] = 13 - [ret] = store.lower(callee3, ft, caller_opts, caller_inst)([0, 0]) + [ret] = store.lower(callee_yield, ft, caller_opts, caller_inst)([83, 0]) state,subi = unpack_result(ret) assert(state == Subtask.State.STARTED) while caller_inst.handles.get(subi).state == Subtask.State.STARTED: - [_] = canon_thread_yield(True) + canon_thread_yield() [ret] = canon_subtask_cancel(True, subi) assert(ret == Subtask.State.RETURNED) assert(caller_heap.memory[0] == 83) [] = canon_subtask_drop(subi) + # A `callback` task that returned `YIELD` is also immediately cancellable. caller_heap.memory[0] = 13 - [ret] = store.lower(callee3, ft, caller_opts, caller_inst)([0, 0]) + [ret] = store.lower(callee_yield, ft, caller_opts, caller_inst)([83, 0]) state,subi = unpack_result(ret) assert(state == Subtask.State.STARTED) [ret] = canon_subtask_cancel(True, subi) - assert(ret == Subtask.State.CANCELLED_BEFORE_RETURNED) + if ret == definitions.BLOCKED: + canon_waitable_join(subi, seti) + [ret] = canon_waitable_set_wait(caller_mem, seti, retp) + assert(ret == EventCode.SUBTASK) + assert(caller_heap.memory[retp+0] == subi) + assert(caller_heap.memory[retp+4] == Subtask.State.CANCELLED_BEFORE_RETURNED) + else: + assert(ret == Subtask.State.CANCELLED_BEFORE_RETURNED) assert(caller_heap.memory[0] == 13) [] = canon_subtask_drop(subi) + # While another task holds the "exclusive" lock of `callee_inst`, the + # cancellation request is remembered and only delivered once the lock is + # released and the cancelled task is resumed: first for a task waiting in + # the event loop, then for a task that returned `YIELD`. + for (callee, cancelled_state, expected) in [(callee_wait, Subtask.State.RETURNED, 42), + (callee_yield, Subtask.State.CANCELLED_BEFORE_RETURNED, 13)]: + caller_heap.memory[0] = 13 + caller_heap.memory[4] = 13 + [ret] = store.lower(callee, ft, caller_opts, caller_inst)([1, 0]) + state,subi = unpack_result(ret) + assert(state == Subtask.State.STARTED) + [ret] = store.lower(callee_lock_hog, ft, caller_opts, caller_inst)([45, 4]) + state,subi_hog = unpack_result(ret) + assert(state == Subtask.State.STARTED) + [ret] = canon_subtask_cancel(True, subi) + if ret == definitions.BLOCKED: + assert(caller_heap.memory[0] == 13) + [] = canon_waitable_join(subi, seti) + [ret] = canon_waitable_set_wait(caller_mem, seti, retp) + assert(ret == EventCode.SUBTASK) + assert(caller_heap.memory[retp+0] == subi) + assert(caller_heap.memory[retp+4] == cancelled_state) + else: + assert(ret == cancelled_state) + assert(caller_heap.memory[0] == expected) + assert(caller_heap.memory[4] == 45) + [] = canon_subtask_drop(subi) + [] = canon_waitable_join(subi_hog, seti) + [ret] = canon_waitable_set_wait(caller_mem, seti, retp) + assert(ret == EventCode.SUBTASK) + assert(caller_heap.memory[retp+0] == subi_hog) + assert(caller_heap.memory[retp+4] == Subtask.State.RETURNED) + [] = canon_subtask_drop(subi_hog) + + # A `callback` task blocked in a synchronous import call is not cancellable + # until it returns to the event loop. `task.cancel` traps until then and + # TASK_CANCELLED is delivered at most once. caller_heap.memory[0] = 13 - [ret] = store.lower(callee4, ft, caller_opts, caller_inst)([0, 0]) + [ret] = store.lower(callee_sync_import, ft, caller_opts, caller_inst)([0, 0]) state,subi = unpack_result(ret) assert(state == Subtask.State.STARTED) [ret] = canon_subtask_cancel(True, subi) assert(ret == definitions.BLOCKED) assert(caller_heap.memory[0] == 13) - host_fut4.set() + host_fut1.set() [] = canon_waitable_join(subi, seti) - waitretp = 4 - [event] = canon_waitable_set_wait(True, MemInst(caller_heap.memory, 'i32'), seti, waitretp) + [event] = canon_waitable_set_wait(caller_mem, seti, retp) assert(event == EventCode.SUBTASK) - assert(caller_heap.memory[waitretp] == subi) - assert(caller_heap.memory[waitretp+4] == Subtask.State.CANCELLED_BEFORE_RETURNED) + assert(caller_heap.memory[retp+0] == subi) + assert(caller_heap.memory[retp+4] == Subtask.State.CANCELLED_BEFORE_RETURNED) assert(caller_heap.memory[0] == 13) [] = canon_subtask_drop(subi) - [ret] = store.lower(callee5, ft, caller_opts, caller_inst)([13, 0]) + # A task using the stackful ABI never becomes cancellable, so the + # cancellation request is silently ignored and the task returns normally. + caller_heap.memory[0] = 13 + [ret] = store.lower(callee_ignores_cancel, ft, caller_opts, caller_inst)([13, 0]) state,subi = unpack_result(ret) assert(state == Subtask.State.STARTED) [ret] = canon_subtask_cancel(True, subi) assert(ret == definitions.BLOCKED) assert(caller_heap.memory[0] == 13) - host_fut5.set() + host_fut2.set() [] = canon_waitable_join(subi, seti) - waitretp = 4 - [event] = canon_waitable_set_wait(True, MemInst(caller_heap.memory, 'i32'), seti, waitretp) + [event] = canon_waitable_set_wait(caller_mem, seti, retp) assert(event == EventCode.SUBTASK) - assert(caller_heap.memory[waitretp] == subi) - assert(caller_heap.memory[waitretp+4] == Subtask.State.RETURNED) + assert(caller_heap.memory[retp+0] == subi) + assert(caller_heap.memory[retp+4] == Subtask.State.RETURNED) assert(caller_heap.memory[0] == 44) [] = canon_subtask_drop(subi) - [ret] = store.lower(callee6, ft, caller_opts, caller_inst)([14, 0]) - state,subi = unpack_result(ret) - assert(state == Subtask.State.STARTED) - - [ret] = canon_subtask_cancel(True, subi) - assert(ret == definitions.BLOCKED) - - [] = canon_waitable_join(subi, seti) - [event] = canon_waitable_set_wait(True, MemInst(caller_heap.memory, 'i32'), seti, 4) - assert(event == EventCode.SUBTASK) - assert(caller_heap.memory[0] == 45) - assert(caller_heap.memory[4] == subi) - assert(caller_heap.memory[8] == Subtask.State.RETURNED) - [] = canon_subtask_drop(subi) - [] = canon_waitable_set_drop(seti) [] = canon_task_return([U8Type()], caller_opts, [42]) return [] @@ -2669,7 +2715,7 @@ def core_func(args): [] = canon_future_drop_readable(FutureType(elemt), rfi) [] = canon_waitable_join(wfi, seti) - [event] = canon_waitable_set_wait(True, MemInst(mem, 'i32'), seti, 0) + [event] = canon_waitable_set_wait(MemInst(mem, 'i32'), seti, 0) assert(event == EventCode.FUTURE_WRITE) assert(mem[0] == wfi) assert(mem[4] == CopyResult.COMPLETED) @@ -2689,7 +2735,7 @@ def core_func(args): [] = canon_stream_drop_readable(StreamType(elemt), rsi) [] = canon_waitable_join(wsi, seti) - [event] = canon_waitable_set_wait(True, MemInst(mem, 'i32'), seti, 0) + [event] = canon_waitable_set_wait(MemInst(mem, 'i32'), seti, 0) assert(event == EventCode.STREAM_WRITE) assert(mem[0] == wsi) result,n = unpack_result(mem[4]) @@ -2768,8 +2814,7 @@ def thread_func1(args): def thread_func2(args): [mainthreadi] = args - [ret] = canon_thread_yield_then_resume(True, mainthreadi) - assert(ret == Cancelled.FALSE) + canon_thread_yield_then_resume(mainthreadi) return [] fi2 = ftbl.add(CoreFuncRef(ft, thread_func2)) @@ -2781,11 +2826,9 @@ def thread_func3(args): def thread_func4(args): [ptr] = args - [ret] = canon_thread_yield(False) - assert(ret == Cancelled.FALSE) + canon_thread_yield() mem[ptr] = mem[ptr] + 1 - [ret] = canon_thread_yield(False) - assert(ret == Cancelled.FALSE) + canon_thread_yield() mem[ptr] = mem[ptr] + 1 return [] fi4 = ftbl.add(CoreFuncRef(ft, thread_func4)) @@ -2796,17 +2839,14 @@ def core_func(args): [mainthreadi] = canon_thread_index() [threadi] = canon_thread_new_indirect(ft, ftbl, fi1, 13) - [ret] = canon_thread_yield_then_resume(True, threadi) - assert(ret == Cancelled.FALSE) + canon_thread_yield_then_resume(threadi) [threadi] = canon_thread_new_indirect(ft, ftbl, fi2, mainthreadi) - [ret] = canon_thread_suspend_then_resume(True, threadi) - assert(ret == Cancelled.FALSE) + canon_thread_suspend_then_resume(threadi) [threadi] = canon_thread_new_indirect(ft, ftbl, fi3, mainthreadi) [] = canon_thread_resume_later(threadi) - [ret] = canon_thread_suspend(True) - assert(ret == Cancelled.FALSE) + canon_thread_suspend() ptr = 4 mem[ptr] = 0 @@ -2814,7 +2854,7 @@ def core_func(args): [threadi] = canon_thread_new_indirect(ft, ftbl, fi4, ptr) [] = canon_thread_resume_later(threadi) while mem[ptr] != 10: - canon_thread_yield(False) + canon_thread_yield() return [42] @@ -2852,7 +2892,7 @@ def thread_func1_1(args): nonlocal ping_count1 while ping_count1 < 3 or pong_count1 < 3: ping_count1 += 1 - [_] = canon_thread_yield(False) + canon_thread_yield() [] = canon_thread_resume_later(threadi2) return [] fi1_1 = ftbl.add(CoreFuncRef(ft, thread_func1_1)) @@ -2862,7 +2902,7 @@ def thread_func1_2(args): nonlocal pong_count1 while ping_count1 < 3 or pong_count1 < 3: pong_count1 += 1 - [_] = canon_thread_yield(False) + canon_thread_yield() return [] fi1_2 = ftbl.add(CoreFuncRef(ft, thread_func1_2)) @@ -2875,7 +2915,7 @@ def thread_func2(args): def thread_func3(args): assert(args == [15]) [] = canon_thread_resume_later(threadi4_1) - [_] = canon_thread_suspend(False) + canon_thread_suspend() return [] fi3 = ftbl.add(CoreFuncRef(ft, thread_func3)) @@ -2886,7 +2926,7 @@ def thread_func4_1(args): [] = canon_thread_resume_later(threadi4_2) nonlocal wsi [wsi] = canon_waitable_set_new() - [event] = canon_waitable_set_wait(True, opts.memory, wsi, 0) + [event] = canon_waitable_set_wait(opts.memory, wsi, 0) assert(event == EventCode.FUTURE_READ) [] = canon_thread_resume_later(threadi5_1) return [] @@ -2909,10 +2949,10 @@ def thread_func5_1(args): nonlocal ping_count5 while ping_count5 < 3: assert(ping_count5 == pong_count5) - [_] = canon_thread_yield_then_promote(False, threadi5_2) + canon_thread_yield_then_promote(threadi5_2) assert(ping_count5 == pong_count5) [] = canon_thread_resume_later(threadi5_2) - [_] = canon_thread_yield_then_promote(False, threadi5_2) + canon_thread_yield_then_promote(threadi5_2) assert(ping_count5 == pong_count5 - 1) ping_count5 += 1 [] = canon_task_return([U8Type()], opts, [42]) @@ -2925,7 +2965,7 @@ def thread_func5_2(args): nonlocal pong_count5 while pong_count5 < 3: pong_count5 += 1 - [_] = canon_thread_suspend_then_promote(False, threadi5_1) + canon_thread_suspend_then_promote(threadi5_1) return [] fi5_2 = ftbl.add(CoreFuncRef(ft, thread_func5_2)) @@ -2973,76 +3013,6 @@ def on_resolve(v): assert(result == 42) assert(other_result == 43) -def test_thread_cancel_callback(): - store = Store() - producer_inst = ComponentInstance(store) - producer_ft = FuncType([], [U32Type()], async_ = True) - - producer_opts1 = mk_opts(async_ = True) - def core_producer1(args): - assert(not args) - return [CallbackCode.YIELD] - def core_producer_callback1(args): - [event,payload1,payload2] = args - assert(event == EventCode.TASK_CANCELLED and payload1 == 0 and payload2 == 0) - [] = canon_task_return([U32Type()], producer_opts1, [42]) - return [CallbackCode.EXIT] - producer_opts1.callback = core_producer_callback1 - producer_callee1 = store.lift(core_producer1, producer_ft, producer_opts1, producer_inst) - - producer_opts2 = mk_opts(async_ = True) - def core_producer2(args): - assert(not args) - [ret] = canon_thread_yield(False) - assert(ret == Cancelled.FALSE) - [] = canon_task_return([U32Type()], producer_opts2, [43]) - return [CallbackCode.EXIT] - def core_producer_callback2(args): - assert(False) - producer_opts2.callback = core_producer_callback2 - producer_callee2 = store.lift(core_producer2, producer_ft, producer_opts2, producer_inst) - - consumer_inst = ComponentInstance(store) - consumer_ft = FuncType([], [], async_ = True) - consumer_mem = bytearray(24) - consumer_opts = mk_opts(MemInst(consumer_mem, 'i32'), async_ = True) - - def core_consumer(args): - assert(len(args) == 0) - - retp1 = 8 - [ret] = store.lower(producer_callee1, producer_ft, consumer_opts, consumer_inst)([retp1]) - state,subi1 = unpack_result(ret) - assert(state == Subtask.State.STARTED) - - retp2 = 12 - [ret] = store.lower(producer_callee2, producer_ft, consumer_opts, consumer_inst)([retp2]) - state,subi2 = unpack_result(ret) - assert(state == Subtask.State.STARTED) - - [ret] = canon_subtask_cancel(True, subi1) - assert(ret == definitions.BLOCKED) - - retp3 = 16 - [seti] = canon_waitable_set_new() - [] = canon_waitable_join(subi1, seti) - [event] = canon_waitable_set_wait(True, MemInst(consumer_mem, 'i32'), seti, retp3) - assert(event == EventCode.SUBTASK) - assert(consumer_mem[retp3] == subi1) - assert(consumer_mem[retp3+4] == Subtask.State.RETURNED) - assert(consumer_mem[retp1] == 42) - - [] = canon_waitable_join(subi2, seti) - [event] = canon_waitable_set_wait(True, MemInst(consumer_mem, 'i32'), seti, retp3) - assert(event == EventCode.SUBTASK) - assert(consumer_mem[retp3] == subi2) - assert(consumer_mem[retp3+4] == Subtask.State.RETURNED) - assert(consumer_mem[retp2] == 43) - - return [] - - lift_and_run(mk_opts(), consumer_inst, consumer_ft, core_consumer, lambda:[], lambda _:()) - test_roundtrips() test_cross_component_realloc() test_handles() @@ -3070,6 +3040,5 @@ def core_consumer(args): test_async_flat_params() test_threads() test_sync_threads() -test_thread_cancel_callback() print("All tests passed") diff --git a/test/async/big-interleaving-test.wast b/test/async/big-interleaving-test.wast index d6c18bdf..5ca9bf85 100644 --- a/test/async/big-interleaving-test.wast +++ b/test/async/big-interleaving-test.wast @@ -222,6 +222,31 @@ (func (export "subtask-drop") (param $sub-slot i32) (call $subtask.drop (call $sub (local.get $sub-slot)))) + ;; Cancel the subtask and expect the given resolved state. Since + ;; `subtask.cancel async` only performs a cooperative yield, whether + ;; cancellation of a started subtask completes eagerly or reports + ;; BLOCKED is nondeterministic, so on BLOCKED, wait for the subtask to + ;; resolve via a waitable set. + (func (export "subtask-cancel-await") (param $sub-slot i32) (param $expected-state i32) + (local $st i32) (local $ret i32) (local $ws i32) + (local.set $st (call $sub (local.get $sub-slot))) + (local.set $ret (call $subtask.cancel (local.get $st))) + (if (i32.eq (local.get $ret) (i32.const -1 (; BLOCKED ;))) + (then + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $st) (local.get $ws)) + (if (i32.ne (call $waitable-set.wait (local.get $ws) (global.get $EVENTP)) (i32.const 1 (; SUBTASK ;))) + (then unreachable)) + (if (i32.ne (i32.load (global.get $EVENTP)) (local.get $st)) + (then unreachable)) + (local.set $ret (i32.load offset=4 (global.get $EVENTP))) + (call $waitable.join (local.get $st) (i32.const 0)) + (call $waitable-set.drop (local.get $ws)))) + (if (i32.ne (local.get $ret) (local.get $expected-state)) + (then unreachable)) + (call $subtask.drop (local.get $st)) + (call $task.return)) + (func (export "mock-bp-inc") (call $bp-inc)) (func (export "mock-bp-dec") (call $bp-dec)) @@ -412,6 +437,8 @@ (func (export "mock-bp-dec") (canon lift (core func $tm "mock-bp-dec"))) (func (export "await-subtask") async (param "sub" u8) (param "state" u8) (canon lift (core func $tm "await-subtask") async (memory (core memory $memory "mem")))) + (func (export "subtask-cancel-await") async (param "sub" u8) (param "state" u8) (canon lift + (core func $tm "subtask-cancel-await") async (memory (core memory $memory "mem")))) (func (export "stream-new") (param "slot" u8) (canon lift (core func $tm "stream-new"))) (func (export "testee-write") (param "handle" u8) (param "bytes" u32) (result s32) (canon lift (core func $tm "testee-write"))) (func (export "testee-read") (param "handle" u8) (param "bytes" u32) (result s32) (canon lift (core func $tm "testee-read"))) @@ -473,7 +500,8 @@ (case "subtask-drop" u8) (case "await-subtask" $sub-expect-e) (case "mock-bp-inc") - (case "mock-bp-dec"))) + (case "mock-bp-dec") + (case "subtask-cancel-await" $sub-expect-e))) (export $command-e "command" (type $command)) (import "call-import" (func $call-import (param "slot" u8) (result s32))) (import "stream-new" (func $stream-new (param "slot" u8))) @@ -500,6 +528,7 @@ (import "subtask-cancel" (func $subtask-cancel (param "sub" u8) (result s32))) (import "subtask-drop" (func $subtask-drop (param "sub" u8))) (import "await-subtask" (func $await-subtask async (param "sub" u8) (param "state" u8))) + (import "subtask-cancel-await" (func $subtask-cancel-await async (param "sub" u8) (param "state" u8))) (import "mock-bp-inc" (func $mock-bp-inc)) (import "mock-bp-dec" (func $mock-bp-dec)) @@ -529,6 +558,7 @@ (import "" "subtask-cancel" (func $subtask-cancel (param i32) (result i32))) (import "" "subtask-drop" (func $subtask-drop (param i32))) (import "" "await-subtask" (func $await-subtask (param i32 i32) (result i32))) + (import "" "subtask-cancel-await" (func $subtask-cancel-await (param i32 i32) (result i32))) (import "" "mock-bp-inc" (func $mock-bp-inc)) (import "" "mock-bp-dec" (func $mock-bp-dec)) (memory (export "mem") 1) @@ -572,6 +602,7 @@ (global $AWAIT_SUBTASK i32 (i32.const 28)) (global $MOCK_BP_INC i32 (i32.const 29)) (global $MOCK_BP_DEC i32 (i32.const 30)) + (global $SUBTASK_CANCEL_AWAIT i32 (i32.const 31)) (global $last (mut i32) (i32.const 0)) (global $VOID_OK i32 (i32.const 1337)) @@ -692,6 +723,11 @@ (global.set $last (call $await-subtask (i32.load8_u offset=4 (local.get $insn)) (i32.load8_u offset=5 (local.get $insn)))))) + (if (i32.eq (local.get $op) (global.get $SUBTASK_CANCEL_AWAIT)) + (then + (global.set $last (call $subtask-cancel-await + (i32.load8_u offset=4 (local.get $insn)) + (i32.load8_u offset=5 (local.get $insn)))))) (if (i32.eq (local.get $op) (global.get $MOCK_BP_INC)) (then @@ -739,6 +775,7 @@ (canon lower (func $subtask-cancel) (core func $subtask-cancel')) (canon lower (func $subtask-drop) (core func $subtask-drop')) (canon lower (func $await-subtask) async (core func $await-subtask')) + (canon lower (func $subtask-cancel-await) async (core func $subtask-cancel-await')) (canon lower (func $mock-bp-inc) (core func $mock-bp-inc')) (canon lower (func $mock-bp-dec) (core func $mock-bp-dec')) (core instance $dm (instantiate $DM (with "" (instance @@ -767,6 +804,7 @@ (export "subtask-cancel" (func $subtask-cancel')) (export "subtask-drop" (func $subtask-drop')) (export "await-subtask" (func $await-subtask')) + (export "subtask-cancel-await" (func $subtask-cancel-await')) (export "mock-bp-inc" (func $mock-bp-inc')) (export "mock-bp-dec" (func $mock-bp-dec')))))) (func (export "run") (param "prog" (list $command-e)) @@ -806,6 +844,7 @@ (with "subtask-cancel" (func $testee "subtask-cancel")) (with "subtask-drop" (func $testee "subtask-drop")) (with "await-subtask" (func $testee "await-subtask")) + (with "subtask-cancel-await" (func $testee "subtask-cancel-await")) (with "mock-bp-inc" (func $testee "mock-bp-inc")) (with "mock-bp-dec" (func $testee "mock-bp-dec")))) (instance $types @@ -1586,9 +1625,7 @@ (list.const (variant.const "call-block-empty" (u8.const 0)) (variant.const "expect-code" (s32.const 1)) - (variant.const "subtask-cancel" (u8.const 0)) - (variant.const "expect-code" (s32.const 4)) - (variant.const "subtask-drop" (u8.const 0))))) + (variant.const "subtask-cancel-await" (record.const (field "sub" u8.const 0) (field "state" u8.const 4)))))) (component instance $i $Tester) (assert_trap @@ -1620,15 +1657,9 @@ (variant.const "expect-code" (s32.const 1)) (variant.const "call-block-empty" (u8.const 2)) (variant.const "expect-code" (s32.const 1)) - (variant.const "subtask-cancel" (u8.const 2)) - (variant.const "expect-code" (s32.const 4)) - (variant.const "subtask-cancel" (u8.const 0)) - (variant.const "expect-code" (s32.const 4)) - (variant.const "subtask-cancel" (u8.const 1)) - (variant.const "expect-code" (s32.const 4)) - (variant.const "subtask-drop" (u8.const 0)) - (variant.const "subtask-drop" (u8.const 1)) - (variant.const "subtask-drop" (u8.const 2))))) + (variant.const "subtask-cancel-await" (record.const (field "sub" u8.const 2) (field "state" u8.const 4))) + (variant.const "subtask-cancel-await" (record.const (field "sub" u8.const 0) (field "state" u8.const 4))) + (variant.const "subtask-cancel-await" (record.const (field "sub" u8.const 1) (field "state" u8.const 4)))))) (assert_return (invoke "run" diff --git a/test/async/cancel-and-exclusive-lock.wast b/test/async/cancel-and-exclusive-lock.wast index 23c5faa2..25063f11 100644 --- a/test/async/cancel-and-exclusive-lock.wast +++ b/test/async/cancel-and-exclusive-lock.wast @@ -1,10 +1,10 @@ ;; This test exercises how a component instance's "exclusive" lock interacts ;; with the delivery of cancellation requests. ;; -;; A `callback`-lifted task waiting in its event loop is only cancellable while -;; the exclusive lock is free, since delivering cancellation resumes the task, -;; which re-enters core wasm. Cancellation requested while the lock is held by -;; another task therefore cannot be delivered immediately and is remembered as +;; A pending cancellation request is delivered to a `callback`-lifted task +;; waiting in its event loop only while the exclusive lock is free, since +;; delivering the TASK_CANCELLED event resumes the task, which re-enters core +;; wasm. While the lock is held by another task, the request therefore stays ;; pending. It must then be delivered as soon as the lock frees, even if the ;; waiting task's own readiness condition is never met. (component diff --git a/test/async/cancel-delivery.wast b/test/async/cancel-delivery.wast new file mode 100644 index 00000000..d8a29b77 --- /dev/null +++ b/test/async/cancel-delivery.wast @@ -0,0 +1,278 @@ +;; This test exercises how a request for cancellation is delivered to the +;; callee: the request is recorded as pending on the callee's task and is only +;; delivered, as a TASK_CANCELLED event, via the `async callback` event loop. +;; +;; Component $C exports three async callback-lifted functions: +;; yield-until-cancel: loops returning YIELD to its event loop until the +;; TASK_CANCELLED event arrives; one more YIELD must then report NONE +;; (cancellation is delivered at most once) before it calls task.cancel +;; deferred: blocks in a synchronous future.read in its initial core +;; function, during which a cancellation request stays pending; the pending +;; request is delivered at the first return to the event loop +;; pending-survives: like deferred, but additionally calls thread.yield and +;; waitable-set.poll after the request is pending: neither may report the +;; pending cancellation (they never return TASK_CANCELLED), which must +;; survive until the task returns to its event loop +;; +;; Component $D calls each function and cancels it. Since `subtask.cancel +;; async` only performs a cooperative yield, whether the cancellation of +;; yield-until-cancel completes eagerly or reports BLOCKED is nondeterministic +;; and $D accepts both. While a task is blocked in its initial core function it +;; cannot be cancelled, so those cancellations deterministically report +;; BLOCKED. +(component + (component $C + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $CM + (import "" "mem" (memory 1)) + (import "" "task.cancel" (func $task.cancel)) + (import "" "future.read" (func $future.read (param i32 i32) (result i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.poll" (func $waitable-set.poll (param i32 i32) (result i32))) + (import "" "thread.yield" (func $thread.yield (result i32))) + + ;; Test 1: cancellation is delivered to a task parked in its event loop + ;; after returning YIELD, and is delivered at most once + (global $cancelled (mut i32) (i32.const 0)) + (func $yield-until-cancel (export "yield-until-cancel") (result i32) + (i32.const 1 (; YIELD ;)) + ) + (func $yield-until-cancel_cb (export "yield-until-cancel_cb") (param $event_code i32) (param $index i32) (param $payload i32) (result i32) + (if (i32.eq (local.get $event_code) (i32.const 6 (; TASK_CANCELLED ;))) + (then + (if (global.get $cancelled) (then unreachable)) + (global.set $cancelled (i32.const 1)) + ;; yield once more; the next event must be NONE, not a second + ;; TASK_CANCELLED + (return (i32.const 1 (; YIELD ;))))) + (if (i32.ne (local.get $event_code) (i32.const 0 (; NONE ;))) + (then unreachable)) + (if (i32.eqz (global.get $cancelled)) + ;; not yet cancelled; keep yielding + (then (return (i32.const 1 (; YIELD ;))))) + (call $task.cancel) + (i32.const 0 (; EXIT ;)) + ) + + ;; Test 2: a cancellation request arriving while this task is blocked in + ;; a synchronous future.read stays pending and is delivered at the first + ;; return to the event loop + (func $deferred (export "deferred") (param $futr i32) (result i32) + ;; blocks until the caller writes (the caller cancels first) + (if (i32.ne (call $future.read (local.get $futr) (i32.const 0)) (i32.const 0 (; COMPLETED ;))) + (then unreachable)) + ;; return to the event loop; the pending cancellation must be + ;; delivered even though this waitable set is empty + (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (call $waitable-set.new) (i32.const 4))) + ) + (func $expect-cancelled_cb (export "expect-cancelled_cb") (param $event_code i32) (param $index i32) (param $payload i32) (result i32) + (if (i32.ne (local.get $event_code) (i32.const 6 (; TASK_CANCELLED ;))) + (then unreachable)) + (call $task.cancel) + (i32.const 0 (; EXIT ;)) + ) + + ;; Test 3: a pending cancellation is not reported by the thread.yield and + ;; waitable-set.poll built-ins (which never return TASK_CANCELLED); it + ;; survives them and is delivered at the event loop + (func $pending-survives (export "pending-survives") (param $futr i32) (result i32) + (local $ws i32) + ;; blocks until the caller writes (the caller cancels first) + (if (i32.ne (call $future.read (local.get $futr) (i32.const 0)) (i32.const 0 (; COMPLETED ;))) + (then unreachable)) + ;; a pending cancellation is now set; thread.yield must not report it + (if (i32.ne (call $thread.yield) (i32.const 0)) + (then unreachable)) + ;; nor may waitable-set.poll, which must report NONE for an empty set + (local.set $ws (call $waitable-set.new)) + (if (i32.ne (call $waitable-set.poll (local.get $ws) (i32.const 0)) (i32.const 0 (; NONE ;))) + (then unreachable)) + ;; only the event loop delivers the still-pending cancellation + (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (local.get $ws) (i32.const 4))) + ) + ) + (type $FT (future)) + (canon task.cancel (core func $task.cancel)) + (canon future.read $FT (memory (core memory $memory "mem")) (core func $future.read)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.poll (memory (core memory $memory "mem")) (core func $waitable-set.poll)) + (canon thread.yield (core func $thread.yield)) + (core instance $cm (instantiate $CM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "task.cancel" (func $task.cancel)) + (export "future.read" (func $future.read)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.poll" (func $waitable-set.poll)) + (export "thread.yield" (func $thread.yield)) + )))) + (func (export "yield-until-cancel") async (canon lift + (core func $cm "yield-until-cancel") + async (callback (core func $cm "yield-until-cancel_cb")) + )) + (func (export "deferred") async (param "fut" $FT) (canon lift + (core func $cm "deferred") + async (callback (core func $cm "expect-cancelled_cb")) + )) + (func (export "pending-survives") async (param "fut" $FT) (canon lift + (core func $cm "pending-survives") + async (callback (core func $cm "expect-cancelled_cb")) + )) + ) + + (component $D + (type $FT (future)) + (import "yield-until-cancel" (func $yield-until-cancel async)) + (import "deferred" (func $deferred async (param "fut" $FT))) + (import "pending-survives" (func $pending-survives async (param "fut" $FT))) + + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) + (core module $DM + (import "" "mem" (memory 1)) + (import "" "subtask.cancel" (func $subtask.cancel (param i32) (result i32))) + (import "" "subtask.drop" (func $subtask.drop (param i32))) + (import "" "future.new" (func $future.new (result i64))) + (import "" "future.write" (func $future.write (param i32 i32) (result i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) + (import "" "yield-until-cancel" (func $yield-until-cancel (result i32))) + (import "" "deferred" (func $deferred (param i32) (result i32))) + (import "" "pending-survives" (func $pending-survives (param i32) (result i32))) + + ;; wait until $sub resolves to CANCELLED_BEFORE_RETURNED, then drop it + (func $await-cancelled (param $sub i32) + (local $ws i32) + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $sub) (local.get $ws)) + (if (i32.ne (call $waitable-set.wait (local.get $ws) (i32.const 8)) (i32.const 1 (; SUBTASK ;))) + (then unreachable)) + (if (i32.ne (i32.load (i32.const 8)) (local.get $sub)) + (then unreachable)) + (if (i32.ne (i32.load (i32.const 12)) (i32.const 4 (; CANCELLED_BEFORE_RETURNED ;))) + (then unreachable)) + (call $waitable.join (local.get $sub) (i32.const 0)) + (call $subtask.drop (local.get $sub)) + ) + + (func $run (export "run") (result i32) + (local $ret i32) (local $ret64 i64) + (local $subtask i32) + (local $futr i32) (local $futw i32) + + ;; ========================================== + ;; Test 1: delivery to a YIELD-parked task + ;; ========================================== + + ;; call yield-until-cancel; it parks in its event loop + (local.set $ret (call $yield-until-cancel)) + (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) + (then unreachable)) + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + + ;; cancel; the callee needs two more turns of its event loop to + ;; resolve, so this may or may not complete eagerly + (local.set $ret (call $subtask.cancel (local.get $subtask))) + (if (i32.eq (local.get $ret) (i32.const -1 (; BLOCKED ;))) + (then (call $await-cancelled (local.get $subtask))) + (else + (if (i32.ne (local.get $ret) (i32.const 4 (; CANCELLED_BEFORE_RETURNED ;))) + (then unreachable)) + (call $subtask.drop (local.get $subtask)))) + + ;; ========================================== + ;; Test 2: deferral while blocked in a synchronous built-in + ;; ========================================== + + ;; create future for deferred to read + (local.set $ret64 (call $future.new)) + (local.set $futr (i32.wrap_i64 (local.get $ret64))) + (local.set $futw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + + ;; call deferred; it blocks in a synchronous future.read + (local.set $ret (call $deferred (local.get $futr))) + (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) + (then unreachable)) + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + + ;; cancel; deterministically blocked, since the callee cannot receive + ;; the request while blocked in its initial core function + (if (i32.ne (call $subtask.cancel (local.get $subtask)) (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + + ;; write to the future, unblocking the callee's synchronous read + (if (i32.ne (call $future.write (local.get $futw) (i32.const 0)) (i32.const 0 (; COMPLETED ;))) + (then unreachable)) + + ;; the pending cancellation is delivered once the callee returns to + ;; its event loop + (call $await-cancelled (local.get $subtask)) + + ;; ========================================== + ;; Test 3: pending cancellation survives thread.yield and + ;; waitable-set.poll + ;; ========================================== + + ;; create future for pending-survives to read + (local.set $ret64 (call $future.new)) + (local.set $futr (i32.wrap_i64 (local.get $ret64))) + (local.set $futw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) + + ;; call pending-survives; it blocks in a synchronous future.read + (local.set $ret (call $pending-survives (local.get $futr))) + (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) + (then unreachable)) + (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) + + ;; cancel; deterministically blocked, as in test 2 + (if (i32.ne (call $subtask.cancel (local.get $subtask)) (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + + ;; write to the future, unblocking the callee's synchronous read + (if (i32.ne (call $future.write (local.get $futw) (i32.const 0)) (i32.const 0 (; COMPLETED ;))) + (then unreachable)) + + ;; the callee's yield and poll must not observe the pending + ;; cancellation, which is delivered at the callee's event loop + (call $await-cancelled (local.get $subtask)) + + ;; all tests passed + (i32.const 42) + ) + ) + (canon subtask.cancel async (core func $subtask.cancel)) + (canon subtask.drop (core func $subtask.drop)) + (canon future.new $FT (core func $future.new)) + (canon future.write $FT async (memory (core memory $memory "mem")) (core func $future.write)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.wait (memory (core memory $memory "mem")) (core func $waitable-set.wait)) + (canon lower (func $yield-until-cancel) async (memory (core memory $memory "mem")) (core func $yield-until-cancel')) + (canon lower (func $deferred) async (memory (core memory $memory "mem")) (core func $deferred')) + (canon lower (func $pending-survives) async (memory (core memory $memory "mem")) (core func $pending-survives')) + (core instance $dm (instantiate $DM (with "" (instance + (export "mem" (memory $memory "mem")) + (export "subtask.cancel" (func $subtask.cancel)) + (export "subtask.drop" (func $subtask.drop)) + (export "future.new" (func $future.new)) + (export "future.write" (func $future.write)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait" (func $waitable-set.wait)) + (export "yield-until-cancel" (func $yield-until-cancel')) + (export "deferred" (func $deferred')) + (export "pending-survives" (func $pending-survives')) + )))) + (func (export "run") async (result u32) (canon lift (core func $dm "run"))) + ) + + (instance $c (instantiate $C)) + (instance $d (instantiate $D + (with "yield-until-cancel" (func $c "yield-until-cancel")) + (with "deferred" (func $c "deferred")) + (with "pending-survives" (func $c "pending-survives")) + )) + (func (export "run") (alias export $d "run")) +) +(assert_return (invoke "run") (u32.const 42)) diff --git a/test/async/cancel-subtask.wast b/test/async/cancel-subtask.wast index e2f87b60..e4278ea3 100644 --- a/test/async/cancel-subtask.wast +++ b/test/async/cancel-subtask.wast @@ -1,7 +1,10 @@ ;; This test contains two components $C and $D where $D imports and calls $C. ;; $D.run calls $C.f, which blocks on an empty waitable set -;; $D.run then subtask.cancels $C.f, which resumes $C.f which promptly resolves -;; without returning a value. +;; $D.run then subtask.cancels $C.f; the pending cancellation request is +;; delivered to $C.f's event loop as a TASK_CANCELLED event, whereupon $C.f +;; resolves without returning a value. Since `subtask.cancel async` only +;; performs a cooperative yield, whether $C.f is resumed before +;; subtask.cancel returns is nondeterministic and $D accepts both outcomes. (component (component $C (core module $Memory (memory (export "mem") 1)) @@ -114,8 +117,21 @@ (then unreachable)) (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) - ;; cancel 'f'; it should complete without blocking + ;; cancel 'f'; this may complete eagerly or report BLOCKED, in which + ;; case waiting on 'f' must produce CANCELLED_BEFORE_RETURNED (local.set $ret (call $subtask.cancel (local.get $subtask))) + (if (i32.eq (i32.const -1 (; BLOCKED ;)) (local.get $ret)) + (then + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (local.get $subtask) (local.get $ws)) + (local.set $retp2 (i32.const 8)) + (local.set $event_code (call $waitable-set.wait (local.get $ws) (local.get $retp2))) + (if (i32.ne (i32.const 1 (; SUBTASK ;)) (local.get $event_code)) + (then unreachable)) + (if (i32.ne (local.get $subtask) (i32.load (local.get $retp2))) + (then unreachable)) + (local.set $ret (i32.load offset=4 (local.get $retp2))) + (call $waitable.join (local.get $subtask) (i32.const 0)))) (if (i32.ne (i32.const 4 (; CANCELLED_BEFORE_RETURNED ;)) (local.get $ret)) (then unreachable)) diff --git a/test/async/cancellable.wast b/test/async/cancellable.wast deleted file mode 100644 index 32cf1992..00000000 --- a/test/async/cancellable.wast +++ /dev/null @@ -1,404 +0,0 @@ -;; This test exercises the 'cancellable' immediate on waitable-set.wait, -;; waitable-set.poll, and thread.yield. -;; -;; Component $C exports five async callback-lifted functions that block in -;; their initial core function (the callbacks are never invoked): -;; wait-cancel: blocks on cancellable waitable-set.wait, expects TASK_CANCELLED -;; yield-cancel: yields with cancellable until the caller cancels -;; poll-cancel-pending: blocks on non-cancellable wait, then polls with cancellable -;; yield-cancel-pending: blocks on non-cancellable wait, then yields with cancellable -;; pending-survives-nc-yield: blocks on non-cancellable wait, then a -;; non-cancellable yield (which must not report the pending cancel), then -;; yields with cancellable -;; -;; Component $D calls each function and cancels it, verifying the cancel is -;; delivered correctly through the cancellable built-in in each case. -(component - (component $C - (core module $Memory (memory (export "mem") 1)) - (core instance $memory (instantiate $Memory)) - (core module $CM - (import "" "mem" (memory 1)) - (import "" "task.cancel" (func $task.cancel)) - (import "" "future.read" (func $future.read (param i32 i32) (result i32))) - (import "" "waitable.join" (func $waitable.join (param i32 i32))) - (import "" "waitable-set.new" (func $waitable-set.new (result i32))) - (import "" "waitable-set.wait-cancellable" (func $waitable-set.wait-cancellable (param i32 i32) (result i32))) - (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) - (import "" "waitable-set.poll-cancellable" (func $waitable-set.poll-cancellable (param i32 i32) (result i32))) - (import "" "thread.yield-cancellable" (func $thread.yield-cancellable (result i32))) - (import "" "thread.yield" (func $thread.yield (result i32))) - - ;; Test 1: direct cancel delivery through cancellable waitable-set.wait - (func $wait-cancel (export "wait-cancel") (result i32) - (local $event_code i32) - (local $ws i32) - (local.set $ws (call $waitable-set.new)) - ;; wait on empty waitable set with cancellable; blocks until cancelled - (local.set $event_code (call $waitable-set.wait-cancellable (local.get $ws) (i32.const 0))) - (if (i32.ne (local.get $event_code) (i32.const 6 (; TASK_CANCELLED ;))) - (then unreachable)) - (call $task.cancel) - (i32.const 0 (; EXIT ;)) - ) - - ;; Test 2: direct cancel delivery through cancellable thread.yield - (func $yield-cancel (export "yield-cancel") (result i32) - ;; yield with cancellable until cancelled by the caller (a single - ;; yield may nondeterministically complete without suspending and - ;; thus without the cancellation being delivered) - (loop $again - (br_if $again (i32.eqz (call $thread.yield-cancellable)))) - (call $task.cancel) - (i32.const 0 (; EXIT ;)) - ) - - ;; Test 3: deferred cancel delivered through cancellable waitable-set.poll - (func $poll-cancel-pending (export "poll-cancel-pending") (param $futr i32) (result i32) - (local $ws i32) - (local $ret i32) - (local $event_code i32) - (local.set $ws (call $waitable-set.new)) - ;; read future - blocks (caller hasn't written yet) - (local.set $ret (call $future.read (local.get $futr) (i32.const 0))) - (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) - (then unreachable)) - (call $waitable.join (local.get $futr) (local.get $ws)) - ;; wait WITHOUT cancellable - cancel will be deferred as PENDING_CANCEL - (local.set $event_code (call $waitable-set.wait (local.get $ws) (i32.const 0))) - (if (i32.ne (i32.const 4 (; FUTURE_READ ;)) (local.get $event_code)) - (then unreachable)) - ;; poll WITH cancellable - delivers the pending cancel - (local.set $event_code (call $waitable-set.poll-cancellable (local.get $ws) (i32.const 0))) - (if (i32.ne (i32.const 6 (; TASK_CANCELLED ;)) (local.get $event_code)) - (then unreachable)) - (call $task.cancel) - (i32.const 0 (; EXIT ;)) - ) - - ;; Test 4: deferred cancel delivered through cancellable thread.yield - (func $yield-cancel-pending (export "yield-cancel-pending") (param $futr i32) (result i32) - (local $ws i32) - (local $ret i32) - (local $event_code i32) - (local.set $ws (call $waitable-set.new)) - ;; read future - blocks (caller hasn't written yet) - (local.set $ret (call $future.read (local.get $futr) (i32.const 0))) - (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) - (then unreachable)) - (call $waitable.join (local.get $futr) (local.get $ws)) - ;; wait WITHOUT cancellable - cancel will be deferred as PENDING_CANCEL - (local.set $event_code (call $waitable-set.wait (local.get $ws) (i32.const 0))) - (if (i32.ne (i32.const 4 (; FUTURE_READ ;)) (local.get $event_code)) - (then unreachable)) - ;; yield WITH cancellable - delivers the pending cancel - (local.set $ret (call $thread.yield-cancellable)) - (if (i32.ne (i32.const 1 (; CANCELLED ;)) (local.get $ret)) - (then unreachable)) - (call $task.cancel) - (i32.const 0 (; EXIT ;)) - ) - - ;; Test 5: a pending cancellation is not delivered to a *non*-cancellable - ;; blocking built-in, and survives it to be delivered at the next - ;; cancellable one - (func $pending-survives-nc-yield (export "pending-survives-nc-yield") (param $futr i32) (result i32) - (local $ws i32) - (local $ret i32) - (local $event_code i32) - (local.set $ws (call $waitable-set.new)) - ;; read future - blocks (caller hasn't written yet) - (local.set $ret (call $future.read (local.get $futr) (i32.const 0))) - (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) - (then unreachable)) - (call $waitable.join (local.get $futr) (local.get $ws)) - ;; wait WITHOUT cancellable - cancel will be deferred as PENDING_CANCEL - (local.set $event_code (call $waitable-set.wait (local.get $ws) (i32.const 0))) - (if (i32.ne (i32.const 4 (; FUTURE_READ ;)) (local.get $event_code)) - (then unreachable)) - ;; yield WITHOUT cancellable - must not report the pending cancel, and - ;; must leave it pending - (local.set $ret (call $thread.yield)) - (if (i32.ne (i32.const 0 (; NOT CANCELLED ;)) (local.get $ret)) - (then unreachable)) - ;; yield WITH cancellable - delivers the still-pending cancel - (local.set $ret (call $thread.yield-cancellable)) - (if (i32.ne (i32.const 1 (; CANCELLED ;)) (local.get $ret)) - (then unreachable)) - (call $task.cancel) - (i32.const 0 (; EXIT ;)) - ) - - ;; callback that should never be called - (func (export "unreachable-cb") (param i32 i32 i32) (result i32) - unreachable - ) - ) - (type $FT (future)) - (canon task.cancel (core func $task.cancel)) - (canon future.read $FT async (memory (core memory $memory "mem")) (core func $future.read)) - (canon waitable.join (core func $waitable.join)) - (canon waitable-set.new (core func $waitable-set.new)) - (canon waitable-set.wait cancellable (memory (core memory $memory "mem")) (core func $waitable-set.wait-cancellable)) - (canon waitable-set.wait (memory (core memory $memory "mem")) (core func $waitable-set.wait)) - (canon waitable-set.poll cancellable (memory (core memory $memory "mem")) (core func $waitable-set.poll-cancellable)) - (canon thread.yield cancellable (core func $thread.yield-cancellable)) - (canon thread.yield (core func $thread.yield)) - (core instance $cm (instantiate $CM (with "" (instance - (export "mem" (memory $memory "mem")) - (export "task.cancel" (func $task.cancel)) - (export "future.read" (func $future.read)) - (export "waitable.join" (func $waitable.join)) - (export "waitable-set.new" (func $waitable-set.new)) - (export "waitable-set.wait-cancellable" (func $waitable-set.wait-cancellable)) - (export "waitable-set.wait" (func $waitable-set.wait)) - (export "waitable-set.poll-cancellable" (func $waitable-set.poll-cancellable)) - (export "thread.yield-cancellable" (func $thread.yield-cancellable)) - (export "thread.yield" (func $thread.yield)) - )))) - (func (export "wait-cancel") async (result u32) (canon lift - (core func $cm "wait-cancel") - async (callback (core func $cm "unreachable-cb")) - )) - (func (export "yield-cancel") async (result u32) (canon lift - (core func $cm "yield-cancel") - async (callback (core func $cm "unreachable-cb")) - )) - (func (export "poll-cancel-pending") async (param "fut" $FT) (result u32) (canon lift - (core func $cm "poll-cancel-pending") - async (callback (core func $cm "unreachable-cb")) - )) - (func (export "yield-cancel-pending") async (param "fut" $FT) (result u32) (canon lift - (core func $cm "yield-cancel-pending") - async (callback (core func $cm "unreachable-cb")) - )) - (func (export "pending-survives-nc-yield") async (param "fut" $FT) (result u32) (canon lift - (core func $cm "pending-survives-nc-yield") - async (callback (core func $cm "unreachable-cb")) - )) - ) - - (component $D - (type $FT (future)) - (import "wait-cancel" (func $wait-cancel async (result u32))) - (import "yield-cancel" (func $yield-cancel async (result u32))) - (import "poll-cancel-pending" (func $poll-cancel-pending async (param "fut" $FT) (result u32))) - (import "yield-cancel-pending" (func $yield-cancel-pending async (param "fut" $FT) (result u32))) - (import "pending-survives-nc-yield" (func $pending-survives-nc-yield async (param "fut" $FT) (result u32))) - - (core module $Memory (memory (export "mem") 1)) - (core instance $memory (instantiate $Memory)) - (core module $DM - (import "" "mem" (memory 1)) - (import "" "subtask.cancel" (func $subtask.cancel (param i32) (result i32))) - (import "" "subtask.drop" (func $subtask.drop (param i32))) - (import "" "future.new" (func $future.new (result i64))) - (import "" "future.write" (func $future.write (param i32 i32) (result i32))) - (import "" "waitable.join" (func $waitable.join (param i32 i32))) - (import "" "waitable-set.new" (func $waitable-set.new (result i32))) - (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) - (import "" "wait-cancel" (func $wait-cancel (param i32) (result i32))) - (import "" "yield-cancel" (func $yield-cancel (param i32) (result i32))) - (import "" "poll-cancel-pending" (func $poll-cancel-pending (param i32 i32) (result i32))) - (import "" "yield-cancel-pending" (func $yield-cancel-pending (param i32 i32) (result i32))) - (import "" "pending-survives-nc-yield" (func $pending-survives-nc-yield (param i32 i32) (result i32))) - - (func $run (export "run") (result i32) - (local $ret i32) (local $ret64 i64) - (local $retp i32) (local $retp2 i32) - (local $subtask i32) - (local $event_code i32) - (local $futr i32) (local $futw i32) - (local $ws i32) - - ;; ========================================== - ;; Test 1: waitable-set.wait cancellable - ;; ========================================== - - ;; call wait-cancel; it should block in cancellable wait - (local.set $retp (i32.const 4)) - (local.set $ret (call $wait-cancel (local.get $retp))) - (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) - (then unreachable)) - (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) - - ;; cancel; completes immediately (C is in cancellable wait) - (local.set $ret (call $subtask.cancel (local.get $subtask))) - (if (i32.ne (i32.const 4 (; CANCELLED_BEFORE_RETURNED ;)) (local.get $ret)) - (then unreachable)) - (call $subtask.drop (local.get $subtask)) - - ;; ========================================== - ;; Test 2: thread.yield cancellable - ;; ========================================== - - ;; call yield-cancel; it should suspend in cancellable yield - (local.set $ret (call $yield-cancel (local.get $retp))) - (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) - (then unreachable)) - (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) - - ;; cancel; completes immediately (C is in cancellable yield) - (local.set $ret (call $subtask.cancel (local.get $subtask))) - (if (i32.ne (i32.const 4 (; CANCELLED_BEFORE_RETURNED ;)) (local.get $ret)) - (then unreachable)) - (call $subtask.drop (local.get $subtask)) - - ;; ========================================== - ;; Test 3: waitable-set.poll cancellable (pending) - ;; ========================================== - - ;; create future for poll-cancel-pending to read - (local.set $ret64 (call $future.new)) - (local.set $futr (i32.wrap_i64 (local.get $ret64))) - (local.set $futw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) - - ;; call poll-cancel-pending; it should block in non-cancellable wait - (local.set $retp (i32.const 4)) - (local.set $retp2 (i32.const 8)) - (local.set $ret (call $poll-cancel-pending (local.get $futr) (local.get $retp))) - (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) - (then unreachable)) - (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) - - ;; cancel; blocks because C's wait is not cancellable - (local.set $ret (call $subtask.cancel (local.get $subtask))) - (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) - (then unreachable)) - - ;; write to future; unblocks C's non-cancellable wait - (local.set $ret (call $future.write (local.get $futw) (i32.const 0))) - (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) - (then unreachable)) - - ;; wait for subtask to complete - (local.set $ws (call $waitable-set.new)) - (call $waitable.join (local.get $subtask) (local.get $ws)) - (local.set $event_code (call $waitable-set.wait (local.get $ws) (local.get $retp2))) - (if (i32.ne (i32.const 1 (; SUBTASK ;)) (local.get $event_code)) - (then unreachable)) - (if (i32.ne (local.get $subtask) (i32.load (local.get $retp2))) - (then unreachable)) - (if (i32.ne (i32.const 4 (; CANCELLED_BEFORE_RETURNED ;)) (i32.load offset=4 (local.get $retp2))) - (then unreachable)) - (call $subtask.drop (local.get $subtask)) - - ;; ========================================== - ;; Test 4: thread.yield cancellable (pending) - ;; ========================================== - - ;; create future for yield-cancel-pending to read - (local.set $ret64 (call $future.new)) - (local.set $futr (i32.wrap_i64 (local.get $ret64))) - (local.set $futw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) - - ;; call yield-cancel-pending; it should block in non-cancellable wait - (local.set $ret (call $yield-cancel-pending (local.get $futr) (local.get $retp))) - (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) - (then unreachable)) - (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) - - ;; cancel; blocks because C's wait is not cancellable - (local.set $ret (call $subtask.cancel (local.get $subtask))) - (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) - (then unreachable)) - - ;; write to future; unblocks C's non-cancellable wait - (local.set $ret (call $future.write (local.get $futw) (i32.const 0))) - (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) - (then unreachable)) - - ;; wait for subtask to complete - (local.set $ws (call $waitable-set.new)) - (call $waitable.join (local.get $subtask) (local.get $ws)) - (local.set $event_code (call $waitable-set.wait (local.get $ws) (local.get $retp2))) - (if (i32.ne (i32.const 1 (; SUBTASK ;)) (local.get $event_code)) - (then unreachable)) - (if (i32.ne (local.get $subtask) (i32.load (local.get $retp2))) - (then unreachable)) - (if (i32.ne (i32.const 4 (; CANCELLED_BEFORE_RETURNED ;)) (i32.load offset=4 (local.get $retp2))) - (then unreachable)) - (call $subtask.drop (local.get $subtask)) - - ;; ========================================== - ;; Test 5: pending cancel survives a non-cancellable yield - ;; ========================================== - - ;; create future for pending-survives-nc-yield to read - (local.set $ret64 (call $future.new)) - (local.set $futr (i32.wrap_i64 (local.get $ret64))) - (local.set $futw (i32.wrap_i64 (i64.shr_u (local.get $ret64) (i64.const 32)))) - - ;; call pending-survives-nc-yield; it should block in non-cancellable wait - (local.set $ret (call $pending-survives-nc-yield (local.get $futr) (local.get $retp))) - (if (i32.ne (i32.const 1 (; STARTED ;)) (i32.and (local.get $ret) (i32.const 0xf))) - (then unreachable)) - (local.set $subtask (i32.shr_u (local.get $ret) (i32.const 4))) - - ;; cancel; blocks because C's wait is not cancellable - (local.set $ret (call $subtask.cancel (local.get $subtask))) - (if (i32.ne (i32.const -1 (; BLOCKED ;)) (local.get $ret)) - (then unreachable)) - - ;; write to future; unblocks C's non-cancellable wait - (local.set $ret (call $future.write (local.get $futw) (i32.const 0))) - (if (i32.ne (i32.const 0 (; COMPLETED ;)) (local.get $ret)) - (then unreachable)) - - ;; wait for subtask to complete - (local.set $ws (call $waitable-set.new)) - (call $waitable.join (local.get $subtask) (local.get $ws)) - (local.set $event_code (call $waitable-set.wait (local.get $ws) (local.get $retp2))) - (if (i32.ne (i32.const 1 (; SUBTASK ;)) (local.get $event_code)) - (then unreachable)) - (if (i32.ne (local.get $subtask) (i32.load (local.get $retp2))) - (then unreachable)) - (if (i32.ne (i32.const 4 (; CANCELLED_BEFORE_RETURNED ;)) (i32.load offset=4 (local.get $retp2))) - (then unreachable)) - (call $subtask.drop (local.get $subtask)) - - ;; all tests passed - (i32.const 42) - ) - ) - (canon subtask.cancel async (core func $subtask.cancel)) - (canon subtask.drop (core func $subtask.drop)) - (canon future.new $FT (core func $future.new)) - (canon future.write $FT async (memory (core memory $memory "mem")) (core func $future.write)) - (canon waitable.join (core func $waitable.join)) - (canon waitable-set.new (core func $waitable-set.new)) - (canon waitable-set.wait (memory (core memory $memory "mem")) (core func $waitable-set.wait)) - (canon lower (func $wait-cancel) async (memory (core memory $memory "mem")) (core func $wait-cancel')) - (canon lower (func $yield-cancel) async (memory (core memory $memory "mem")) (core func $yield-cancel')) - (canon lower (func $poll-cancel-pending) async (memory (core memory $memory "mem")) (core func $poll-cancel-pending')) - (canon lower (func $yield-cancel-pending) async (memory (core memory $memory "mem")) (core func $yield-cancel-pending')) - (canon lower (func $pending-survives-nc-yield) async (memory (core memory $memory "mem")) (core func $pending-survives-nc-yield')) - (core instance $dm (instantiate $DM (with "" (instance - (export "mem" (memory $memory "mem")) - (export "subtask.cancel" (func $subtask.cancel)) - (export "subtask.drop" (func $subtask.drop)) - (export "future.new" (func $future.new)) - (export "future.write" (func $future.write)) - (export "waitable.join" (func $waitable.join)) - (export "waitable-set.new" (func $waitable-set.new)) - (export "waitable-set.wait" (func $waitable-set.wait)) - (export "wait-cancel" (func $wait-cancel')) - (export "yield-cancel" (func $yield-cancel')) - (export "poll-cancel-pending" (func $poll-cancel-pending')) - (export "yield-cancel-pending" (func $yield-cancel-pending')) - (export "pending-survives-nc-yield" (func $pending-survives-nc-yield')) - )))) - (func (export "run") async (result u32) (canon lift (core func $dm "run"))) - ) - - (instance $c (instantiate $C)) - (instance $d (instantiate $D - (with "wait-cancel" (func $c "wait-cancel")) - (with "yield-cancel" (func $c "yield-cancel")) - (with "poll-cancel-pending" (func $c "poll-cancel-pending")) - (with "yield-cancel-pending" (func $c "yield-cancel-pending")) - (with "pending-survives-nc-yield" (func $c "pending-survives-nc-yield")) - )) - (func (export "run") (alias export $d "run")) -) -(assert_return (invoke "run") (u32.const 42)) diff --git a/test/async/reentrance.wast b/test/async/reentrance.wast index bbc9d4d4..339d8533 100644 --- a/test/async/reentrance.wast +++ b/test/async/reentrance.wast @@ -428,30 +428,37 @@ ) (assert_trap (invoke "a") "deadlock detected: event loop cannot make further progress") -;; 'subtask.cancel' delivers cancellation immediately even when the callee's -;; component instance is on the stack. +;; 'subtask.cancel' may be called even when the callee's component instance is +;; on the stack. The request is recorded on the callee's task and delivered via +;; the callee's event loop once the callee's thread is next scheduled. Because +;; the callee cannot be scheduled while the caller's synchronous call chain +;; pins the stack, the async 'subtask.cancel' performed by the reentrant call +;; deterministically reports BLOCKED and the supertask observes the resolution +;; via a waitable set afterwards. The second subtask is cancelled with the +;; synchronous form of 'subtask.cancel' which blocks until the cancellation +;; has been delivered and the subtask resolved. (component + (core module $Memory (memory (export "mem") 1)) + (core instance $memory (instantiate $Memory)) (canon subtask.cancel (core func $subtask.cancel)) (canon subtask.cancel async (core func $subtask.cancel-async)) (canon subtask.drop (core func $subtask.drop)) + (canon waitable.join (core func $waitable.join)) + (canon waitable-set.new (core func $waitable-set.new)) + (canon waitable-set.wait (memory (core memory $memory "mem")) (core func $waitable-set.wait)) (core module $M1 - (import "" "subtask.cancel" (func $subtask.cancel (param i32) (result i32))) (import "" "subtask.cancel-async" (func $subtask.cancel-async (param i32) (result i32))) - (import "" "subtask.drop" (func $subtask.drop (param i32))) (global $s1 (export "s1") (mut i32) (i32.const 0)) - (global $s2 (export "s2") (mut i32) (i32.const 0)) + ;; called (via $B.g) while $B is on the stack; $B.f's task cannot run + ;; while this synchronous call chain is pinned, so the cancellation + ;; request deterministically blocks (func (export "h") (result i32) - (local $r1 i32) (local $r2 i32) - (local.set $r1 (call $subtask.cancel (global.get $s1))) - (local.set $r2 (call $subtask.cancel-async (global.get $s2))) - (call $subtask.drop (global.get $s1)) - (call $subtask.drop (global.get $s2)) - (i32.or (local.get $r1) (i32.shl (local.get $r2) (i32.const 8)))) + (if (i32.ne (call $subtask.cancel-async (global.get $s1)) (i32.const -1 (; BLOCKED ;))) + (then unreachable)) + (i32.const 42)) ) (core instance $m1 (instantiate $M1 (with "" (instance - (export "subtask.cancel" (func $subtask.cancel)) - (export "subtask.cancel-async" (func $subtask.cancel-async)) - (export "subtask.drop" (func $subtask.drop)))))) + (export "subtask.cancel-async" (func $subtask.cancel-async)))))) (func $h (result u32) (canon lift (core func $m1 "h"))) (component $B @@ -466,7 +473,8 @@ (global $ws (mut i32) (i32.const 0)) (func $start (global.set $ws (call $waitable-set.new))) (start $start) - ;; parks cancellably in its event loop on a set that never gets an event + ;; parks in its event loop on a set that never gets an event, so only the + ;; delivery of a pending cancellation can wake this task (func (export "f") (result i32) (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (global.get $ws) (i32.const 4)))) (func (export "f-cb") (param i32 i32 i32) (result i32) @@ -489,12 +497,17 @@ (canon lower (func $b "g") (core func $g')) (core module $M2 + (import "" "mem" (memory 1)) (import "" "f" (func $f (result i32))) (import "" "g" (func $g (result i32))) (import "" "s1" (global $s1 (mut i32))) - (import "" "s2" (global $s2 (mut i32))) + (import "" "subtask.cancel" (func $subtask.cancel (param i32) (result i32))) + (import "" "subtask.drop" (func $subtask.drop (param i32))) + (import "" "waitable.join" (func $waitable.join (param i32 i32))) + (import "" "waitable-set.new" (func $waitable-set.new (result i32))) + (import "" "waitable-set.wait" (func $waitable-set.wait (param i32 i32) (result i32))) (func (export "run") (result i32) - (local $p i32) + (local $p i32) (local $s2 i32) (local $ws i32) (local.set $p (call $f)) (if (i32.ne (i32.and (local.get $p) (i32.const 0xf)) (i32.const 1 (; STARTED ;))) (then unreachable)) @@ -502,23 +515,50 @@ (local.set $p (call $f)) (if (i32.ne (i32.and (local.get $p) (i32.const 0xf)) (i32.const 1 (; STARTED ;))) (then unreachable)) - (global.set $s2 (i32.shr_u (local.get $p) (i32.const 4))) - ;; both cancels report CANCELLED_BEFORE_RETURNED (4), neither blocks - (if (i32.ne (call $g) (i32.const 0x404)) (then unreachable)) + (local.set $s2 (i32.shr_u (local.get $p) (i32.const 4))) + ;; reentrantly cancel s1 from inside the call to $B.g, which reports + ;; BLOCKED (checked in "h" above) + (if (i32.ne (call $g) (i32.const 42)) (then unreachable)) + ;; this async-typed task may block, so a synchronous cancel of s2 waits + ;; until the cancellation has been delivered and s2 has resolved + (if (i32.ne (call $subtask.cancel (local.get $s2)) (i32.const 4 (; CANCELLED_BEFORE_RETURNED ;))) + (then unreachable)) + (call $subtask.drop (local.get $s2)) + ;; s1's cancellation, requested inside the reentrant call, has been + ;; delivered by now (or will be shortly); wait for s1 to resolve + (local.set $ws (call $waitable-set.new)) + (call $waitable.join (global.get $s1) (local.get $ws)) + (if (i32.ne (call $waitable-set.wait (local.get $ws) (i32.const 8)) (i32.const 1 (; SUBTASK ;))) + (then unreachable)) + (if (i32.ne (i32.load (i32.const 8)) (global.get $s1)) (then unreachable)) + (if (i32.ne (i32.load (i32.const 12)) (i32.const 4 (; CANCELLED_BEFORE_RETURNED ;))) + (then unreachable)) + (call $waitable.join (global.get $s1) (i32.const 0)) + (call $subtask.drop (global.get $s1)) (i32.const 42)) ) (core instance $m2 (instantiate $M2 (with "" (instance + (export "mem" (memory $memory "mem")) (export "f" (func $f')) (export "g" (func $g')) (export "s1" (global $m1 "s1")) - (export "s2" (global $m1 "s2")))))) - (func (export "run") (result u32) (canon lift (core func $m2 "run"))) + (export "subtask.cancel" (func $subtask.cancel)) + (export "subtask.drop" (func $subtask.drop)) + (export "waitable.join" (func $waitable.join)) + (export "waitable-set.new" (func $waitable-set.new)) + (export "waitable-set.wait" (func $waitable-set.wait)))))) + ;; async-typed (but sync-ABI-lifted) so that "run" may block + (func (export "run") async (result u32) (canon lift (core func $m2 "run"))) ) (assert_return (invoke "run") (u32.const 42)) ;; Reentrance between two sibling instances, where there is an outer sync ;; call and an inner async call that blocks: ;; $A.run -> $B.mid -> $A.inner (blocks) -> run yield-then-resumes inner then returns 42 +;; "inner" parks itself with a thread.suspend-then-promote that targets run's +;; thread which, at that point, is *running* (on the stack below the reentrant +;; call). A running thread is not "ready", so the promote must not switch to +;; it and instead falls back to a plain suspend. (component (core module $Wiring (type $ft (func (result i32))) @@ -536,20 +576,22 @@ (canon lower (func $mid) (core func $mid')) (canon lower (func $check) (core func $check')) (canon thread.index (core func $thread.index)) - (canon thread.suspend (core func $thread.suspend)) + (canon thread.suspend-then-promote (core func $thread.suspend-then-promote)) (canon thread.yield-then-resume (core func $thread.yield-then-resume)) (canon thread.suspend-then-resume (core func $thread.suspend-then-resume)) (core module $AM (import "" "mid" (func $mid (result i32))) (import "" "check" (func $check (result i32))) (import "" "thread.index" (func $thread.index (result i32))) - (import "" "thread.suspend" (func $thread.suspend (result i32))) + (import "" "thread.suspend-then-promote" (func $thread.suspend-then-promote (param i32) (result i32))) (import "" "thread.yield-then-resume" (func $thread.yield-then-resume (param i32) (result i32))) (import "" "thread.suspend-then-resume" (func $thread.suspend-then-resume (param i32) (result i32))) + (global $run-thread (mut i32) (i32.const 0)) (global $inner-thread (mut i32) (i32.const 0)) ;; reenters $A through $B; "mid" hands back the state of its call to ;; "inner", which must be STARTED because "inner" blocked (func (export "run") (result i32) + (global.set $run-thread (call $thread.index)) (if (i32.ne (call $mid) (i32.const 1 (; STARTED ;))) (then unreachable)) ;; control is back here with "inner" still suspended, still holding @@ -562,14 +604,17 @@ ;; and holds it until it returns (func (export "inner") (result i32) (global.set $inner-thread (call $thread.index)) - (if (call $thread.suspend) (then unreachable)) + ;; run's thread is running (this reentrant call is on top of it), so + ;; it is not "ready" and this must fall back to a plain suspend + (if (call $thread.suspend-then-promote (global.get $run-thread)) + (then unreachable)) (i32.const 42)) ) (core instance $am (instantiate $AM (with "" (instance (export "mid" (func $mid')) (export "check" (func $check')) (export "thread.index" (func $thread.index)) - (export "thread.suspend" (func $thread.suspend)) + (export "thread.suspend-then-promote" (func $thread.suspend-then-promote)) (export "thread.yield-then-resume" (func $thread.yield-then-resume)) (export "thread.suspend-then-resume" (func $thread.suspend-then-resume)))))) ;; sync-typed: doesn't take $A's exclusive lock @@ -654,6 +699,10 @@ ;; the caller: the explicit thread is still on the stack underneath. Instead, ;; the sync mini-scheduler driving the reentrant "inner" call must switch back ;; to inner's implicit thread, which stayed ready. +;; Additionally, "inner" starts by thread.yield-then-promote-ing the explicit +;; thread which, at that point, is *running* (on the stack below the reentrant +;; call). A running thread is not "ready", so the promote must not switch to +;; it and instead falls back to a plain yield. (component (core module $Wiring (type $ft (func (result i32))) @@ -675,6 +724,7 @@ (canon thread.index (core func $thread.index)) (canon thread.suspend-then-resume (core func $thread.suspend-then-resume)) (canon thread.yield-then-resume (core func $thread.yield-then-resume)) + (canon thread.yield-then-promote (core func $thread.yield-then-promote)) (core module $AM (import "" "tbl" (table $tbl 1 1 funcref)) (import "" "mid" (func $mid (result i32))) @@ -682,7 +732,9 @@ (import "" "thread.new-indirect" (func $thread.new-indirect (param i32 i32) (result i32))) (import "" "thread.suspend-then-resume" (func $thread.suspend-then-resume (param i32) (result i32))) (import "" "thread.yield-then-resume" (func $thread.yield-then-resume (param i32) (result i32))) + (import "" "thread.yield-then-promote" (func $thread.yield-then-promote (param i32) (result i32))) (global $outer-thread (mut i32) (i32.const 0)) + (global $explicit-thread (mut i32) (i32.const 0)) (global $phase (mut i32) (i32.const 0)) ;; the explicit thread spawned by "run"; the call into $B only returns @@ -695,9 +747,9 @@ (func (export "run") (result i32) (global.set $outer-thread (call $thread.index)) + (global.set $explicit-thread (call $thread.new-indirect (i32.const 0) (i32.const 0))) ;; switch to a fresh explicit thread of this same task, suspending - (if (call $thread.suspend-then-resume - (call $thread.new-indirect (i32.const 0) (i32.const 0))) + (if (call $thread.suspend-then-resume (global.get $explicit-thread)) (then unreachable)) ;; resumed by "inner"'s implicit thread, which stayed ready (if (i32.ne (global.get $phase) (i32.const 1)) (then unreachable)) @@ -709,6 +761,11 @@ ;; the implicit thread of a fresh task (func (export "inner") (result i32) (if (i32.ne (global.get $phase) (i32.const 0)) (then unreachable)) + ;; the explicit thread is running (this reentrant call is on top of + ;; it), so it is not "ready" and this must fall back to a plain yield + ;; without switching + (if (call $thread.yield-then-promote (global.get $explicit-thread)) + (then unreachable)) (global.set $phase (i32.const 1)) ;; switch to run's implicit thread, staying ready ourselves (if (call $thread.yield-then-resume (global.get $outer-thread)) @@ -725,7 +782,8 @@ (export "thread.index" (func $thread.index)) (export "thread.new-indirect" (func $thread.new-indirect)) (export "thread.suspend-then-resume" (func $thread.suspend-then-resume)) - (export "thread.yield-then-resume" (func $thread.yield-then-resume)))))) + (export "thread.yield-then-resume" (func $thread.yield-then-resume)) + (export "thread.yield-then-promote" (func $thread.yield-then-promote)))))) (func (export "run") (result u32) (canon lift (core func $am "run"))) (func (export "inner") (result u32) (canon lift (core func $am "inner"))) ) @@ -794,7 +852,8 @@ (global $ws (mut i32) (i32.const 0)) (func $start (global.set $ws (call $waitable-set.new))) (start $start) - ;; parks cancellably in its event loop on a set that never gets an event + ;; parks in its event loop on a set that never gets an event, so only the + ;; delivery of a pending cancellation can wake this task (func (export "f") (result i32) (i32.or (i32.const 2 (; WAIT ;)) (i32.shl (global.get $ws) (i32.const 4)))) (func (export "f-cb") (param i32 i32 i32) (result i32) diff --git a/test/binary/binary.wast b/test/binary/binary.wast index dbade4ea..3766e018 100644 --- a/test/binary/binary.wast +++ b/test/binary/binary.wast @@ -1084,18 +1084,18 @@ "\1b\04" ;; 0x1b future.drop-writable "\1f" ;; 0x1f waitable-set.new "\20\00\00" ;; 0x20 waitable-set.wait (memory 0) - "\21\01\00" ;; 0x21 waitable-set.poll cancellable (memory 0) + "\21\00\00" ;; 0x21 waitable-set.poll (memory 0) "\22" ;; 0x22 waitable-set.drop "\23" ;; 0x23 waitable.join "\26" ;; 0x26 thread.index "\27\00\00" ;; 0x27 thread.new-indirect (core type 0) (table 0) "\28" ;; 0x28 thread.resume-later "\29\00" ;; 0x29 thread.suspend - "\0c\01" ;; 0x0c thread.yield cancellable + "\0c\00" ;; 0x0c thread.yield "\2a\00" ;; 0x2a thread.suspend-then-resume "\2b\00" ;; 0x2b thread.yield-then-resume "\2c\00" ;; 0x2c thread.suspend-then-promote - "\2d\01" ;; 0x2d thread.yield-then-promote cancellable + "\2d\00" ;; 0x2d thread.yield-then-promote ) (assert_malformed @@ -1182,6 +1182,106 @@ "invalid boolean value" ) +;; the 'cancellable' immediate was removed; the vestigial byte must be 0x00 +(assert_malformed + (component binary + "\00asm" "\0d\00\01\00" ;; preamble + "\01\16" ;; core module section (22 bytes) + "\00asm" "\01\00\00\00" ;; core module preamble + "\05\03\01\00\01" ;; memory section: 1 memory + "\07\07\01" ;; export section, 1 export + "\03mem" ;; name "mem" + "\02\00" ;; memory 0 + "\02\04" ;; core instance section (4 bytes) + "\01" ;; 1 core instance + "\00\00\00" ;; instantiate module 0, 0 args + "\06\09" ;; alias section (9 bytes) + "\01" ;; 1 alias + "\00\02\01\00" ;; core memory + "\03mem" ;; name "mem" + "\08\04" ;; canon section (4 bytes) + "\01" ;; 1 canon + "\20\01\00" ;; 0x20 waitable-set.wait, reserved byte 0x01, (memory 0) + ) + "non-zero reserved byte" +) +(assert_malformed + (component binary + "\00asm" "\0d\00\01\00" ;; preamble + "\01\16" ;; core module section (22 bytes) + "\00asm" "\01\00\00\00" ;; core module preamble + "\05\03\01\00\01" ;; memory section: 1 memory + "\07\07\01" ;; export section, 1 export + "\03mem" ;; name "mem" + "\02\00" ;; memory 0 + "\02\04" ;; core instance section (4 bytes) + "\01" ;; 1 core instance + "\00\00\00" ;; instantiate module 0, 0 args + "\06\09" ;; alias section (9 bytes) + "\01" ;; 1 alias + "\00\02\01\00" ;; core memory + "\03mem" ;; name "mem" + "\08\04" ;; canon section (4 bytes) + "\01" ;; 1 canon + "\21\01\00" ;; 0x21 waitable-set.poll, reserved byte 0x01, (memory 0) + ) + "non-zero reserved byte" +) +(assert_malformed + (component binary + "\00asm" "\0d\00\01\00" ;; preamble + "\08\03" ;; canon section (3 bytes) + "\01" ;; 1 canon + "\29\01" ;; 0x29 thread.suspend, reserved byte 0x01 + ) + "non-zero reserved byte" +) +(assert_malformed + (component binary + "\00asm" "\0d\00\01\00" ;; preamble + "\08\03" ;; canon section (3 bytes) + "\01" ;; 1 canon + "\0c\01" ;; 0x0c thread.yield, reserved byte 0x01 + ) + "non-zero reserved byte" +) +(assert_malformed + (component binary + "\00asm" "\0d\00\01\00" ;; preamble + "\08\03" ;; canon section (3 bytes) + "\01" ;; 1 canon + "\2a\01" ;; 0x2a thread.suspend-then-resume, reserved byte 0x01 + ) + "non-zero reserved byte" +) +(assert_malformed + (component binary + "\00asm" "\0d\00\01\00" ;; preamble + "\08\03" ;; canon section (3 bytes) + "\01" ;; 1 canon + "\2b\01" ;; 0x2b thread.yield-then-resume, reserved byte 0x01 + ) + "non-zero reserved byte" +) +(assert_malformed + (component binary + "\00asm" "\0d\00\01\00" ;; preamble + "\08\03" ;; canon section (3 bytes) + "\01" ;; 1 canon + "\2c\01" ;; 0x2c thread.suspend-then-promote, reserved byte 0x01 + ) + "non-zero reserved byte" +) +(assert_malformed + (component binary + "\00asm" "\0d\00\01\00" ;; preamble + "\08\03" ;; canon section (3 bytes) + "\01" ;; 1 canon + "\2d\01" ;; 0x2d thread.yield-then-promote, reserved byte 0x01 + ) + "non-zero reserved byte" +) + ;; import section (id 10) and export section (id 11) (component definition binary diff --git a/test/nyi.txt b/test/nyi.txt index 3b501c43..9df9cebd 100644 --- a/test/nyi.txt +++ b/test/nyi.txt @@ -8,3 +8,4 @@ ./async/sync-streams.wast ./validation/kebab.wast ./async/trap-if-sync-and-waitable-set.wast +./binary/binary.wast