From d31f12a441abd52539c006f22c7df25ec284ddd2 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Sat, 4 Oct 2025 15:58:03 -0700 Subject: [PATCH 01/10] Update the MSRV to 1.82 (cherry picked from commit 3dbac6aafe4cf0f7a1006ff3da224d0a7dfcdb61) --- .github/workflows/ci.yml | 24 +++++------------------- Cargo.toml | 2 +- README.md | 2 +- src/lib.rs | 2 +- 4 files changed, 8 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e166236..b6c6db2 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 d632e69..28c5d6a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ 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 100c9a7..f4675ce 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![build status](https://github.com/indexmap-rs/ordermap/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/indexmap-rs/ordermap/actions) [![crates.io](https://img.shields.io/crates/v/ordermap.svg)](https://crates.io/crates/ordermap) [![docs](https://docs.rs/ordermap/badge.svg)](https://docs.rs/ordermap) -[![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 c6a6f0a..385b1ff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,7 +74,7 @@ //! //! ### Rust Version //! -//! This version of ordermap requires Rust 1.63 or later. +//! This version of ordermap requires Rust 1.82 or later. //! //! The ordermap 0.x release series will use a carefully considered version //! upgrade policy, where in a later 0.x version, we will raise the minimum From ccd97d78db91731f896e3922f214f311b4546f5f Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Sat, 11 Oct 2025 10:42:54 -0700 Subject: [PATCH 02/10] Add map and set `pop_if`, similar to Rust 1.86's `Vec::pop_if` (cherry picked from commit 09fb43fe9731f0df9b1e8b5250255d016b630719) --- Cargo.toml | 2 +- src/map.rs | 25 +++++++++++++++++++++++++ src/set.rs | 24 ++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 28c5d6a..1c9770b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ rust-version = "1.82" bench = false [dependencies] -indexmap = { version = "2.11.3", default-features = false } +indexmap = { version = "2.12.0", default-features = false } arbitrary = { version = "1.0", optional = true, default-features = false } quickcheck = { version = "1.0", optional = true, default-features = false } diff --git a/src/map.rs b/src/map.rs index 91804a1..0a4b18a 100644 --- a/src/map.rs +++ b/src/map.rs @@ -941,6 +941,31 @@ impl OrderMap { self.inner.pop() } + /// Removes and returns the last key-value pair from a map if the predicate + /// returns `true`, or [`None`] if the predicate returns false or the map + /// is empty (the predicate will not be called in that case). + /// + /// This preserves the order of the remaining elements. + /// + /// Computes in **O(1)** time (average). + /// + /// # Examples + /// + /// ``` + /// use ordermap::OrderMap; + /// + /// let init = [(1, 'a'), (2, 'b'), (3, 'c'), (4, 'd')]; + /// let mut map = OrderMap::from(init); + /// let pred = |key: &i32, _value: &mut char| *key % 2 == 0; + /// + /// assert_eq!(map.pop_if(pred), Some((4, 'd'))); + /// assert_eq!(map.as_slice(), &init[..3]); + /// assert_eq!(map.pop_if(pred), None); + /// ``` + pub fn pop_if(&mut self, predicate: impl FnOnce(&K, &mut V) -> bool) -> Option<(K, V)> { + self.inner.pop_if(predicate) + } + /// Scan through each key-value pair in the map and keep those where the /// closure `keep` returns `true`. /// diff --git a/src/set.rs b/src/set.rs index b2d5967..e7a1709 100644 --- a/src/set.rs +++ b/src/set.rs @@ -850,6 +850,30 @@ impl OrderSet { self.inner.pop() } + /// Removes and returns the last value from a set if the predicate + /// returns `true`, or [`None`] if the predicate returns false or the set + /// is empty (the predicate will not be called in that case). + /// + /// This preserves the order of the remaining elements. + /// + /// Computes in **O(1)** time (average). + /// + /// # Examples + /// + /// ``` + /// use ordermap::OrderSet; + /// + /// let mut set = OrderSet::from([1, 2, 3, 4]); + /// let pred = |x: &i32| *x % 2 == 0; + /// + /// assert_eq!(set.pop_if(pred), Some(4)); + /// assert_eq!(set.as_slice(), &[1, 2, 3]); + /// assert_eq!(set.pop_if(pred), None); + /// ``` + pub fn pop_if(&mut self, predicate: impl FnOnce(&T) -> bool) -> Option { + self.inner.pop_if(predicate) + } + /// Scan through each value in the set and keep those where the /// closure `keep` returns `true`. /// From 0e9ac6d4acd76f4345a5b13b5522a6f9fd5a38d8 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Sat, 4 Oct 2025 16:00:08 -0700 Subject: [PATCH 03/10] bench: use stable `std::hint::black_box` (cherry picked from commit 2c150abc20fded034477a6dc3245d5046b2ef3fc) --- benches/bench.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benches/bench.rs b/benches/bench.rs index 6233413..5d867df 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 ordermap::OrderMap; From 80827bcd507ebdd02f9c6cb8365a533698d18424 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Sat, 4 Oct 2025 16:00:52 -0700 Subject: [PATCH 04/10] 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. (cherry picked from commit 38fb76adc2d3eb8de7fa8a9c7d35fee884b0c88f) --- 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 385b1ff..8ee3cec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,7 +55,7 @@ //! ### Alternate Hashers //! //! [`OrderMap`] and [`OrderSet`] 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 0a4b18a..43d93ad 100644 --- a/src/map.rs +++ b/src/map.rs @@ -47,7 +47,7 @@ use indexmap::IndexMap; use alloc::vec::Vec; #[cfg(feature = "std")] -use std::collections::hash_map::RandomState; +use std::hash::RandomState; use crate::{Equivalent, GetDisjointMutError, TryReserveError}; diff --git a/src/set.rs b/src/set.rs index e7a1709..4c5410d 100644 --- a/src/set.rs +++ b/src/set.rs @@ -37,7 +37,7 @@ use indexmap::IndexSet; use alloc::vec::Vec; #[cfg(feature = "std")] -use std::collections::hash_map::RandomState; +use std::hash::RandomState; use crate::{Equivalent, TryReserveError}; From 3aad0c26c80167a84ba9f7262ea34ed8178de0f8 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Sat, 4 Oct 2025 16:03:04 -0700 Subject: [PATCH 05/10] Use `BuildHasher::hash_one` (cherry picked from commit c115d2653fd7b4e4a0b6c8a7885ec614d89dd8f7) --- src/map/raw_entry_v1.rs | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/src/map/raw_entry_v1.rs b/src/map/raw_entry_v1.rs index d67e4d9..e4d0b99 100644 --- a/src/map/raw_entry_v1.rs +++ b/src/map/raw_entry_v1.rs @@ -42,21 +42,14 @@ pub trait RawEntryApiV1: private::Sealed { /// # Examples /// /// ``` - /// use core::hash::{BuildHasher, Hash}; + /// use core::hash::BuildHasher; /// use ordermap::map::{OrderMap, RawEntryApiV1}; /// /// let mut map = OrderMap::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); @@ -103,20 +96,13 @@ pub trait RawEntryApiV1: private::Sealed { /// # Examples /// /// ``` - /// use core::hash::{BuildHasher, Hash}; + /// use core::hash::BuildHasher; /// use ordermap::map::{OrderMap, RawEntryApiV1}; /// use ordermap::map::raw_entry_v1::RawEntryMut; /// /// let mut map = OrderMap::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!(), @@ -134,7 +120,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) => { @@ -147,7 +133,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) => { From 931e7521b6623a5f8f4ad5d0643b0121ee76a05d Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Mon, 13 Oct 2025 18:06:22 -0700 Subject: [PATCH 06/10] Use bounds in associated type position (cherry picked from commit cfff4b7d03e53688b82b7afc350d472cca2d2e32) --- tests/quick.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/quick.rs b/tests/quick.rs index 75bfa25..dcd694b 100644 --- a/tests/quick.rs +++ b/tests/quick.rs @@ -833,8 +833,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 bc9aa6b703e603adaedeae68c0fe1b990d17d4d6 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Wed, 15 Oct 2025 16:30:01 -0700 Subject: [PATCH 07/10] Make use of RFC2145 type privacy for sealed traits (cherry picked from commit 4849b1679f6594112805ef8d542e230dfb4c37cf) --- Cargo.toml | 6 ++++++ src/map/mutable.rs | 20 ++++++++++---------- src/map/raw_entry_v1.rs | 9 ++++----- src/set/mutable.rs | 9 ++++----- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1c9770b..5f1aff1 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/mutable.rs b/src/map/mutable.rs index c81bdf3..a57998d 100644 --- a/src/map/mutable.rs +++ b/src/map/mutable.rs @@ -17,7 +17,8 @@ use indexmap::map::MutableKeys as _; /// `use` this trait to enable its methods for `OrderMap`. /// /// 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; @@ -97,7 +98,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, @@ -148,12 +150,10 @@ impl MutableEntryKey for IndexedEntry<'_, K, V> { } } -mod private { - pub trait Sealed {} +trait Sealed {} - impl Sealed for super::OrderMap {} - 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 OrderMap {} +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/map/raw_entry_v1.rs b/src/map/raw_entry_v1.rs index e4d0b99..a05f540 100644 --- a/src/map/raw_entry_v1.rs +++ b/src/map/raw_entry_v1.rs @@ -21,7 +21,8 @@ use alloc::vec::Vec; /// 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 [`OrderMap`]. /// /// Raw entries provide the lowest level of control for searching and @@ -593,8 +594,6 @@ impl<'a, K, V, S> RawVacantEntryMut<'a, K, V, S> { } } -mod private { - pub trait Sealed {} +trait Sealed {} - impl Sealed for super::OrderMap {} -} +impl Sealed for OrderMap {} diff --git a/src/set/mutable.rs b/src/set/mutable.rs index 3256924..fbfe434 100644 --- a/src/set/mutable.rs +++ b/src/set/mutable.rs @@ -16,7 +16,8 @@ use indexmap::set::MutableValues as _; /// `use` this trait to enable its methods for `OrderSet`. /// /// 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 @@ -72,8 +73,6 @@ where } } -mod private { - pub trait Sealed {} +trait Sealed {} - impl Sealed for super::OrderSet {} -} +impl Sealed for OrderSet {} From c4f6b4e9238ae7edc5e6ee266c4fabca92e7c0f2 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Wed, 15 Oct 2025 16:34:27 -0700 Subject: [PATCH 08/10] Move more to the lints table (cherry picked from commit b46a32a5859fb5aa26f9a7e38c9c93c30fb773f9) --- Cargo.toml | 3 +++ benches/faststring.rs | 2 ++ src/lib.rs | 2 -- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5f1aff1..c019ff5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,9 @@ private-bounds = "deny" private-interfaces = "deny" unnameable-types = "deny" unreachable-pub = "deny" +unsafe-code = "deny" + +rust-2018-idioms = "warn" [lints.clippy] style = "allow" diff --git a/benches/faststring.rs b/benches/faststring.rs index 53f35a8..c1d9760 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 8ee3cec..5bfdbc0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,3 @@ -#![forbid(unsafe_code)] -#![warn(rust_2018_idioms)] #![no_std] //! [`OrderMap`] is a hash table where the iteration order of the key-value From a6a589331c1a5de42de3eb3d2a84043133581304 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Fri, 17 Oct 2025 15:29:03 -0700 Subject: [PATCH 09/10] 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. (cherry picked from commit 61c9c94672c2862b29dd65202ccf892969b0fe4c) --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6c6db2..81290b3 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: From cdfb62e12104ad14a922deec44f9a3fd1bd41596 Mon Sep 17 00:00:00 2001 From: Josh Stone Date: Fri, 17 Oct 2025 16:52:33 -0700 Subject: [PATCH 10/10] Release 1.0.0 --- Cargo.toml | 2 +- RELEASES.md | 7 +++++++ src/lib.rs | 4 ++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c019ff5..bd3705f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ordermap" edition = "2021" -version = "0.5.12" +version = "1.0.0" documentation = "https://docs.rs/ordermap/" repository = "https://github.com/indexmap-rs/ordermap" license = "Apache-2.0 OR MIT" diff --git a/RELEASES.md b/RELEASES.md index a1cd2d1..cdac4e7 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,5 +1,12 @@ # Releases +## 1.0.0 (2025-10-17) + +- **MSRV**: Rust 1.82.0 or later is now required. +- Updated the `indexmap` dependency to version 2.12.0. +- Added `pop_if` methods to `OrderMap` and `OrderSet`, similar to the + method for `Vec` added in Rust 1.86. + ## 0.5.12 (2025-09-15) - Make the minimum `serde` version only apply when "serde" is enabled. diff --git a/src/lib.rs b/src/lib.rs index 5bfdbc0..291eaf4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,8 +74,8 @@ //! //! This version of ordermap requires Rust 1.82 or later. //! -//! The ordermap 0.x release series will use a carefully considered version -//! upgrade policy, where in a later 0.x version, we will raise the minimum +//! The ordermap 1.x release series will use a carefully considered version +//! upgrade policy, where in a later 1.x version, we will raise the minimum //! required Rust version. //! //! ## No Standard Library Targets