From c8d343e5c8e74f1033f5501acc5cb4ac57123b63 Mon Sep 17 00:00:00 2001 From: Mahdi Ali-Raihan Date: Sun, 1 Mar 2026 21:07:07 -0500 Subject: [PATCH] Added guarantee and non-guarantee comments + tests for intersperse/intersperse_with regarding fused/non-fused iterators --- library/core/src/iter/traits/iterator.rs | 30 ++- .../tests/iter/adapters/intersperse.rs | 200 ++++++++++++++++++ 2 files changed, 223 insertions(+), 7 deletions(-) diff --git a/library/core/src/iter/traits/iterator.rs b/library/core/src/iter/traits/iterator.rs index d919230d094d8..feb3bcee1ca4c 100644 --- a/library/core/src/iter/traits/iterator.rs +++ b/library/core/src/iter/traits/iterator.rs @@ -631,8 +631,15 @@ pub const trait Iterator { Zip::new(self, other.into_iter()) } - /// Creates a new iterator which places a copy of `separator` between adjacent - /// items of the original iterator. + /// Creates a new iterator which places a copy of `separator` between items + /// of the original iterator. + /// + /// Specifically on fused iterators, it is guaranteed that the new iterator + /// places a copy of `separator` between adjacent `Some(_)` items. However, + /// for non-fused iterators, [`intersperse`] will create a new iterator that + /// is a fused version of the original iterator and place a copy of `separator` + /// between adjacent `Some(_)` items. This behavior for non-fused iterators + /// is subject to change. /// /// In case `separator` does not implement [`Clone`] or needs to be /// computed every time, use [`intersperse_with`]. @@ -663,6 +670,7 @@ pub const trait Iterator { /// ``` /// /// [`Clone`]: crate::clone::Clone + /// [`intersperse`]: Iterator::intersperse /// [`intersperse_with`]: Iterator::intersperse_with #[inline] #[unstable(feature = "iter_intersperse", issue = "79524")] @@ -676,12 +684,19 @@ pub const trait Iterator { } /// Creates a new iterator which places an item generated by `separator` - /// between adjacent items of the original iterator. + /// between items of the original iterator. /// - /// The closure will be called exactly once each time an item is placed - /// between two adjacent items from the underlying iterator; specifically, - /// the closure is not called if the underlying iterator yields less than - /// two items and after the last item is yielded. + /// Specifically on fused iterators, it is guaranteed that the new iterator + /// places an item generated by `separator` between adjacent `Some(_)` items. + /// However, for non-fused iterators, [`intersperse_with`] will create a new + /// iterator that is a fused version of the original iterator and place an item + /// generated by `separator` between adjacent `Some(_)` items. This + /// behavior for non-fused iterators is subject to change. + /// + /// The `separator` closure will be called exactly once each time an item + /// is placed between two adjacent items from the underlying iterator; + /// specifically, the closure is not called if the underlying iterator yields + /// less than two items and after the last item is yielded. /// /// If the iterator's item implements [`Clone`], it may be easier to use /// [`intersperse`]. @@ -723,6 +738,7 @@ pub const trait Iterator { /// ``` /// [`Clone`]: crate::clone::Clone /// [`intersperse`]: Iterator::intersperse + /// [`intersperse_with`]: Iterator::intersperse_with #[inline] #[unstable(feature = "iter_intersperse", issue = "79524")] #[rustc_non_const_trait_method] diff --git a/library/coretests/tests/iter/adapters/intersperse.rs b/library/coretests/tests/iter/adapters/intersperse.rs index 72ae59b6b2f5f..df6be79308be7 100644 --- a/library/coretests/tests/iter/adapters/intersperse.rs +++ b/library/coretests/tests/iter/adapters/intersperse.rs @@ -152,3 +152,203 @@ fn test_try_fold_specialization_intersperse_err() { iter.try_for_each(|item| if item == "b" { None } else { Some(()) }); assert_eq!(iter.next(), None); } + +// FIXME(iter_intersperse): `intersperse` current behavior may change for +// non-fused iterators, so this test will likely have to +// be adjusted; see PR #152855 and issue #79524 +// if `intersperse` doesn't change, remove this FIXME. +#[test] +fn test_non_fused_iterator_intersperse() { + #[derive(Debug)] + struct TestCounter { + counter: usize, + } + + /// Given a counter of 0, this produces: + /// `None` -> `Some(2)` -> `None` -> `Some(4)` -> `None` -> `Some(6)` + /// -> and then `None` endlessly + impl Iterator for TestCounter { + type Item = usize; + fn next(&mut self) -> Option { + if self.counter > 6 { + None + } else if self.counter % 2 == 0 { + self.counter += 1; + None + } else { + self.counter += 1; + Some(self.counter) + } + } + } + + let counter = 0; + // places a 2 between `Some(_)` items + let non_fused_iter = TestCounter { counter }; + let mut intersperse_iter = non_fused_iter.intersperse(2); + // Since `intersperse` currently transforms the original + // iterator into a fused iterator, this intersperse_iter + // should always have `None` + for _ in 0..counter + 6 { + assert_eq!(intersperse_iter.next(), None); + } + + // Extra check to make sure it is `None` after processing 6 items + assert_eq!(intersperse_iter.next(), None); +} + +// FIXME(iter_intersperse): `intersperse` current behavior may change for +// non-fused iterators, so this test will likely have to +// be adjusted; see PR #152855 and issue #79524 +// if `intersperse` doesn't change, remove this FIXME. +#[test] +fn test_non_fused_iterator_intersperse_2() { + #[derive(Debug)] + struct TestCounter { + counter: usize, + } + + // Given a counter of 0, this produces: + // `Some(1)` -> `Some(2)` -> `None` -> `Some(4)` -> `Some(5)` -> + // and then `None` endlessly + impl Iterator for TestCounter { + type Item = usize; + fn next(&mut self) -> Option { + if self.counter < 5 { + self.counter += 1; + if self.counter % 3 != 0 { + return Some(self.counter); + } else { + return None; + } + } + self.counter += 1; + None + } + } + + let counter = 0; + // places a 2 between `Some(_)` items + let non_fused_iter = TestCounter { counter }; + let mut intersperse_iter = non_fused_iter.intersperse(2); + // Since `intersperse` currently transforms the original + // iterator into a fused iterator, this interspersed iter + // will be `Some(1)` -> `Some(2)` -> `Some(2)` -> and then + // `None` endlessly + let mut items_processed = 0; + for num in 0..counter + 6 { + if num < 3 { + if num % 2 != 0 { + assert_eq!(intersperse_iter.next(), Some(2)); + } else { + items_processed += 1; + assert_eq!(intersperse_iter.next(), Some(items_processed)); + } + } else { + assert_eq!(intersperse_iter.next(), None); + } + } + + // Extra check to make sure it is `None` after processing 6 items + assert_eq!(intersperse_iter.next(), None); +} + +// FIXME(iter_intersperse): `intersperse_with` current behavior may change for +// non-fused iterators, so this test will likely have to +// be adjusted; see PR #152855 and issue #79524 +// if `intersperse_with` doesn't change, remove this FIXME. +#[test] +fn test_non_fused_iterator_intersperse_with() { + #[derive(Debug)] + struct TestCounter { + counter: usize, + } + + // Given a counter of 0, this produces: + // `None` -> `Some(2)` -> `None` -> `Some(4)` -> `None` -> `Some(6)` + // -> and then `None` endlessly + impl Iterator for TestCounter { + type Item = usize; + fn next(&mut self) -> Option { + if self.counter > 6 { + None + } else if self.counter % 2 == 0 { + self.counter += 1; + None + } else { + self.counter += 1; + Some(self.counter) + } + } + } + + let counter = 0; + let non_fused_iter = TestCounter { counter }; + // places a 2 between `Some(_)` items + let mut intersperse_iter = non_fused_iter.intersperse_with(|| 2); + // Since `intersperse` currently transforms the original + // iterator into a fused iterator, this intersperse_iter + // should always have `None` + for _ in 0..counter + 6 { + assert_eq!(intersperse_iter.next(), None); + } + + // Extra check to make sure it is `None` after processing 6 items + assert_eq!(intersperse_iter.next(), None); +} + +// FIXME(iter_intersperse): `intersperse_with` current behavior may change for +// non-fused iterators, so this test will likely have to +// be adjusted; see PR #152855 and issue #79524 +// if `intersperse_with` doesn't change, remove this FIXME. +#[test] +fn test_non_fused_iterator_intersperse_with_2() { + #[derive(Debug)] + struct TestCounter { + counter: usize, + } + + // Given a counter of 0, this produces: + // `Some(1)` -> `Some(2)` -> `None` -> `Some(4)` -> `Some(5)` -> + // and then `None` endlessly + impl Iterator for TestCounter { + type Item = usize; + fn next(&mut self) -> Option { + if self.counter < 5 { + self.counter += 1; + if self.counter % 3 != 0 { + return Some(self.counter); + } else { + return None; + } + } + self.counter += 1; + None + } + } + + let counter = 0; + // places a 2 between `Some(_)` items + let non_fused_iter = TestCounter { counter }; + let mut intersperse_iter = non_fused_iter.intersperse(2); + // Since `intersperse` currently transforms the original + // iterator into a fused iterator, this interspersed iter + // will be `Some(1)` -> `Some(2)` -> `Some(2)` -> and then + // `None` endlessly + let mut items_processed = 0; + for num in 0..counter + 6 { + if num < 3 { + if num % 2 != 0 { + assert_eq!(intersperse_iter.next(), Some(2)); + } else { + items_processed += 1; + assert_eq!(intersperse_iter.next(), Some(items_processed)); + } + } else { + assert_eq!(intersperse_iter.next(), None); + } + } + + // Extra check to make sure it is `None` after processing 6 items + assert_eq!(intersperse_iter.next(), None); +}