const_chunks/
lib.rs

1//! This crate provides an extension trait that lets you chunk iterators into constant-length arrays using `const` generics.
2//!
3//! See the [`IteratorConstChunks::const_chunks`] docs for more info.
4//!
5//! ```rust
6//! use const_chunks::IteratorConstChunks;
7//!
8//! let mut iter = vec![1, 2, 3, 4, 5].into_iter().const_chunks::<2>();
9//! assert_eq!(iter.next(), Some([1,2]));
10//! assert_eq!(iter.next(), Some([3,4]));
11//! assert_eq!(iter.next(), None);
12//!
13//! let mut remainder = iter.into_remainder().unwrap();
14//! assert_eq!(remainder.next(), Some(5));
15//! assert_eq!(remainder.next(), None);
16//! ```
17
18#![no_std]
19
20mod panic_guard;
21mod remainder;
22
23use core::{
24    mem::{forget, MaybeUninit},
25    ptr,
26};
27
28use panic_guard::PanicGuard;
29use remainder::Remainder;
30
31/// An iterator that iterates over constant-length chunks, where the length is known at compile time.
32///
33/// This struct is created by the [`IteratorConstChunks::const_chunks`] method. See its documentation for more.
34pub struct ConstChunks<const N: usize, I: Iterator> {
35    /// The inner iterator from which we take chunks.
36    inner: I,
37    /// The remainder that couldn't fill a chunk completely.
38    ///
39    /// This field is None if the underlying iterator hasn't been completely consumed
40    /// or if there are no remaining items.
41    remainder: Option<Remainder<N, I::Item>>,
42}
43
44impl<const N: usize, I: Iterator> ConstChunks<N, I> {
45    /// This asserts a const-time that `N` is non-zero.
46    /// This is useful to prevent accidental bugs, but
47    /// this also acts as a safety invariant.
48    const N_GT_ZERO: () = assert!(N > 0, "chunk size must be non-zero");
49
50    /// Consumes self and returns the remainder that could not fill a chunk completely.
51    ///
52    /// # Usage
53    ///
54    /// ```rust
55    /// use const_chunks::IteratorConstChunks;
56    ///
57    /// let mut v_iter = vec![1, 2, 3, 4, 5, 6].into_iter().const_chunks::<4>();
58    ///
59    /// // Collect chunks
60    /// let chunks = (&mut v_iter).collect::<Vec<_>>();
61    /// assert_eq!(chunks, vec![[1, 2, 3, 4]]);
62    ///
63    /// // Collect remainder
64    /// let remainder = v_iter.into_remainder().unwrap().collect::<Vec<_>>();
65    /// assert_eq!(remainder, vec![5, 6]);
66    /// ```
67    pub fn into_remainder(self) -> Option<Remainder<N, I::Item>> {
68        self.remainder
69    }
70}
71
72impl<const N: usize, I: Iterator> Iterator for ConstChunks<N, I> {
73    type Item = [I::Item; N];
74
75    fn next(&mut self) -> Option<Self::Item> {
76        // Early return if the underlying iterator is empty
77        let Some(first_item) = self.inner.next() else {
78            return None;
79        };
80
81        // Create array of unitialized values
82        //
83        // SAFETY: The `assume_init` is sound because `MaybeUninit`s do not require initialization.
84        let mut array: [MaybeUninit<I::Item>; N] = unsafe { MaybeUninit::uninit().assume_init() };
85        // Create panic guard
86        let mut guard = PanicGuard {
87            slice: &mut array,
88            initialized: 0,
89        };
90        // SAFETY: We enforce N > 0 at compile-time, so it's sound to assume at least one item.
91        unsafe { guard.init_next_unchecked(first_item) };
92
93        // Initialize remaining items
94        for i in 1..N {
95            let Some(item) = self.inner.next() else {
96                // Disarm panic guard. `ConstChunksRemainder` will
97                // handle the partially initialized array.
98                forget(guard);
99
100                // Set remainder
101                self.remainder = Some(Remainder {
102                    remainder_chunk: array,
103                    init_range: 0..i,
104                });
105
106                // No more chunks
107                return None;
108            };
109            // SAFETY: Will be called at most N times (including the initial
110            // `init_next_unchecked` call before the loop)
111            unsafe { guard.init_next_unchecked(item) };
112        }
113
114        // Disarm panic guard. At this point all the items are initialized
115        // and we're about to get rid of the `MaybeUninit`s.
116        forget(guard);
117
118        // Cast to an array of definitely initialized items
119        //
120        // SAFETY: If we've reached this point, all the items in the chunk have been initialized.
121        //
122        // TODO: use `array_assume_init` when stabilized.
123        let init_arr = unsafe { ptr::addr_of!(array).cast::<[I::Item; N]>().read() };
124
125        Some(init_arr)
126    }
127
128    fn size_hint(&self) -> (usize, Option<usize>) {
129        let (lower, upper) = self.inner.size_hint();
130        (lower / N, upper.map(|upper| upper / N))
131    }
132}
133
134impl<const N: usize, I: ExactSizeIterator> ExactSizeIterator for ConstChunks<N, I> {
135    fn len(&self) -> usize {
136        self.inner.len() / N
137    }
138}
139
140/// An extension trait providing [`Iterator`]s with the capability to iterate
141/// over const-sized arrays of items.
142pub trait IteratorConstChunks {
143    /// The type of iterator from which we take chunks.
144    type Inner: Iterator;
145
146    /// This function returns an iterator over constant-length chunks of items, where
147    /// the length is provided as a const-generic.
148    ///
149    /// # Usage
150    ///
151    /// ```rust
152    /// use const_chunks::IteratorConstChunks;
153    ///
154    /// let v = vec![1, 2, 3, 4, 5, 6];
155    /// let mut v_iter = v.into_iter().const_chunks::<2>();
156    /// assert_eq!(v_iter.next(), Some([1,2]));
157    /// assert_eq!(v_iter.next(), Some([3,4]));
158    /// assert_eq!(v_iter.next(), Some([5,6]));
159    /// ```
160    ///
161    /// When the number of items in the iterator cannot be divided exactly
162    /// into chunks, then the iterator will be fully consumed, but the last
163    /// chunk will not be yielded.
164    ///
165    /// ```rust
166    /// use const_chunks::IteratorConstChunks;
167    ///
168    /// // Five items cannot fit into chunks of length 2!
169    /// let v = [1, 2, 3, 4, 5];
170    ///
171    /// let mut v_iter = v.into_iter().const_chunks::<2>();
172    /// assert_eq!(v_iter.next(), Some([1, 2]));
173    /// assert_eq!(v_iter.next(), Some([3, 4]));
174    ///
175    /// // `None`, even though there was still one item
176    /// assert_eq!(v_iter.next(), None);
177    /// ```
178    ///
179    /// To get the remaining items, you can use the [`ConstChunks::into_remainder`] method (see for more).
180    ///
181    /// Note that trying to build chunks of size 0 will fail to compile:
182    ///
183    // TODO: Workaround until MIRI can catch const-eval panics as compilation errors (https://github.com/rust-lang/miri/issues/2423).
184    #[cfg_attr(miri, doc = "```should_panic")]
185    #[cfg_attr(not(miri), doc = "```compile_fail,E0080")]
186    /// use const_chunks::IteratorConstChunks;
187    ///
188    /// let _ = vec![1, 2].into_iter().const_chunks::<0>();
189    /// ```
190    ///
191    /// You should get an error similar to this one:
192    /// ```text
193    ///     |     const N_GT_ZERO: () = assert!(N > 0, "chunk size must be non-zero");
194    ///     |                           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the evaluated program panicked at 'chunk size must be non-zero'
195    /// ```
196    fn const_chunks<const N: usize>(self) -> ConstChunks<N, Self::Inner>;
197}
198
199/// Blanket implementation over all [`Iterator`]s.
200impl<I: Iterator> IteratorConstChunks for I {
201    type Inner = Self;
202
203    fn const_chunks<const N: usize>(self) -> ConstChunks<N, Self::Inner> {
204        // Assert N > 0 (see `ConstChunks::N_GT_ZERO`)
205        #[allow(clippy::let_unit_value)]
206        let _ = ConstChunks::<N, Self::Inner>::N_GT_ZERO;
207
208        ConstChunks {
209            inner: self,
210            remainder: None,
211        }
212    }
213}
214
215/// Drops all the initialized items in the underlying array.
216///
217/// # Safety
218///
219/// The slice must contain only initialized objects.
220unsafe fn drop_slice<T>(slice: &mut [MaybeUninit<T>]) {
221    for init in slice {
222        init.assume_init_drop();
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    extern crate std;
229    use std::{
230        panic::catch_unwind,
231        string::{String, ToString},
232        vec,
233        vec::Vec,
234    };
235
236    use crate::IteratorConstChunks;
237
238    #[test]
239    fn test_panic_leak() {
240        // Setup an iterator that can panic on `next`.
241        struct PanicIter<I: Iterator> {
242            inner: I,
243        }
244        impl<I: Iterator> Iterator for PanicIter<I> {
245            type Item = I::Item;
246
247            fn next(&mut self) -> Option<Self::Item> {
248                // Causes a panic when the inner iterator is exhausted
249                Some(self.inner.next().unwrap())
250            }
251        }
252        let panic_iter = PanicIter {
253            inner: [String::from("1")].into_iter(),
254        };
255
256        // Catch the panic to try to cause a leak
257        let _ = catch_unwind(|| panic_iter.const_chunks::<4>().collect::<Vec<_>>());
258    }
259
260    #[test]
261    fn test_exhausted() {
262        // Five items cannot fit into chunks of length 2!
263        let mut v_iter = (1..=5).map(|n| n.to_string()).const_chunks::<2>();
264        assert_eq!(v_iter.next(), Some([1, 2].map(|n| n.to_string())));
265        assert_eq!(v_iter.next(), Some([3, 4].map(|n| n.to_string())));
266
267        // Assert iterator is exhausted.
268        assert_eq!(v_iter.next(), None);
269    }
270
271    #[test]
272    fn test_remainder() {
273        let v = vec![1, 2, 3, 4, 5, 6];
274        let mut v_iter = v.into_iter().const_chunks::<4>();
275        let chunks = (&mut v_iter).collect::<Vec<_>>();
276        let remainder = v_iter.into_remainder().unwrap().collect::<Vec<_>>();
277        assert_eq!(chunks, vec![[1, 2, 3, 4]]);
278        assert_eq!(remainder, vec![5, 6]);
279    }
280
281    #[test]
282    fn test_remainder_leak() {
283        let mut v_iter = (1..=6).map(|n| n.to_string()).const_chunks::<4>();
284        // Exhaust iterator.
285        let _ = (&mut v_iter).collect::<Vec<_>>();
286        // Assert iterator is exhausted.
287        assert_eq!(v_iter.next(), None);
288
289        // Get remainder
290        let mut remainder = v_iter.into_remainder().unwrap();
291        // Fetch the next value out of the remainder
292        assert_eq!(remainder.next(), Some(5.to_string()));
293        drop(remainder);
294    }
295}