Skip to content

Fix an edge case with StepBy::nth on non-fused iterators - #160025

Merged
rust-bors[bot] merged 1 commit into
rust-lang:mainfrom
asder8215:step_by_nth_one
Aug 1, 2026
Merged

Fix an edge case with StepBy::nth on non-fused iterators#160025
rust-bors[bot] merged 1 commit into
rust-lang:mainfrom
asder8215:step_by_nth_one

Conversation

@asder8215

@asder8215 asder8215 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Fixes #159965.

From the nth documentation as pointed out by theemathas:

nth() will return None if n is greater than or equal to the length of the iterator.

Currently, there is an edge case for non-fused iterator where it's able to return Some through StepBy::nth iterator even though the first item from the non-fused iterator returns None (it is logically an empty iterator).

The issue came from how in the first take block it advances the underlying iterator forward using .next() and does not check if what it returns is a None value or not. This wouldn't be a problem for fused iterators because all the values that it would return after reaching the None point will also be None. However since non-fused iterators do not have to abide by continuously yielding None after reaching a None, it allows for a case, where after falling down from first_take block it can return Some(_) from a self.iter.nth() call in there.

In the first_take block we should definitely check if the first item we got from self.iter.next() is None item, and return None if it is so.

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Jul 27, 2026
@rustbot

rustbot commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

r? @JohnTitor

rustbot has assigned @JohnTitor.
They will have a look at your PR within the next two weeks and either review your PR or reassign to another reviewer.

Use r? to explicitly pick a reviewer

Why was this reviewer chosen?

The reviewer was selected based on:

  • Owners of files modified in this PR: libs
  • libs expanded to 12 candidates
  • Random selection from 7 candidates

@theemathas

Copy link
Copy Markdown
Contributor

The later calls to self.iter.nth can also return None.

@asder8215

asder8215 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

The later calls to self.iter.nth can also return None.

Ah you're talking about when it overflows? You're right, I'll fix that when I'm back on my PC.

@asder8215

asder8215 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

@theemathas I guess the one thing that I have a question about with self.iter.nth() is: should we advance the iterator forward still after observing that self.iter.nth() returned None?

The reason why I ask that is because this would affect both non-fused/fused iterators at least from understanding this test case that I'm seeing here:

#[test]
#[allow(non_local_definitions)]
fn test_iterator_step_by_nth_overflow() {
    #[cfg(target_pointer_width = "16")]
    type Bigger = u32;
    #[cfg(target_pointer_width = "32")]
    type Bigger = u64;
    #[cfg(target_pointer_width = "64")]
    type Bigger = u128;

    #[derive(Clone)]
    struct Test(Bigger);
    impl Iterator for &mut Test {
        type Item = i32;
        fn next(&mut self) -> Option<Self::Item> {
            Some(21)
        }
        fn nth(&mut self, n: usize) -> Option<Self::Item> {
            self.0 += n as Bigger + 1;
            Some(42)
        }
    }

    let mut it = Test(0);
    let root = usize::MAX >> (usize::BITS / 2);
    let n = root + 20;
    (&mut it).step_by(n).nth(n);
    assert_eq!(it.0, n as Bigger * n as Bigger);

    // large step
    let mut it = Test(0);
    (&mut it).step_by(usize::MAX).nth(5);
    assert_eq!(it.0, (usize::MAX as Bigger) * 5);

    // n + 1 overflows
    let mut it = Test(0);
    (&mut it).step_by(2).nth(usize::MAX);
    assert_eq!(it.0, (usize::MAX as Bigger) * 2);

    // n + 1 overflows
    let mut it = Test(0);
    (&mut it).step_by(1).nth(usize::MAX);
    assert_eq!(it.0, (usize::MAX as Bigger) * 1);
}

The thing that I'm thinking about is if we have a custom nth function for our fused iterator that, let's say returns None always, and we create a StepBy iterator through a mutable borrow of the iterator, should our StepBy iterator stop calling self.iter.nth() in the first time it sees a None (in which case we'd only observe assert_eq!(it.0, n)) or should we still run through self.iter.nth() after finishing our overflow check (in which case we'd observe assert_eq!(it.0, n * n)).

My personal thought is the former because it wouldn't be advancing a non-fused iterator past the first None point it observed. However, this decision does affect behavior on both fused/non-fused.

@asder8215

Copy link
Copy Markdown
Contributor Author

Out of caution, I'm going to mark this as libs-api nominated.

@rustbot label +I-libs-api-nominated

To clarify what the problem is, if you look into what StepBy::nth does, there's an overflow handling loop that occurs at the end:

default fn spec_nth(&mut self, mut n: usize) -> Option<I::Item> {
        ....
        // overflow handling
        loop {
            let mul = n.checked_mul(step);
            {
                if intrinsics::likely(mul.is_some()) {
                    return self.iter.nth(mul.unwrap() - 1);
                }
            }
            let div_n = usize::MAX / n;
            let div_step = usize::MAX / step;
            let nth_n = div_n * n;
            let nth_step = div_step * step;
            let nth = if nth_n > nth_step {
                step -= div_n;
                nth_n
            } else {
                n -= div_step;
                nth_step
            };
            self.iter.nth(nth - 1);
        }
}

This loop repeatedly calls on self.iter.nth(n - 1) until we reach a point where n * step doesn't overflow. The question here is how should we handle non-fused iterators in this scenario where they could return some Some(_) values, then return None, and then go back to returning Some(_) values (before we reach an overflow point/usize::MAX elements)? Should we stop the loop on the first sight of seeing a None value from self.iter.nth(nth - 1), or should we reserve returning None until after we fully resolve the overflow? If the former, this would mean that fused and non-fused iterators could have custom nth function be affected in the event that our StepBy iterator is constructed from a mutable borrow of the original iterator (see the test case I reference above). If the latter, we cause non-fused iterators to advance forward from the point that we receive a None from self.iter.nth(nth - 1) (which might not be the ideal behavior we intend for non-fused iterators).

@rustbot rustbot added the I-libs-api-nominated Nominated for discussion during a libs-api team meeting. label Jul 27, 2026
@theemathas

Copy link
Copy Markdown
Contributor

I don't understand what the problem here. We should return None as soon as self.iter.nth() returns None, right? I fail to see the issue with doing that.

I don't understand what your code snippet is supposed to demonstrate. Your description after the code talks about methods that always return None, but the iterator implementation in the code has methods that always returns Some.

@theemathas

Copy link
Copy Markdown
Contributor

Here's a test case that hopefully demonstrates that we should stop iterating after any self.iter.nth() call returns None:

// An iterator that returns Some(0), then None,
// then returns Some(1) indefinitely
enum Wonky {
    First,
    Second,
    After,
}
impl Iterator for Wonky {
    type Item = i32;
    fn next(&mut self) -> Option<i32> {
        match self {
            Wonky::First => {
                *self = Wonky::Second;
                Some(0)
            },
            Wonky::Second => {
                *self = Wonky::After;
                None
            },
            Wonky::After => {
                Some(1)
            }
        }
    }
}

fn main() {
    let mut iter = Wonky::First.step_by(1);
    let _ = iter.next();
    // Currently returns Some(1) in release mode, which is wrong.
    // It should return None.
    // (It takes too long to run in debug mode.)
    println!("{:?}", iter.nth(usize::MAX));
}

@asder8215

asder8215 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor Author

I don't understand what your #160025 (comment) is supposed to demonstrate. Your description after the code talks about methods that always return None, but the iterator implementation in the code has methods that always returns Some.

My bad, I referenced that test case because it made me realize that overflow could affect how the original iterator custom nth function is being called multiple times. To demonstrate the issue:

#[test]
#[allow(non_local_definitions)]
fn test_iterator_step_by_nth_overflow() {
    type Bigger = u128;

    #[derive(Clone)]
    struct Test(Bigger);
    impl Iterator for &mut Test {
        type Item = i32;
        fn next(&mut self) -> Option<Self::Item> {
            None
        }
        fn nth(&mut self, n: usize) -> Option<Self::Item> {
            self.0 += n as Bigger + 1;
            None
        }
    }

    let mut it = Test(0);
    let root = usize::MAX >> (usize::BITS / 2);
    let n = root + 20;
    (&mut it).step_by(n).nth(n);
    assert_eq!(it.0, n as Bigger * n as Bigger); // our iterator u128 value would hold n * n
}

If we go by what the current code does it doesn't handle returning None early on fused iterator on overflow (only until after we resolve the overflow n * step). That also means that the underlying iterator custom nth function is called n times, which from the test case above would have the Test struct wrap around the value n * n.

However, if we decide to return None early on the first instance of seeing it from self.iter.nth(), we'd observe that that this Test struct would wrap around the value n.

I get that for non-fused iterator that we should return None on the first sight that we see None from self.iter.nth(). That makes sense to me so that we don't advance iterator forward to a section where we observe Some() values. But does that mean we consider the behavior with fused iterators calling on the original iterator custom nth function n times a bug on overflow or should that be intended behavior?

@theemathas

Copy link
Copy Markdown
Contributor

But does that mean we consider the behavior with fused iterators calling on the original iterator custom nth function n times a bug on overflow or should that be intended behavior?

I personally consider this previous behavior to either be a bug, or be an internal implementation detail that's not stably guaranteed.

@the8472
the8472 self-requested a review July 28, 2026 16:27
@nia-e nia-e removed the I-libs-api-nominated Nominated for discussion during a libs-api team meeting. label Jul 28, 2026
@nia-e

nia-e commented Jul 28, 2026

Copy link
Copy Markdown
Member

Per today's meeting, we think this is not necessarily a libs-api thing and behaviour is up to reviewer discretion; we make no stable guarantees as to what StepBy means for non-fused iterators. Either way, thanks for checking ^^

@asder8215

Copy link
Copy Markdown
Contributor Author

That sounds good to me, I'll push the change to early return on None from self.iter.nth(). Will leave it to JohnTitor on if the behavior is okay.

@JohnTitor

Copy link
Copy Markdown
Member

@rustbot reroll

@rustbot rustbot assigned clarfonthey and unassigned JohnTitor Jul 30, 2026
@@ -255,7 +255,7 @@ unsafe impl<I: Iterator> StepByImpl<I> for StepBy<I> {
if self.first_take {
self.first_take = false;
let first = self.iter.next();
if n == 0 {
if first.is_none() || n == 0 {

@clarfonthey clarfonthey Jul 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps using the ? try syntax would be cleaner? This would be return Some(first) then, but in the other places this kind of branch is used it would be cleaner.

View changes since the review

@clarfonthey

Copy link
Copy Markdown
Contributor

@rustbot author

@rustbot rustbot added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Aug 1, 2026
@rustbot

rustbot commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Reminder, once the PR becomes ready for a review, use @rustbot ready.

…turn None always. Additionally, if any self.iter.nth() calls return a None, early return None as well. This fixes an edge case with non fused iterators to not advance the iterator beyond the first None item it observed in accordance with nth documentation saying that nth() will return None if n is greater than or equal to the length of the iterator.
@asder8215

Copy link
Copy Markdown
Contributor Author

@rustbot ready

@rustbot rustbot removed the S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. label Aug 1, 2026
@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Aug 1, 2026
@clarfonthey

Copy link
Copy Markdown
Contributor

Latest version looks okay to me. Thank you!

@bors r+ rollup

@rust-bors

rust-bors Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

📌 Commit 366ebd6 has been approved by clarfonthey

It is now in the queue for this repository.

@rust-bors rust-bors Bot added S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. and removed S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. labels Aug 1, 2026
rust-bors Bot pushed a commit that referenced this pull request Aug 1, 2026
…uwer

Rollup of 8 pull requests

Successful merges:

 - #160262 (Library lock file maintenance)
 - #158548 (Move `std::io::copy` to `alloc::io`)
 - #158814 (Produce an error when `#[inline]` and `#[rust_force_inline]` are used together)
 - #160025 (Fix an edge case with `StepBy::nth` on non-fused iterators)
 - #160271 (Resolver: Introduce `CmRef` which has a speclative borrow variant for `CmRefCell`)
 - #160281 (Fix(lib/fs/tests): Avoid permission denials when cleaning up TempDirs in `set_get_permissions_nofollows*`)
 - #160325 (tidy: Check `proc_macro_deps.rs` by reading it, not by including it)
 - #160334 (Add regression test for unused_allocation on boxed comparison)
@rust-bors
rust-bors Bot merged commit 2825c03 into rust-lang:main Aug 1, 2026
13 checks passed
@rustbot rustbot added this to the 1.99.0 milestone Aug 1, 2026
rust-timer added a commit that referenced this pull request Aug 1, 2026
Rollup merge of #160025 - asder8215:step_by_nth_one, r=clarfonthey

Fix an edge case with `StepBy::nth` on non-fused iterators

Fixes #159965.

From the `nth` documentation as pointed out by theemathas:

> `nth()` will return `None` if `n` is greater than or equal to the length of the iterator.

Currently, there is an edge case for non-fused iterator where it's able to return `Some` through `StepBy::nth` iterator even though the first item from the non-fused iterator returns `None` (it is logically an empty iterator).

The issue came from how in the first take block it advances the underlying iterator forward using `.next()` and does not check if what it returns is a `None` value or not. This wouldn't be a problem for fused iterators because all the values that it would return after reaching the `None` point will also be `None`. However since non-fused iterators do not have to abide by continuously yielding `None` after reaching a `None`, it allows for a case, where after falling down from `first_take` block it can return `Some(_)` from a `self.iter.nth()` call in there.

In the `first_take` block we should definitely check if the first item we got from `self.iter.next()` is `None` item, and return `None` if it is so.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-bors Status: Waiting on bors to run and complete tests. Bors will change the label on completion. T-libs Relevant to the library team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

StepBy::nth is buggy with non-fused iterators

6 participants