From 3dbac6aafe4cf0f7a1006ff3da224d0a7dfcdb61 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Sat, 4 Oct 2025 15:58:03 -0700 Subject: [PATCH 01/15] Update the MSRV to 1.82 --- .github/workflows/ci.yml | 24 +++++------------------- Cargo.toml | 4 ++-- README.md | 2 +- src/lib.rs | 2 +- 4 files changed, 9 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e166236f..b6c6db24 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: include: - - rust: 1.63.0 # MSRV + - rust: 1.82.0 # MSRV features: - rust: stable features: arbitrary @@ -40,13 +40,8 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/cache@v4 - if: matrix.rust == '1.63.0' - with: - path: ~/.cargo/registry/index - key: cargo-git-index - name: Lock MSRV-compatible dependencies - if: matrix.rust == '1.63.0' + if: matrix.rust == '1.82.0' env: CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS: fallback # Note that this uses the runner's pre-installed stable cargo @@ -77,20 +72,15 @@ jobs: strategy: matrix: include: - - rust: 1.63.0 + - rust: 1.82.0 target: thumbv6m-none-eabi - rust: stable target: thumbv6m-none-eabi steps: - uses: actions/checkout@v4 - - uses: actions/cache@v4 - if: matrix.rust == '1.63.0' - with: - path: ~/.cargo/registry/index - key: cargo-git-index - name: Lock MSRV-compatible dependencies - if: matrix.rust == '1.63.0' + if: matrix.rust == '1.82.0' env: CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS: fallback # Note that this uses the runner's pre-installed stable cargo @@ -131,12 +121,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/cache@v4 - with: - path: ~/.cargo/registry/index - key: cargo-git-index - uses: dtolnay/rust-toolchain@nightly - - uses: dtolnay/rust-toolchain@1.63.0 # MSRV + - uses: dtolnay/rust-toolchain@1.82.0 # MSRV - uses: taiki-e/install-action@v2 with: tool: cargo-hack diff --git a/Cargo.toml b/Cargo.toml index 241127ce..ac035a2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,14 @@ [package] name = "indexmap" edition = "2021" -version = "2.11.4" +version = "2.12.0" documentation = "https://docs.rs/indexmap/" repository = "https://github.com/indexmap-rs/indexmap" license = "Apache-2.0 OR MIT" description = "A hash table with consistent order and fast iteration." keywords = ["hashmap", "no_std"] categories = ["data-structures", "no-std"] -rust-version = "1.63" +rust-version = "1.82" [lib] bench = false diff --git a/README.md b/README.md index c597e2b4..d0919765 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![build status](https://github.com/indexmap-rs/indexmap/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/indexmap-rs/indexmap/actions) [![crates.io](https://img.shields.io/crates/v/indexmap.svg)](https://crates.io/crates/indexmap) [![docs](https://docs.rs/indexmap/badge.svg)](https://docs.rs/indexmap) -[![rustc](https://img.shields.io/badge/rust-1.63%2B-orange.svg)](https://img.shields.io/badge/rust-1.63%2B-orange.svg) +[![rustc](https://img.shields.io/badge/rust-1.82%2B-orange.svg)](https://img.shields.io/badge/rust-1.82%2B-orange.svg) A pure-Rust hash table which preserves (in a limited sense) insertion order. diff --git a/src/lib.rs b/src/lib.rs index 792edbcd..84b149e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,7 +79,7 @@ //! //! ### Rust Version //! -//! This version of indexmap requires Rust 1.63 or later. +//! This version of indexmap requires Rust 1.82 or later. //! //! The indexmap 2.x release series will use a carefully considered version //! upgrade policy, where in a later 2.x version, we will raise the minimum From 8e4b40f44527884baffc377bf77b3708d295ebc5 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Sat, 4 Oct 2025 15:58:22 -0700 Subject: [PATCH 02/15] Upgrade to `hashbrown v0.16` exclusively --- Cargo.toml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ac035a2a..3b5ae96f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ bench = false [dependencies] equivalent = { version = "1.0", default-features = false } +hashbrown = { version = "0.16", default-features = false } arbitrary = { version = "1.0", optional = true, default-features = false } quickcheck = { version = "1.0", optional = true, default-features = false } @@ -25,10 +26,6 @@ sval = { version = "2", optional = true, default-features = false } # deprecated: use borsh's "indexmap" feature instead. borsh = { version = "1.2", optional = true, default-features = false } -[dependencies.hashbrown] -version = ">= 0.15.0, < 0.17.0" -default-features = false - # serde v1.0.220 is the first version that released with `serde_core`. # This is required to avoid conflict with other `serde` users which may require an older version. [target.'cfg(any())'.dependencies] From 7dbe50dc4217827841afcbfda54a9818657e4387 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Sat, 4 Oct 2025 15:59:45 -0700 Subject: [PATCH 03/15] Use `size_of` via the prelude --- src/borsh.rs | 1 - src/map/core.rs | 2 +- src/serde.rs | 5 ++--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/borsh.rs b/src/borsh.rs index dfa45e73..b1049208 100644 --- a/src/borsh.rs +++ b/src/borsh.rs @@ -3,7 +3,6 @@ use alloc::vec::Vec; use core::hash::BuildHasher; use core::hash::Hash; -use core::mem::size_of; use borsh::error::ERROR_ZST_FORBIDDEN; use borsh::io::{Error, ErrorKind, Read, Result, Write}; diff --git a/src/map/core.rs b/src/map/core.rs index b7c55b66..ca96953e 100644 --- a/src/map/core.rs +++ b/src/map/core.rs @@ -111,7 +111,7 @@ where impl IndexMapCore { /// The maximum capacity before the `entries` allocation would exceed `isize::MAX`. - const MAX_ENTRIES_CAPACITY: usize = (isize::MAX as usize) / mem::size_of::>(); + const MAX_ENTRIES_CAPACITY: usize = (isize::MAX as usize) / size_of::>(); #[inline] pub(crate) const fn new() -> Self { diff --git a/src/serde.rs b/src/serde.rs index 930ac47e..fe188665 100644 --- a/src/serde.rs +++ b/src/serde.rs @@ -9,7 +9,6 @@ use serde_core::ser::{Serialize, Serializer}; use core::fmt::{self, Formatter}; use core::hash::{BuildHasher, Hash}; use core::marker::PhantomData; -use core::{cmp, mem}; use crate::{Bucket, IndexMap, IndexSet}; @@ -23,9 +22,9 @@ use crate::{Bucket, IndexMap, IndexSet}; pub(crate) fn cautious_capacity(hint: Option) -> usize { const MAX_PREALLOC_BYTES: usize = 1024 * 1024; - cmp::min( + Ord::min( hint.unwrap_or(0), - MAX_PREALLOC_BYTES / mem::size_of::>(), + MAX_PREALLOC_BYTES / size_of::>(), ) } From 2c150abc20fded034477a6dc3245d5046b2ef3fc Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Sat, 4 Oct 2025 16:00:08 -0700 Subject: [PATCH 04/15] bench: use stable `std::hint::black_box` --- benches/bench.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benches/bench.rs b/benches/bench.rs index a011d547..3c5e1993 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -5,10 +5,10 @@ extern crate test; use fnv::FnvHasher; use std::hash::BuildHasherDefault; use std::hash::Hash; +use std::hint::black_box; use std::sync::LazyLock; type FnvBuilder = BuildHasherDefault; -use test::black_box; use test::Bencher; use indexmap::IndexMap; From 38fb76adc2d3eb8de7fa8a9c7d35fee884b0c88f Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Sat, 4 Oct 2025 16:00:52 -0700 Subject: [PATCH 05/15] Use `std::hash::RandomState` It's the same type as from `std::collections::hash_map`, just a different export path, so this isn't a breaking change. --- src/lib.rs | 2 +- src/map.rs | 2 +- src/set.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 84b149e5..c651d334 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,7 +60,7 @@ //! ### Alternate Hashers //! //! [`IndexMap`] and [`IndexSet`] have a default hasher type -//! [`S = RandomState`][std::collections::hash_map::RandomState], +//! [`S = RandomState`][std::hash::RandomState], //! just like the standard `HashMap` and `HashSet`, which is resistant to //! HashDoS attacks but not the most performant. Type aliases can make it easier //! to use alternate hashers: diff --git a/src/map.rs b/src/map.rs index bc8514ef..cde02830 100644 --- a/src/map.rs +++ b/src/map.rs @@ -35,7 +35,7 @@ use alloc::boxed::Box; use alloc::vec::Vec; #[cfg(feature = "std")] -use std::collections::hash_map::RandomState; +use std::hash::RandomState; pub(crate) use self::core::{ExtractCore, IndexMapCore}; use crate::util::{third, try_simplify_range}; diff --git a/src/set.rs b/src/set.rs index 75621417..45d5d126 100644 --- a/src/set.rs +++ b/src/set.rs @@ -18,7 +18,7 @@ pub use crate::rayon::set as rayon; use crate::TryReserveError; #[cfg(feature = "std")] -use std::collections::hash_map::RandomState; +use std::hash::RandomState; use crate::util::try_simplify_range; use alloc::boxed::Box; From c115d2653fd7b4e4a0b6c8a7885ec614d89dd8f7 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Sat, 4 Oct 2025 16:03:04 -0700 Subject: [PATCH 06/15] Use `BuildHasher::hash_one` --- src/map.rs | 7 +++---- src/map/core/raw_entry_v1.rs | 36 ++++++++++-------------------------- 2 files changed, 13 insertions(+), 30 deletions(-) diff --git a/src/map.rs b/src/map.rs index cde02830..a8f21ba2 100644 --- a/src/map.rs +++ b/src/map.rs @@ -28,7 +28,7 @@ pub use crate::rayon::map as rayon; use ::core::cmp::Ordering; use ::core::fmt; -use ::core::hash::{BuildHasher, Hash, Hasher}; +use ::core::hash::{BuildHasher, Hash}; use ::core::mem; use ::core::ops::{Index, IndexMut, RangeBounds}; use alloc::boxed::Box; @@ -813,9 +813,8 @@ where S: BuildHasher, { pub(crate) fn hash(&self, key: &Q) -> HashValue { - let mut h = self.hash_builder.build_hasher(); - key.hash(&mut h); - HashValue(h.finish() as usize) + let h = self.hash_builder.hash_one(key); + HashValue(h as usize) } /// Return `true` if an equivalent to `key` exists in the map. diff --git a/src/map/core/raw_entry_v1.rs b/src/map/core/raw_entry_v1.rs index 719033af..4f9898d5 100644 --- a/src/map/core/raw_entry_v1.rs +++ b/src/map/core/raw_entry_v1.rs @@ -12,7 +12,7 @@ use super::{Entries, RefMut}; use crate::{Equivalent, HashValue, IndexMap}; use core::fmt; -use core::hash::{BuildHasher, Hash, Hasher}; +use core::hash::{BuildHasher, Hash}; use core::marker::PhantomData; use core::mem; use hashbrown::hash_table; @@ -41,21 +41,14 @@ pub trait RawEntryApiV1: private::Sealed { /// # Examples /// /// ``` - /// use core::hash::{BuildHasher, Hash}; + /// use core::hash::BuildHasher; /// use indexmap::map::{IndexMap, RawEntryApiV1}; /// /// let mut map = IndexMap::new(); /// map.extend([("a", 100), ("b", 200), ("c", 300)]); /// - /// fn compute_hash(hash_builder: &S, key: &K) -> u64 { - /// use core::hash::Hasher; - /// let mut state = hash_builder.build_hasher(); - /// key.hash(&mut state); - /// state.finish() - /// } - /// /// for k in ["a", "b", "c", "d", "e", "f"] { - /// let hash = compute_hash(map.hasher(), k); + /// let hash = map.hasher().hash_one(k); /// let i = map.get_index_of(k); /// let v = map.get(k); /// let kv = map.get_key_value(k); @@ -102,20 +95,13 @@ pub trait RawEntryApiV1: private::Sealed { /// # Examples /// /// ``` - /// use core::hash::{BuildHasher, Hash}; + /// use core::hash::BuildHasher; /// use indexmap::map::{IndexMap, RawEntryApiV1}; /// use indexmap::map::raw_entry_v1::RawEntryMut; /// /// let mut map = IndexMap::new(); /// map.extend([("a", 100), ("b", 200), ("c", 300)]); /// - /// fn compute_hash(hash_builder: &S, key: &K) -> u64 { - /// use core::hash::Hasher; - /// let mut state = hash_builder.build_hasher(); - /// key.hash(&mut state); - /// state.finish() - /// } - /// /// // Existing key (insert and update) /// match map.raw_entry_mut_v1().from_key("a") { /// RawEntryMut::Vacant(_) => unreachable!(), @@ -133,7 +119,7 @@ pub trait RawEntryApiV1: private::Sealed { /// assert_eq!(map.len(), 3); /// /// // Existing key (take) - /// let hash = compute_hash(map.hasher(), "c"); + /// let hash = map.hasher().hash_one("c"); /// match map.raw_entry_mut_v1().from_key_hashed_nocheck(hash, "c") { /// RawEntryMut::Vacant(_) => unreachable!(), /// RawEntryMut::Occupied(view) => { @@ -146,7 +132,7 @@ pub trait RawEntryApiV1: private::Sealed { /// /// // Nonexistent key (insert and update) /// let key = "d"; - /// let hash = compute_hash(map.hasher(), key); + /// let hash = map.hasher().hash_one(key); /// match map.raw_entry_mut_v1().from_hash(hash, |q| *q == key) { /// RawEntryMut::Occupied(_) => unreachable!(), /// RawEntryMut::Vacant(view) => { @@ -613,9 +599,8 @@ impl<'a, K, V, S> RawVacantEntryMut<'a, K, V, S> { K: Hash, S: BuildHasher, { - let mut h = self.hash_builder.build_hasher(); - key.hash(&mut h); - self.insert_hashed_nocheck(h.finish(), key, value) + let h = self.hash_builder.hash_one(&key); + self.insert_hashed_nocheck(h, key, value) } /// Inserts the given key and value into the map with the provided hash, @@ -637,9 +622,8 @@ impl<'a, K, V, S> RawVacantEntryMut<'a, K, V, S> { K: Hash, S: BuildHasher, { - let mut h = self.hash_builder.build_hasher(); - key.hash(&mut h); - self.shift_insert_hashed_nocheck(index, h.finish(), key, value) + let h = self.hash_builder.hash_one(&key); + self.shift_insert_hashed_nocheck(index, h, key, value) } /// Inserts the given key and value into the map with the provided hash From 09db3cce19a24b74cfa13a1510fd72b8ab58ae83 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Sat, 4 Oct 2025 16:03:21 -0700 Subject: [PATCH 07/15] Use inherent `usize::div_ceil` --- src/map.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/map.rs b/src/map.rs index a8f21ba2..6ea7cb9a 100644 --- a/src/map.rs +++ b/src/map.rs @@ -1825,10 +1825,11 @@ where // Otherwise reserve half the hint (rounded up), so the map // will only resize twice in the worst case. let iter = iterable.into_iter(); + let (lower_len, _) = iter.size_hint(); let reserve = if self.is_empty() { - iter.size_hint().0 + lower_len } else { - (iter.size_hint().0 + 1) / 2 + lower_len.div_ceil(2) }; self.reserve(reserve); iter.for_each(move |(k, v)| { From b3d9cc355e227a847a2d2ebcae38a230c005174d Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Sat, 4 Oct 2025 16:04:07 -0700 Subject: [PATCH 08/15] Use the primitive slice's `is_sorted` methods --- src/map/slice.rs | 29 +++++------------------------ src/set/slice.rs | 25 +++---------------------- 2 files changed, 8 insertions(+), 46 deletions(-) diff --git a/src/map/slice.rs b/src/map/slice.rs index 59e685e3..f37411e2 100644 --- a/src/map/slice.rs +++ b/src/map/slice.rs @@ -264,8 +264,7 @@ impl Slice { where K: PartialOrd, { - // TODO(MSRV 1.82): self.entries.is_sorted_by(|a, b| a.key <= b.key) - self.is_sorted_by_key(|k, _| k) + self.entries.is_sorted_by(|a, b| a.key <= b.key) } /// Checks if this slice is sorted using the given comparator function. @@ -274,17 +273,8 @@ impl Slice { where F: FnMut(&'a K, &'a V, &'a K, &'a V) -> bool, { - // TODO(MSRV 1.82): self.entries - // .is_sorted_by(move |a, b| cmp(&a.key, &a.value, &b.key, &b.value)) - let mut iter = self.entries.iter(); - match iter.next() { - Some(mut prev) => iter.all(move |next| { - let sorted = cmp(&prev.key, &prev.value, &next.key, &next.value); - prev = next; - sorted - }), - None => true, - } + self.entries + .is_sorted_by(move |a, b| cmp(&a.key, &a.value, &b.key, &b.value)) } /// Checks if this slice is sorted using the given sort-key function. @@ -294,17 +284,8 @@ impl Slice { F: FnMut(&'a K, &'a V) -> T, T: PartialOrd, { - // TODO(MSRV 1.82): self.entries - // .is_sorted_by_key(move |a| sort_key(&a.key, &a.value)) - let mut iter = self.entries.iter().map(move |a| sort_key(&a.key, &a.value)); - match iter.next() { - Some(mut prev) => iter.all(move |next| { - let sorted = prev <= next; - prev = next; - sorted - }), - None => true, - } + self.entries + .is_sorted_by_key(move |a| sort_key(&a.key, &a.value)) } /// Returns the index of the partition point of a sorted map according to the given predicate diff --git a/src/set/slice.rs b/src/set/slice.rs index 666459db..b2bcdfbd 100644 --- a/src/set/slice.rs +++ b/src/set/slice.rs @@ -166,8 +166,7 @@ impl Slice { where T: PartialOrd, { - // TODO(MSRV 1.82): self.entries.is_sorted_by(|a, b| a.key <= b.key) - self.is_sorted_by(T::le) + self.entries.is_sorted_by(|a, b| a.key <= b.key) } /// Checks if this slice is sorted using the given comparator function. @@ -176,16 +175,7 @@ impl Slice { where F: FnMut(&'a T, &'a T) -> bool, { - // TODO(MSRV 1.82): self.entries.is_sorted_by(move |a, b| cmp(&a.key, &b.key)) - let mut iter = self.entries.iter(); - match iter.next() { - Some(mut prev) => iter.all(move |next| { - let sorted = cmp(&prev.key, &next.key); - prev = next; - sorted - }), - None => true, - } + self.entries.is_sorted_by(move |a, b| cmp(&a.key, &b.key)) } /// Checks if this slice is sorted using the given sort-key function. @@ -195,16 +185,7 @@ impl Slice { F: FnMut(&'a T) -> K, K: PartialOrd, { - // TODO(MSRV 1.82): self.entries.is_sorted_by_key(move |a| sort_key(&a.key)) - let mut iter = self.entries.iter().map(move |a| sort_key(&a.key)); - match iter.next() { - Some(mut prev) => iter.all(move |next| { - let sorted = prev <= next; - prev = next; - sorted - }), - None => true, - } + self.entries.is_sorted_by_key(move |a| sort_key(&a.key)) } /// Returns the index of the partition point of a sorted set according to the given predicate From 76b459b82cd7de7035d96f7b6f0a178e02c4425c Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Mon, 13 Oct 2025 17:52:46 -0700 Subject: [PATCH 09/15] Use more precise capturing for some `impl Trait` I think it doesn't really matter in this internal-only case, but it's still closer to the true intent. --- src/map/core.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/map/core.rs b/src/map/core.rs index ca96953e..d4acadb0 100644 --- a/src/map/core.rs +++ b/src/map/core.rs @@ -47,7 +47,7 @@ struct RefMut<'a, K, V> { } #[inline(always)] -fn get_hash(entries: &[Bucket]) -> impl Fn(&usize) -> u64 + '_ { +fn get_hash(entries: &[Bucket]) -> impl Fn(&usize) -> u64 + use<'_, K, V> { move |&i| entries[i].hash.get() } @@ -55,7 +55,7 @@ fn get_hash(entries: &[Bucket]) -> impl Fn(&usize) -> u64 + '_ { fn equivalent<'a, K, V, Q: ?Sized + Equivalent>( key: &'a Q, entries: &'a [Bucket], -) -> impl Fn(&usize) -> bool + 'a { +) -> impl Fn(&usize) -> bool + use<'a, K, V, Q> { move |&i| Q::equivalent(key, &entries[i].key) } From c7178d73c45fe0cc52aec684282a0aef5b3675b2 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Mon, 13 Oct 2025 17:57:38 -0700 Subject: [PATCH 10/15] Use `core::error::Error` --- src/lib.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index c651d334..30539299 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -255,9 +255,7 @@ impl core::fmt::Display for TryReserveError { } } -#[cfg(feature = "std")] -#[cfg_attr(docsrs, doc(cfg(feature = "std")))] -impl std::error::Error for TryReserveError {} +impl core::error::Error for TryReserveError {} // NOTE: This is copied from the slice module in the std lib. /// The error type returned by [`get_disjoint_indices_mut`][`IndexMap::get_disjoint_indices_mut`]. @@ -285,6 +283,4 @@ impl core::fmt::Display for GetDisjointMutError { } } -#[cfg(feature = "std")] -#[cfg_attr(docsrs, doc(cfg(feature = "std")))] -impl std::error::Error for GetDisjointMutError {} +impl core::error::Error for GetDisjointMutError {} From cfff4b7d03e53688b82b7afc350d472cca2d2e32 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Mon, 13 Oct 2025 18:06:22 -0700 Subject: [PATCH 11/15] Use bounds in associated type position --- tests/quick.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/quick.rs b/tests/quick.rs index 21225fbe..1150bffb 100644 --- a/tests/quick.rs +++ b/tests/quick.rs @@ -829,8 +829,7 @@ quickcheck_limit! { fn assert_sorted_by_key(iterable: I, key: Key) where - I: IntoIterator, - I::Item: Ord + Clone + Debug, + I: IntoIterator, Key: Fn(&I::Item) -> X, X: Ord, { From 4849b1679f6594112805ef8d542e230dfb4c37cf Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Wed, 15 Oct 2025 16:30:01 -0700 Subject: [PATCH 12/15] Make use of RFC2145 type privacy for sealed traits --- Cargo.toml | 6 ++++++ src/map/core/raw_entry_v1.rs | 9 ++++----- src/map/mutable.rs | 20 ++++++++++---------- src/set/mutable.rs | 9 ++++----- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3b5ae96f..4c3b0b98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,5 +61,11 @@ rustdoc-args = ["--cfg", "docsrs"] [workspace] members = ["test-nostd", "test-serde", "test-sval"] +[lints.rust] +private-bounds = "deny" +private-interfaces = "deny" +unnameable-types = "deny" +unreachable-pub = "deny" + [lints.clippy] style = "allow" diff --git a/src/map/core/raw_entry_v1.rs b/src/map/core/raw_entry_v1.rs index 4f9898d5..15abd8ad 100644 --- a/src/map/core/raw_entry_v1.rs +++ b/src/map/core/raw_entry_v1.rs @@ -20,7 +20,8 @@ use hashbrown::hash_table; /// Opt-in access to the experimental raw entry API. /// /// See the [`raw_entry_v1`][self] module documentation for more information. -pub trait RawEntryApiV1: private::Sealed { +#[expect(private_bounds)] +pub trait RawEntryApiV1: Sealed { /// Creates a raw immutable entry builder for the [`IndexMap`]. /// /// Raw entries provide the lowest level of control for searching and @@ -646,8 +647,6 @@ impl<'a, K, V, S> RawVacantEntryMut<'a, K, V, S> { } } -mod private { - pub trait Sealed {} +trait Sealed {} - impl Sealed for super::IndexMap {} -} +impl Sealed for IndexMap {} diff --git a/src/map/mutable.rs b/src/map/mutable.rs index d0d05e96..3977bbea 100644 --- a/src/map/mutable.rs +++ b/src/map/mutable.rs @@ -18,7 +18,8 @@ use super::{ /// `use` this trait to enable its methods for `IndexMap`. /// /// This trait is sealed and cannot be implemented for types outside this crate. -pub trait MutableKeys: private::Sealed { +#[expect(private_bounds)] +pub trait MutableKeys: Sealed { type Key; type Value; @@ -103,7 +104,8 @@ where /// `use` this trait to enable its methods for `Entry`. /// /// This trait is sealed and cannot be implemented for types outside this crate. -pub trait MutableEntryKey: private::Sealed { +#[expect(private_bounds)] +pub trait MutableEntryKey: Sealed { type Key; /// Gets a mutable reference to the entry's key, either within the map if occupied, @@ -154,12 +156,10 @@ impl MutableEntryKey for IndexedEntry<'_, K, V> { } } -mod private { - pub trait Sealed {} +trait Sealed {} - impl Sealed for super::IndexMap {} - impl Sealed for super::Entry<'_, K, V> {} - impl Sealed for super::OccupiedEntry<'_, K, V> {} - impl Sealed for super::VacantEntry<'_, K, V> {} - impl Sealed for super::IndexedEntry<'_, K, V> {} -} +impl Sealed for IndexMap {} +impl Sealed for Entry<'_, K, V> {} +impl Sealed for OccupiedEntry<'_, K, V> {} +impl Sealed for VacantEntry<'_, K, V> {} +impl Sealed for IndexedEntry<'_, K, V> {} diff --git a/src/set/mutable.rs b/src/set/mutable.rs index 0e43b7e8..b555f9ae 100644 --- a/src/set/mutable.rs +++ b/src/set/mutable.rs @@ -17,7 +17,8 @@ use crate::map::MutableKeys; /// `use` this trait to enable its methods for `IndexSet`. /// /// This trait is sealed and cannot be implemented for types outside this crate. -pub trait MutableValues: private::Sealed { +#[expect(private_bounds)] +pub trait MutableValues: Sealed { type Value; /// Return item index and mutable reference to the value @@ -79,8 +80,6 @@ where } } -mod private { - pub trait Sealed {} +trait Sealed {} - impl Sealed for super::IndexSet {} -} +impl Sealed for IndexSet {} From b46a32a5859fb5aa26f9a7e38c9c93c30fb773f9 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Wed, 15 Oct 2025 16:34:27 -0700 Subject: [PATCH 13/15] Move more to the lints table --- Cargo.toml | 5 +++++ benches/faststring.rs | 2 ++ src/lib.rs | 3 --- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4c3b0b98..c5463603 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,5 +67,10 @@ private-interfaces = "deny" unnameable-types = "deny" unreachable-pub = "deny" +# We *mostly* avoid unsafe code, but there are a few fine-grained cases allowed +unsafe-code = "deny" + +rust-2018-idioms = "warn" + [lints.clippy] style = "allow" diff --git a/benches/faststring.rs b/benches/faststring.rs index 670a51c0..bfe39590 100644 --- a/benches/faststring.rs +++ b/benches/faststring.rs @@ -33,8 +33,10 @@ impl<'a, S> From<&'a S> for &'a OneShot where S: AsRef, { + #[allow(unsafe_code)] fn from(s: &'a S) -> Self { let s: &str = s.as_ref(); + // SAFETY: OneShot is a `repr(transparent)` wrapper unsafe { &*(s as *const str as *const OneShot) } } } diff --git a/src/lib.rs b/src/lib.rs index 30539299..8b3a2551 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,3 @@ -// We *mostly* avoid unsafe code, but `Slice` allows it for DST casting. -#![deny(unsafe_code)] -#![warn(rust_2018_idioms)] #![no_std] //! [`IndexMap`] is a hash table where the iteration order of the key-value From db43f1945a7f0c39f28754e3557d7f34f1cb1ab3 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Fri, 17 Oct 2025 15:04:22 -0700 Subject: [PATCH 14/15] Release 2.12.0 --- RELEASES.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/RELEASES.md b/RELEASES.md index 6df2a7c4..2a8a99a0 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,5 +1,13 @@ # Releases +## 2.12.0 (2025-10-17) + +- **MSRV**: Rust 1.82.0 or later is now required. +- Updated the `hashbrown` dependency to 0.16 alone. +- Error types now implement `core::error::Error`. +- Added `pop_if` methods to `IndexMap` and `IndexSet`, similar to the + method for `Vec` added in Rust 1.86. + ## 2.11.4 (2025-09-18) - Updated the `hashbrown` dependency to a range allowing 0.15 or 0.16. From 61c9c94672c2862b29dd65202ccf892969b0fe4c Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Fri, 17 Oct 2025 15:29:03 -0700 Subject: [PATCH 15/15] ci: only run full miri in the merge queue It's a valuable test to have, but it's quite a bit slower than everything else, and there's not much value in running it twice, for both the pull request and merge queue. --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6c6db24..81290b30 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,7 +113,9 @@ jobs: - uses: taiki-e/install-action@v2 with: tool: cargo-nextest + if: github.event_name == 'merge_group' - run: cargo miri nextest run + if: github.event_name == 'merge_group' - run: cargo miri test --doc minimal-versions: