engine/collections/
queue.rs

1// SPDX-FileCopyrightText: 2025 Jens Pitkänen <jens.pitkanen@helsinki.fi>
2//
3// SPDX-License-Identifier: GPL-3.0-or-later
4
5use core::mem::{transmute, MaybeUninit};
6
7use crate::allocators::LinearAllocator;
8
9/// Bounded FIFO queue of `T`.
10pub struct Queue<'a, T> {
11    /// Backing memory. Invariant: everything from index `init_offset`
12    /// (inclusive) to `(init_offset + init_len) % uninit_slice.len()`
13    /// (exclusive, possibly wrapping around the end of the slice) is
14    /// initialized, and the rest is uninitialized.
15    uninit_slice: &'a mut [MaybeUninit<T>],
16    initialized_offset: usize,
17    initialized_len: usize,
18}
19
20impl<T> Queue<'_, T> {
21    /// Allocates room for `capacity` of `T` and creates a [`Queue`] using it.
22    pub fn new<'a>(allocator: &'a LinearAllocator, capacity: usize) -> Option<Queue<'a, T>> {
23        let uninit_slice = allocator.try_alloc_uninit_slice(capacity, None)?;
24        Some(Queue {
25            initialized_offset: 0,
26            initialized_len: 0,
27            uninit_slice,
28        })
29    }
30
31    /// Creates a [`Queue`] using the given backing memory.
32    pub fn from_mut(buffer: &mut [MaybeUninit<T>]) -> Option<Queue<T>> {
33        Some(Queue {
34            initialized_offset: 0,
35            initialized_len: 0,
36            uninit_slice: buffer,
37        })
38    }
39
40    /// Pushes `value` to the back of the queue, returning it back if there's no
41    /// room.
42    pub fn push_back(&mut self, value: T) -> Result<(), T> {
43        if self.initialized_len >= self.uninit_slice.len() {
44            return Err(value);
45        }
46
47        // Since `init_len < self.uninit_slice.len()`, this will only "wrap
48        // once" and won't reach the indices at the start of the initialized
49        // indices.
50        let i = (self.initialized_offset + self.initialized_len) % self.uninit_slice.len();
51
52        // The value at `i` is uninitialized due to the invariant stated in the
53        // doc comment of `self.uninit_slice`, so overwriting it does not leak
54        // (in the drop sense) any value.
55        self.uninit_slice[i].write(value);
56
57        // Value at `i` is now initialized, bump up the length to maintain the
58        // `self.uninit_slice` invariant.
59        self.initialized_len += 1;
60
61        Ok(())
62    }
63
64    /// Removes and returns the value at the front of the queue, or None if the
65    /// queue is empty.
66    pub fn pop_front(&mut self) -> Option<T> {
67        if self.initialized_len == 0 {
68            return None;
69        }
70
71        // Safety: due to the invariant these functions maintain, explained in
72        // the documentation of `self.uninit_slice`, we know that the value at
73        // `self.init_offset` is initialized. Duplicates caused by
74        // `MaybeUninit::assume_init_read` are avoided by incrementing
75        // `self.init_offset` after this.
76        let value = unsafe { self.uninit_slice[self.initialized_offset].assume_init_read() };
77
78        // Now that we have an owned version of the value at `self.init_offset`,
79        // pop out the first index of the init slice.
80        self.initialized_offset = (self.initialized_offset + 1) % self.uninit_slice.len();
81        self.initialized_len -= 1;
82
83        Some(value)
84    }
85
86    /// Returns a borrow of the value at the front of the queue without removing
87    /// it, or None if the queue is empty.
88    pub fn peek_front(&mut self) -> Option<&mut T> {
89        if self.initialized_len == 0 {
90            return None;
91        }
92        // Safety: due to the invariant these functions maintain, explained in
93        // the documentation of `self.uninit_slice`, we know that the value at
94        // `self.init_offset` is initialized.
95        Some(unsafe { self.uninit_slice[self.initialized_offset].assume_init_mut() })
96    }
97
98    /// The amount of elements that could be pushed before the array is full.
99    pub fn spare_capacity(&self) -> usize {
100        self.uninit_slice.len() - self.initialized_len
101    }
102
103    /// Returns `true` if there's no more capacity for additional elements.
104    pub fn is_full(&self) -> bool {
105        self.initialized_len == self.uninit_slice.len()
106    }
107
108    /// Returns `true` if there's no elements in the queue.
109    pub fn is_empty(&self) -> bool {
110        self.initialized_len == 0
111    }
112
113    /// Returns an iterator of the elements currently in the queue.
114    pub fn iter(&self) -> impl Iterator<Item = &T> {
115        let len = self.uninit_slice.len();
116
117        let head = &self.uninit_slice
118            [self.initialized_offset..(self.initialized_offset + self.initialized_len).min(len)];
119        // Safety: the above indices are included in the span of initialized
120        // elements of `self.uninit_slice`, and transmuting a fully initialized
121        // `&[MaybeUninit<T>]` to `&[T]` is safe.
122        let head = unsafe { transmute::<&[MaybeUninit<T>], &[T]>(head) };
123
124        let tail = &self.uninit_slice
125            [..(self.initialized_offset + self.initialized_len).saturating_sub(len)];
126        // Safety: the above indices are included in the span of initialized
127        // elements of `self.uninit_slice`, and transmuting a fully initialized
128        // `&[MaybeUninit<T>]` to `&[T]` is safe.
129        let tail = unsafe { transmute::<&[MaybeUninit<T>], &[T]>(tail) };
130
131        head.iter().chain(tail.iter())
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use crate::allocators::{static_allocator, LinearAllocator};
138
139    use super::Queue;
140
141    #[test]
142    fn pushes_and_pops_in_fifo_order() {
143        static ARENA: &LinearAllocator = static_allocator!(2);
144        let alloc = LinearAllocator::new(ARENA, 2).unwrap();
145        let mut queue = Queue::<u8>::new(&alloc, 2).unwrap();
146
147        assert!(queue.push_back(0).is_ok());
148        assert!(queue.push_back(1).is_ok());
149        assert!(
150            queue.push_back(2).is_err(),
151            "pushed a third element into a queue with capacity for two?",
152        );
153        assert_eq!(Some(0), queue.pop_front());
154        assert!(queue.push_back(2).is_ok());
155        assert_eq!(Some(1), queue.pop_front());
156        assert_eq!(Some(2), queue.pop_front());
157        assert_eq!(
158            None,
159            queue.pop_front(),
160            "popped a fourth element after only pushing three elements?",
161        );
162    }
163
164    #[test]
165    fn iter_works() {
166        static ARENA: &LinearAllocator = static_allocator!(3);
167        let alloc = LinearAllocator::new(ARENA, 3).unwrap();
168        let mut queue = Queue::<u8>::new(&alloc, 3).unwrap();
169        queue.push_back(0).unwrap();
170        queue.push_back(1).unwrap();
171        queue.push_back(2).unwrap();
172        queue.pop_front().unwrap();
173        queue.push_back(3).unwrap();
174
175        let mut iter = queue.iter();
176        assert_eq!(Some(&1), iter.next());
177        assert_eq!(Some(&2), iter.next());
178        assert_eq!(Some(&3), iter.next());
179        assert_eq!(None, iter.next());
180    }
181}