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}