Skip to main content

catallaxy_runtime/
dict.rs

1//! Insertion-ordered open-addressing hash table (the CPython 3.6+ layout): a dense entry array in
2//! insertion order plus a sparse index array.
3//!
4//! Key equality may require calling user code (`==` on a class), which could re-enter and mutate
5//! the table. So lookups are split: [`DictTable::probe`] lists entries whose hash matches and
6//! whose keys are not bitwise-identical; the caller compares keys (possibly calling user code with
7//! no borrow held) and then acts by entry index, rechecking [`DictTable::version`].
8//!
9//! **Concurrency.** Every word of the table is atomic and replaced arrays are retired to the
10//! collector rather than freed, so a reader racing a writer can never fault; it can only see a
11//! mix of old and new words. A sequence counter (odd while a write is in progress) turns that
12//! into a seqlock: readers of a *shared* dict run optimistically and retry, or fall back to the
13//! object's lock, if a write overlapped ([`DictTable::read_optimistic`]). Optimistic reads never
14//! dereference a key, because the key's object may have been published by the overlapping write.
15//!
16//! **Hidden classes.** `Dyn` records (parsed JSON objects) start in *shape mode*: their string
17//! keys live in a shared, immutable [`Shape`], reached through a transition tree from the empty
18//! shape, and the table holds only the values. Objects parsed from the same kind of document share
19//! one shape, so a `Dyn` member access can cache `(shape, slot)` per site. Deleting a key, adding
20//! a non-string key, or growing past [`MAX_SHAPE_KEYS`] converts the table to the ordinary layout.
21
22use std::sync::atomic::{fence, AtomicPtr, AtomicU64, AtomicUsize, Ordering};
23use std::sync::{LazyLock, Once};
24
25use catallaxy_gc::{RootSource, Tracer};
26use parking_lot::Mutex;
27use rustc_hash::FxHashMap;
28
29use crate::buf::{release, WordBuf};
30use crate::object::{mark_value, str_of};
31use crate::ownership::Access;
32use crate::value::Value;
33
34/// Records with more keys than this use the ordinary table layout.
35pub const MAX_SHAPE_KEYS: usize = 32;
36/// Past this many shapes, new records use the ordinary layout (bounds memory for documents with
37/// unbounded key sets).
38const MAX_SHAPES: usize = 1 << 16;
39
40/// A hidden class: an ordered list of string keys. Shapes are immutable and live forever; their
41/// keys are frozen strings kept alive by a global root.
42pub struct Shape {
43    pub id: u32,
44    keys: Vec<(u64, Value)>,
45    transitions: Mutex<FxHashMap<Box<str>, &'static Shape>>,
46}
47
48struct ShapeRegistry {
49    count: usize,
50    /// Every key held by any shape, for the GC.
51    keys: Vec<Value>,
52}
53
54static REGISTRY: Mutex<ShapeRegistry> = Mutex::new(ShapeRegistry { count: 1, keys: Vec::new() });
55static ROOT_SHAPE: LazyLock<&'static Shape> =
56    LazyLock::new(|| Box::leak(Box::new(Shape { id: 0, keys: Vec::new(), transitions: Mutex::default() })));
57static SHAPE_ROOTS: Once = Once::new();
58
59struct ShapeKeyRoots;
60impl RootSource for ShapeKeyRoots {
61    fn trace_roots(&self, t: &mut Tracer) {
62        // The world is stopped, so no mutator holds the registry lock.
63        for &k in REGISTRY.lock().keys.iter() {
64            mark_value(t, k);
65        }
66    }
67}
68
69impl Shape {
70    pub fn len(&self) -> usize {
71        self.keys.len()
72    }
73    pub fn is_empty(&self) -> bool {
74        self.keys.is_empty()
75    }
76
77    /// The shape with `key` appended, or `None` if `key` is not a string or the shape limit is
78    /// reached.
79    fn add(&'static self, hash: u64, key: Value) -> Option<&'static Shape> {
80        let k = str_of(key)?;
81        let mut tr = self.transitions.lock();
82        if let Some(&next) = tr.get(k) {
83            return Some(next);
84        }
85        let mut reg = REGISTRY.lock();
86        if reg.count >= MAX_SHAPES {
87            return None;
88        }
89        SHAPE_ROOTS.call_once(|| catallaxy_gc::add_global_roots(Box::new(ShapeKeyRoots)));
90        // The registry is a global root, which minor collections do not scan.
91        if let Some(h) = key.header() {
92            h.tenure();
93        }
94        let id = reg.count as u32;
95        reg.count += 1;
96        reg.keys.push(key);
97        let mut keys = self.keys.clone();
98        keys.push((hash, key));
99        let next: &'static Shape = Box::leak(Box::new(Shape { id, keys, transitions: Mutex::default() }));
100        tr.insert(k.into(), next);
101        Some(next)
102    }
103}
104
105#[derive(Copy, Clone)]
106pub struct Entry {
107    pub hash: u64,
108    pub key: Value,
109    pub value: Value,
110}
111
112const TOMB: Entry = Entry { hash: 0, key: Value::TOMBSTONE, value: Value::NIL };
113
114/// An empty index slot.
115const EMPTY_IX: u64 = u64::MAX;
116/// Words per entry in table mode: hash, key, value.
117const W: usize = 3;
118
119pub struct DictTable {
120    /// Even when stable, odd while a write is in progress; advances by 2 per write. Doubles as
121    /// the version that lookups recheck.
122    seq: AtomicU64,
123    live: AtomicUsize,
124    /// Entries (or shape slots) in use, tombstones included.
125    used: AtomicUsize,
126    /// Table mode: `W` words per entry. Shape mode: one value per slot.
127    entries: AtomicPtr<WordBuf>,
128    /// Table mode only: entry indices, `EMPTY_IX` when free. Power-of-two capacity.
129    indices: AtomicPtr<WordBuf>,
130    /// Set in shape mode.
131    shape: AtomicPtr<Shape>,
132}
133
134/// Result of probing for a key.
135pub enum Probe {
136    /// An entry with a bitwise-identical key (always equal).
137    Found(usize),
138    /// No entry with this hash; nothing to compare.
139    Absent,
140    /// Entries with the same hash that need an equality check, in probe order.
141    Check(Vec<usize>),
142}
143
144impl Default for DictTable {
145    fn default() -> Self {
146        DictTable::new()
147    }
148}
149
150fn index_cap(n: usize) -> usize {
151    (n * 3 / 2 + 1).next_power_of_two().max(8)
152}
153
154impl DictTable {
155    pub fn new() -> Self {
156        DictTable {
157            seq: AtomicU64::new(0),
158            live: AtomicUsize::new(0),
159            used: AtomicUsize::new(0),
160            entries: AtomicPtr::new(WordBuf::empty()),
161            indices: AtomicPtr::new(WordBuf::empty()),
162            shape: AtomicPtr::new(std::ptr::null_mut()),
163        }
164    }
165
166    pub fn with_capacity(n: usize) -> Self {
167        let t = DictTable::new();
168        if n > 0 {
169            t.entries.store(WordBuf::alloc(n * W, Value::NIL.0), Ordering::Relaxed);
170            t.indices.store(WordBuf::alloc(index_cap(n), EMPTY_IX), Ordering::Relaxed);
171        }
172        t
173    }
174
175    /// An empty table in shape mode, for `Dyn` records.
176    pub fn record(n: usize) -> Self {
177        let t = DictTable::new();
178        t.entries.store(WordBuf::alloc(n, Value::NIL.0), Ordering::Relaxed);
179        t.shape.store(*ROOT_SHAPE as *const Shape as *mut Shape, Ordering::Relaxed);
180        t
181    }
182
183    // ---- reads ------------------------------------------------------------------------------
184    // Safe to call while another thread writes: every load is atomic and bounds-checked against
185    // the buffer it came from. Under a racing write the answer may be wrong (and is discarded by
186    // the seqlock), but never out of bounds.
187
188    #[inline]
189    fn ents(&self) -> &[AtomicU64] {
190        unsafe { WordBuf::slots(self.entries.load(Ordering::Acquire)) }
191    }
192
193    /// The hidden class, if the table is in shape mode.
194    #[inline]
195    pub fn shape(&self) -> Option<&'static Shape> {
196        let p = self.shape.load(Ordering::Acquire);
197        (!p.is_null()).then(|| unsafe { &*p })
198    }
199
200    /// The value in `slot` of a shape-mode table.
201    #[inline]
202    pub fn slot_value(&self, slot: usize) -> Option<Value> {
203        if slot >= self.used.load(Ordering::Relaxed) {
204            return None;
205        }
206        self.ents().get(slot).map(|w| Value(w.load(Ordering::Relaxed)))
207    }
208
209    pub fn len(&self) -> usize {
210        self.live.load(Ordering::Relaxed)
211    }
212    pub fn is_empty(&self) -> bool {
213        self.len() == 0
214    }
215    pub fn version(&self) -> u64 {
216        self.seq.load(Ordering::Acquire)
217    }
218
219    pub fn heap_bytes(&self) -> usize {
220        unsafe {
221            8 * (WordBuf::cap(self.entries.load(Ordering::Relaxed))
222                + WordBuf::cap(self.indices.load(Ordering::Relaxed)))
223        }
224    }
225
226    /// Called with the world stopped.
227    pub fn trace(&self, t: &mut Tracer) {
228        // Shape keys are global roots.
229        for e in self.iter() {
230            mark_value(t, e.key);
231            mark_value(t, e.value);
232        }
233    }
234
235    /// Live entries in insertion order.
236    pub fn iter(&self) -> impl Iterator<Item = Entry> + '_ {
237        (0..self.raw_len()).filter_map(|i| self.raw_entry(i))
238    }
239
240    /// The entry at `idx` (a tombstone if out of range).
241    pub fn entry(&self, idx: usize) -> Entry {
242        let ents = self.ents();
243        match self.shape() {
244            Some(shape) => match (shape.keys.get(idx), ents.get(idx)) {
245                (Some(&(hash, key)), Some(v)) => Entry { hash, key, value: Value(v.load(Ordering::Relaxed)) },
246                _ => TOMB,
247            },
248            None => match ents.get(idx * W..idx * W + W) {
249                Some(w) => Entry {
250                    hash: w[0].load(Ordering::Relaxed),
251                    key: Value(w[1].load(Ordering::Relaxed)),
252                    value: Value(w[2].load(Ordering::Relaxed)),
253                },
254                None => TOMB,
255            },
256        }
257    }
258
259    /// Number of entry slots in use, tombstones included (for indexed iteration that tolerates
260    /// growth).
261    pub fn raw_len(&self) -> usize {
262        self.used.load(Ordering::Relaxed)
263    }
264    pub fn raw_entry(&self, i: usize) -> Option<Entry> {
265        if i >= self.raw_len() {
266            return None;
267        }
268        Some(self.entry(i)).filter(|e| e.key != Value::TOMBSTONE)
269    }
270
271    pub fn first_live(&self) -> Option<usize> {
272        (0..self.raw_len()).find(|&i| self.raw_entry(i).is_some())
273    }
274
275    pub fn probe(&self, hash: u64, key: Value) -> Probe {
276        let mut check = Vec::new();
277        if let Some(shape) = self.shape() {
278            let n = self.raw_len().min(shape.keys.len());
279            for (i, &(h, k)) in shape.keys[..n].iter().enumerate() {
280                if h == hash {
281                    if k == key {
282                        return Probe::Found(i);
283                    }
284                    check.push(i);
285                }
286            }
287            return if check.is_empty() { Probe::Absent } else { Probe::Check(check) };
288        }
289        let idx = unsafe { WordBuf::slots(self.indices.load(Ordering::Acquire)) };
290        if idx.is_empty() {
291            return Probe::Absent;
292        }
293        let mask = idx.len() - 1;
294        let mut i = (hash as usize) & mask;
295        let mut perturb = hash;
296        // Bounded so a reader racing a rebuild cannot loop forever.
297        for _ in 0..idx.len() {
298            let ix = idx[i].load(Ordering::Relaxed);
299            if ix == EMPTY_IX {
300                break;
301            }
302            let e = self.entry(ix as usize);
303            if e.key != Value::TOMBSTONE && e.hash == hash {
304                if e.key == key {
305                    return Probe::Found(ix as usize);
306                }
307                check.push(ix as usize);
308            }
309            perturb >>= 5;
310            i = (i.wrapping_mul(5).wrapping_add(1).wrapping_add(perturb as usize)) & mask;
311        }
312        if check.is_empty() {
313            Probe::Absent
314        } else {
315            Probe::Check(check)
316        }
317    }
318
319    /// Runs the read `f` without the lock, retrying while writes overlap it. `None` means it kept
320    /// colliding with writers; the caller should take the lock. `f` must only read the table.
321    pub fn read_optimistic<R>(&self, f: &impl Fn(&DictTable) -> R) -> Option<R> {
322        for _ in 0..4 {
323            let s1 = self.seq.load(Ordering::Acquire);
324            if s1 & 1 == 1 {
325                std::hint::spin_loop();
326                continue;
327            }
328            let r = f(self);
329            fence(Ordering::Acquire);
330            if self.seq.load(Ordering::Relaxed) == s1 {
331                return Some(r);
332            }
333        }
334        None
335    }
336
337    // ---- writes -----------------------------------------------------------------------------
338    // Each requires write access per the ownership rules: the owning thread, or the holder of a
339    // shared dict's lock. `access` says which.
340
341    fn begin(&self, access: Access) -> u64 {
342        let s = self.seq.load(Ordering::Relaxed);
343        self.seq.store(s + 1, Ordering::Relaxed);
344        if access == Access::Shared {
345            fence(Ordering::Release);
346        }
347        s
348    }
349
350    fn end(&self, s: u64) {
351        self.seq.store(s + 2, Ordering::Release);
352    }
353
354    /// # Safety
355    /// The caller has write access (see above).
356    pub unsafe fn set_value_at(&self, idx: usize, v: Value, access: Access) {
357        let s = self.begin(access);
358        let slot = if self.shape().is_some() { idx } else { idx * W + 2 };
359        if let Some(w) = self.ents().get(slot) {
360            w.store(v.0, Ordering::Relaxed);
361        }
362        self.end(s);
363    }
364
365    /// Appends a new entry (the caller has established the key is absent).
366    ///
367    /// # Safety
368    /// The caller has write access.
369    pub unsafe fn insert_new(&self, hash: u64, key: Value, value: Value, access: Access) {
370        let s = self.begin(access);
371        if let Some(shape) = self.shape() {
372            if shape.len() < MAX_SHAPE_KEYS {
373                if let Some(next) = shape.add(hash, key) {
374                    let n = self.raw_len();
375                    let cap = WordBuf::cap(self.entries.load(Ordering::Relaxed));
376                    if n == cap {
377                        let buf = WordBuf::alloc((cap * 2).max(4), Value::NIL.0);
378                        for (d, w) in WordBuf::slots(buf).iter().zip(self.ents()) {
379                            d.store(w.load(Ordering::Relaxed), Ordering::Relaxed);
380                        }
381                        release(self.entries.swap(buf, Ordering::AcqRel), access);
382                    }
383                    self.ents()[n].store(value.0, Ordering::Relaxed);
384                    self.shape.store(next as *const Shape as *mut Shape, Ordering::Release);
385                    self.used.store(n + 1, Ordering::Relaxed);
386                    self.live.fetch_add(1, Ordering::Relaxed);
387                    self.end(s);
388                    return;
389                }
390            }
391            self.leave_shape_mode(access);
392        }
393        let n = self.raw_len();
394        let icap = WordBuf::cap(self.indices.load(Ordering::Relaxed));
395        let ecap = WordBuf::cap(self.entries.load(Ordering::Relaxed)) / W;
396        if (n + 1) * 3 >= icap * 2 || n == ecap {
397            self.rebuild(self.len() + 1, access);
398        }
399        let n = self.raw_len();
400        let w = &self.ents()[n * W..n * W + W];
401        w[0].store(hash, Ordering::Relaxed);
402        w[1].store(key.0, Ordering::Relaxed);
403        w[2].store(value.0, Ordering::Relaxed);
404        Self::place(WordBuf::slots(self.indices.load(Ordering::Relaxed)), hash, n);
405        self.used.store(n + 1, Ordering::Relaxed);
406        self.live.fetch_add(1, Ordering::Relaxed);
407        self.end(s);
408    }
409
410    fn place(idx: &[AtomicU64], hash: u64, at: usize) {
411        let mask = idx.len() - 1;
412        let mut i = (hash as usize) & mask;
413        let mut perturb = hash;
414        while idx[i].load(Ordering::Relaxed) != EMPTY_IX {
415            perturb >>= 5;
416            i = (i.wrapping_mul(5).wrapping_add(1).wrapping_add(perturb as usize)) & mask;
417        }
418        idx[i].store(at as u64, Ordering::Relaxed);
419    }
420
421    /// Rebuilds the table layout with room for `want` live entries, dropping tombstones.
422    /// Inside a write section.
423    unsafe fn rebuild(&self, want: usize, access: Access) {
424        let live: Vec<Entry> = self.iter().collect();
425        let ents = WordBuf::alloc((want.max(live.len()) * 2).max(4) * W, Value::NIL.0);
426        let idx = WordBuf::alloc(index_cap(want.max(live.len()) * 2), EMPTY_IX);
427        let (ew, iw) = (WordBuf::slots(ents), WordBuf::slots(idx));
428        for (i, e) in live.iter().enumerate() {
429            ew[i * W].store(e.hash, Ordering::Relaxed);
430            ew[i * W + 1].store(e.key.0, Ordering::Relaxed);
431            ew[i * W + 2].store(e.value.0, Ordering::Relaxed);
432            Self::place(iw, e.hash, i);
433        }
434        release(self.entries.swap(ents, Ordering::AcqRel), access);
435        release(self.indices.swap(idx, Ordering::AcqRel), access);
436        self.used.store(live.len(), Ordering::Relaxed);
437    }
438
439    /// Leaves shape mode, keeping entry indices (entry `i` is slot `i`). Inside a write section.
440    unsafe fn leave_shape_mode(&self, access: Access) {
441        if self.shape().is_none() {
442            return;
443        }
444        let n = self.raw_len();
445        let ents = WordBuf::alloc((n + 1).max(4) * W, Value::NIL.0);
446        let idx = WordBuf::alloc(index_cap(n + 1), EMPTY_IX);
447        let (ew, iw) = (WordBuf::slots(ents), WordBuf::slots(idx));
448        for i in 0..n {
449            let e = self.entry(i);
450            ew[i * W].store(e.hash, Ordering::Relaxed);
451            ew[i * W + 1].store(e.key.0, Ordering::Relaxed);
452            ew[i * W + 2].store(e.value.0, Ordering::Relaxed);
453            Self::place(iw, e.hash, i);
454        }
455        release(self.indices.swap(idx, Ordering::AcqRel), access);
456        let old = self.entries.swap(ents, Ordering::AcqRel);
457        self.shape.store(std::ptr::null_mut(), Ordering::Release);
458        release(old, access);
459    }
460
461    /// Removes the entry at `idx`, returning its value.
462    ///
463    /// # Safety
464    /// The caller has write access.
465    pub unsafe fn remove_at(&self, idx: usize, access: Access) -> Value {
466        let s = self.begin(access);
467        self.leave_shape_mode(access);
468        let mut v = Value::NIL;
469        if let Some(w) = self.ents().get(idx * W..idx * W + W) {
470            v = Value(w[2].load(Ordering::Relaxed));
471            // The index slot keeps pointing at the tombstone so probe chains stay intact.
472            w[1].store(Value::TOMBSTONE.0, Ordering::Relaxed);
473            w[2].store(Value::NIL.0, Ordering::Relaxed);
474            self.live.fetch_sub(1, Ordering::Relaxed);
475        }
476        self.end(s);
477        v
478    }
479
480    /// # Safety
481    /// The caller has write access.
482    pub unsafe fn clear(&self, access: Access) {
483        let s = self.begin(access);
484        if self.shape().is_some() {
485            self.shape.store(*ROOT_SHAPE as *const Shape as *mut Shape, Ordering::Release);
486        }
487        self.used.store(0, Ordering::Relaxed);
488        self.live.store(0, Ordering::Relaxed);
489        release(self.entries.swap(WordBuf::empty(), Ordering::AcqRel), access);
490        release(self.indices.swap(WordBuf::empty(), Ordering::AcqRel), access);
491        self.end(s);
492    }
493}
494
495impl Drop for DictTable {
496    fn drop(&mut self) {
497        unsafe {
498            WordBuf::free(*self.entries.get_mut() as *mut u8);
499            WordBuf::free(*self.indices.get_mut() as *mut u8);
500        }
501    }
502}
503
504impl Clone for DictTable {
505    fn clone(&self) -> Self {
506        let n = self.raw_len();
507        if let Some(shape) = self.shape() {
508            let t = DictTable::record(n);
509            for (d, w) in t.ents().iter().zip(&self.ents()[..n.min(self.ents().len())]) {
510                d.store(w.load(Ordering::Relaxed), Ordering::Relaxed);
511            }
512            t.shape.store(shape as *const Shape as *mut Shape, Ordering::Relaxed);
513            t.used.store(n, Ordering::Relaxed);
514            t.live.store(self.len(), Ordering::Relaxed);
515            return t;
516        }
517        let t = DictTable::with_capacity(self.len());
518        for e in self.iter() {
519            // The clone is unpublished: only this thread can see it.
520            unsafe { t.insert_new(e.hash, e.key, e.value, Access::Local) };
521        }
522        t
523    }
524}