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}