From 574e9c16d3ee4711a68b1c44b5314298b7f514a2 Mon Sep 17 00:00:00 2001 From: Roman Volosatovs Date: Mon, 24 Aug 2026 16:29:53 +0200 Subject: [PATCH] CABI: allow dropping a readable future end with a pending write The Explainer specifies that a component may call `future.drop-readable` before reading a value to signal loss of interest, in which case a blocked `future.write` completes with `future-write-result.dropped`. But `SharedFutureImpl.drop` asserted that any pending buffer is a `WritableBuffer`, i.e. a pending *read*, while the only pending buffer actually possible at this point is the buffer of a pending *write*: a reader cannot drop its end while its own read is pending (`CopyEnd.drop` traps while copying) and a writer cannot drop its end before the write resolves (`WritableFutureEnd.drop` traps unless DONE). Exercising this spec-legal path failed the assert instead of notifying the writer with DROPPED. Flip the assert to expect a `ReadableBuffer` (the source buffer of the pending write), make `ReadableBufferGuestImpl` actually subclass `ReadableBuffer` (it previously only derived `BufferGuestImpl`, unlike `WritableBufferGuestImpl`, which does mix in `WritableBuffer`), and add a test reproducing the scenario. Signed-off-by: Roman Volosatovs Assisted-by: claude:claude-fable-5 --- design/mvp/CanonicalABI.md | 4 +-- design/mvp/canonical-abi/definitions.py | 4 +-- design/mvp/canonical-abi/run_tests.py | 37 +++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/design/mvp/CanonicalABI.md b/design/mvp/CanonicalABI.md index 0c222b79..1cb97a3f 100644 --- a/design/mvp/CanonicalABI.md +++ b/design/mvp/CanonicalABI.md @@ -1751,7 +1751,7 @@ class BufferGuestImpl(Buffer): def is_zero_length(self): return self.length == 0 -class ReadableBufferGuestImpl(BufferGuestImpl): +class ReadableBufferGuestImpl(BufferGuestImpl, ReadableBuffer): def read(self, n): assert(n <= self.remain()) if self.t: @@ -2124,7 +2124,7 @@ by `WritableFutureEnd.drop` so it can be asserted here: if not self.dropped: self.dropped = True if self.pending_buffer: - assert(isinstance(self.pending_buffer, WritableBuffer)) + assert(isinstance(self.pending_buffer, ReadableBuffer)) self.reset_and_notify_pending(CopyResult.DROPPED) ``` Lastly, `read` and `write` work mostly like streams, but simplified based on diff --git a/design/mvp/canonical-abi/definitions.py b/design/mvp/canonical-abi/definitions.py index 99fb6fc6..807cfae8 100644 --- a/design/mvp/canonical-abi/definitions.py +++ b/design/mvp/canonical-abi/definitions.py @@ -962,7 +962,7 @@ def remain(self): def is_zero_length(self): return self.length == 0 -class ReadableBufferGuestImpl(BufferGuestImpl): +class ReadableBufferGuestImpl(BufferGuestImpl, ReadableBuffer): def read(self, n): assert(n <= self.remain()) if self.t: @@ -1158,7 +1158,7 @@ def drop(self): if not self.dropped: self.dropped = True if self.pending_buffer: - assert(isinstance(self.pending_buffer, WritableBuffer)) + assert(isinstance(self.pending_buffer, ReadableBuffer)) self.reset_and_notify_pending(CopyResult.DROPPED) def read(self, inst, dst_buffer, on_copy_done): diff --git a/design/mvp/canonical-abi/run_tests.py b/design/mvp/canonical-abi/run_tests.py index b4bd0c34..30573d41 100644 --- a/design/mvp/canonical-abi/run_tests.py +++ b/design/mvp/canonical-abi/run_tests.py @@ -2308,6 +2308,42 @@ def core_func(args): lift_and_run(lift_opts, inst, caller_ft, core_func, lambda:[], lambda _:()) +def test_future_drop_readable_with_pending_write(): + store = Store() + inst = ComponentInstance(store) + mem = bytearray(24) + opts = mk_opts(memory=MemInst(mem, 'i32'), async_=True) + future_t = FutureType(U8Type()) + + def core_func(args): + assert(len(args) == 0) + [] = canon_task_return([], opts, []) + [packed] = canon_future_new(future_t) + rfi,wfi = unpack_new_ends(packed) + + mem[0] = 42 + [ret] = canon_future_write(future_t, opts, wfi, 0) + assert(ret == definitions.BLOCKED) + + # The reader may drop its end before reading a value; the blocked write + # is notified that the readable end was dropped. + [] = canon_future_drop_readable(future_t, rfi) + retp = 16 + [seti] = canon_waitable_set_new() + [] = canon_waitable_join(wfi, seti) + [event] = canon_waitable_set_wait(True, MemInst(mem, 'i32'), seti, retp) + assert(event == EventCode.FUTURE_WRITE) + assert(mem[retp+0] == wfi) + assert(mem[retp+4] == CopyResult.DROPPED) + [] = canon_waitable_join(wfi, 0) + [] = canon_waitable_set_drop(seti) + [] = canon_future_drop_writable(future_t, wfi) + return [] + + caller_ft = FuncType([], [], async_ = True) + lift_and_run(opts, inst, caller_ft, core_func, lambda:[], lambda _:()) + + def test_cancel_subtask(): store = Store() root_inst = ComponentInstance(store) @@ -3039,6 +3075,7 @@ def core_consumer(args): test_wasm_to_wasm_stream_empty() test_cancel_copy() test_futures() +test_future_drop_readable_with_pending_write() test_cancel_subtask() test_self_copy(None) test_self_copy(U8Type())