From 7cd62c1a679f9415ed1e8a448b032700f55a43a4 Mon Sep 17 00:00:00 2001 From: Luke Wagner Date: Tue, 6 Jan 2026 15:57:17 -0600 Subject: [PATCH 1/2] Trap on call to {stream,future}.cancel-{read,write} after a prior cancellation blocked --- design/mvp/CanonicalABI.md | 24 ++++++++++++++++-------- design/mvp/canonical-abi/definitions.py | 11 +++++++++-- design/mvp/canonical-abi/run_tests.py | 5 +++++ 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/design/mvp/CanonicalABI.md b/design/mvp/CanonicalABI.md index fa4c23a6..269461f9 100644 --- a/design/mvp/CanonicalABI.md +++ b/design/mvp/CanonicalABI.md @@ -1525,7 +1525,8 @@ class CopyState(Enum): IDLE = 1 SYNC_COPYING = 2 ASYNC_COPYING = 3 - DONE = 4 + CANCELLING_COPY = 4 + DONE = 5 class CopyEnd(Waitable): state: CopyState @@ -1537,7 +1538,12 @@ class CopyEnd(Waitable): self.shared = shared def copying(self): - return self.state == CopyState.SYNC_COPYING or self.state == CopyState.ASYNC_COPYING + match self.state: + case CopyState.IDLE | CopyState.DONE: + return False + case CopyState.SYNC_COPYING | CopyState.ASYNC_COPYING | CopyState.CANCELLING_COPY: + return True + assert(False) def drop(self): trap_if(self.copying()) @@ -1553,11 +1559,11 @@ class WritableStreamEnd(CopyEnd): self.shared.write(inst, src, on_copy, on_copy_done) ``` As shown in `drop`, attempting to drop a readable or writable end while a copy -is in progress traps. This means that client code must take care to wait for -these operations to finish (potentially cancelling them via -`stream.cancel-{read,write}`) before dropping. The `SYNC_COPY` vs. `ASYNC_COPY` -distinction is tracked in the state to determine whether the copy operation can -be cancelled. +is in progress or in the process of being cancelled traps. This means that +client code must take care to wait for these operations to finish (potentially +cancelling them via `stream.cancel-{read,write}`) before dropping. The +`SYNC_COPY` vs. `ASYNC_COPY` distinction is tracked in the state to determine +whether the copy operation can be cancelled. The polymorphic `copy` method dispatches to either `ReadableStream.read` or `WritableStream.write` and allows the implementations of `stream.{read,write}` @@ -4270,6 +4276,7 @@ def cancel_copy(EndT, event_code, stream_or_future_t, async_, thread, i): trap_if(not isinstance(e, EndT)) trap_if(e.shared.t != stream_or_future_t.t) trap_if(e.state != CopyState.ASYNC_COPYING) + e.state = CopyState.CANCELLING_COPY if not e.has_pending_event(): e.shared.cancel() if not e.has_pending_event(): @@ -4286,7 +4293,8 @@ unconditionally traps if it transitively attempts to make a synchronous call to `cancel-read` or `cancel-write` (regardless of whether the cancellation would have completed without blocking). There is also a trap if there is not currently an async copy in progress (sync copies do not expect or check for -cancellation and thus cannot be cancelled). +cancellation and thus cannot be cancelled and repeatedly cancelling the same +async copy after the first call blocked is not allowed). The *first* check for `e.has_pending_event()` catches the case where the copy has already racily finished, in which case we must *not* call `cancel()`. Calling diff --git a/design/mvp/canonical-abi/definitions.py b/design/mvp/canonical-abi/definitions.py index edd68241..95e04aec 100644 --- a/design/mvp/canonical-abi/definitions.py +++ b/design/mvp/canonical-abi/definitions.py @@ -886,7 +886,8 @@ class CopyState(Enum): IDLE = 1 SYNC_COPYING = 2 ASYNC_COPYING = 3 - DONE = 4 + CANCELLING_COPY = 4 + DONE = 5 class CopyEnd(Waitable): state: CopyState @@ -898,7 +899,12 @@ def __init__(self, shared): self.shared = shared def copying(self): - return self.state == CopyState.SYNC_COPYING or self.state == CopyState.ASYNC_COPYING + match self.state: + case CopyState.IDLE | CopyState.DONE: + return False + case CopyState.SYNC_COPYING | CopyState.ASYNC_COPYING | CopyState.CANCELLING_COPY: + return True + assert(False) def drop(self): trap_if(self.copying()) @@ -2431,6 +2437,7 @@ def cancel_copy(EndT, event_code, stream_or_future_t, async_, thread, i): trap_if(not isinstance(e, EndT)) trap_if(e.shared.t != stream_or_future_t.t) trap_if(e.state != CopyState.ASYNC_COPYING) + e.state = CopyState.CANCELLING_COPY if not e.has_pending_event(): e.shared.cancel() if not e.has_pending_event(): diff --git a/design/mvp/canonical-abi/run_tests.py b/design/mvp/canonical-abi/run_tests.py index 70a197f3..b82b4489 100644 --- a/design/mvp/canonical-abi/run_tests.py +++ b/design/mvp/canonical-abi/run_tests.py @@ -2031,6 +2031,11 @@ def core_func(thread, args): host_source.block_cancel() [ret] = canon_stream_cancel_read(StreamType(U8Type()), True, thread, rsi) assert(ret == definitions.BLOCKED) + try: + canon_stream_cancel_read(StreamType(U8Type()), True, thread, rsi) + assert(False) + except Trap: + pass host_source.write([7,8]) host_source.unblock_cancel() [seti] = canon_waitable_set_new(thread) From 16fb8ce902418ccb9ee956bc00659be466440a1d Mon Sep 17 00:00:00 2001 From: Luke Wagner Date: Wed, 7 Jan 2026 10:49:29 -0600 Subject: [PATCH 2/2] Add comma Co-authored-by: Joel Dice --- design/mvp/CanonicalABI.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design/mvp/CanonicalABI.md b/design/mvp/CanonicalABI.md index 269461f9..309968cf 100644 --- a/design/mvp/CanonicalABI.md +++ b/design/mvp/CanonicalABI.md @@ -4293,7 +4293,7 @@ unconditionally traps if it transitively attempts to make a synchronous call to `cancel-read` or `cancel-write` (regardless of whether the cancellation would have completed without blocking). There is also a trap if there is not currently an async copy in progress (sync copies do not expect or check for -cancellation and thus cannot be cancelled and repeatedly cancelling the same +cancellation and thus cannot be cancelled, and repeatedly cancelling the same async copy after the first call blocked is not allowed). The *first* check for `e.has_pending_event()` catches the case where the copy has