Skip to main content

hstr/
dynamic.rs

1use std::{
2    borrow::Cow,
3    cell::RefCell,
4    ffi::c_void,
5    hash::{BuildHasherDefault, Hash, Hasher},
6    mem::ManuallyDrop,
7    num::NonZeroU8,
8    ops::Deref,
9    ptr::NonNull,
10};
11
12use rustc_hash::FxHasher;
13use triomphe::ThinArc;
14
15use crate::{
16    tagged_value::{TaggedValue, MAX_INLINE_LEN},
17    wtf8::Wtf8,
18    Atom, Wtf8Atom, INLINE_TAG, INLINE_TAG_INIT, LEN_OFFSET, TAG_MASK,
19};
20
21#[derive(PartialEq, Eq)]
22pub(crate) struct Metadata {
23    pub hash: u64,
24}
25
26#[derive(Clone, PartialEq, Eq)]
27pub(crate) struct Item(pub ThinArc<Metadata, u8>);
28
29impl Deref for Item {
30    type Target = <ThinArc<Metadata, u8> as Deref>::Target;
31
32    fn deref(&self) -> &Self::Target {
33        &self.0
34    }
35}
36
37impl Hash for Item {
38    fn hash<H: Hasher>(&self, state: &mut H) {
39        state.write_u64(self.0.header.header.hash);
40    }
41}
42
43pub(crate) unsafe fn deref_from(ptr: TaggedValue) -> ManuallyDrop<Item> {
44    let item = restore_arc(ptr);
45
46    ManuallyDrop::new(item)
47}
48
49pub(crate) unsafe fn restore_arc(v: TaggedValue) -> Item {
50    let ptr = v.get_ptr();
51    Item(ThinArc::from_raw(ptr))
52}
53
54/// A store that stores [Atom]s. Can be merged with other [AtomStore]s for
55/// better performance.
56///
57///
58/// # Merging [AtomStore]
59pub struct AtomStore {
60    pub(crate) data: hashbrown::HashMap<Item, (), BuildEntryHasher>,
61}
62
63impl Default for AtomStore {
64    fn default() -> Self {
65        Self {
66            data: hashbrown::HashMap::with_capacity_and_hasher(64, BuildEntryHasher::default()),
67        }
68    }
69}
70
71impl AtomStore {
72    #[inline(always)]
73    pub fn atom<'a>(&mut self, text: impl Into<Cow<'a, str>>) -> Atom {
74        atom_in(self, &text.into())
75    }
76
77    #[inline(always)]
78    pub fn wtf8_atom<'a>(&mut self, text: impl Into<Cow<'a, Wtf8>>) -> Wtf8Atom {
79        wtf8_atom_in(self, text.into().as_bytes())
80    }
81
82    fn gc(&mut self) {
83        self.data.retain(|item, _| {
84            let count = ThinArc::strong_count(&item.0);
85            debug_assert!(count > 0);
86            count > 1
87        });
88    }
89}
90
91thread_local! {
92    static GLOBAL_DATA: RefCell<AtomStore> = Default::default();
93}
94
95/// Cleans up atoms in the global store that are no longer referenced.
96pub fn global_atom_store_gc() {
97    GLOBAL_DATA.with(|global| {
98        let mut store = global.borrow_mut();
99        store.gc();
100    });
101}
102
103pub(crate) fn global_wtf8_atom(text: &[u8]) -> Wtf8Atom {
104    GLOBAL_DATA.with(|global| {
105        let mut store = global.borrow_mut();
106
107        wtf8_atom_in(&mut *store, text)
108    })
109}
110
111pub(crate) fn global_atom(text: &str) -> Atom {
112    GLOBAL_DATA.with(|global| {
113        let mut store = global.borrow_mut();
114
115        atom_in(&mut *store, text)
116    })
117}
118
119fn wtf8_atom_in<S>(storage: S, text: &[u8]) -> Wtf8Atom
120where
121    S: Storage,
122{
123    let len = text.len();
124
125    if len <= MAX_INLINE_LEN {
126        // INLINE_TAG ensures this is never zero
127        let tag = INLINE_TAG_INIT | ((len as u8) << LEN_OFFSET);
128        let mut unsafe_data = TaggedValue::new_tag(tag);
129        unsafe {
130            unsafe_data.data_mut()[..len].copy_from_slice(text);
131        }
132        return Wtf8Atom { unsafe_data };
133    }
134
135    let hash = calc_hash(text);
136    let entry = storage.insert_entry(text, hash);
137    let entry = ThinArc::into_raw(entry.0) as *mut c_void;
138
139    let ptr: NonNull<c_void> = unsafe {
140        // Safety: Arc::into_raw returns a non-null pointer
141        NonNull::new_unchecked(entry)
142    };
143    debug_assert!(0 == ptr.as_ptr() as u8 & TAG_MASK);
144    Wtf8Atom {
145        unsafe_data: TaggedValue::new_ptr(ptr),
146    }
147}
148
149/// This can create any kind of [Atom], although this lives in the `dynamic`
150/// module.
151fn atom_in<S>(storage: S, text: &str) -> Atom
152where
153    S: Storage,
154{
155    // SAFETY: `text` is valid UTF-8
156    unsafe { Atom::from_wtf8_unchecked(wtf8_atom_in(storage, text.as_bytes())) }
157}
158
159/// Attempts to construct an [Atom] but only if it can be constructed inline.
160/// This is primarily useful in constant contexts.
161pub(crate) const fn inline_atom(text: &str) -> Option<Atom> {
162    let len = text.len();
163    if len <= MAX_INLINE_LEN {
164        // INLINE_TAG ensures this is never zero
165        let tag = INLINE_TAG | ((len as u8) << LEN_OFFSET);
166        let mut unsafe_data = TaggedValue::new_tag(match NonZeroU8::new(tag) {
167            Some(tag) => tag,
168            None => unreachable!(),
169        });
170        // This odd pattern is needed because we cannot create slices from ranges or
171        // ranges at all in constant context.  So we write our own copying loop.
172        unsafe {
173            let data = unsafe_data.data_mut();
174            let bytes = text.as_bytes();
175            let mut i = 0;
176            while i < len {
177                data[i] = bytes[i];
178                i += 1;
179            }
180        }
181        return Some(Atom { unsafe_data });
182    }
183    None
184}
185
186trait Storage {
187    fn insert_entry(self, text: &[u8], hash: u64) -> Item;
188}
189
190impl Storage for &'_ mut AtomStore {
191    fn insert_entry(self, text: &[u8], hash: u64) -> Item {
192        // If the text is too long, interning is not worth it.
193        if text.len() > 512 {
194            return Item(ThinArc::from_header_and_slice(Metadata { hash }, text));
195        }
196
197        let (entry, _) = self
198            .data
199            .raw_entry_mut()
200            .from_hash(hash, |key| {
201                key.header.header.hash == hash && key.slice.eq(text)
202            })
203            .or_insert_with(move || {
204                (
205                    Item(ThinArc::from_header_and_slice(Metadata { hash }, text)),
206                    (),
207                )
208            });
209        entry.clone()
210    }
211}
212
213#[inline(always)]
214fn calc_hash(text: &[u8]) -> u64 {
215    let mut hasher = FxHasher::default();
216    text.hash(&mut hasher);
217    hasher.finish()
218}
219
220type BuildEntryHasher = BuildHasherDefault<EntryHasher>;
221
222/// A "no-op" hasher for [Entry] that returns [Entry::hash]. The design is
223/// inspired by the `nohash-hasher` crate.
224///
225/// Assumption: [Arc]'s implementation of [Hash] is a simple pass-through.
226#[derive(Default)]
227pub(crate) struct EntryHasher {
228    hash: u64,
229    #[cfg(debug_assertions)]
230    write_called: bool,
231}
232
233impl Hasher for EntryHasher {
234    fn finish(&self) -> u64 {
235        #[cfg(debug_assertions)]
236        debug_assert!(
237            self.write_called,
238            "EntryHasher expects write_u64 to have been called",
239        );
240        self.hash
241    }
242
243    fn write(&mut self, _bytes: &[u8]) {
244        panic!("EntryHasher expects to be called with write_u64");
245    }
246
247    fn write_u64(&mut self, val: u64) {
248        #[cfg(debug_assertions)]
249        {
250            debug_assert!(
251                !self.write_called,
252                "EntryHasher expects write_u64 to be called only once",
253            );
254            self.write_called = true;
255        }
256
257        self.hash = val;
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use crate::{dynamic::GLOBAL_DATA, global_atom_store_gc, Atom};
264
265    fn expect_size(expected: usize) {
266        // This is a helper function to count the number of bytes in the global store.
267        GLOBAL_DATA.with(|global| {
268            let store = global.borrow();
269            assert_eq!(store.data.len(), expected);
270        })
271    }
272
273    #[test]
274    fn global_ref_count_dynamic_0() {
275        expect_size(0);
276
277        // The strings should be long enough so that they are not inline even under
278        // feature `atom_size_128`
279        let atom1 = Atom::new("Hello, beautiful world!");
280
281        expect_size(1);
282
283        let atom2 = Atom::new("Hello, beautiful world!");
284
285        expect_size(1);
286
287        // 2 for the two atoms, 1 for the global store
288        assert_eq!(atom1.ref_count(), 3);
289        assert_eq!(atom2.ref_count(), 3);
290
291        drop(atom1);
292
293        expect_size(1);
294
295        // 1 for the atom2, 1 for the global store
296        assert_eq!(atom2.ref_count(), 2);
297
298        drop(atom2);
299
300        expect_size(1);
301        global_atom_store_gc();
302        expect_size(0);
303    }
304
305    #[test]
306    fn global_ref_count_dynamic_1() {
307        expect_size(0);
308
309        {
310            expect_size(0);
311            let atom = Atom::new("Hello, beautiful world!");
312            assert_eq!(atom.ref_count(), 2);
313            expect_size(1);
314        }
315
316        expect_size(1);
317        global_atom_store_gc();
318        expect_size(0);
319
320        {
321            let atom = Atom::new("Hello, beautiful world!");
322            assert_eq!(atom.ref_count(), 2);
323            expect_size(1);
324        }
325        let atom = Atom::new("Hello, beautiful world!");
326        assert_eq!(atom.ref_count(), 2);
327
328        expect_size(1);
329        global_atom_store_gc();
330        expect_size(1);
331
332        drop(atom);
333        expect_size(1);
334        global_atom_store_gc();
335        expect_size(0);
336    }
337}