Self-contained implementation ticket (for a dedicated agent). Entirely within this fork's async runtime; no other repo involved. Follows #1, whose perf/async-stream-amortize-single-item-alloc branch already made next()/write_one() amortized zero-alloc (reused heap buffer). This ticket is the remaining step: make the scalar single-item path true zero-heap (no allocator dependency at all) — the piece the no_std/MCU line needs (meld#299, gale#89).
Goal
For RawStreamReader::next / RawStreamWriter::write_one, when O::native_abi_matches_canonical_abi() (scalar payloads: u32/f32/…), use an inline [MaybeUninit<O::Payload>; 1] buffer instead of a heap Vec, so the path performs zero heap allocations including the first call and has no global-allocator dependency.
Why it's sound (do NOT skip — this is the load-bearing property)
The single-item buffer is moved into a WaitableOperation held across await. waitable.rs guarantees the op is PhantomPinned + Pin<&mut Self> and its destructor synchronously cancels the in-flight canon op before the memory is freed (in_progress_cancel "must synchronously complete… can block"; waitable.rs ~182-190). Therefore the runtime cannot reference the buffer after the future's Drop returns, so an inline buffer dropped after that synchronous cancel is not a use-after-free. Any implementation MUST preserve this ordering (cancel-then-free).
Scope limit (honest)
Only the scalar (native_abi_matches_canonical_abi) path can be zero-heap. When canonical ≠ native, AbiBuffer::new always allocates a separate Cleanup buffer for the lowered form (abi_buffer.rs:44-65) — non-scalar single-item streams keep one heap alloc regardless. Gate the inline path strictly on native_abi_matches_canonical_abi(); fall back to the existing reused-heap path otherwise.
Design
AbiBuffer storage becomes an enum: Heap(Vec<MaybeUninit<T>>) (today) vs Inline([MaybeUninit<T>; 1]) — OR a parallel SingleAbiBuffer used only by next/write_one. abi_ptr_and_len/advance/remaining operate on &mut [MaybeUninit<T>] either way; into_vec is unsupported on the inline variant (these callers never call it — they pop() the single item).
- The inline storage lives in the
WaitableOperation (future frame) — sound per the cancel property above; the self.next_buf/one_buf heap Vecs are no longer needed for the scalar path.
- Preserve the existing
Vec-based multi-item API (read/write/write_all/collect) unchanged.
Oracle (required gates)
- Strengthen
next_zero_alloc_in_steady_state / write_one_zero_alloc_in_steady_state to assert zero allocations including the first call (true-no-heap counter) for a scalar payload.
- Miri in CI over the new path — this catches the cancellation/Pin use-after-free that the functional test cannot. Must not merge without the Miri gate green (the failure mode is UB, not a wrong value).
Refs
#1 (amortization predecessor), meld#299, gale#89 (the no-grow MCU chain this unblocks at the async layer).
Self-contained implementation ticket (for a dedicated agent). Entirely within this fork's async runtime; no other repo involved. Follows #1, whose
perf/async-stream-amortize-single-item-allocbranch already madenext()/write_one()amortized zero-alloc (reused heap buffer). This ticket is the remaining step: make the scalar single-item path true zero-heap (no allocator dependency at all) — the piece the no_std/MCU line needs (meld#299, gale#89).Goal
For
RawStreamReader::next/RawStreamWriter::write_one, whenO::native_abi_matches_canonical_abi()(scalar payloads: u32/f32/…), use an inline[MaybeUninit<O::Payload>; 1]buffer instead of a heapVec, so the path performs zero heap allocations including the first call and has no global-allocator dependency.Why it's sound (do NOT skip — this is the load-bearing property)
The single-item buffer is moved into a
WaitableOperationheld acrossawait.waitable.rsguarantees the op isPhantomPinned+Pin<&mut Self>and its destructor synchronously cancels the in-flight canon op before the memory is freed (in_progress_cancel"must synchronously complete… can block"; waitable.rs ~182-190). Therefore the runtime cannot reference the buffer after the future'sDropreturns, so an inline buffer dropped after that synchronous cancel is not a use-after-free. Any implementation MUST preserve this ordering (cancel-then-free).Scope limit (honest)
Only the scalar (
native_abi_matches_canonical_abi) path can be zero-heap. When canonical ≠ native,AbiBuffer::newalways allocates a separateCleanupbuffer for the lowered form (abi_buffer.rs:44-65) — non-scalar single-item streams keep one heap alloc regardless. Gate the inline path strictly onnative_abi_matches_canonical_abi(); fall back to the existing reused-heap path otherwise.Design
AbiBufferstorage becomes an enum:Heap(Vec<MaybeUninit<T>>)(today) vsInline([MaybeUninit<T>; 1])— OR a parallelSingleAbiBufferused only bynext/write_one.abi_ptr_and_len/advance/remainingoperate on&mut [MaybeUninit<T>]either way;into_vecis unsupported on the inline variant (these callers never call it — theypop()the single item).WaitableOperation(future frame) — sound per the cancel property above; theself.next_buf/one_bufheapVecs are no longer needed for the scalar path.Vec-based multi-item API (read/write/write_all/collect) unchanged.Oracle (required gates)
next_zero_alloc_in_steady_state/write_one_zero_alloc_in_steady_stateto assert zero allocations including the first call (true-no-heap counter) for a scalar payload.Refs
#1 (amortization predecessor), meld#299, gale#89 (the no-grow MCU chain this unblocks at the async layer).