From 1c64c6589a41471f3c9975898812dceeda3f7a63 Mon Sep 17 00:00:00 2001 From: Rain Date: Wed, 22 Apr 2026 16:32:58 -0700 Subject: [PATCH 1/2] [spr] initial version Created using spr 1.3.6-beta.1 --- crates/iddqd/src/bi_hash_map/imp.rs | 36 ++++++++++--- crates/iddqd/src/id_hash_map/imp.rs | 22 ++++++-- crates/iddqd/src/support/hash_table.rs | 36 ++++++++++--- crates/iddqd/src/tri_hash_map/imp.rs | 50 ++++++++++++++----- crates/iddqd/tests/integration/bi_hash_map.rs | 28 ++++++++++- crates/iddqd/tests/integration/id_hash_map.rs | 31 ++++++++++-- .../iddqd/tests/integration/tri_hash_map.rs | 28 ++++++++++- 7 files changed, 195 insertions(+), 36 deletions(-) diff --git a/crates/iddqd/src/bi_hash_map/imp.rs b/crates/iddqd/src/bi_hash_map/imp.rs index 69976b66..00d267c9 100644 --- a/crates/iddqd/src/bi_hash_map/imp.rs +++ b/crates/iddqd/src/bi_hash_map/imp.rs @@ -758,8 +758,14 @@ impl BiHashMap { /// ``` pub fn reserve(&mut self, additional: usize) { self.items.reserve(additional); - self.tables.k1_to_item.reserve(additional); - self.tables.k2_to_item.reserve(additional); + let items = &self.items; + let state = &self.tables.state; + self.tables + .k1_to_item + .reserve(additional, |ix| state.hash_one(items[*ix].key1())); + self.tables + .k2_to_item + .reserve(additional, |ix| state.hash_one(items[*ix].key2())); } /// Tries to reserve capacity for at least `additional` more elements to be @@ -815,13 +821,15 @@ impl BiHashMap { self.items .try_reserve(additional) .map_err(crate::errors::TryReserveError::from_hashbrown)?; + let items = &self.items; + let state = &self.tables.state; self.tables .k1_to_item - .try_reserve(additional) + .try_reserve(additional, |ix| state.hash_one(items[*ix].key1())) .map_err(crate::errors::TryReserveError::from_hashbrown)?; self.tables .k2_to_item - .try_reserve(additional) + .try_reserve(additional, |ix| state.hash_one(items[*ix].key2())) .map_err(crate::errors::TryReserveError::from_hashbrown)?; Ok(()) } @@ -864,8 +872,14 @@ impl BiHashMap { /// ``` pub fn shrink_to_fit(&mut self) { self.items.shrink_to_fit(); - self.tables.k1_to_item.shrink_to_fit(); - self.tables.k2_to_item.shrink_to_fit(); + let items = &self.items; + let state = &self.tables.state; + self.tables + .k1_to_item + .shrink_to_fit(|ix| state.hash_one(items[*ix].key1())); + self.tables + .k2_to_item + .shrink_to_fit(|ix| state.hash_one(items[*ix].key2())); } /// Shrinks the capacity of the map with a lower limit. It will drop @@ -911,8 +925,14 @@ impl BiHashMap { /// ``` pub fn shrink_to(&mut self, min_capacity: usize) { self.items.shrink_to(min_capacity); - self.tables.k1_to_item.shrink_to(min_capacity); - self.tables.k2_to_item.shrink_to(min_capacity); + let items = &self.items; + let state = &self.tables.state; + self.tables + .k1_to_item + .shrink_to(min_capacity, |ix| state.hash_one(items[*ix].key1())); + self.tables + .k2_to_item + .shrink_to(min_capacity, |ix| state.hash_one(items[*ix].key2())); } /// Returns an iterator over all items in the map. diff --git a/crates/iddqd/src/id_hash_map/imp.rs b/crates/iddqd/src/id_hash_map/imp.rs index 2efdcdf4..952cc9b4 100644 --- a/crates/iddqd/src/id_hash_map/imp.rs +++ b/crates/iddqd/src/id_hash_map/imp.rs @@ -641,7 +641,11 @@ impl IdHashMap { /// ``` pub fn reserve(&mut self, additional: usize) { self.items.reserve(additional); - self.tables.key_to_item.reserve(additional); + let items = &self.items; + let state = &self.tables.state; + self.tables + .key_to_item + .reserve(additional, |ix| state.hash_one(items[*ix].key())); } /// Tries to reserve capacity for at least `additional` more elements to be @@ -693,9 +697,11 @@ impl IdHashMap { self.items .try_reserve(additional) .map_err(crate::errors::TryReserveError::from_hashbrown)?; + let items = &self.items; + let state = &self.tables.state; self.tables .key_to_item - .try_reserve(additional) + .try_reserve(additional, |ix| state.hash_one(items[*ix].key())) .map_err(crate::errors::TryReserveError::from_hashbrown)?; Ok(()) } @@ -734,7 +740,11 @@ impl IdHashMap { /// ``` pub fn shrink_to_fit(&mut self) { self.items.shrink_to_fit(); - self.tables.key_to_item.shrink_to_fit(); + let items = &self.items; + let state = &self.tables.state; + self.tables + .key_to_item + .shrink_to_fit(|ix| state.hash_one(items[*ix].key())); } /// Shrinks the capacity of the map with a lower limit. It will drop @@ -776,7 +786,11 @@ impl IdHashMap { /// ``` pub fn shrink_to(&mut self, min_capacity: usize) { self.items.shrink_to(min_capacity); - self.tables.key_to_item.shrink_to(min_capacity); + let items = &self.items; + let state = &self.tables.state; + self.tables + .key_to_item + .shrink_to(min_capacity, |ix| state.hash_one(items[*ix].key())); } /// Iterates over the items in the map. diff --git a/crates/iddqd/src/support/hash_table.rs b/crates/iddqd/src/support/hash_table.rs index d9c87c6f..c35e5652 100644 --- a/crates/iddqd/src/support/hash_table.rs +++ b/crates/iddqd/src/support/hash_table.rs @@ -173,29 +173,51 @@ impl MapHashTable { } /// Reserves capacity for at least `additional` more items. + /// + /// `hasher` is invoked for every entry that hashbrown has to + /// re-slot during a growth rehash, and must return the same hash + /// that was used to insert that entry originally. Passing a + /// constant (for example `|_| 0`) silently corrupts the table: + /// all surviving entries land in the same bucket, and subsequent + /// lookups — which probe using the real key hash — miss. #[inline] - pub(crate) fn reserve(&mut self, additional: usize) { - self.items.reserve(additional, |_| 0); + pub(crate) fn reserve( + &mut self, + additional: usize, + hasher: impl Fn(&usize) -> u64, + ) { + self.items.reserve(additional, hasher); } /// Shrinks the capacity of the hash table as much as possible. + /// + /// See [`Self::reserve`] for the contract `hasher` must satisfy. #[inline] - pub(crate) fn shrink_to_fit(&mut self) { - self.items.shrink_to_fit(|_| 0); + pub(crate) fn shrink_to_fit(&mut self, hasher: impl Fn(&usize) -> u64) { + self.items.shrink_to_fit(hasher); } /// Shrinks the capacity of the hash table with a lower limit. + /// + /// See [`Self::reserve`] for the contract `hasher` must satisfy. #[inline] - pub(crate) fn shrink_to(&mut self, min_capacity: usize) { - self.items.shrink_to(min_capacity, |_| 0); + pub(crate) fn shrink_to( + &mut self, + min_capacity: usize, + hasher: impl Fn(&usize) -> u64, + ) { + self.items.shrink_to(min_capacity, hasher); } /// Tries to reserve capacity for at least `additional` more items. + /// + /// See [`Self::reserve`] for the contract `hasher` must satisfy. #[inline] pub(crate) fn try_reserve( &mut self, additional: usize, + hasher: impl Fn(&usize) -> u64, ) -> Result<(), hashbrown::TryReserveError> { - self.items.try_reserve(additional, |_| 0) + self.items.try_reserve(additional, hasher) } } diff --git a/crates/iddqd/src/tri_hash_map/imp.rs b/crates/iddqd/src/tri_hash_map/imp.rs index 1e0c25c6..3bb4f981 100644 --- a/crates/iddqd/src/tri_hash_map/imp.rs +++ b/crates/iddqd/src/tri_hash_map/imp.rs @@ -854,9 +854,17 @@ impl TriHashMap { /// ``` pub fn reserve(&mut self, additional: usize) { self.items.reserve(additional); - self.tables.k1_to_item.reserve(additional); - self.tables.k2_to_item.reserve(additional); - self.tables.k3_to_item.reserve(additional); + let items = &self.items; + let state = &self.tables.state; + self.tables + .k1_to_item + .reserve(additional, |ix| state.hash_one(items[*ix].key1())); + self.tables + .k2_to_item + .reserve(additional, |ix| state.hash_one(items[*ix].key2())); + self.tables + .k3_to_item + .reserve(additional, |ix| state.hash_one(items[*ix].key3())); } /// Tries to reserve capacity for at least `additional` more elements to be @@ -917,17 +925,19 @@ impl TriHashMap { self.items .try_reserve(additional) .map_err(crate::errors::TryReserveError::from_hashbrown)?; + let items = &self.items; + let state = &self.tables.state; self.tables .k1_to_item - .try_reserve(additional) + .try_reserve(additional, |ix| state.hash_one(items[*ix].key1())) .map_err(crate::errors::TryReserveError::from_hashbrown)?; self.tables .k2_to_item - .try_reserve(additional) + .try_reserve(additional, |ix| state.hash_one(items[*ix].key2())) .map_err(crate::errors::TryReserveError::from_hashbrown)?; self.tables .k3_to_item - .try_reserve(additional) + .try_reserve(additional, |ix| state.hash_one(items[*ix].key3())) .map_err(crate::errors::TryReserveError::from_hashbrown)?; Ok(()) } @@ -985,9 +995,17 @@ impl TriHashMap { /// ``` pub fn shrink_to_fit(&mut self) { self.items.shrink_to_fit(); - self.tables.k1_to_item.shrink_to_fit(); - self.tables.k2_to_item.shrink_to_fit(); - self.tables.k3_to_item.shrink_to_fit(); + let items = &self.items; + let state = &self.tables.state; + self.tables + .k1_to_item + .shrink_to_fit(|ix| state.hash_one(items[*ix].key1())); + self.tables + .k2_to_item + .shrink_to_fit(|ix| state.hash_one(items[*ix].key2())); + self.tables + .k3_to_item + .shrink_to_fit(|ix| state.hash_one(items[*ix].key3())); } /// Shrinks the capacity of the map with a lower limit. It will drop @@ -1048,9 +1066,17 @@ impl TriHashMap { /// ``` pub fn shrink_to(&mut self, min_capacity: usize) { self.items.shrink_to(min_capacity); - self.tables.k1_to_item.shrink_to(min_capacity); - self.tables.k2_to_item.shrink_to(min_capacity); - self.tables.k3_to_item.shrink_to(min_capacity); + let items = &self.items; + let state = &self.tables.state; + self.tables + .k1_to_item + .shrink_to(min_capacity, |ix| state.hash_one(items[*ix].key1())); + self.tables + .k2_to_item + .shrink_to(min_capacity, |ix| state.hash_one(items[*ix].key2())); + self.tables + .k3_to_item + .shrink_to(min_capacity, |ix| state.hash_one(items[*ix].key3())); } /// Iterates over the items in the map. diff --git a/crates/iddqd/tests/integration/bi_hash_map.rs b/crates/iddqd/tests/integration/bi_hash_map.rs index c38ebcea..952c46d1 100644 --- a/crates/iddqd/tests/integration/bi_hash_map.rs +++ b/crates/iddqd/tests/integration/bi_hash_map.rs @@ -242,6 +242,13 @@ enum Operation { #[weight(2)] RetainModulo(#[strategy(0..3_u8)] u8, #[strategy(1..4_u8)] u8, bool), Clear, + // `additional` is kept modest so that reservations frequently exceed + // the current `growth_left` (triggering hashbrown's rehash path) + // without over-allocating during long proptest runs. + #[weight(1)] + Reserve(#[strategy(0..256_usize)] usize), + #[weight(1)] + TryReserve(#[strategy(0..256_usize)] usize), } impl Operation { @@ -249,7 +256,9 @@ impl Operation { match self { Operation::InsertUnique(_) | Operation::Get1(_) - | Operation::Get2(_) => CompactnessChange::NoChange, + | Operation::Get2(_) + | Operation::Reserve(_) + | Operation::TryReserve(_) => CompactnessChange::NoChange, // The act of removing items, including calls to insert_overwrite, // can make the map non-compact. Operation::InsertOverwrite(_) @@ -366,6 +375,23 @@ fn proptest_ops( }); map.validate(compactness).expect("map should be valid"); } + Operation::Reserve(additional) => { + map.reserve(additional); + // `reserve` has no observable effect beyond capacity; the + // naive map has no equivalent. `validate` is the real + // check — it iterates items and asks `find_index` for + // each, which catches a hash-table left mis-bucketed by + // a regrowth rehash. + map.validate(compactness).expect("map should be valid"); + } + Operation::TryReserve(additional) => { + // Mirror `Reserve`; we don't assert `Ok` because the + // allocator could (legitimately) refuse a large request, + // and bailing on that would mask the actual regression + // we care about (silent hash-table corruption). + let _ = map.try_reserve(additional); + map.validate(compactness).expect("map should be valid"); + } Operation::Clear => { map.clear(); naive_map.clear(); diff --git a/crates/iddqd/tests/integration/id_hash_map.rs b/crates/iddqd/tests/integration/id_hash_map.rs index f6529b9f..4d39bd18 100644 --- a/crates/iddqd/tests/integration/id_hash_map.rs +++ b/crates/iddqd/tests/integration/id_hash_map.rs @@ -195,14 +195,22 @@ enum Operation { #[weight(2)] RetainModulo(#[strategy(0..3_u8)] u8, #[strategy(1..4_u8)] u8, bool), Clear, + // `additional` is kept modest so that reservations frequently exceed + // the current `growth_left` (triggering hashbrown's rehash path) + // without over-allocating during long proptest runs. + #[weight(1)] + Reserve(#[strategy(0..256_usize)] usize), + #[weight(1)] + TryReserve(#[strategy(0..256_usize)] usize), } impl Operation { fn compactness_change(&self) -> CompactnessChange { match self { - Operation::InsertUnique(_) | Operation::Get(_) => { - CompactnessChange::NoChange - } + Operation::InsertUnique(_) + | Operation::Get(_) + | Operation::Reserve(_) + | Operation::TryReserve(_) => CompactnessChange::NoChange, // The act of removing items, including calls to insert_overwrite, // can make the map non-compact. Operation::InsertOverwrite(_) @@ -301,6 +309,23 @@ fn proptest_ops( naive_map.clear(); map.validate(compactness).expect("map should be valid"); } + Operation::Reserve(additional) => { + map.reserve(additional); + // `reserve` has no observable effect beyond capacity; the + // naive map has no equivalent. `validate` is the real + // check — it iterates items and asks `find_index` for + // each, which catches a hash-table left mis-bucketed by + // a regrowth rehash. + map.validate(compactness).expect("map should be valid"); + } + Operation::TryReserve(additional) => { + // Mirror `Reserve`; we don't assert `Ok` because the + // allocator could (legitimately) refuse a large request, + // and bailing on that would mask the actual regression + // we care about (silent hash-table corruption). + let _ = map.try_reserve(additional); + map.validate(compactness).expect("map should be valid"); + } } // Check that the iterators work correctly. diff --git a/crates/iddqd/tests/integration/tri_hash_map.rs b/crates/iddqd/tests/integration/tri_hash_map.rs index 0759dcd1..9d990bec 100644 --- a/crates/iddqd/tests/integration/tri_hash_map.rs +++ b/crates/iddqd/tests/integration/tri_hash_map.rs @@ -271,6 +271,13 @@ enum Operation { #[weight(2)] RetainModulo(#[strategy(0..3_u8)] u8, #[strategy(1..4_u8)] u8, bool), Clear, + // `additional` is kept modest so that reservations frequently exceed + // the current `growth_left` (triggering hashbrown's rehash path) + // without over-allocating during long proptest runs. + #[weight(1)] + Reserve(#[strategy(0..256_usize)] usize), + #[weight(1)] + TryReserve(#[strategy(0..256_usize)] usize), } impl Operation { @@ -279,7 +286,9 @@ impl Operation { Operation::InsertUnique(_) | Operation::Get1(_) | Operation::Get2(_) - | Operation::Get3(_) => CompactnessChange::NoChange, + | Operation::Get3(_) + | Operation::Reserve(_) + | Operation::TryReserve(_) => CompactnessChange::NoChange, // The act of removing items, including calls to insert_overwrite, // can make the map non-compact. Operation::InsertOverwrite(_) @@ -415,6 +424,23 @@ fn proptest_ops( naive_map.clear(); map.validate(compactness).expect("map should be valid"); } + Operation::Reserve(additional) => { + map.reserve(additional); + // `reserve` has no observable effect beyond capacity; the + // naive map has no equivalent. `validate` is the real + // check — it iterates items and asks `find_index` for + // each, which catches a hash-table left mis-bucketed by + // a regrowth rehash. + map.validate(compactness).expect("map should be valid"); + } + Operation::TryReserve(additional) => { + // Mirror `Reserve`; we don't assert `Ok` because the + // allocator could (legitimately) refuse a large request, + // and bailing on that would mask the actual regression + // we care about (silent hash-table corruption). + let _ = map.try_reserve(additional); + map.validate(compactness).expect("map should be valid"); + } } // Check that the iterators work correctly. From c81ad9aa9927549572e805d22d3cb127993581b8 Mon Sep 17 00:00:00 2001 From: Rain Date: Wed, 22 Apr 2026 17:57:24 -0700 Subject: [PATCH 2/2] also add ShrinkToFit and ShrinkTo to proptests Created using spr 1.3.6-beta.1 --- crates/iddqd/src/support/hash_table.rs | 11 ++++---- crates/iddqd/tests/integration/bi_hash_map.rs | 26 ++++++++++++++----- crates/iddqd/tests/integration/id_hash_map.rs | 26 ++++++++++++++----- .../iddqd/tests/integration/tri_hash_map.rs | 26 ++++++++++++++----- 4 files changed, 65 insertions(+), 24 deletions(-) diff --git a/crates/iddqd/src/support/hash_table.rs b/crates/iddqd/src/support/hash_table.rs index c35e5652..0341e84d 100644 --- a/crates/iddqd/src/support/hash_table.rs +++ b/crates/iddqd/src/support/hash_table.rs @@ -174,12 +174,11 @@ impl MapHashTable { /// Reserves capacity for at least `additional` more items. /// - /// `hasher` is invoked for every entry that hashbrown has to - /// re-slot during a growth rehash, and must return the same hash - /// that was used to insert that entry originally. Passing a - /// constant (for example `|_| 0`) silently corrupts the table: - /// all surviving entries land in the same bucket, and subsequent - /// lookups — which probe using the real key hash — miss. + /// `hasher` is invoked for every entry that hashbrown has to re-slot during + /// a growth rehash, and must return the same hash that was used to insert + /// that entry originally. Anything else silently corrupts the table: all + /// surviving entries land in the same bucket, and subsequent lookups — + /// which probe using the real key hash — miss. #[inline] pub(crate) fn reserve( &mut self, diff --git a/crates/iddqd/tests/integration/bi_hash_map.rs b/crates/iddqd/tests/integration/bi_hash_map.rs index 952c46d1..0ec1aa03 100644 --- a/crates/iddqd/tests/integration/bi_hash_map.rs +++ b/crates/iddqd/tests/integration/bi_hash_map.rs @@ -242,23 +242,29 @@ enum Operation { #[weight(2)] RetainModulo(#[strategy(0..3_u8)] u8, #[strategy(1..4_u8)] u8, bool), Clear, - // `additional` is kept modest so that reservations frequently exceed - // the current `growth_left` (triggering hashbrown's rehash path) - // without over-allocating during long proptest runs. - #[weight(1)] + // `additional` is kept modest so that reservations frequently + // exceed the current `growth_left` and so trigger hashbrown's + // rehash path. Reserve(#[strategy(0..256_usize)] usize), - #[weight(1)] TryReserve(#[strategy(0..256_usize)] usize), + ShrinkToFit, + ShrinkTo(#[strategy(0..256_usize)] usize), } impl Operation { fn compactness_change(&self) -> CompactnessChange { match self { + // `shrink_to_fit` / `shrink_to` flow through hashbrown's + // rehash path the same way `reserve` does; like `reserve` + // they touch the allocation, not the item set's index + // space, so compactness is unchanged. Operation::InsertUnique(_) | Operation::Get1(_) | Operation::Get2(_) | Operation::Reserve(_) - | Operation::TryReserve(_) => CompactnessChange::NoChange, + | Operation::TryReserve(_) + | Operation::ShrinkToFit + | Operation::ShrinkTo(_) => CompactnessChange::NoChange, // The act of removing items, including calls to insert_overwrite, // can make the map non-compact. Operation::InsertOverwrite(_) @@ -392,6 +398,14 @@ fn proptest_ops( let _ = map.try_reserve(additional); map.validate(compactness).expect("map should be valid"); } + Operation::ShrinkToFit => { + map.shrink_to_fit(); + map.validate(compactness).expect("map should be valid"); + } + Operation::ShrinkTo(min_capacity) => { + map.shrink_to(min_capacity); + map.validate(compactness).expect("map should be valid"); + } Operation::Clear => { map.clear(); naive_map.clear(); diff --git a/crates/iddqd/tests/integration/id_hash_map.rs b/crates/iddqd/tests/integration/id_hash_map.rs index 4d39bd18..85ad1b54 100644 --- a/crates/iddqd/tests/integration/id_hash_map.rs +++ b/crates/iddqd/tests/integration/id_hash_map.rs @@ -195,22 +195,28 @@ enum Operation { #[weight(2)] RetainModulo(#[strategy(0..3_u8)] u8, #[strategy(1..4_u8)] u8, bool), Clear, - // `additional` is kept modest so that reservations frequently exceed - // the current `growth_left` (triggering hashbrown's rehash path) - // without over-allocating during long proptest runs. - #[weight(1)] + // `additional` is kept modest so that reservations frequently + // exceed the current `growth_left` and so trigger hashbrown's + // rehash path. Reserve(#[strategy(0..256_usize)] usize), - #[weight(1)] TryReserve(#[strategy(0..256_usize)] usize), + ShrinkToFit, + ShrinkTo(#[strategy(0..256_usize)] usize), } impl Operation { fn compactness_change(&self) -> CompactnessChange { match self { + // `shrink_to_fit` / `shrink_to` flow through hashbrown's + // rehash path the same way `reserve` does; like `reserve` + // they touch the allocation, not the item set's index + // space, so compactness is unchanged. Operation::InsertUnique(_) | Operation::Get(_) | Operation::Reserve(_) - | Operation::TryReserve(_) => CompactnessChange::NoChange, + | Operation::TryReserve(_) + | Operation::ShrinkToFit + | Operation::ShrinkTo(_) => CompactnessChange::NoChange, // The act of removing items, including calls to insert_overwrite, // can make the map non-compact. Operation::InsertOverwrite(_) @@ -326,6 +332,14 @@ fn proptest_ops( let _ = map.try_reserve(additional); map.validate(compactness).expect("map should be valid"); } + Operation::ShrinkToFit => { + map.shrink_to_fit(); + map.validate(compactness).expect("map should be valid"); + } + Operation::ShrinkTo(min_capacity) => { + map.shrink_to(min_capacity); + map.validate(compactness).expect("map should be valid"); + } } // Check that the iterators work correctly. diff --git a/crates/iddqd/tests/integration/tri_hash_map.rs b/crates/iddqd/tests/integration/tri_hash_map.rs index 9d990bec..40168091 100644 --- a/crates/iddqd/tests/integration/tri_hash_map.rs +++ b/crates/iddqd/tests/integration/tri_hash_map.rs @@ -271,24 +271,30 @@ enum Operation { #[weight(2)] RetainModulo(#[strategy(0..3_u8)] u8, #[strategy(1..4_u8)] u8, bool), Clear, - // `additional` is kept modest so that reservations frequently exceed - // the current `growth_left` (triggering hashbrown's rehash path) - // without over-allocating during long proptest runs. - #[weight(1)] + // `additional` is kept modest so that reservations frequently + // exceed the current `growth_left` and so trigger hashbrown's + // rehash path. Reserve(#[strategy(0..256_usize)] usize), - #[weight(1)] TryReserve(#[strategy(0..256_usize)] usize), + ShrinkToFit, + ShrinkTo(#[strategy(0..256_usize)] usize), } impl Operation { fn compactness_change(&self) -> CompactnessChange { match self { + // `shrink_to_fit` / `shrink_to` flow through hashbrown's + // rehash path the same way `reserve` does; like `reserve` + // they touch the allocation, not the item set's index + // space, so compactness is unchanged. Operation::InsertUnique(_) | Operation::Get1(_) | Operation::Get2(_) | Operation::Get3(_) | Operation::Reserve(_) - | Operation::TryReserve(_) => CompactnessChange::NoChange, + | Operation::TryReserve(_) + | Operation::ShrinkToFit + | Operation::ShrinkTo(_) => CompactnessChange::NoChange, // The act of removing items, including calls to insert_overwrite, // can make the map non-compact. Operation::InsertOverwrite(_) @@ -441,6 +447,14 @@ fn proptest_ops( let _ = map.try_reserve(additional); map.validate(compactness).expect("map should be valid"); } + Operation::ShrinkToFit => { + map.shrink_to_fit(); + map.validate(compactness).expect("map should be valid"); + } + Operation::ShrinkTo(min_capacity) => { + map.shrink_to(min_capacity); + map.validate(compactness).expect("map should be valid"); + } } // Check that the iterators work correctly.