| //! Global `Arc`-based object interning infrastructure. |
| //! |
| //! Eventually this should probably be replaced with salsa-based interning. |
| |
| use std::{ |
| borrow::Borrow, |
| fmt::{self, Debug, Display}, |
| hash::{BuildHasher, BuildHasherDefault, Hash, Hasher}, |
| ops::Deref, |
| sync::OnceLock, |
| }; |
| |
| use dashmap::{DashMap, SharedValue}; |
| use hashbrown::raw::RawTable; |
| use rustc_hash::FxHasher; |
| use triomphe::Arc; |
| |
| type InternMap<T> = DashMap<Arc<T>, (), BuildHasherDefault<FxHasher>>; |
| type Guard<T> = dashmap::RwLockWriteGuard<'static, RawTable<(Arc<T>, SharedValue<()>)>>; |
| |
| mod symbol; |
| pub use self::symbol::{Symbol, symbols as sym}; |
| |
| pub struct Interned<T: Internable + ?Sized> { |
| arc: Arc<T>, |
| } |
| |
| impl<T: Internable> Interned<T> { |
| #[inline] |
| pub fn new(obj: T) -> Self { |
| Self::new_generic(obj) |
| } |
| } |
| |
| impl Interned<str> { |
| #[inline] |
| pub fn new_str(s: &str) -> Self { |
| Self::new_generic(s) |
| } |
| } |
| |
| impl<T: Internable + ?Sized> Interned<T> { |
| #[inline] |
| pub fn new_generic<U>(obj: U) -> Self |
| where |
| U: Borrow<T>, |
| Arc<T>: From<U>, |
| { |
| let storage = T::storage().get(); |
| let (mut shard, hash) = Self::select(storage, obj.borrow()); |
| // Atomically, |
| // - check if `obj` is already in the map |
| // - if so, clone its `Arc` and return it |
| // - if not, box it up, insert it, and return a clone |
| // This needs to be atomic (locking the shard) to avoid races with other thread, which could |
| // insert the same object between us looking it up and inserting it. |
| let bucket = match shard.find_or_find_insert_slot( |
| hash, |
| |(other, _)| **other == *obj.borrow(), |
| |(x, _)| Self::hash(storage, x), |
| ) { |
| Ok(bucket) => bucket, |
| // SAFETY: The slot came from `find_or_find_insert_slot()`, and the table wasn't modified since then. |
| Err(insert_slot) => unsafe { |
| shard.insert_in_slot(hash, insert_slot, (Arc::from(obj), SharedValue::new(()))) |
| }, |
| }; |
| // SAFETY: We just retrieved/inserted this bucket. |
| unsafe { Self { arc: bucket.as_ref().0.clone() } } |
| } |
| |
| #[inline] |
| fn select(storage: &'static InternMap<T>, obj: &T) -> (Guard<T>, u64) { |
| let hash = Self::hash(storage, obj); |
| let shard_idx = storage.determine_shard(hash as usize); |
| let shard = &storage.shards()[shard_idx]; |
| (shard.write(), hash) |
| } |
| |
| #[inline] |
| fn hash(storage: &'static InternMap<T>, obj: &T) -> u64 { |
| storage.hasher().hash_one(obj) |
| } |
| } |
| |
| impl<T: Internable + ?Sized> Drop for Interned<T> { |
| #[inline] |
| fn drop(&mut self) { |
| // When the last `Ref` is dropped, remove the object from the global map. |
| if Arc::count(&self.arc) == 2 { |
| // Only `self` and the global map point to the object. |
| |
| self.drop_slow(); |
| } |
| } |
| } |
| |
| impl<T: Internable + ?Sized> Interned<T> { |
| #[cold] |
| fn drop_slow(&mut self) { |
| let storage = T::storage().get(); |
| let (mut shard, hash) = Self::select(storage, &self.arc); |
| |
| if Arc::count(&self.arc) != 2 { |
| // Another thread has interned another copy |
| return; |
| } |
| |
| shard.remove_entry(hash, |(other, _)| **other == *self.arc); |
| |
| // Shrink the backing storage if the shard is less than 50% occupied. |
| if shard.len() * 2 < shard.capacity() { |
| let len = shard.len(); |
| shard.shrink_to(len, |(x, _)| Self::hash(storage, x)); |
| } |
| } |
| } |
| |
| /// Compares interned `Ref`s using pointer equality. |
| impl<T: Internable> PartialEq for Interned<T> { |
| // NOTE: No `?Sized` because `ptr_eq` doesn't work right with trait objects. |
| |
| #[inline] |
| fn eq(&self, other: &Self) -> bool { |
| Arc::ptr_eq(&self.arc, &other.arc) |
| } |
| } |
| |
| impl<T: Internable> Eq for Interned<T> {} |
| |
| impl PartialEq for Interned<str> { |
| fn eq(&self, other: &Self) -> bool { |
| Arc::ptr_eq(&self.arc, &other.arc) |
| } |
| } |
| |
| impl Eq for Interned<str> {} |
| |
| impl<T: Internable + ?Sized> Hash for Interned<T> { |
| fn hash<H: Hasher>(&self, state: &mut H) { |
| // NOTE: Cast disposes vtable pointer / slice/str length. |
| state.write_usize(Arc::as_ptr(&self.arc) as *const () as usize) |
| } |
| } |
| |
| impl<T: Internable + ?Sized> AsRef<T> for Interned<T> { |
| #[inline] |
| fn as_ref(&self) -> &T { |
| &self.arc |
| } |
| } |
| |
| impl<T: Internable + ?Sized> Deref for Interned<T> { |
| type Target = T; |
| |
| #[inline] |
| fn deref(&self) -> &Self::Target { |
| &self.arc |
| } |
| } |
| |
| impl<T: Internable + ?Sized> Clone for Interned<T> { |
| fn clone(&self) -> Self { |
| Self { arc: self.arc.clone() } |
| } |
| } |
| |
| impl<T: Debug + Internable + ?Sized> Debug for Interned<T> { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| (*self.arc).fmt(f) |
| } |
| } |
| |
| impl<T: Display + Internable + ?Sized> Display for Interned<T> { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| (*self.arc).fmt(f) |
| } |
| } |
| |
| pub struct InternStorage<T: ?Sized> { |
| map: OnceLock<InternMap<T>>, |
| } |
| |
| #[allow( |
| clippy::new_without_default, |
| reason = "this a const fn, so it can't be default yet. See <https://github.com/rust-lang/rust/issues/63065>" |
| )] |
| impl<T: ?Sized> InternStorage<T> { |
| pub const fn new() -> Self { |
| Self { map: OnceLock::new() } |
| } |
| } |
| |
| impl<T: Internable + ?Sized> InternStorage<T> { |
| fn get(&self) -> &InternMap<T> { |
| self.map.get_or_init(DashMap::default) |
| } |
| } |
| |
| pub trait Internable: Hash + Eq + 'static { |
| fn storage() -> &'static InternStorage<Self>; |
| } |
| |
| /// Implements `Internable` for a given list of types, making them usable with `Interned`. |
| #[macro_export] |
| #[doc(hidden)] |
| macro_rules! _impl_internable { |
| ( $($t:path),+ $(,)? ) => { $( |
| impl $crate::Internable for $t { |
| fn storage() -> &'static $crate::InternStorage<Self> { |
| static STORAGE: $crate::InternStorage<$t> = $crate::InternStorage::new(); |
| &STORAGE |
| } |
| } |
| )+ }; |
| } |
| |
| pub use crate::_impl_internable as impl_internable; |
| |
| impl_internable!(str,); |