hurray_io/stream/sans_io/mod.rs
1//! A sans-io core for the stream wire format: a state machine over bytes, with no file,
2//! socket or runtime anywhere in it (ADR-041 § 1).
3//!
4//! The format already has this shape. Descriptors precede their buffers, the encoding is
5//! self-delimiting, back-references are forbidden, and nothing is ever re-read — so a
6//! reader never looks backwards, and a reader that never looks backwards wants to be a
7//! state machine.
8//!
9//! Two machines, because the two directions are not mirror images:
10//! [`StreamReaderCore`] *owns* the memory each payload lands in (§ 2), while
11//! [`StreamWriterCore`] borrows the caller's payloads and emits them one slice at a time
12//! without ever assembling them (§ 3).
13//!
14//! Both are driven the same way: ask with `step`, transfer the bytes, report the count.
15//! Neither call blocks, which is what lets one core serve a blocking C driver and an
16//! event loop alike (§ 4).
17//!
18//! Composites are assembled a layer up, by [`CompositeAssembler`]. Rebuilding the tree
19//! needs no bytes — the tensors it works on are already decoded — so it is bookkeeping,
20//! not I/O, and keeping it out of the byte-level machine is what lets one core serve both
21//! the flat and the assembled view of the same stream.
22
23mod composite;
24mod dest;
25mod read;
26mod write;
27
28pub use composite::CompositeAssembler;
29pub use dest::{
30 AlignedDestination, Destination, DestinationAlloc, DestinationRequest, SystemDestinations,
31};
32pub use read::{ReadStep, StreamReaderCore};
33pub use write::{StreamWriterCore, WriteOp, WriteStep};
34
35use std::fmt;
36
37/// Why a core became unusable: a transport moved bytes and their count was lost, so the
38/// core's position no longer matches the transport's (ADR-041 § 6).
39///
40/// No inspection recovers it — `0x00` is valid tensor data — so the state is named and
41/// reported rather than guessed at. "Probably still fine" is not a state.
42///
43/// # Examples
44///
45/// ```
46/// use hurray_io::stream::StreamReaderCore;
47/// use hurray_io::Error;
48///
49/// let mut core = StreamReaderCore::new();
50/// core.mark_unusable();
51/// assert!(matches!(core.step(), Err(Error::CoreUnusable(_))));
52/// ```
53// `non_exhaustive`: the reasons a core can be retired are open-ended, and a caller
54// matching on them exhaustively would break on every future one.
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56#[non_exhaustive]
57pub enum Unusable {
58 /// A read moved bytes into the core's destination and the count never arrived. Only
59 /// this core's position is lost; the transport is untouched.
60 LostReadCount,
61 /// A write put an unknown number of payload bytes on the wire, so the position is
62 /// *unknown*. Narrower than [`Self::IncompleteOutput`], which is a known-incomplete
63 /// item: a stream cannot be un-sent, so the sink is poisoned too — the reader at the
64 /// far end will see a truncated item, and a fresh writer over the same connection is
65 /// not a recovery.
66 LostWriteCount,
67 /// An item was begun and cannot be completed, so the output is incomplete. The count
68 /// is *known*, unlike [`Self::LostWriteCount`]; what is lost is the ability to produce
69 /// a conforming output, because a forward-only writer has no way back to the item's
70 /// start (§ 6b).
71 IncompleteOutput,
72}
73
74impl fmt::Display for Unusable {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 match self {
77 Self::LostReadCount => f.write_str("a read's byte count was lost"),
78 Self::LostWriteCount => {
79 f.write_str("a write's byte count was lost; the sink is poisoned")
80 }
81 // Worded to cover both reachable cases: an item torn mid-emission, and a sink
82 // that closed before the item's first byte. "Begun and not finished" would be
83 // false for the second.
84 Self::IncompleteOutput => {
85 f.write_str("the output is incomplete and cannot be completed")
86 }
87 }
88 }
89}