Skip to main content

sim_lib_stream_core/
buffer.rs

1//! Buffer policy values and small expr field-extraction helpers.
2//!
3//! This module supplies the stream fabric's buffering contract:
4//! [`BufferPolicy`] (a bounded capacity plus an overflow rule),
5//! [`BufferOverflowPolicy`] (what to do when a full buffer receives a packet),
6//! and [`BackpressureOutcome`] (the result a producer observes when it offers a
7//! packet). Each carries a stable `stream/*` symbol so the policy round-trips
8//! through the runtime's symbol and [`Expr`] surfaces. The crate-private
9//! `field` helper reads a bare-symbol entry out of an [`Expr::Map`] and is
10//! reused by sibling modules that decode stream values; the typed
11//! `string_field`/`symbol_field` readers are thin wrappers over the shared
12//! `sim_value::access` slice readers.
13
14use sim_kernel::{Error, Expr, Result, Symbol};
15use sim_value::access;
16pub(crate) use sim_value::kind::expr_kind;
17
18/// Result a producer observes when it offers a packet to a buffered stream.
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
20pub enum BackpressureOutcome {
21    /// The packet was buffered.
22    Accepted,
23    /// The buffer was full and this newest packet was dropped.
24    DroppedNewest,
25    /// The buffer was full and the oldest buffered packet was dropped to admit
26    /// this one.
27    DroppedOldest,
28    /// The producer is blocked until capacity frees up.
29    Blocked,
30    /// The offer timed out before capacity freed up.
31    TimedOut,
32    /// The offer was rejected by policy.
33    Rejected,
34    /// The stream is closed and accepts no further packets.
35    Closed,
36}
37
38impl BackpressureOutcome {
39    /// Returns the stable wire label for this outcome.
40    pub fn wire_label(self) -> &'static str {
41        match self {
42            Self::Accepted => "accepted",
43            Self::DroppedNewest => "dropped-newest",
44            Self::DroppedOldest => "dropped-oldest",
45            Self::Blocked => "blocked",
46            Self::TimedOut => "timed-out",
47            Self::Rejected => "rejected",
48            Self::Closed => "closed",
49        }
50    }
51
52    /// Returns the `stream/backpressure/<label>` symbol for this outcome.
53    pub fn symbol(self) -> Symbol {
54        Symbol::qualified("stream/backpressure", self.wire_label())
55    }
56
57    /// Parses an outcome from its bare or `stream/backpressure`-qualified
58    /// symbol.
59    ///
60    /// Returns an error for an unrecognized outcome symbol.
61    pub fn from_symbol(symbol: &Symbol) -> Result<Self> {
62        match symbol.as_qualified_str().as_str() {
63            "accepted" | "stream/backpressure/accepted" => Ok(Self::Accepted),
64            "dropped-newest" | "stream/backpressure/dropped-newest" => Ok(Self::DroppedNewest),
65            "dropped-oldest" | "stream/backpressure/dropped-oldest" => Ok(Self::DroppedOldest),
66            "blocked" | "stream/backpressure/blocked" => Ok(Self::Blocked),
67            "timed-out" | "stream/backpressure/timed-out" => Ok(Self::TimedOut),
68            "rejected" | "stream/backpressure/rejected" => Ok(Self::Rejected),
69            "closed" | "stream/backpressure/closed" => Ok(Self::Closed),
70            other => Err(Error::Eval(format!(
71                "unknown stream backpressure outcome {other}"
72            ))),
73        }
74    }
75}
76
77/// Rule applied when a full buffer receives another packet.
78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79pub enum BufferOverflowPolicy {
80    /// Drop the incoming (newest) packet.
81    DropNewest,
82    /// Drop the oldest buffered packet to make room.
83    DropOldest,
84    /// Treat the overflow as an error.
85    Error,
86}
87
88impl BufferOverflowPolicy {
89    /// Returns the `stream/overflow/<rule>` symbol for this policy.
90    pub fn symbol(self) -> Symbol {
91        match self {
92            Self::DropNewest => Symbol::qualified("stream/overflow", "drop-newest"),
93            Self::DropOldest => Symbol::qualified("stream/overflow", "drop-oldest"),
94            Self::Error => Symbol::qualified("stream/overflow", "error"),
95        }
96    }
97
98    /// Parses an overflow policy from its `stream/overflow`-qualified symbol.
99    ///
100    /// Returns an error for an unrecognized overflow symbol.
101    pub fn from_symbol(symbol: &Symbol) -> Result<Self> {
102        match symbol.as_qualified_str().as_str() {
103            "stream/overflow/drop-newest" => Ok(Self::DropNewest),
104            "stream/overflow/drop-oldest" => Ok(Self::DropOldest),
105            "stream/overflow/error" => Ok(Self::Error),
106            other => Err(Error::Eval(format!(
107                "unknown stream buffer overflow policy {other}"
108            ))),
109        }
110    }
111}
112
113/// Buffering contract for a stream: a bounded capacity plus an overflow rule.
114///
115/// A policy is always bounded with a capacity of at least one; the constructors
116/// reject a zero capacity. The overflow rule decides what happens when the
117/// buffer is full.
118///
119/// # Examples
120///
121/// ```
122/// use sim_lib_stream_core::{BufferOverflowPolicy, BufferPolicy};
123///
124/// let policy = BufferPolicy::bounded(8).expect("capacity is nonzero");
125/// assert_eq!(policy.capacity(), 8);
126/// assert_eq!(policy.overflow(), BufferOverflowPolicy::DropNewest);
127/// assert!(BufferPolicy::bounded(0).is_err());
128/// ```
129#[derive(Clone, Debug, PartialEq, Eq)]
130pub struct BufferPolicy {
131    capacity: usize,
132    overflow: BufferOverflowPolicy,
133}
134
135impl BufferPolicy {
136    /// Builds a bounded policy of `capacity` with the default
137    /// [`BufferOverflowPolicy::DropNewest`] overflow rule.
138    ///
139    /// Returns an error if `capacity` is zero.
140    pub fn bounded(capacity: usize) -> Result<Self> {
141        Self::bounded_with_overflow(capacity, BufferOverflowPolicy::DropNewest)
142    }
143
144    /// Builds a bounded policy of `capacity` with an explicit overflow rule.
145    ///
146    /// Returns an error if `capacity` is zero.
147    pub fn bounded_with_overflow(capacity: usize, overflow: BufferOverflowPolicy) -> Result<Self> {
148        if capacity == 0 {
149            return Err(Error::Eval(
150                "stream buffer capacity must be greater than zero".to_owned(),
151            ));
152        }
153        Ok(Self { capacity, overflow })
154    }
155
156    /// Returns the buffer capacity.
157    pub fn capacity(&self) -> usize {
158        self.capacity
159    }
160
161    /// Returns the overflow rule.
162    pub fn overflow(&self) -> BufferOverflowPolicy {
163        self.overflow
164    }
165
166    /// Returns the `stream/buffer/bounded-<capacity>` symbol for this policy.
167    pub fn symbol(&self) -> Symbol {
168        Symbol::qualified("stream/buffer", format!("bounded-{}", self.capacity))
169    }
170
171    /// Encodes this policy as an [`Expr::Map`] with `capacity` and `overflow`
172    /// fields.
173    pub fn to_expr(&self) -> Expr {
174        Expr::Map(vec![
175            (
176                Expr::Symbol(Symbol::new("capacity")),
177                Expr::String(self.capacity.to_string()),
178            ),
179            (
180                Expr::Symbol(Symbol::new("overflow")),
181                Expr::Symbol(self.overflow.symbol()),
182            ),
183        ])
184    }
185
186    /// Decodes a policy from an [`Expr::Map`] produced by
187    /// [`to_expr`](Self::to_expr).
188    ///
189    /// Returns an error if the expression is not a map, a field is missing or
190    /// the wrong type, or the capacity fails to parse.
191    pub fn from_expr(expr: &Expr) -> Result<Self> {
192        let Expr::Map(entries) = expr else {
193            return Err(Error::TypeMismatch {
194                expected: "stream buffer policy map",
195                found: expr_kind(expr),
196            });
197        };
198        let capacity = string_field(entries, "capacity")?
199            .parse::<usize>()
200            .map_err(|err| Error::Eval(format!("invalid stream buffer capacity: {err}")))?;
201        let overflow = BufferOverflowPolicy::from_symbol(symbol_field(entries, "overflow")?)?;
202        Self::bounded_with_overflow(capacity, overflow)
203    }
204}
205
206pub(crate) fn string_field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a str> {
207    access::entry_required_str(entries, name, "string field")
208}
209
210pub(crate) fn symbol_field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a Symbol> {
211    access::entry_required_sym(entries, name, "symbol field")
212}
213
214pub(crate) fn field<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Result<&'a Expr> {
215    entries
216        .iter()
217        .find_map(|(key, value)| match key {
218            Expr::Symbol(symbol) if symbol.namespace.is_none() && symbol.name.as_ref() == name => {
219                Some(value)
220            }
221            _ => None,
222        })
223        .ok_or_else(|| Error::Eval(format!("stream value missing {name} field")))
224}