Fix an edge case with StepBy::nth on non-fused iterators - #160025
Conversation
|
r? @JohnTitor rustbot has assigned @JohnTitor. Use Why was this reviewer chosen?The reviewer was selected based on:
|
|
The later calls to |
Ah you're talking about when it overflows? You're right, I'll fix that when I'm back on my PC. |
|
@theemathas I guess the one thing that I have a question about with 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 My personal thought is the former because it wouldn't be advancing a non-fused iterator past the first |
|
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 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 |
|
I don't understand what the problem here. We should return I don't understand what your code snippet is supposed to demonstrate. Your description after the code talks about methods that always return |
|
Here's a test case that hopefully demonstrates that we should stop iterating after any // 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));
} |
My bad, I referenced that test case because it made me realize that overflow could affect how the original iterator custom #[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 However, if we decide to return I get that for non-fused iterator that we should return |
I personally consider this previous behavior to either be a bug, or be an internal implementation detail that's not stably guaranteed. |
|
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 |
|
That sounds good to me, I'll push the change to early return on |
6dca436 to
9a4b7ea
Compare
|
@rustbot reroll |
| @@ -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 { | |||
There was a problem hiding this comment.
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.
|
@rustbot author |
|
Reminder, once the PR becomes ready for a review, use |
…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.
9a4b7ea to
366ebd6
Compare
|
@rustbot ready |
|
Latest version looks okay to me. Thank you! @bors r+ rollup |
…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)
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.
Fixes #159965.
From the
nthdocumentation as pointed out by theemathas:Currently, there is an edge case for non-fused iterator where it's able to return
SomethroughStepBy::nthiterator even though the first item from the non-fused iterator returnsNone(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 aNonevalue or not. This wouldn't be a problem for fused iterators because all the values that it would return after reaching theNonepoint will also beNone. However since non-fused iterators do not have to abide by continuously yieldingNoneafter reaching aNone, it allows for a case, where after falling down fromfirst_takeblock it can returnSome(_)from aself.iter.nth()call in there.In the
first_takeblock we should definitely check if the first item we got fromself.iter.next()isNoneitem, and returnNoneif it is so.