//! Global `Arc`-based object interning infrastructure. //! //! Eventually this should probably be replaced with salsa-based interning. use std::{ collections::HashMap, fmt::{self, Debug, Display}, hash::{BuildHasherDefault, Hash, Hasher}, ops::Deref, sync::Arc, }; use dashmap::{lock::RwLockWriteGuard, DashMap, SharedValue}; use once_cell::sync::OnceCell; use rustc_hash::FxHasher; use crate::generics::GenericParams; type InternMap = DashMap, (), BuildHasherDefault>; type Guard = RwLockWriteGuard<'static, HashMap, SharedValue<()>, BuildHasherDefault>>; pub struct Interned { arc: Arc, } impl Interned { pub fn new(obj: T) -> Self { match Interned::lookup(&obj) { Ok(this) => this, Err(shard) => { let arc = Arc::new(obj); Self::alloc(arc, shard) } } } } impl Interned { fn lookup(obj: &T) -> Result> { let storage = T::storage().get(); let shard_idx = storage.determine_map(obj); let shard = &storage.shards()[shard_idx]; let shard = shard.write(); // 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. // FIXME: avoid double lookup/hashing by using raw entry API (once stable, or when // hashbrown can be plugged into dashmap) match shard.get_key_value(obj) { Some((arc, _)) => Ok(Self { arc: arc.clone() }), None => Err(shard), } } fn alloc(arc: Arc, mut shard: Guard) -> Self { let arc2 = arc.clone(); shard.insert(arc2, SharedValue::new(())); Self { arc } } } impl Interned { pub fn new_str(s: &str) -> Self { match Interned::lookup(s) { Ok(this) => this, Err(shard) => { let arc = Arc::::from(s); Self::alloc(arc, shard) } } } } impl Drop for Interned { #[inline] fn drop(&mut self) { // When the last `Ref` is dropped, remove the object from the global map. if Arc::strong_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 storage = T::storage().get(); let shard_idx = storage.determine_map(&self.arc); let shard = &storage.shards()[shard_idx]; let mut shard = shard.write(); // FIXME: avoid double lookup let (arc, _) = shard.get_key_value(&self.arc).expect("interned value removed prematurely"); if Arc::strong_count(arc) != 2 { // Another thread has interned another copy return; } shard.remove(&self.arc); // 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: OnceCell>, } impl InternStorage { pub const fn new() -> Self { Self { map: OnceCell::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 Internable for $t { fn storage() -> &'static InternStorage { static STORAGE: InternStorage<$t> = InternStorage::new(); &STORAGE } } )+ }; } pub use crate::_impl_internable as impl_internable; impl_internable!( crate::type_ref::TypeRef, crate::type_ref::TraitRef, crate::type_ref::TypeBound, crate::path::ModPath, GenericParams, str, );