From dfd45fdaa1e9a6c249346f18ddf6dddc04126543 Mon Sep 17 00:00:00 2001 From: overlookmotel Date: Thu, 16 Jan 2025 14:06:55 +0000 Subject: [PATCH] feat(allocator): add `HashMap` --- Cargo.lock | 2 + crates/oxc_allocator/Cargo.toml | 2 + crates/oxc_allocator/src/hash_map.rs | 185 +++++++++++++++++++++++++++ crates/oxc_allocator/src/lib.rs | 2 + 4 files changed, 191 insertions(+) create mode 100644 crates/oxc_allocator/src/hash_map.rs diff --git a/Cargo.lock b/Cargo.lock index 6add3c1d4d284..ce7ed0c069e14 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1543,6 +1543,8 @@ version = "0.46.0" dependencies = [ "allocator-api2", "bumpalo", + "hashbrown 0.15.2", + "rustc-hash", "serde", "serde_json", "simdutf8", diff --git a/crates/oxc_allocator/Cargo.toml b/crates/oxc_allocator/Cargo.toml index 3f2b2c0ce2bf3..93c9ed3b85c12 100644 --- a/crates/oxc_allocator/Cargo.toml +++ b/crates/oxc_allocator/Cargo.toml @@ -21,6 +21,8 @@ doctest = false [dependencies] allocator-api2 = { workspace = true } bumpalo = { workspace = true, features = ["allocator-api2", "collections"] } +hashbrown = { workspace = true, features = ["allocator-api2"] } +rustc-hash = { workspace = true } simdutf8 = { workspace = true } serde = { workspace = true, optional = true } diff --git a/crates/oxc_allocator/src/hash_map.rs b/crates/oxc_allocator/src/hash_map.rs new file mode 100644 index 0000000000000..e28c3ed052fae --- /dev/null +++ b/crates/oxc_allocator/src/hash_map.rs @@ -0,0 +1,185 @@ +//! A hash map without `Drop`, that uses [`FxHasher`] to hash keys, and stores data in arena allocator. +//! +//! See [`HashMap`] for more details. +//! +//! [`FxHasher`]: rustc_hash::FxHasher + +use std::{ + hash::Hash, + mem::ManuallyDrop, + ops::{Deref, DerefMut}, +}; + +use bumpalo::Bump; +use rustc_hash::FxBuildHasher; + +// Re-export additional types from `hashbrown` +pub use hashbrown::{ + hash_map::{ + Drain, Entry, EntryRef, ExtractIf, IntoIter, IntoKeys, IntoValues, Iter, IterMut, Keys, + OccupiedError, RawEntryBuilder, RawEntryBuilderMut, Values, ValuesMut, + }, + Equivalent, TryReserveError, +}; + +use crate::Allocator; + +type FxHashMap<'alloc, K, V> = hashbrown::HashMap; + +/// A hash map without `Drop`, that uses [`FxHasher`] to hash keys, and stores data in arena allocator. +/// +/// Just a thin wrapper around [`hashbrown::HashMap`], which disables the `Drop` implementation. +/// +/// All APIs are the same, except create a [`HashMap`] with +/// either [`new_in`](HashMap::new_in) or [`with_capacity_in`](HashMap::with_capacity_in). +/// +/// Must NOT be used to store types which have a [`Drop`] implementation. +/// `K::drop` and `V::drop` will NOT be called on the `HashMap`'s contents when the `HashMap` is dropped. +/// If `K` or `V` own memory outside of the arena, this will be a memory leak. +/// +/// Note: This is not a soundness issue, as Rust does not support relying on `drop` +/// being called to guarantee soundness. +/// +/// [`FxHasher`]: rustc_hash::FxHasher +pub struct HashMap<'alloc, K, V>(ManuallyDrop>); + +// Note: All methods marked `#[inline]` as they just delegate to `hashbrown`'s methods. + +// TODO: `IntoIter`, `Drain`, and other consuming iterators provided by `hashbrown` are `Drop`. +// Wrap them in `ManuallyDrop` to prevent that. + +impl<'alloc, K, V> HashMap<'alloc, K, V> { + /// Creates an empty [`HashMap`]. It will be allocated with the given allocator. + /// + /// The hash map is initially created with a capacity of 0, so it will not allocate + /// until it is first inserted into. + #[inline] + pub fn new_in(allocator: &'alloc Allocator) -> Self { + let inner = FxHashMap::with_hasher_in(FxBuildHasher, allocator); + Self(ManuallyDrop::new(inner)) + } + + /// Creates an empty [`HashMap`] with the specified capacity. It will be allocated with the given allocator. + /// + /// The hash map will be able to hold at least capacity elements without reallocating. + /// If capacity is 0, the hash map will not allocate. + #[inline] + pub fn with_capacity_in(capacity: usize, allocator: &'alloc Allocator) -> Self { + let inner = FxHashMap::with_capacity_and_hasher_in(capacity, FxBuildHasher, allocator); + Self(ManuallyDrop::new(inner)) + } + + /// Creates a consuming iterator visiting all the keys in arbitrary order. + /// + /// The map cannot be used after calling this. The iterator element type is `K`. + #[inline] + pub fn into_keys(self) -> IntoKeys { + let inner = ManuallyDrop::into_inner(self.0); + inner.into_keys() + } + + /// Creates a consuming iterator visiting all the values in arbitrary order. + /// + /// The map cannot be used after calling this. The iterator element type is `V`. + #[inline] + pub fn into_values(self) -> IntoValues { + let inner = ManuallyDrop::into_inner(self.0); + inner.into_values() + } +} + +// Provide access to all `hashbrown::HashMap`'s methods via deref +impl<'alloc, K, V> Deref for HashMap<'alloc, K, V> { + type Target = FxHashMap<'alloc, K, V>; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'alloc, K, V> DerefMut for HashMap<'alloc, K, V> { + #[inline] + fn deref_mut(&mut self) -> &mut FxHashMap<'alloc, K, V> { + &mut self.0 + } +} + +impl<'alloc, K, V> IntoIterator for HashMap<'alloc, K, V> { + type IntoIter = IntoIter; + type Item = (K, V); + + /// Creates a consuming iterator, that is, one that moves each key-value pair out of the map + /// in arbitrary order. + /// + /// The map cannot be used after calling this. + #[inline] + fn into_iter(self) -> Self::IntoIter { + let inner = ManuallyDrop::into_inner(self.0); + // TODO: `hashbrown::hash_map::IntoIter` is `Drop`. + // Wrap it in `ManuallyDrop` to prevent that. + inner.into_iter() + } +} + +impl<'alloc, 'i, K, V> IntoIterator for &'i HashMap<'alloc, K, V> { + type IntoIter = <&'i FxHashMap<'alloc, K, V> as IntoIterator>::IntoIter; + type Item = (&'i K, &'i V); + + /// Creates an iterator over the entries of a `HashMap` in arbitrary order. + /// + /// The iterator element type is `(&'a K, &'a V)`. + /// + /// Return the same [`Iter`] struct as by the `iter` method on [`HashMap`]. + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.0.iter() + } +} + +impl<'alloc, 'i, K, V> IntoIterator for &'i mut HashMap<'alloc, K, V> { + type IntoIter = <&'i mut FxHashMap<'alloc, K, V> as IntoIterator>::IntoIter; + type Item = (&'i K, &'i mut V); + + /// Creates an iterator over the entries of a `HashMap` in arbitrary order + /// with mutable references to the values. + /// + /// The iterator element type is `(&'a K, &'a mut V)`. + /// + /// Return the same [`IterMut`] struct as by the `iter_mut` method on [`HashMap`]. + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.0.iter_mut() + } +} + +impl PartialEq for HashMap<'_, K, V> +where + K: Eq + Hash, + V: PartialEq, +{ + #[inline] + fn eq(&self, other: &Self) -> bool { + self.0.eq(&other.0) + } +} + +impl Eq for HashMap<'_, K, V> +where + K: Eq + Hash, + V: Eq, +{ +} + +// Note: `Index` and `Extend` are implemented via `Deref` + +/* +// Uncomment once we also provide `oxc_allocator::HashSet` +impl<'alloc, T> From> for HashSet<'alloc, T> { + fn from(map: HashMap<'alloc, T, ()>) -> Self { + let inner_map = ManuallyDrop::into_inner(map.0); + let inner_set = FxHashSet::from(inner_map); + Self(ManuallyDrop::new(inner_set)) + } +} +*/ diff --git a/crates/oxc_allocator/src/lib.rs b/crates/oxc_allocator/src/lib.rs index 8fd4bfa13116b..c2b11bac5d0be 100644 --- a/crates/oxc_allocator/src/lib.rs +++ b/crates/oxc_allocator/src/lib.rs @@ -52,12 +52,14 @@ mod allocator_api2; mod boxed; mod clone_in; mod convert; +pub mod hash_map; mod vec; pub use address::{Address, GetAddress}; pub use boxed::Box; pub use clone_in::CloneIn; pub use convert::{FromIn, IntoIn}; +pub use hash_map::HashMap; pub use vec::Vec; /// A bump-allocated memory arena based on [bumpalo].