//! Global `Arc`-based object interning infrastructure. //! //! Eventually this should probably be replaced with salsa-based interning. use std::{ fmt::{self, Debug, Display}, hash::{BuildHasherDefault, Hash, Hasher}, ops::Deref, sync::OnceLock, }; use dashmap::{DashMap, SharedValue}; use hashbrown::{hash_map::RawEntryMut, HashMap}; use rustc_hash::FxHasher; use triomphe::Arc; type InternMap = DashMap, (), BuildHasherDefault>; type Guard = dashmap::RwLockWriteGuard< 'static, HashMap, SharedValue<()>, BuildHasherDefault>, >; pub struct Interned { arc: Arc, } impl Interned { pub fn new(obj: T) -> Self { let (mut shard, hash) = Self::select(&obj); // 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. match shard.raw_entry_mut().from_key_hashed_nocheck(hash, &obj) { RawEntryMut::Occupied(occ) => Self { arc: occ.key().clone() }, RawEntryMut::Vacant(vac) => Self { arc: vac.insert_hashed_nocheck(hash, Arc::new(obj), SharedValue::new(())).0.clone(), }, } } } impl Interned { pub fn new_str(s: &str) -> Self { let (mut shard, hash) = Self::select(s); // 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. match shard.raw_entry_mut().from_key_hashed_nocheck(hash, s) { RawEntryMut::Occupied(occ) => Self { arc: occ.key().clone() }, RawEntryMut::Vacant(vac) => Self { arc: vac.insert_hashed_nocheck(hash, Arc::from(s), SharedValue::new(())).0.clone(), }, } } } impl Interned { #[inline] fn select(obj: &T) -> (Guard, u64) { let storage = T::storage().get(); let hash = { let mut hasher = std::hash::BuildHasher::build_hasher(storage.hasher()); obj.hash(&mut hasher); hasher.finish() }; let shard_idx = storage.determine_shard(hash as usize); let shard = &storage.shards()[shard_idx]; (shard.write(), hash) } } impl Drop for Interned { #[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 Interned { #[cold] fn drop_slow(&mut self) { let (mut shard, hash) = Self::select(&self.arc); if Arc::count(&self.arc) != 2 { // Another thread has interned another copy return; } match shard.raw_entry_mut().from_key_hashed_nocheck(hash, &self.arc) { RawEntryMut::Occupied(occ) => occ.remove(), RawEntryMut::Vacant(_) => unreachable!(), }; // Shrink the backing storage if the shard is less than 50% occupied. if shard.len() * 2 < shard.capacity() { shard.shrink_to_fit(); } } } /// Compares interned `Ref`s using pointer equality. impl PartialEq for Interned { // 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 Eq for Interned {} impl PartialEq for Interned { fn eq(&self, other: &Self) -> bool { Arc::ptr_eq(&self.arc, &other.arc) } } impl Eq for Interned {} impl Hash for Interned { fn hash(&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 AsRef for Interned { #[inline] fn as_ref(&self) -> &T { &self.arc } } impl Deref for Interned { type Target = T; #[inline] fn deref(&self) -> &Self::Target { &self.arc } } impl Clone for Interned { fn clone(&self) -> Self { Self { arc: self.arc.clone() } } } impl Debug for Interned { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { (*self.arc).fmt(f) } } impl Display for Interned { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { (*self.arc).fmt(f) } } pub struct InternStorage { map: OnceLock>, } impl InternStorage { pub const fn new() -> Self { Self { map: OnceLock::new() } } } impl InternStorage { fn get(&self) -> &InternMap { self.map.get_or_init(DashMap::default) } } pub trait Internable: Hash + Eq + 'static { fn storage() -> &'static InternStorage; } /// 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 { static STORAGE: $crate::InternStorage<$t> = $crate::InternStorage::new(); &STORAGE } } )+ }; } pub use crate::_impl_internable as impl_internable; impl_internable!(str,);