Repository: rust-itertools/itertools
Pull request #560 (2021-07-15 15:00:01)

Add `next_array` and `collect_array`
With this pull request I add two new functions to the `Itertools` trait:

```rust
fn next_array<T, const N: usize>(&mut self) -> Option<[T; N]>
where Self: Sized + Iterator<Item = T>;

fn collect_array<T, const N: usize>(mut self) -> Option<[T; N]>
where Self: Sized + Iterator<Item = T>;
```

These behave exactly like `next_tuple` and `collect_tuple`, however they return arrays instead. Since these functions require `min_const_generics`, I added a tiny build script that checks if Rust's version is 1.51 or higher, and if yes to set the `has_min_const_generics` config variable. This means that `Itertools` does not suddenly require 1.51 or higher, only these two functions do.  

In order to facilitate this I did have to bump the minimum required Rust version to 1.34 from the (documented) 1.32, since Rust 1.32 and 1.33 have trouble parsing the file even if stuff is conditionally compiled. However, this should not result in any (new) breakage, because `Itertools` actually already requires Rust 1.34 for 9+ months, since https://github.com/rust-itertools/itertools/commit/83c0f046c077a71185042e020a04262f388a5157 uses `saturating_pow` which wasn't stabilized until 1.34.

---

As for rationale, I think these functions are useful, especially for pattern matching and parsing. I don't think there's a high probability they get added to the standard library either, so that's why I directly make a pull request here. When/if `TryFromIterator` stabilizes we can simplify the implementation, but even then I believe these functions remain a good addition similarly how `collect_vec` is nice to have despite `.collect::<Vec<_>>` existing.

orlp: A possible enhancement might be to return `A` where `A: FromArray<Self::Item, N>` instead, and adding the `FromArray` trait.

```rust
impl<T, const N: usize> FromArray<T, N> for [T; N] {
    fn from_array(array: [T; N]) -> Self {
        array
    }
}

impl<T, const N: usize> FromArray<T, N> for Box<[T; N]> { /* .. */ }
impl<'a, T: Clone, const N: usize> FromArray<T, N> for Cow<'a, [T; N]> { /* .. */ }
impl<T, const N: usize> FromArray<T, N> for Rc<[T; N]> { /* .. */ }
impl<T, const N: usize> FromArray<T, N> for Arc<[T; N]> { /* .. */ }
impl<T, const N: usize> FromArray<Option<T>, N> for Option<[T; N]> { /* .. */ }
impl<T, E, const N: usize> FromArray<Result<T, E>, N> for Result<[T; N], E> { /* .. */ }
```

In fact, I think this is highly useful because it allows things like

```rust
let ints = line.split_whitespace().map(|n| n.parse());
if let Ok([x, y, z]) = ints.collect_array() {
    ...
}
```
orlp: So I have a working implementation of the above here: <https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=c009d90c3c1921d86428fa33e305559a>.

It makes this compile:

```rust
fn main() {
    let line = "32 -12 24";
    let nums = line.split_whitespace().map(|n| n.parse::<i32>());
    if let Some(Ok([x, y, z])) = nums.collect_array() {
        println!("x: {} y: {} z: {}", x, y, z);
    }
}
```

It would change the interface to:

```rust
trait ArrayCollectible<T>: Sized {
    fn array_from_iter<I: IntoIterator<Item = T>>(iterable: I) -> Option<Self>;
}

trait Itertools: Iterator {
    fn collect_array<A>(self) -> Option<A>
    where
        Self: Sized,
        A: ArrayCollectible<Self::Item>;
}
```

where
 - `ArrayCollectible<T>` is implemented for `[T; N]`, `Box<[T; N]>`, `Cow<'a, [T; N]>`, `Rc<[T; N]>`, `Arc<[T; N]>`;
 - `ArrayCollectible<Option<T>>` is implemented for `Option<[T; N]>`;
 - `ArrayCollectible<Result<T, E>>` is implemented for `Result<[T; N], E>`.
orlp: @phimuemue Any update on this? 
phimuemue: > @phimuemue Any update on this?

I appreciate your effort, but unfortunately nothing substantial from my side: I changed my mind regarding `version-check` (so we could use it as a `dev-dependency`), but I do not have enough time right now to review and merge PRs that ample.
orlp: @phimuemue Just for posterity's sake, `version-check` would be a `build-dependency`, not `dev-dependency`.
orlp: @phimuemue Just checking in what the status is, I feel very strongly about the usefulness of `collect_array`. I miss it very often in `itertools`.
scottmcm: Note that if you want `collect_array`, you can use https://lib.rs/crates/arrayvec, as the usual way to collect into an array.

I'll also mention [`Iterator::next_chunk`](https://doc.rust-lang.org/nightly/std/iter/trait.Iterator.html#method.next_chunk) (https://github.com/rust-lang/rust/issues/98326) as a nightly API that'll be `next_array`.
Expurple: This is a very useful feature. Today there was a [thread](https://www.reddit.com/r/rust/comments/1bovowu/collecting_arrays/) on Reddit where the author basically asks if there's a crate that provides `collect_array()`. IMO, `itertools` should be the crate to do it
Philippe-Cholet: @Expurple
I sure would like to do use const generics and `collect_array` is one of them.
Our MSRV is quite old (1.43.1 currently) while min-const-generics is 1.51 but I do not think it's the main blocker.
The fact is that there is not much available in recent stable Rust yet which is sad. `Iterator::next_chunk` and `core::array::try_from_fn` would be nice to have.
Plus, we currently don't really use `unsafe` ourselves (only in `EitherOrBoth::insert*` with obvious unfaillable patterns). I guess we prefer that the std does the heavy work.
phimuemue: I sometimes think about adding `arrayvec` as a dependency - and falling back to `std` as soon it's possible. I think it might also solve some other issues (e.g. [`ExactlyOneError` having a manual two-element-arrayvec](https://docs.rs/itertools/latest/src/itertools/exactly_one_err.rs.html#20-26)). Would require Rust 1.51.

Another option I just saw: Crates can offer "nightly-only experimental API" (see https://docs.rs/arrayvec/latest/arrayvec/struct.ArrayVec.html#method.first_chunk for an example) - maybe this would help _some_ users.

I personally would lean towards `arrayvec`. @jswrenn @Philippe-Cholet Opinions?
Philippe-Cholet: @phimuemue 

> Another option I just saw: Crates can offer "nightly-only experimental API" (see https://docs.rs/arrayvec/latest/arrayvec/struct.ArrayVec.html#method.first_chunk for an example) - maybe this would help _some_ users.

`ArrayVec<T, CAP>` implements `Deref<Target = [T]>` so (nightly-available) slice methods are directly accessible, that seems to be it.

> I sometimes think about adding `arrayvec` as a dependency - and falling back to `std` as soon it's possible. I think it might also solve some other issues (e.g. [`ExactlyOneError` having a manual two-element-arrayvec](https://docs.rs/itertools/latest/src/itertools/exactly_one_err.rs.html#20-26)). Would require Rust 1.51.

I'm definitely not opposed to the idea but the `ExactlyOneError` use case is quite small.
I did not give thoughts before, do you have other examples in mind? (with private usage, in order to fall back to `std` ASAP).
orlp: @jswrenn Please don't forget that we are discussing this on a PR that already has a working implementation without adding dependencies...
jswrenn: @orlp, thanks, I had forgotten that this was a PR and not an issue when I made my reply. Still, we're talking about adding some *extremely* subtle unsafe code to Itertools. I'd like us to take extreme care to avoid accidentally introducing UB.

A PR adding `unsafe` to itertools should:
- rigorously document the safety and panicking conditions of every `unsafe` function it introduces
- prove that every invocation of an unsafe function (even invocations occurring within other unsafe functions) satisfies the safety precondition of that invocation, with citations to official Rust documentation
- rigorously document why any potentially panicking function within an unsafe function does not create invalid state that would cause UB upon panicking unwinds
- intensively test its API with miri

If you can update this PR to do those things, I can see a path forward to merging it. 
orlp: @jswrenn I will be busy the upcoming week but I'm willing to bring this up to standards after that. If before then you could decide on whether or not to bump the MSRV to 1.51 I could include that in the rewrite.
jswrenn: @orlp Think you might have time to revisit this soon? :-)
orlp: @jswrenn Yes, I do. Have you reached a decision yet on the MSRV issue?
Philippe-Cholet: I think it's time to set the MSRV to 1.51, I'm pretty sure @jswrenn and @phimuemue will agree.
jswrenn: Absolutely. I'd even be fine going up to 1.55, which is nearly three years old. In my other crates, I've found that to be the lowest MSRV that users actually need. 
Philippe-Cholet: After quickly going though [release notes](https://github.com/rust-lang/rust/releases?page=4), I may have missed something but I only noted two things 1.55 has over 1.51 that I considered to be of potential use for us:
- [`<[T; N]>::map`](https://doc.rust-lang.org/stable/std/primitive.array.html#method.map)
- [`MaybeUninit::write`](https://doc.rust-lang.org/stable/std/mem/union.MaybeUninit.html#method.write-1)?
phimuemue: Out of curiosity and slightly off-topic: What's a real reason to *not* update to stable Rust? Does it ever remove support for some platform or raise the system requirements dramatically? Or, put alternatively: Are there situations where someone *could* use cutting-edge `itertools` but *not* stable Rust?
jswrenn: > Are there situations where someone _could_ use cutting-edge `itertools` but _not_ stable Rust?

Yes: Libraries that depend on itertools, but set a MSRV lower than stable. They are, of course, welcome to use an older, MSRV-compatible version of itertools, but we currently don't backport bugfixes to older versions.

> What's a real reason to not update to stable Rust? Does it ever remove support for some platform or raise the system requirements dramatically?

Rust occasionally does remove support for platforms; e.g.: https://blog.rust-lang.org/2022/08/01/Increasing-glibc-kernel-requirements.html

(The above post suggests that, conservatively, we could increase our MSRV to 1.63 without causing major problem for users. Maybe that's a good target MSRV for now?)
Philippe-Cholet: After skimming all release notes up to 1.78.0, I noted this:
- 1.57.0: `Iterator::map_while`, `[T; N]::as_[mut_]slice`
- 1.60.0: `MaybeUninit::assume_init_[drop|read]`
- 1.62.0: `bool::then_some`
- 1.63.0: `array::from_fn`
- 1.65.0: `let else`
- 1.71.0: `From<[T; N]>` <--> `(T, ...)` (N in 1..=12)

I'm not particularly waiting anything in there (maybe `array::from_fn`). I'd prefer some nightly stuff like the `Try` trait and `array::try_from_fn`.

Unless we need [`<[T; N]>::map`](https://doc.rust-lang.org/stable/std/primitive.array.html#method.map) (1.55.0), I'm fine with 1.51 for quite some time.
Is there something currently stable that you are waiting for? Maybe something I missed?
orlp: @Philippe-Cholet I feel [`<[T; N]>::map`](https://doc.rust-lang.org/stable/std/primitive.array.html#method.map) is rather fundamental and a strong reason to prefer 1.55 over 1.51. Similarly with `array::from_fn` for 1.63. The other stuff does not feel very relevant for this crate.
orlp: I have force-pushed with a new cleaner implementation, based on 1.55 for `[T; N]::map`.
orlp: @jswrenn I actually wanted to suggest bumping the MSRV to whatever version you decide on in a separate PR we'd merge first rather than tacking it onto this one.
Philippe-Cholet: **Breaking news:** the MSRV has been bumped to 1.63.0! 🎉

@scottmcm If I were dreaming of itertools at night, I would have nightmares not having `Iterator::next_chunk` available to us (stable and within our MSRV).
arrayvec/tinyvec was rejected (probable comments above).
This unsafe work sure is a _temporary fix_ for all that. We will rely on the stdlib in the end.
jswrenn: @Philippe-Cholet, @phimuemue and @scottmcm, could you give this PR a final review? I've:
- revised the safety proofs
- modified the `Drop` impl avoid raw pointers and to have smaller `unsafe` blocks
- modified `push` to avoid unspecified behavior between builds with checked and wraparound arithmetic

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/rust-itertools/itertools/pull/560?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
Attention: Patch coverage is `97.65625%` with `3 lines` in your changes missing coverage. Please review.
> Project coverage is 94.67%. Comparing base [(`6814180`)](https://app.codecov.io/gh/rust-itertools/itertools/commit/68141809aabec2816d3250820c8d6608f3b2293f?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) to head [(`461be14`)](https://app.codecov.io/gh/rust-itertools/itertools/commit/461be14fb94929aa72957814e2603e63dd777cb0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).
> Report is 107 commits behind head on master.

| [Files](https://app.codecov.io/gh/rust-itertools/itertools/pull/560?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Patch % | Lines |
|---|---|---|
| [src/next\_array.rs](https://app.codecov.io/gh/rust-itertools/itertools/pull/560?src=pr&el=tree&filepath=src%2Fnext_array.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL25leHRfYXJyYXkucnM=) | 98.05% | [2 Missing :warning: ](https://app.codecov.io/gh/rust-itertools/itertools/pull/560?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) |
| [src/lib.rs](https://app.codecov.io/gh/rust-itertools/itertools/pull/560?src=pr&el=tree&filepath=src%2Flib.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2xpYi5ycw==) | 96.00% | [1 Missing :warning: ](https://app.codecov.io/gh/rust-itertools/itertools/pull/560?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #560      +/-   ##
==========================================
+ Coverage   94.38%   94.67%   +0.28%     
==========================================
  Files          48       50       +2     
  Lines        6665     7188     +523     
==========================================
+ Hits         6291     6805     +514     
- Misses        374      383       +9     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/rust-itertools/itertools/pull/560?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).

orlp: @jswrenn In your refactor you've introduced two bugs, one of which is undefined behavior:

1. The new code only frees the first element and leaks the rest on drop.
2. The new code calls `drop` on an uninitialized value if the `ArrayBuilder` is empty.

Please read my original version carefully, I intentionally used [`ptr::slice_from_raw_parts_mut`](https://github.com/rust-itertools/itertools/blob/70842fc3d2173a35c03459d6077927838daf6f13/src/next_array.rs#L65) to transform the pointer-to-first-element (`*mut T`) returned by [`[T]::as_mut_ptr`](https://doc.rust-lang.org/std/primitive.slice.html#method.as_mut_ptr) into a pointer-to-slice (`*mut [T]`) to then drop the slice in-place.  That's why the variables were called `ptr_to_first` and `ptr_to_slice`.

Your new code only makes a pointer to the first element, and forcibly only drops that element, regardless of whether it is initialized or not.

I can't help but point out that adding 60 lines of comments and various extra steps to what used to be a correct 3 LOC + 4 lines of comments implementation did not help with preventing this bug for the writer, and for me personally as a reader it made the issue harder to spot.

jswrenn: I appreciate the catch! Given that this passed CI, it looks like you've also uncovered a gap in our test coverage.

Orson, you will not have me in agreement on the commenting issue. I wouldn't have to read your original version so carefully to guess at your intentions if had you had left comments making them explicit. The comments are for the benefit of maintainers, who need to make sure we don't regress on any of these subtleties in perpetuity.

And the comments *did* result in a potential soundness issue being discovered by a reviewer — the original code leaves a live reference to an invalid (dropped) referent.

orlp: > And the comments did result in a potential soundness issue being discovered by a reviewer — the original code leaves a live reference to an invalid (dropped) referent. In fact, the regression you just discovered [was introduced](https://github.com/rust-itertools/itertools/compare/4af66ad8f0af45e517bb98a230961f695ad019b7..9c33e3dccbdcb105856380a9b362c60cb323f47a) as part of fixing **this potential soundness issue that was present in your original PR**.

That was never part of my original PR, but also due to your refactor. My original code never left a live reference. I implore you to look at [the original code](https://github.com/rust-itertools/itertools/blob/70842fc3d2173a35c03459d6077927838daf6f13/src/next_array.rs#L65) again.

Even if we take that at face value, your purported solution does nothing to resolve it. The following code compiles:

```rust
let mut x = 42;
let mut r = &mut x;
{
    // <pointer stuff>
    let _ = r; // This does not drop r! This is a no-op!
}
*r = 42; // r is still live!
```

orlp: @jswrenn And it gets worse.

Even if you had used `ptr::slice_from_raw_parts_mut` to turn `valid: &mut [T]` into `*mut [T]` and even if you had indeed used `drop(valid)` to drop `valid`... it **still** would be undefined behavior by Miri's stacked borrows model. The slice from which a pointer is derived must stay alive for that pointer to remain valid under stacked borrows. That is, this is UB under stacked borrows:

```rust
let mut arr = [1, 2, 3];
let r = &mut arr[..];
let p = r.as_mut_ptr();
drop(r);
unsafe { dbg!(&*p) };
```


|  `drop`  as intended | pointer-to-slice as intended | Outcome |
| ------------- | ------------- | - |
| No  | No  | Undefined behavior, uninitialized element dropped |
| No  | Yes  | Code accidentally correct, comments incorrect and perceived potential unsoundness from live `&mut [T]` unadressed |
| Yes  | No  | Undefined behavior, invalidated pointer accessed and uninitialized element dropped |
| Yes  | Yes  | Undefined behavior, invalidated pointer accessed |

I kindly ask you to reflect on why the refactor, despite what I assume is your best intent and care with respect to the safety comments, did not catch either of the above issues nor that their intended combination still would be invalid. 
jswrenn: > I kindly ask you to reflect on why the refactor, despite what I assume is your best intent and care with respect to the safety comments, did not catch either of the above issues nor that their intended combination still would be invalid.

Whatever the reason, I'm sure it's not because I wrote too many comments.

I appreciate your knowledgeability, but, again, this abstraction has to survive idiots like me and whoever else contributes to itertools in the future. Its soundness cannot depend on the brilliance of a lone genius. I'm delighted to take your suggestions, but I *am* going to make the reasoning behind those suggestions explicit with new comments.


jswrenn: Stripping most of the comments out for your benefit, could you review this implementation (essentially undoing [this](https://github.com/rust-itertools/itertools/compare/4af66ad8f0af45e517bb98a230961f695ad019b7..9c33e3dccbdcb105856380a9b362c60cb323f47a)):

```rust
fn drop(&mut self) {
    // Select the valid elements of `self.arr`.
    let (valid, _) = self.arr.split_at_mut(self.len);

    // Cast `valid` from `&[MaybeUninit<T>]` to `&[T]`
    let (_, valid, _): (_, &mut [T], _) = unsafe { valid.align_to_mut::<T>() };

    // Drop `valid`.
    unsafe {
        ptr::drop_in_place(valid);
    }
}
```
orlp: @jswrenn While I believe that is sound, I find using `align_to_mut` to assume the slice is initialized to be very strange, and it's certainly an intent-implementation semantic mismatch.

As an alternative, I would propose that we copy [`MaybeUninit::slice_assume_init_mut`](https://doc.rust-lang.org/std/mem/union.MaybeUninit.html#method.slice_assume_init_mut) from the standard library with a simple safety comment deferring to the standard library:

```rust
unsafe fn maybeuninit_slice_assume_init_mut<T>(slice: &mut [MaybeUninit<T>]) -> &mut [T] {
    // SAFETY: see the standard library implementation of MaybeUninit::slice_assume_init_mut,
    // this implementation is copied from there as it is not available at our MSRV.
    unsafe { &mut *(slice as *mut [MaybeUninit<T>] as *mut [T]) }
}
```

Furthermore I also don't understand why you use `split_at_mut` and then promptly ignore one half. May I suggest instead the following, also making things more re-usable / composable:

```rust
impl<T, const N: usize> AsMut<[T]> for ArrayBuilder<T, N> {
    fn as_mut(&mut self) -> &mut [T] {
        // SAFETY: self.arr[..self.len] is valid.
        unsafe { maybeuninit_slice_assume_init_mut(&mut self.arr[..self.len]) }
    }
}
```

Then our `drop` is painfully simple:

```rust
impl<T, const N: usize> Drop for ArrayBuilder<T, N> {
    fn drop(&mut self) {
        unsafe { core::ptr::drop_in_place(self.as_mut()) }
    }
}
```
jswrenn: I quite like that formulation! 
Philippe-Cholet: @jswrenn About CI, I just noticed 😮 that the `needs` list in `all-jobs-succeed` does not include `miri`. Let's add it here.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1139248..6b08f68 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@
 ### Added
 - Added `array_combinations` (#991)
 - Added `k_smallest_relaxed` and variants (#925)
+- Added `next_array` and `collect_array` (#560)
 - Implemented `DoubleEndedIterator` for `FilterOk` (#948)
 - Implemented `DoubleEndedIterator` for `FilterMapOk` (#950)
 
diff --git a/src/lib.rs b/src/lib.rs
index 37c3b1f..20226d8 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -209,6 +209,7 @@ mod merge_join;
 mod minmax;
 #[cfg(feature = "use_alloc")]
 mod multipeek_impl;
+mod next_array;
 mod pad_tail;
 #[cfg(feature = "use_alloc")]
 mod peek_nth;
@@ -1968,6 +1969,50 @@ pub trait Itertools: Iterator {
     }
 
     // non-adaptor methods
+    /// Advances the iterator and returns the next items grouped in an array of
+    /// a specific size.
+    ///
+    /// If there are enough elements to be grouped in an array, then the array
+    /// is returned inside `Some`, otherwise `None` is returned.
+    ///
+    /// ```
+    /// use itertools::Itertools;
+    ///
+    /// let mut iter = 1..5;
+    ///
+    /// assert_eq!(Some([1, 2]), iter.next_array());
+    /// ```
+    fn next_array<const N: usize>(&mut self) -> Option<[Self::Item; N]>
+    where
+        Self: Sized,
+    {
+        next_array::next_array(self)
+    }
+
+    /// Collects all items from the iterator into an array of a specific size.
+    ///
+    /// If the number of elements inside the iterator is **exactly** equal to
+    /// the array size, then the array is returned inside `Some`, otherwise
+    /// `None` is returned.
+    ///
+    /// ```
+    /// use itertools::Itertools;
+    ///
+    /// let iter = 1..3;
+    ///
+    /// if let Some([x, y]) = iter.collect_array() {
+    ///     assert_eq!([x, y], [1, 2])
+    /// } else {
+    ///     panic!("Expected two elements")
+    /// }
+    /// ```
+    fn collect_array<const N: usize>(mut self) -> Option<[Self::Item; N]>
+    where
+        Self: Sized,
+    {
+        self.next_array().filter(|_| self.next().is_none())
+    }
+
     /// Advances the iterator and returns the next items grouped in a tuple of
     /// a specific size (up to 12).
     ///
diff --git a/src/next_array.rs b/src/next_array.rs
new file mode 100644
index 0000000..86480b1
--- /dev/null
+++ b/src/next_array.rs
@@ -0,0 +1,269 @@
+use core::mem::{self, MaybeUninit};
+
+/// An array of at most `N` elements.
+struct ArrayBuilder<T, const N: usize> {
+    /// The (possibly uninitialized) elements of the `ArrayBuilder`.
+    ///
+    /// # Safety
+    ///
+    /// The elements of `arr[..len]` are valid `T`s.
+    arr: [MaybeUninit<T>; N],
+
+    /// The number of leading elements of `arr` that are valid `T`s, len <= N.
+    len: usize,
+}
+
+impl<T, const N: usize> ArrayBuilder<T, N> {
+    /// Initializes a new, empty `ArrayBuilder`.
+    pub fn new() -> Self {
+        // SAFETY: The safety invariant of `arr` trivially holds for `len = 0`.
+        Self {
+            arr: [(); N].map(|_| MaybeUninit::uninit()),
+            len: 0,
+        }
+    }
+
+    /// Pushes `value` onto the end of the array.
+    ///
+    /// # Panics
+    ///
+    /// This panics if `self.len >= N`.
+    #[inline(always)]
+    pub fn push(&mut self, value: T) {
+        // PANICS: This will panic if `self.len >= N`.
+        let place = &mut self.arr[self.len];
+        // SAFETY: The safety invariant of `self.arr` applies to elements at
+        // indices `0..self.len` — not to the element at `self.len`. Writing to
+        // the element at index `self.len` therefore does not violate the safety
+        // invariant of `self.arr`. Even if this line panics, we have not
+        // created any intermediate invalid state.
+        *place = MaybeUninit::new(value);
+        // Lemma: `self.len < N`. By invariant, `self.len <= N`. Above, we index
+        // into `self.arr`, which has size `N`, at index `self.len`. If `self.len == N`
+        // at that point, that index would be out-of-bounds, and the index
+        // operation would panic. Thus, `self.len != N`, and since `self.len <= N`,
+        // that means that `self.len < N`.
+        //
+        // PANICS: Since `self.len < N`, and since `N <= usize::MAX`,
+        // `self.len + 1 <= usize::MAX`, and so `self.len += 1` will not
+        // overflow. Overflow is the only panic condition of `+=`.
+        //
+        // SAFETY:
+        // - We are required to uphold the invariant that `self.len <= N`.
+        //   Since, by the preceding lemma, `self.len < N` at this point in the
+        //   code, `self.len += 1` results in `self.len <= N`.
+        // - We are required to uphold the invariant that `self.arr[..self.len]`
+        //   are valid instances of `T`. Since this invariant already held when
+        //   this method was called, and since we only increment `self.len`
+        //   by 1 here, we only need to prove that the element at
+        //   `self.arr[self.len]` (using the value of `self.len` before incrementing)
+        //   is valid. Above, we construct `place` to point to `self.arr[self.len]`,
+        //   and then initialize `*place` to `MaybeUninit::new(value)`, which is
+        //   a valid `T` by construction.
+        self.len += 1;
+    }
+
+    /// Consumes the elements in the `ArrayBuilder` and returns them as an array
+    /// `[T; N]`.
+    ///
+    /// If `self.len() < N`, this returns `None`.
+    pub fn take(&mut self) -> Option<[T; N]> {
+        if self.len == N {
+            // SAFETY: Decreasing the value of `self.len` cannot violate the
+            // safety invariant on `self.arr`.
+            self.len = 0;
+
+            // SAFETY: Since `self.len` is 0, `self.arr` may safely contain
+            // uninitialized elements.
+            let arr = mem::replace(&mut self.arr, [(); N].map(|_| MaybeUninit::uninit()));
+
+            Some(arr.map(|v| {
+                // SAFETY: We know that all elements of `arr` are valid because
+                // we checked that `len == N`.
+                unsafe { v.assume_init() }
+            }))
+        } else {
+            None
+        }
+    }
+}
+
+impl<T, const N: usize> AsMut<[T]> for ArrayBuilder<T, N> {
+    fn as_mut(&mut self) -> &mut [T] {
+        let valid = &mut self.arr[..self.len];
+        // SAFETY: By invariant on `self.arr`, the elements of `self.arr` at
+        // indices `0..self.len` are in a valid state. Since `valid` references
+        // only these elements, the safety precondition of
+        // `slice_assume_init_mut` is satisfied.
+        unsafe { slice_assume_init_mut(valid) }
+    }
+}
+
+impl<T, const N: usize> Drop for ArrayBuilder<T, N> {
+    // We provide a non-trivial `Drop` impl, because the trivial impl would be a
+    // no-op; `MaybeUninit<T>` has no innate awareness of its own validity, and
+    // so it can only forget its contents. By leveraging the safety invariant of
+    // `self.arr`, we do know which elements of `self.arr` are valid, and can
+    // selectively run their destructors.
+    fn drop(&mut self) {
+        // SAFETY:
+        // - by invariant on `&mut [T]`, `self.as_mut()` is:
+        //   - valid for reads and writes
+        //   - properly aligned
+        //   - non-null
+        // - the dropped `T` are valid for dropping; they do not have any
+        //   additional library invariants that we've violated
+        // - no other pointers to `valid` exist (since we're in the context of
+        //   `drop`)
+        unsafe { core::ptr::drop_in_place(self.as_mut()) }
+    }
+}
+
+/// Assuming all the elements are initialized, get a mutable slice to them.
+///
+/// # Safety
+///
+/// The caller guarantees that the elements `T` referenced by `slice` are in a
+/// valid state.
+unsafe fn slice_assume_init_mut<T>(slice: &mut [MaybeUninit<T>]) -> &mut [T] {
+    // SAFETY: Casting `&mut [MaybeUninit<T>]` to `&mut [T]` is sound, because
+    // `MaybeUninit<T>` is guaranteed to have the same size, alignment and ABI
+    // as `T`, and because the caller has guaranteed that `slice` is in the
+    // valid state.
+    unsafe { &mut *(slice as *mut [MaybeUninit<T>] as *mut [T]) }
+}
+
+/// Equivalent to `it.next_array()`.
+pub(crate) fn next_array<I, const N: usize>(it: &mut I) -> Option<[I::Item; N]>
+where
+    I: Iterator,
+{
+    let mut builder = ArrayBuilder::new();
+    for _ in 0..N {
+        builder.push(it.next()?);
+    }
+    builder.take()
+}
+
+#[cfg(test)]
+mod test {
+    use super::ArrayBuilder;
+
+    #[test]
+    fn zero_len_take() {
+        let mut builder = ArrayBuilder::<(), 0>::new();
+        let taken = builder.take();
+        assert_eq!(taken, Some([(); 0]));
+    }
+
+    #[test]
+    #[should_panic]
+    fn zero_len_push() {
+        let mut builder = ArrayBuilder::<(), 0>::new();
+        builder.push(());
+    }
+
+    #[test]
+    fn push_4() {
+        let mut builder = ArrayBuilder::<(), 4>::new();
+        assert_eq!(builder.take(), None);
+
+        builder.push(());
+        assert_eq!(builder.take(), None);
+
+        builder.push(());
+        assert_eq!(builder.take(), None);
+
+        builder.push(());
+        assert_eq!(builder.take(), None);
+
+        builder.push(());
+        assert_eq!(builder.take(), Some([(); 4]));
+    }
+
+    #[test]
+    fn tracked_drop() {
+        use std::panic::{catch_unwind, AssertUnwindSafe};
+        use std::sync::atomic::{AtomicU16, Ordering};
+
+        static DROPPED: AtomicU16 = AtomicU16::new(0);
+
+        #[derive(Debug, PartialEq)]
+        struct TrackedDrop;
+
+        impl Drop for TrackedDrop {
+            fn drop(&mut self) {
+                DROPPED.fetch_add(1, Ordering::Relaxed);
+            }
+        }
+
+        {
+            let builder = ArrayBuilder::<TrackedDrop, 0>::new();
+            assert_eq!(DROPPED.load(Ordering::Relaxed), 0);
+            drop(builder);
+            assert_eq!(DROPPED.load(Ordering::Relaxed), 0);
+        }
+
+        {
+            let mut builder = ArrayBuilder::<TrackedDrop, 2>::new();
+            builder.push(TrackedDrop);
+            assert_eq!(builder.take(), None);
+            assert_eq!(DROPPED.load(Ordering::Relaxed), 0);
+            drop(builder);
+            assert_eq!(DROPPED.swap(0, Ordering::Relaxed), 1);
+        }
+
+        {
+            let mut builder = ArrayBuilder::<TrackedDrop, 2>::new();
+            builder.push(TrackedDrop);
+            builder.push(TrackedDrop);
+            assert!(matches!(builder.take(), Some(_)));
+            assert_eq!(DROPPED.swap(0, Ordering::Relaxed), 2);
+            drop(builder);
+            assert_eq!(DROPPED.load(Ordering::Relaxed), 0);
+        }
+
+        {
+            let mut builder = ArrayBuilder::<TrackedDrop, 2>::new();
+
+            builder.push(TrackedDrop);
+            builder.push(TrackedDrop);
+
+            assert!(catch_unwind(AssertUnwindSafe(|| {
+                builder.push(TrackedDrop);
+            }))
+            .is_err());
+
+            assert_eq!(DROPPED.load(Ordering::Relaxed), 1);
+
+            drop(builder);
+
+            assert_eq!(DROPPED.swap(0, Ordering::Relaxed), 3);
+        }
+
+        {
+            let mut builder = ArrayBuilder::<TrackedDrop, 2>::new();
+
+            builder.push(TrackedDrop);
+            builder.push(TrackedDrop);
+
+            assert!(catch_unwind(AssertUnwindSafe(|| {
+                builder.push(TrackedDrop);
+            }))
+            .is_err());
+
+            assert_eq!(DROPPED.load(Ordering::Relaxed), 1);
+
+            assert!(matches!(builder.take(), Some(_)));
+
+            assert_eq!(DROPPED.load(Ordering::Relaxed), 3);
+
+            builder.push(TrackedDrop);
+            builder.push(TrackedDrop);
+
+            assert!(matches!(builder.take(), Some(_)));
+
+            assert_eq!(DROPPED.swap(0, Ordering::Relaxed), 5);
+        }
+    }
+}


Test patch:
diff --git a/tests/test_core.rs b/tests/test_core.rs
index 32af246..4936160 100644
--- a/tests/test_core.rs
+++ b/tests/test_core.rs
@@ -372,3 +372,28 @@ fn product1() {
     assert_eq!(v[1..3].iter().cloned().product1::<i32>(), Some(2));
     assert_eq!(v[1..5].iter().cloned().product1::<i32>(), Some(24));
 }
+
+#[test]
+fn next_array() {
+    let v = [1, 2, 3, 4, 5];
+    let mut iter = v.iter();
+    assert_eq!(iter.next_array(), Some([]));
+    assert_eq!(iter.next_array().map(|[&x, &y]| [x, y]), Some([1, 2]));
+    assert_eq!(iter.next_array().map(|[&x, &y]| [x, y]), Some([3, 4]));
+    assert_eq!(iter.next_array::<2>(), None);
+}
+
+#[test]
+fn collect_array() {
+    let v = [1, 2];
+    let iter = v.iter().cloned();
+    assert_eq!(iter.collect_array(), Some([1, 2]));
+
+    let v = [1];
+    let iter = v.iter().cloned();
+    assert_eq!(iter.collect_array::<2>(), None);
+
+    let v = [1, 2, 3];
+    let iter = v.iter().cloned();
+    assert_eq!(iter.collect_array::<2>(), None);
+}