Skip to content

Commit feeba46

Browse files
committed
feat(allocator): add HashMap::from_iter_in
1 parent 1767176 commit feeba46

File tree

3 files changed

+200
-12
lines changed

3 files changed

+200
-12
lines changed

crates/oxc_allocator/src/hash_map.rs

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ type FxHashMap<'alloc, K, V> = hashbrown::HashMap<K, V, FxBuildHasher, &'alloc B
4949
///
5050
/// [`FxHasher`]: rustc_hash::FxHasher
5151
#[derive(Debug)]
52-
pub struct HashMap<'alloc, K, V>(ManuallyDrop<FxHashMap<'alloc, K, V>>);
52+
pub struct HashMap<'alloc, K, V>(pub(crate) ManuallyDrop<FxHashMap<'alloc, K, V>>);
5353

5454
/// SAFETY: Even though `Bump` is not `Sync`, we can make `HashMap<K, V>` `Sync` if both `K` and `V`
5555
/// are `Sync` because:
@@ -275,14 +275,3 @@ where
275275
}
276276

277277
// Note: `Index` and `Extend` are implemented via `Deref`
278-
279-
/*
280-
// Uncomment once we also provide `oxc_allocator::HashSet`
281-
impl<'alloc, T> From<HashMap<'alloc, T, ()>> for HashSet<'alloc, T> {
282-
fn from(map: HashMap<'alloc, T, ()>) -> Self {
283-
let inner_map = ManuallyDrop::into_inner(map.0);
284-
let inner_set = FxHashSet::from(inner_map);
285-
Self(ManuallyDrop::new(inner_set))
286-
}
287-
}
288-
*/
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
//! A hash set without `Drop`, that uses [`FxHasher`] to hash keys, and stores data in arena allocator.
2+
//!
3+
//! See [`HashSet`] for more details.
4+
//!
5+
//! [`FxHasher`]: rustc_hash::FxHasher
6+
7+
// All methods which just delegate to `hashbrown::HashSet` methods marked `#[inline(always)]`
8+
#![expect(clippy::inline_always)]
9+
10+
use std::{
11+
hash::Hash,
12+
mem::ManuallyDrop,
13+
ops::{Deref, DerefMut},
14+
};
15+
16+
use bumpalo::Bump;
17+
use rustc_hash::FxBuildHasher;
18+
19+
// Re-export additional types from `hashbrown`
20+
pub use hashbrown::hash_set::{Difference, IntoIter, Iter};
21+
22+
use crate::Allocator;
23+
24+
type FxHashSet<'alloc, T> = hashbrown::HashSet<T, FxBuildHasher, &'alloc Bump>;
25+
26+
/// A hash set without `Drop`, that uses [`FxHasher`] to hash keys, and stores data in arena allocator.
27+
///
28+
/// Just a thin wrapper around [`hashbrown::HashSet`], which disables the `Drop` implementation.
29+
///
30+
/// All APIs are the same, except create a [`HashSet`] with
31+
/// either [`new_in`](HashSet::new_in) or [`with_capacity_in`](HashSet::with_capacity_in).
32+
///
33+
/// # No `Drop`s
34+
///
35+
/// Objects allocated into Oxc memory arenas are never [`Dropped`](Drop). Memory is released in bulk
36+
/// when the allocator is dropped, without dropping the individual objects in the arena.
37+
///
38+
/// Therefore, it would produce a memory leak if you allocated [`Drop`] types into the arena
39+
/// which own memory allocations outside the arena.
40+
///
41+
/// Static checks make this impossible to do. [`HashSet::new_in`] and all other methods which create
42+
/// a [`HashSet`] will refuse to compile if the key is a [`Drop`] type.
43+
///
44+
/// [`FxHasher`]: rustc_hash::FxHasher
45+
#[derive(Debug)]
46+
pub struct HashSet<'alloc, T>(ManuallyDrop<FxHashSet<'alloc, T>>);
47+
48+
/// SAFETY: Same as `HashMap`. See `HashMap`'s doc comment for details.
49+
unsafe impl<T: Sync> Sync for HashSet<'_, T> {}
50+
51+
// TODO: `IntoIter` and other consuming iterators provided by `hashbrown` are `Drop`.
52+
// Wrap them in `ManuallyDrop` to prevent that.
53+
54+
impl<'alloc, T> HashSet<'alloc, T> {
55+
/// Const assertions that `T` is not `Drop`.
56+
/// Must be referenced in all methods which create a `HashSet`.
57+
const ASSERT_T_IS_NOT_DROP: () = {
58+
assert!(!std::mem::needs_drop::<T>(), "Cannot create a HashSet<T> where T is a Drop type");
59+
};
60+
61+
/// Creates an empty [`HashSet`]. It will be allocated with the given allocator.
62+
///
63+
/// The hash set is initially created with a capacity of 0, so it will not allocate
64+
/// until it is first inserted into.
65+
#[inline(always)]
66+
pub fn new_in(allocator: &'alloc Allocator) -> Self {
67+
const { Self::ASSERT_T_IS_NOT_DROP };
68+
69+
let inner = FxHashSet::with_hasher_in(FxBuildHasher, allocator.bump());
70+
Self(ManuallyDrop::new(inner))
71+
}
72+
73+
/// Creates an empty [`HashSet`] with the specified capacity. It will be allocated with the given allocator.
74+
///
75+
/// The hash set will be able to hold at least capacity elements without reallocating.
76+
/// If capacity is 0, the hash set will not allocate.
77+
#[inline(always)]
78+
pub fn with_capacity_in(capacity: usize, allocator: &'alloc Allocator) -> Self {
79+
const { Self::ASSERT_T_IS_NOT_DROP };
80+
81+
let inner =
82+
FxHashSet::with_capacity_and_hasher_in(capacity, FxBuildHasher, allocator.bump());
83+
Self(ManuallyDrop::new(inner))
84+
}
85+
86+
/// Create a new [`HashSet`] whose elements are taken from an iterator and
87+
/// allocated in the given `allocator`.
88+
///
89+
/// This is behaviorially identical to [`FromIterator::from_iter`].
90+
#[inline]
91+
pub fn from_iter_in<I: IntoIterator<Item = T>>(iter: I, allocator: &'alloc Allocator) -> Self
92+
where
93+
T: Eq + Hash,
94+
{
95+
const { Self::ASSERT_T_IS_NOT_DROP };
96+
97+
let iter = iter.into_iter();
98+
let mut set = ManuallyDrop::new(FxHashSet::with_capacity_and_hasher_in(
99+
iter.size_hint().0,
100+
FxBuildHasher,
101+
allocator.bump(),
102+
));
103+
iter.for_each(|v| {
104+
set.insert(v);
105+
});
106+
Self(set)
107+
}
108+
109+
/// Calling this method produces a compile-time panic.
110+
///
111+
/// This method would be unsound, because [`HashSet`] is `Sync`, and the underlying allocator
112+
/// (`bumpalo::Bump`) is not `Sync`.
113+
///
114+
/// This method exists only to block access as much as possible to the underlying
115+
/// `hashbrown::HashSet::allocator` method. That method can still be accessed via explicit `Deref`
116+
/// (`hash_map.deref().allocator()`), but that's unsound.
117+
///
118+
/// We'll prevent access to it completely and remove this method as soon as we can.
119+
// TODO: Do that!
120+
#[expect(clippy::unused_self)]
121+
pub fn allocator(&self) -> &'alloc Bump {
122+
const { panic!("This method cannot be called") };
123+
unreachable!();
124+
}
125+
}
126+
127+
// Provide access to all `hashbrown::HashSet`'s methods via deref
128+
impl<'alloc, T> Deref for HashSet<'alloc, T> {
129+
type Target = FxHashSet<'alloc, T>;
130+
131+
#[inline]
132+
fn deref(&self) -> &Self::Target {
133+
&self.0
134+
}
135+
}
136+
137+
impl<'alloc, T> DerefMut for HashSet<'alloc, T> {
138+
#[inline]
139+
fn deref_mut(&mut self) -> &mut FxHashSet<'alloc, T> {
140+
&mut self.0
141+
}
142+
}
143+
144+
impl<'alloc, T> IntoIterator for HashSet<'alloc, T> {
145+
type IntoIter = IntoIter<T, &'alloc Bump>;
146+
type Item = T;
147+
148+
/// Creates a consuming iterator, that is, one that moves each value out of the set
149+
/// in arbitrary order.
150+
///
151+
/// The set cannot be used after calling this.
152+
#[inline(always)]
153+
fn into_iter(self) -> Self::IntoIter {
154+
let inner = ManuallyDrop::into_inner(self.0);
155+
// TODO: `hashbrown::hash_set::IntoIter` is `Drop`.
156+
// Wrap it in `ManuallyDrop` to prevent that.
157+
inner.into_iter()
158+
}
159+
}
160+
161+
impl<'alloc, 'i, T> IntoIterator for &'i HashSet<'alloc, T> {
162+
type IntoIter = <&'i FxHashSet<'alloc, T> as IntoIterator>::IntoIter;
163+
type Item = &'i T;
164+
165+
/// Creates an iterator over the values of a `HashSet` in arbitrary order.
166+
///
167+
/// The iterator element type is `&'a T`.
168+
///
169+
/// Return the same [`Iter`] struct as by the `iter` method on [`HashSet`].
170+
#[inline(always)]
171+
fn into_iter(self) -> Self::IntoIter {
172+
self.0.iter()
173+
}
174+
}
175+
176+
impl<T> PartialEq for HashSet<'_, T>
177+
where
178+
T: Eq + Hash,
179+
{
180+
#[inline(always)]
181+
fn eq(&self, other: &Self) -> bool {
182+
self.0.eq(&other.0)
183+
}
184+
}
185+
186+
impl<T> Eq for HashSet<'_, T> where T: Eq + Hash {}
187+
188+
// Note: `Index` and `Extend` are implemented via `Deref`
189+
190+
impl<'alloc, T> From<crate::HashMap<'alloc, T, ()>> for HashSet<'alloc, T> {
191+
fn from(map: crate::HashMap<'alloc, T, ()>) -> Self {
192+
let inner_map = ManuallyDrop::into_inner(map.0);
193+
let inner_set = hashbrown::HashSet::from(inner_map);
194+
Self(ManuallyDrop::new(inner_set))
195+
}
196+
}

crates/oxc_allocator/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
//! * [`Vec`]
1010
//! * [`String`]
1111
//! * [`HashMap`]
12+
//! * [`HashSet`]
1213
//!
1314
//! See [`Allocator`] docs for information on efficient use of [`Allocator`].
1415
//!
@@ -50,6 +51,7 @@ mod convert;
5051
#[cfg(feature = "from_raw_parts")]
5152
mod from_raw_parts;
5253
pub mod hash_map;
54+
pub mod hash_set;
5355
#[cfg(feature = "pool")]
5456
mod pool;
5557
mod string_builder;
@@ -68,6 +70,7 @@ pub use boxed::Box;
6870
pub use clone_in::CloneIn;
6971
pub use convert::{FromIn, IntoIn};
7072
pub use hash_map::HashMap;
73+
pub use hash_set::HashSet;
7174
#[cfg(feature = "pool")]
7275
pub use pool::*;
7376
pub use string_builder::StringBuilder;

0 commit comments

Comments
 (0)