Skip to main content

ph_eventing/
ring.rs

1//! Fixed-size, stack-allocated ring buffer — no heap, no alloc, no atomics.
2//!
3//! [`RingBuf`] is a single-owner (`&mut self`) ring that overwrites the
4//! oldest element when full. It requires only `T: Copy` and is ideal for
5//! sample windows, local event logs, and anywhere a simple circular buffer
6//! is needed without cross-thread sharing.
7//!
8//! Slots are `MaybeUninit<T>` and only live entries are ever read, which is
9//! what lets the `Default` bound go. The cost is that this type is no longer
10//! free of `unsafe`: see the safety note on [`RingBuf`].
11//!
12//! For a lock-free SPSC ring with sequence tracking, see [`crate::SeqRing`].
13//! For a lock-free SPSC ring with backpressure, see [`crate::EventBuf`].
14//!
15//! # Example
16//! ```
17//! use ph_eventing::RingBuf;
18//!
19//! let mut r = RingBuf::<u32, 4>::new();
20//! r.push(10);
21//! r.push(20);
22//! assert_eq!(r.latest(), Some(20));
23//! assert_eq!(r.get(0), Some(10)); // oldest
24//! ```
25
26use core::mem::MaybeUninit;
27
28/// A ring buffer of `N` elements stored entirely on the stack.
29///
30/// Once full, new pushes overwrite the oldest entry. Iteration with
31/// [`iter()`](RingBuf::iter) yields elements from oldest to newest.
32///
33/// # Safety note
34/// Slots are stored as `MaybeUninit<T>` so that `T: Default` is not required.
35/// Exactly one invariant makes every read sound: **the `len` entries ending at
36/// `head` have all been written by [`push`](RingBuf::push)**. Every read goes
37/// through the private `index` helper, which addresses only that range, and
38/// every public accessor checks `len` before calling it. A change that lets
39/// `len` outrun the number of writes is undefined behaviour, not a logic bug.
40pub struct RingBuf<T: Copy, const N: usize> {
41    buf: [MaybeUninit<T>; N],
42    /// Write cursor — always points to the *next* slot to write.
43    head: usize,
44    /// Number of elements currently stored (≤ N).
45    len: usize,
46}
47
48impl<T: Copy, const N: usize> RingBuf<T, N> {
49    /// Create a new, empty ring buffer.
50    ///
51    /// This is a `const fn`, so the buffer can be built in a `const` or
52    /// `static` initialiser. Note that `push`, `pop`, and `clear` take
53    /// `&mut self`, so a bare `static RingBuf` is read-only and of little use,
54    /// and `static mut` is a hard error to reference under edition 2024. The
55    /// pattern this actually enables is const-initialising the buffer *inside*
56    /// an interior-mutability wrapper, which is how a single-owner buffer is
57    /// reached from an interrupt context:
58    ///
59    /// ```text
60    /// // with critical-section, cortex-m, or similar:
61    /// static LOG: Mutex<RefCell<RingBuf<u32, 64>>> =
62    ///     Mutex::new(RefCell::new(RingBuf::new()));
63    /// ```
64    ///
65    /// Without a const `new` that initialiser is impossible and you need a
66    /// `StaticCell` or a `OnceCell` and a runtime init step.
67    ///
68    /// # Capacity `0` is a build failure
69    /// The `N > 0` check is a *const* assertion, so a zero-capacity ring cannot
70    /// be constructed at all -- there is no runtime panic left to catch, and
71    /// therefore no way to write the negative case as a `#[test]`
72    /// (`zero_capacity_panics` was deleted for exactly this reason). This
73    /// `compile_fail` doctest is that coverage, and pinning the error code
74    /// keeps it honest: a bare `compile_fail` would also pass on a typo.
75    ///
76    /// ```compile_fail,E0080
77    /// let _ = ph_eventing::RingBuf::<u32, 0>::new();
78    /// ```
79    ///
80    /// # Panics
81    /// Does not panic.
82    pub const fn new() -> Self {
83        const {
84            assert!(N > 0, "RingBuf capacity N must be > 0");
85        }
86        Self {
87            buf: [const { MaybeUninit::uninit() }; N],
88            head: 0,
89            len: 0,
90        }
91    }
92
93    /// Append a value, overwriting the oldest entry once the ring is full.
94    ///
95    /// This never fails and never blocks; if losing the oldest entry is not
96    /// acceptable, use [`crate::EventBuf`], whose `push` reports when full.
97    pub fn push(&mut self, val: T) {
98        self.buf[self.head] = MaybeUninit::new(val);
99        self.head = (self.head + 1) % N;
100        if self.len < N {
101            self.len += 1;
102        }
103    }
104
105    /// Number of elements currently stored, always in `0..=N`.
106    pub fn len(&self) -> usize {
107        self.len
108    }
109
110    /// Returns `true` if the ring holds no elements.
111    pub fn is_empty(&self) -> bool {
112        self.len == 0
113    }
114
115    /// Returns `true` if the ring is at capacity, so the next
116    /// [`push`](Self::push) will overwrite the oldest entry.
117    pub fn is_full(&self) -> bool {
118        self.len == N
119    }
120
121    /// Number of elements the ring can hold.
122    #[inline]
123    pub const fn capacity(&self) -> usize {
124        N
125    }
126
127    /// Drop every element, resetting the ring to empty.
128    ///
129    /// The backing array is left as-is; only the cursors are reset, so this is
130    /// O(1) and does not touch the stored values.
131    pub fn clear(&mut self) {
132        self.head = 0;
133        self.len = 0;
134    }
135
136    /// Index of the `i`-th live element, 0 = oldest.
137    ///
138    /// Callers must ensure `i < self.len`; the result is only a valid slot
139    /// under that precondition.
140    ///
141    /// Steps back from `head` rather than computing `(head + N - len + i) % N`.
142    /// That form is easier to read but forms an intermediate up to `3N`, which
143    /// overflows for a large `N` -- and a large `N` is reachable, because a
144    /// zero-sized `T` makes `RingBuf<(), { usize::MAX }>` constructible. An
145    /// accessor that panics in an overflow-checking build would break the
146    /// no-panic guarantee. Here nothing exceeds `N`.
147    #[inline(always)]
148    const fn index(&self, i: usize) -> usize {
149        let back = self.len - i; // 1..=len, so no underflow given i < len
150        if self.head >= back {
151            self.head - back
152        } else {
153            N - (back - self.head)
154        }
155    }
156
157    /// Read the `i`-th element (0 = oldest).
158    pub fn get(&self, i: usize) -> Option<T> {
159        if i >= self.len {
160            return None;
161        }
162        let idx = self.index(i);
163        // SAFETY: `i < len`, so `index` addresses one of the `len` slots
164        // written by `push`.
165        Some(unsafe { self.buf[idx].assume_init() })
166    }
167
168    /// Most recently pushed element.
169    pub fn latest(&self) -> Option<T> {
170        if self.len == 0 {
171            return None;
172        }
173        // Routed through `index` like every other read. The open-coded
174        // `head - 1` it replaces was correct, but it was a second, independent
175        // slot calculation -- so a future change to the cursor representation
176        // could fix `get` and silently invalidate this one. One indexing path
177        // means one unsafe proof.
178        let idx = self.index(self.len - 1);
179        // SAFETY: `len > 0`, so `index(len - 1)` is the newest live slot,
180        // written by `push`.
181        Some(unsafe { self.buf[idx].assume_init() })
182    }
183
184    /// Iterate over elements oldest→newest.
185    pub fn iter(&self) -> RingIter<'_, T, N> {
186        RingIter { ring: self, pos: 0 }
187    }
188}
189
190impl<T: Copy, const N: usize> Default for RingBuf<T, N> {
191    fn default() -> Self {
192        Self::new()
193    }
194}
195
196impl<T: Copy, const N: usize> core::fmt::Debug for RingBuf<T, N> {
197    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
198        f.debug_struct("RingBuf")
199            .field("len", &self.len)
200            .field("capacity", &N)
201            .finish()
202    }
203}
204
205/// Iterator over [`RingBuf`] elements from oldest to newest.
206pub struct RingIter<'a, T: Copy, const N: usize> {
207    ring: &'a RingBuf<T, N>,
208    pos: usize,
209}
210
211impl<'a, T: Copy, const N: usize> Iterator for RingIter<'a, T, N> {
212    type Item = T;
213
214    fn next(&mut self) -> Option<T> {
215        let val = self.ring.get(self.pos)?;
216        self.pos += 1;
217        Some(val)
218    }
219
220    fn size_hint(&self) -> (usize, Option<usize>) {
221        let remaining = self.ring.len().saturating_sub(self.pos);
222        (remaining, Some(remaining))
223    }
224}
225
226impl<T: Copy, const N: usize> ExactSizeIterator for RingIter<'_, T, N> {}
227
228impl<T: Copy, const N: usize> core::fmt::Debug for RingIter<'_, T, N> {
229    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
230        f.debug_struct("RingIter")
231            .field("remaining", &(self.ring.len().saturating_sub(self.pos)))
232            .finish()
233    }
234}
235
236impl<'a, T: Copy, const N: usize> IntoIterator for &'a RingBuf<T, N> {
237    type Item = T;
238    type IntoIter = RingIter<'a, T, N>;
239
240    fn into_iter(self) -> RingIter<'a, T, N> {
241        self.iter()
242    }
243}
244
245impl<T: Copy, const N: usize> crate::traits::Sink<T> for RingBuf<T, N> {
246    type Error = core::convert::Infallible;
247
248    #[inline]
249    fn try_push(&mut self, val: T) -> Result<(), core::convert::Infallible> {
250        self.push(val);
251        Ok(())
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    /// Deliberately does not derive `Default`. If the bound ever comes back,
260    /// this stops compiling -- which is the point: the whole value of storing
261    /// slots as `MaybeUninit` is that `T` no longer has to have a meaningless
262    /// "zero" value invented for it.
263    #[derive(Copy, Clone, Debug, PartialEq, Eq)]
264    struct NoDefault(u32);
265
266    #[test]
267    fn new_ring_is_empty() {
268        let r = RingBuf::<u32, 4>::new();
269        assert!(r.is_empty());
270        assert!(!r.is_full());
271        assert_eq!(r.len(), 0);
272        assert_eq!(r.latest(), None);
273        assert_eq!(r.get(0), None);
274    }
275
276    #[test]
277    fn push_and_get() {
278        let mut r = RingBuf::<u32, 4>::new();
279        r.push(10);
280        r.push(20);
281        r.push(30);
282        assert_eq!(r.len(), 3);
283        assert_eq!(r.get(0), Some(10));
284        assert_eq!(r.get(1), Some(20));
285        assert_eq!(r.get(2), Some(30));
286        assert_eq!(r.get(3), None);
287        assert_eq!(r.latest(), Some(30));
288    }
289
290    #[test]
291    fn overwrite_oldest_when_full() {
292        let mut r = RingBuf::<u32, 3>::new();
293        r.push(1);
294        r.push(2);
295        r.push(3);
296        assert!(r.is_full());
297
298        r.push(4); // overwrites 1
299        assert_eq!(r.len(), 3);
300        assert_eq!(r.get(0), Some(2));
301        assert_eq!(r.get(1), Some(3));
302        assert_eq!(r.get(2), Some(4));
303        assert_eq!(r.latest(), Some(4));
304    }
305
306    #[test]
307    fn clear_resets_state() {
308        let mut r = RingBuf::<u32, 4>::new();
309        r.push(1);
310        r.push(2);
311        r.clear();
312        assert!(r.is_empty());
313        assert_eq!(r.len(), 0);
314        assert_eq!(r.latest(), None);
315    }
316
317    #[test]
318    fn iter_oldest_to_newest() {
319        let mut r = RingBuf::<u32, 4>::new();
320        for i in 1..=6 {
321            r.push(i);
322        }
323        // capacity 4, pushed 6 → oldest is 3
324        let v: std::vec::Vec<u32> = r.iter().collect();
325        assert_eq!(v, [3, 4, 5, 6]);
326    }
327
328    #[test]
329    fn iter_exact_size() {
330        let mut r = RingBuf::<u32, 4>::new();
331        r.push(1);
332        r.push(2);
333        let it = r.iter();
334        assert_eq!(it.len(), 2);
335    }
336
337    #[test]
338    fn default_is_new() {
339        let r: RingBuf<u8, 8> = RingBuf::default();
340        assert!(r.is_empty());
341    }
342
343    // `zero_capacity_panics` used to live here and could not survive the const
344    // assertion: `RingBuf::<u32, 0>::new()` no longer builds, so there is no
345    // runtime panic left to catch and no way to write the negative case as a
346    // `#[test]`. The rejection is now enforced by the compiler instead, which
347    // is stronger -- but it means nothing in this suite covers it, so the
348    // const assertion itself is the only thing keeping N > 0 true.
349
350    #[test]
351    fn const_new_works_in_const_context() {
352        // The const initialiser is the feature. A `static RingBuf` is itself
353        // near-useless because every mutator needs `&mut self` -- the real
354        // shape is this one nested inside an interior-mutability wrapper, and
355        // that is exactly what a non-const `new` makes impossible.
356        const EMPTY: RingBuf<u32, 4> = RingBuf::new();
357        static LOG: RingBuf<u32, 8> = RingBuf::new();
358
359        assert!(EMPTY.is_empty());
360        assert_eq!(EMPTY.capacity(), 4);
361        assert!(LOG.is_empty());
362        assert_eq!(LOG.capacity(), 8);
363
364        // Const-constructed and runtime-constructed rings behave identically.
365        let mut r = EMPTY;
366        r.push(1);
367        assert_eq!(r.get(0), Some(1));
368    }
369
370    /// A zero-sized `T` makes an enormous `N` genuinely constructible, since
371    /// the backing array is zero-sized too. The earlier `(head + N - len + i)`
372    /// index formed an intermediate up to `3N` and panicked here in an
373    /// overflow-checking build -- which unit tests are. Accessors must stay
374    /// panic-free.
375    #[test]
376    fn huge_capacity_does_not_overflow_the_index() {
377        let mut r = RingBuf::<(), { usize::MAX }>::new();
378        r.push(());
379        assert_eq!(r.len(), 1);
380        assert_eq!(r.get(0), Some(()));
381        assert_eq!(r.latest(), Some(()));
382        r.push(());
383        assert_eq!(r.get(1), Some(()));
384        assert_eq!(r.latest(), Some(()));
385    }
386
387    #[test]
388    fn works_without_default_bound() {
389        let mut r = RingBuf::<NoDefault, 2>::new();
390        r.push(NoDefault(1));
391        r.push(NoDefault(2));
392        assert_eq!(r.get(0), Some(NoDefault(1)));
393        assert_eq!(r.latest(), Some(NoDefault(2)));
394        r.push(NoDefault(3)); // overwrites
395        assert_eq!(r.get(0), Some(NoDefault(2)));
396        assert_eq!(r.latest(), Some(NoDefault(3)));
397    }
398
399    #[test]
400    fn capacity_returns_n() {
401        let r = RingBuf::<u32, 8>::new();
402        assert_eq!(r.capacity(), 8);
403    }
404
405    #[test]
406    fn into_iter_for_ref() {
407        let mut r = RingBuf::<u32, 4>::new();
408        r.push(1);
409        r.push(2);
410        r.push(3);
411        let v: std::vec::Vec<u32> = (&r).into_iter().collect();
412        assert_eq!(v, [1, 2, 3]);
413    }
414}