From 4ba7ecbd554e1775d032140081ea6cac0b548147 Mon Sep 17 00:00:00 2001 From: nullstalgia Date: Fri, 5 Jun 2026 21:14:20 -0700 Subject: [PATCH 1/6] Add retain_back method to Deque --- src/deque.rs | 314 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 314 insertions(+) diff --git a/src/deque.rs b/src/deque.rs index 49c689f24f..617e0c571b 100644 --- a/src/deque.rs +++ b/src/deque.rs @@ -954,6 +954,78 @@ impl + ?Sized> DequeInner { } } + /// Shortens the deque, keeping the last `len` elements and dropping + /// the rest. + /// + /// If `len` is greater or equal to the deque's current length, this has + /// no effect. + /// + /// # Examples + /// + /// ``` + /// use heapless::Deque; + /// + /// let mut buf: Deque<_, 5> = Deque::new(); + /// buf.push_back(5); + /// buf.push_back(10); + /// buf.push_back(15); + /// buf.retain_back(1); + /// assert_eq!(buf.make_contiguous(), [15]); + /// ``` + #[doc(alias = "truncate_front")] + pub fn retain_back(&mut self, len: usize) { + /// Runs the destructor for all items in the slice when it gets dropped (gracefully or + /// during unwinding). + struct Dropper<'a, T>(&'a mut [T]); + + impl<'a, T> Drop for Dropper<'a, T> { + fn drop(&mut self) { + unsafe { + ptr::drop_in_place(self.0); + } + } + } + + // Safety: + // * Any slice passed to `drop_in_place` is valid; the second case has `len <= back.len()` + // and returning on `len > self.storage_len()` ensures `end <= front.len()` in the first + // case + // * Deque front/back cursors are moved before calling `drop_in_place`, so no value is + // dropped twice if `drop_in_place` panics + unsafe { + // If new desired length is greater or equal, we don't need to act. + if len >= self.storage_len() { + return; + } + + let (front, back) = self.as_mut_slices(); + + if len > back.len() { + let end = front.len() - (len - back.len()); + let drop_front = core::ptr::from_mut(front.get_unchecked_mut(..end)); + + self.front = self.to_physical_index(end); + self.full = false; + + ptr::drop_in_place(drop_front); + } else { + let drop_front = core::ptr::from_mut(front); + // 'end' is non-negative by the condition above + let end = back.len() - len; + let drop_back = core::ptr::from_mut(back.get_unchecked_mut(..end)); + + self.front = self.to_physical_index(self.storage_len() - len); + self.full = false; + + // If `drop_front` causes a panic, the Dropper will still be called to drop it's + // slice during unwinding. In either case, front will always be + // dropped before back. + let _back_dropper = Dropper(&mut *drop_back); + ptr::drop_in_place(drop_front); + } + } + } + /// Retains only the elements specified by the predicate. /// /// In other words, remove all elements `e` for which `f(&e)` returns false. @@ -2243,6 +2315,248 @@ mod tests { } } + // Checking that no invalid destructors are called with empty Deques + #[test] + fn retain_back_empty() { + droppable!(); + + const LEN: usize = 1; + let mut tester: Deque<_, LEN> = Deque::new(); + + // Retaining 0 from 0 + tester.retain_back(0); + assert_eq!(tester.len(), 0); + assert_eq!(Droppable::count(), 0); + + // Retaining 123 from 0 (thus clamping back down to 0) + tester.retain_back(123); + assert_eq!(tester.len(), 0); + assert_eq!(Droppable::count(), 0); + + // Ensure state is still valid by pushing one element in and then truncating again + assert!(tester.push_front(Droppable::new()).is_ok()); + assert_eq!(tester.len(), 1); + assert_eq!(Droppable::count(), 1); + + // Retaining 0 from 1 + tester.retain_back(0); + assert_eq!(tester.len(), 0); + assert_eq!(Droppable::count(), 0); + } + + // Testing retaining the back elements of contiguous Deques + #[test] + fn retain_back_contiguous() { + droppable!(); + + fn slice_lengths(slices: (&[T], &[T])) -> (usize, usize) { + let (a, b) = slices; + (a.len(), b.len()) + } + + const LEN: usize = 20; + let mut tester: Deque<_, LEN> = Deque::new(); + + // Filling from front. + for _ in 0..5 { + assert!(tester.push_front(Droppable::new()).is_ok()); + } + + // Retaining more than the elements present, no change. + tester.retain_back(10); + let lens = slice_lengths(tester.as_slices()); + assert_eq!(lens, (5, 0)); + assert_eq!(Droppable::count(), 5); + + // Retaining equal to elements present, no change. + tester.retain_back(5); + let lens = slice_lengths(tester.as_slices()); + assert_eq!(lens, (5, 0)); + assert_eq!(Droppable::count(), 5); + + // Retaining none. + tester.retain_back(0); + assert_eq!(tester.len(), 0); + assert_eq!(Droppable::count(), 0); + + // Refill from front. + for _ in 0..5 { + assert!(tester.push_front(Droppable::new()).is_ok()); + } + + let lens = slice_lengths(tester.as_slices()); + assert_eq!(lens, (5, 0)); + assert_eq!(Droppable::count(), 5); + + // Retaining up to the middle of elements. + tester.retain_back(3); + let lens = slice_lengths(tester.as_slices()); + assert_eq!(lens, (3, 0)); + assert_eq!(Droppable::count(), 3); + + // Retaining none. + tester.retain_back(0); + assert_eq!(tester.len(), 0); + assert_eq!(Droppable::count(), 0); + + // Resetting cursors. + tester.clear(); + + // Filling from back... + for _ in 0..5 { + assert!(tester.push_back(Droppable::new()).is_ok()); + } + + tester.retain_back(10); + let lens = slice_lengths(tester.as_slices()); + assert_eq!(lens, (5, 0)); + assert_eq!(Droppable::count(), 5); + + tester.retain_back(5); + let lens = slice_lengths(tester.as_slices()); + assert_eq!(lens, (5, 0)); + assert_eq!(Droppable::count(), 5); + + tester.retain_back(0); + assert_eq!(tester.len(), 0); + assert_eq!(Droppable::count(), 0); + + for _ in 0..5 { + assert!(tester.push_back(Droppable::new()).is_ok()); + } + + let lens = slice_lengths(tester.as_slices()); + assert_eq!(lens, (5, 0)); + assert_eq!(Droppable::count(), 5); + + tester.retain_back(3); + let lens = slice_lengths(tester.as_slices()); + assert_eq!(lens, (3, 0)); + assert_eq!(Droppable::count(), 3); + + tester.retain_back(0); + assert_eq!(tester.len(), 0); + assert_eq!(Droppable::count(), 0); + } + + // Testing retention with non-contiguous Deques + #[test] + fn retain_back_non_contiguous() { + const LEN: usize = 20; + let mut tester: Deque = Deque::new(); + + // Filling non-contiguously. + // + // Expecting [3, 2, 1, 1, 2, 3] + for x in 1..=3 { + assert!(tester.push_front(x).is_ok()); + } + for y in 1..=3 { + assert!(tester.push_back(y).is_ok()); + } + + // Retaining more than the elements present, no change. + tester.retain_back(10); + assert_eq!(tester.as_slices(), (&[3, 2, 1][..], &[1, 2, 3][..])); + println!("{} {}", tester.front, tester.back); + // Retaining equal to elements present, no change. + tester.retain_back(6); + assert_eq!(tester.as_slices(), (&[3, 2, 1][..], &[1, 2, 3][..])); + + // Retaining none. + tester.retain_back(0); + assert_eq!(tester.as_slices(), (&[][..], &[][..])); + + // Resetting cursors. + tester.clear(); + + // Refilling. + for x in 1..=3 { + assert!(tester.push_front(x).is_ok()); + } + for y in 1..=3 { + assert!(tester.push_back(y).is_ok()); + } + + assert_eq!(tester.as_slices(), (&[3, 2, 1][..], &[1, 2, 3][..])); + + // Retaining all of back and part of front. + tester.retain_back(5); + assert_eq!(tester.as_slices(), (&[2, 1][..], &[1, 2, 3][..])); + + // Replacing the truncated element. + assert!(tester.push_front(3).is_ok()); + assert_eq!(tester.as_slices(), (&[3, 2, 1][..], &[1, 2, 3][..])); + + // Retaining only back contents, thus making it the front slice. + tester.retain_back(3); + assert_eq!(tester.as_slices(), (&[1, 2, 3][..], &[][..])); + + // Replacing the truncated elements. + for y in 1..=3 { + assert!(tester.push_front(y).is_ok()); + } + assert_eq!(tester.as_slices(), (&[3, 2, 1][..], &[1, 2, 3][..])); + + // Retaining only some of back, thus also dropping all of front, + // again making it the front slice as a result. + tester.retain_back(2); + assert_eq!(tester.as_slices(), (&[2, 3][..], &[][..])); + + // Retaining none. + tester.retain_back(0); + assert_eq!(tester.as_slices(), (&[][..], &[][..])); + + // Should remain empty. + tester.retain_back(123); + assert_eq!(tester.as_slices(), (&[][..], &[][..])); + } + + // Tests that each element's destructor is called when not being retained. + #[test] + fn retain_back_drop_count() { + droppable!(); + + const LEN: usize = 20; + const TRUNC: usize = 3; + for push_front_amt in 0..=LEN { + let mut tester: Deque<_, LEN> = Deque::new(); + for index in 0..LEN { + if index < push_front_amt { + assert!( + tester.push_front(Droppable::new()).is_ok(), + "deque must have room for all {LEN} entries" + ); + } else { + assert!( + tester.push_back(Droppable::new()).is_ok(), + "deque must have room for all {LEN} entries" + ); + } + } + + assert_eq!(Droppable::count(), LEN as i32); + + let (front, back) = tester.as_slices(); + println!("A: {:?} {:?}\n", front, back); + + tester.retain_back(TRUNC); + assert_eq!(tester.len(), TRUNC); + assert_eq!(Droppable::count(), TRUNC as i32); + + let (front, back) = tester.as_slices(); + println!("B: {:?} {:?}\n", front, back); + + tester.retain_back(0); + + let (front, back) = tester.as_slices(); + println!("C: {:?} {:?}\n", front, back); + + assert_eq!(tester.len(), 0); + assert_eq!(Droppable::count(), 0); + } + } + #[test] fn retain() { droppable!(); From c326acce6c1d6f8a5dda6045174997fc8c67aa83 Mon Sep 17 00:00:00 2001 From: nullstalgia Date: Fri, 5 Jun 2026 21:21:46 -0700 Subject: [PATCH 2/6] Update changelog for retain_front --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index caa938303f..0742f0ff31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] - Added `resize_with` to `Vec` +- Added `retain_back` (aka `truncate_front`) to `Deque` ## [v0.9.3] 2025-04-15 From 3084de4b0e0e23635fb82dbc96eaeb00a79b33ec Mon Sep 17 00:00:00 2001 From: nullstalgia Date: Mon, 8 Jun 2026 19:48:33 -0700 Subject: [PATCH 3/6] Improve comments, limit scope of unsafe within Deque::retain_back --- src/deque.rs | 92 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 54 insertions(+), 38 deletions(-) diff --git a/src/deque.rs b/src/deque.rs index 617e0c571b..979edaa534 100644 --- a/src/deque.rs +++ b/src/deque.rs @@ -974,52 +974,68 @@ impl + ?Sized> DequeInner { /// ``` #[doc(alias = "truncate_front")] pub fn retain_back(&mut self, len: usize) { - /// Runs the destructor for all items in the slice when it gets dropped (gracefully or - /// during unwinding). - struct Dropper<'a, T>(&'a mut [T]); - - impl<'a, T> Drop for Dropper<'a, T> { - fn drop(&mut self) { - unsafe { - ptr::drop_in_place(self.0); - } - } + // If new desired length is greater or equal, we don't need to act. + if len >= self.storage_len() { + return; } - // Safety: - // * Any slice passed to `drop_in_place` is valid; the second case has `len <= back.len()` - // and returning on `len > self.storage_len()` ensures `end <= front.len()` in the first - // case - // * Deque front/back cursors are moved before calling `drop_in_place`, so no value is - // dropped twice if `drop_in_place` panics - unsafe { - // If new desired length is greater or equal, we don't need to act. - if len >= self.storage_len() { - return; - } + let (front, back) = self.as_mut_slices(); - let (front, back) = self.as_mut_slices(); + if len > back.len() { + // The `back` slice remains unchanged (`len` intends to retain more elements than `back` contains), + // front.len() + back.len() == self.len, so `end` is non-negative and end <= front.len(). + let end = front.len() - (len - back.len()); - if len > back.len() { - let end = front.len() - (len - back.len()); - let drop_front = core::ptr::from_mut(front.get_unchecked_mut(..end)); + // Safety: `end` is always less than or equal to `front.len()` + let drop_front = core::ptr::from_mut(unsafe { front.get_unchecked_mut(..end) }); - self.front = self.to_physical_index(end); - self.full = false; + // Turn the back slice into the front slice by + // placing the front cursor under `self.back`, spaced by `end` elements. + self.front = self.to_physical_index(end); + self.full = false; - ptr::drop_in_place(drop_front); - } else { - let drop_front = core::ptr::from_mut(front); - // 'end' is non-negative by the condition above - let end = back.len() - len; - let drop_back = core::ptr::from_mut(back.get_unchecked_mut(..end)); + // Safety: Any slice passed to `drop_in_place` is valid: + // * Only valid elements previously contained within `front` are present. + // * The elements are valid for reading/writing, originating from a &mut [T]. + // * Deque front cursor is moved before calling `drop_in_place`, so no value is + // dropped twice if `drop_in_place` panics. + unsafe { ptr::drop_in_place(drop_front) } + } else { + /// Runs the destructor for all items in the slice when it gets dropped + /// (gracefully at scope's end or during panic unwinding). + struct Dropper<'a, T>(&'a mut [T]); + impl<'a, T> Drop for Dropper<'a, T> { + fn drop(&mut self) { + // Safety: + // The slice given to the Dropper should only contain elements + // that have been truncated by the movement of the front cursor. + unsafe { ptr::drop_in_place(self.0) } + } + } - self.front = self.to_physical_index(self.storage_len() - len); - self.full = false; + // We intend to only keep fewer elements than `back` contains, + // so all elements within `front` will be dropped, + // before proceeding to truncate values within `back`. + let drop_front = core::ptr::from_mut(front); - // If `drop_front` causes a panic, the Dropper will still be called to drop it's - // slice during unwinding. In either case, front will always be - // dropped before back. + // 'end' is non-negative by the conditional above + let end = back.len() - len; + + // Safety: `end` is always less than or equal to `back.len()` + let drop_back = core::ptr::from_mut(unsafe { back.get_unchecked_mut(..end) }); + + // Turn the back slice into the front slice, + // additionally shortening its length to `len`. + self.front = self.to_physical_index(self.storage_len() - len); + self.full = false; + + // Safety: + // * If `drop_front` causes a panic, the Dropper will still be called to drop its + // slice during unwinding. In either case, front will always be + // dropped before back. + // * Deque front cursor is moved before calling `drop_in_place`, so no value is + // dropped twice if either `drop_in_place` panics. + unsafe { let _back_dropper = Dropper(&mut *drop_back); ptr::drop_in_place(drop_front); } From 661bd18d10d326a01ae39233a17b8e1421864b5f Mon Sep 17 00:00:00 2001 From: nullstalgia Date: Mon, 8 Jun 2026 20:21:16 -0700 Subject: [PATCH 4/6] Fix cargo fmt being unhappy with changes to Deque::retain_back --- src/deque.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/deque.rs b/src/deque.rs index 979edaa534..6201db72f5 100644 --- a/src/deque.rs +++ b/src/deque.rs @@ -982,8 +982,9 @@ impl + ?Sized> DequeInner { let (front, back) = self.as_mut_slices(); if len > back.len() { - // The `back` slice remains unchanged (`len` intends to retain more elements than `back` contains), - // front.len() + back.len() == self.len, so `end` is non-negative and end <= front.len(). + // The `back` slice remains unchanged (`len` intends to retain more elements than `back` + // contains), front.len() + back.len() == self.len, so `end` is non-negative + // and end <= front.len(). let end = front.len() - (len - back.len()); // Safety: `end` is always less than or equal to `front.len()` @@ -997,8 +998,8 @@ impl + ?Sized> DequeInner { // Safety: Any slice passed to `drop_in_place` is valid: // * Only valid elements previously contained within `front` are present. // * The elements are valid for reading/writing, originating from a &mut [T]. - // * Deque front cursor is moved before calling `drop_in_place`, so no value is - // dropped twice if `drop_in_place` panics. + // * Deque front cursor is moved before calling `drop_in_place`, so no value is dropped + // twice if `drop_in_place` panics. unsafe { ptr::drop_in_place(drop_front) } } else { /// Runs the destructor for all items in the slice when it gets dropped @@ -1030,11 +1031,10 @@ impl + ?Sized> DequeInner { self.full = false; // Safety: - // * If `drop_front` causes a panic, the Dropper will still be called to drop its - // slice during unwinding. In either case, front will always be - // dropped before back. - // * Deque front cursor is moved before calling `drop_in_place`, so no value is - // dropped twice if either `drop_in_place` panics. + // * If `drop_front` causes a panic, the Dropper will still be called to drop its slice + // during unwinding. In either case, front will always be dropped before back. + // * Deque front cursor is moved before calling `drop_in_place`, so no value is dropped + // twice if either `drop_in_place` panics. unsafe { let _back_dropper = Dropper(&mut *drop_back); ptr::drop_in_place(drop_front); From b28508dd0a17871db1b5e4e22ea7d36635fbeb0d Mon Sep 17 00:00:00 2001 From: nullstalgia Date: Tue, 9 Jun 2026 09:30:11 -0700 Subject: [PATCH 5/6] Remove println!'s from Deque::retain_back unit tests --- src/deque.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/deque.rs b/src/deque.rs index 6201db72f5..76518e35d5 100644 --- a/src/deque.rs +++ b/src/deque.rs @@ -2474,7 +2474,7 @@ mod tests { // Retaining more than the elements present, no change. tester.retain_back(10); assert_eq!(tester.as_slices(), (&[3, 2, 1][..], &[1, 2, 3][..])); - println!("{} {}", tester.front, tester.back); + // Retaining equal to elements present, no change. tester.retain_back(6); assert_eq!(tester.as_slices(), (&[3, 2, 1][..], &[1, 2, 3][..])); @@ -2554,19 +2554,16 @@ mod tests { assert_eq!(Droppable::count(), LEN as i32); let (front, back) = tester.as_slices(); - println!("A: {:?} {:?}\n", front, back); tester.retain_back(TRUNC); assert_eq!(tester.len(), TRUNC); assert_eq!(Droppable::count(), TRUNC as i32); let (front, back) = tester.as_slices(); - println!("B: {:?} {:?}\n", front, back); tester.retain_back(0); let (front, back) = tester.as_slices(); - println!("C: {:?} {:?}\n", front, back); assert_eq!(tester.len(), 0); assert_eq!(Droppable::count(), 0); From 544b20f5c0c8010228aca0f42d4d4f161a6af01f Mon Sep 17 00:00:00 2001 From: nullstalgia Date: Tue, 9 Jun 2026 09:36:06 -0700 Subject: [PATCH 6/6] Remove (now) unused variables from retain_back unit test --- src/deque.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/deque.rs b/src/deque.rs index 76518e35d5..3064928fb8 100644 --- a/src/deque.rs +++ b/src/deque.rs @@ -2553,18 +2553,12 @@ mod tests { assert_eq!(Droppable::count(), LEN as i32); - let (front, back) = tester.as_slices(); - tester.retain_back(TRUNC); assert_eq!(tester.len(), TRUNC); assert_eq!(Droppable::count(), TRUNC as i32); - let (front, back) = tester.as_slices(); - tester.retain_back(0); - let (front, back) = tester.as_slices(); - assert_eq!(tester.len(), 0); assert_eq!(Droppable::count(), 0); }