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)) + WordBuf::cap(self.indices.load(Ordering::Relaxed)))
222        }
223    }
224
225    /// Called with the world stopped.
226    pub fn trace(&self, t: &mut Tracer) {
227        // Shape keys are global roots.
228        for e in self.iter() {
229            mark_value(t, e.key);
230            mark_value(t, e.value);
231        }
232    }
233
234    /// Live entries in insertion order.
235    pub fn iter(&self) -> impl Iterator<Item = Entry> + '_ {
236        (0..self.raw_len()).filter_map(|i| self.raw_entry(i))
237    }
238
239    /// The entry at `idx` (a tombstone if out of range).
240    pub fn entry(&self, idx: usize) -> Entry {
241        let ents = self.ents();
242        match self.shape() {
243            Some(shape) => match (shape.keys.get(idx), ents.get(idx)) {
244                (Some(&(hash, key)), Some(v)) => Entry { hash, key, value: Value(v.load(Ordering::Relaxed)) },
245                _ => TOMB,
246            },
247            None => match ents.get(idx * W..idx * W + W) {
248                Some(w) => Entry {
249                    hash: w[0].load(Ordering::Relaxed),
250                    key: Value(w[1].load(Ordering::Relaxed)),
251                    value: Value(w[2].load(Ordering::Relaxed)),
252                },
253                None => TOMB,
254            },
255        }
256    }
257
258    /// Number of entry slots in use, tombstones included (for indexed iteration that tolerates
259    /// growth).
260    pub fn raw_len(&self) -> usize {
261        self.used.load(Ordering::Relaxed)
262    }
263    pub fn raw_entry(&self, i: usize) -> Option<Entry> {
264        if i >= self.raw_len() {
265            return None;
266        }
267        Some(self.entry(i)).filter(|e| e.key != Value::TOMBSTONE)
268    }
269
270    pub fn first_live(&self) -> Option<usize> {
271        (0..self.raw_len()).find(|&i| self.raw_entry(i).is_some())
272    }
273
274    pub fn probe(&self, hash: u64, key: Value) -> Probe {
275        let mut check = Vec::new();
276        if let Some(shape) = self.shape() {
277            let n = self.raw_len().min(shape.keys.len());
278            for (i, &(h, k)) in shape.keys[..n].iter().enumerate() {
279                if h == hash {
280                    if k == key {
281                        return Probe::Found(i);
282                    }
283                    check.push(i);
284                }
285            }
286            return if check.is_empty() { Probe::Absent } else { Probe::Check(check) };
287        }
288        let idx = unsafe { WordBuf::slots(self.indices.load(Ordering::Acquire)) };
289        if idx.is_empty() {
290            return Probe::Absent;
291        }
292        let mask = idx.len() - 1;
293        let mut i = (hash as usize) & mask;
294        let mut perturb = hash;
295        // Bounded so a reader racing a rebuild cannot loop forever.
296        for _ in 0..idx.len() {
297            let ix = idx[i].load(Ordering::Relaxed);
298            if ix == EMPTY_IX {
299                break;
300            }
301            let e = self.entry(ix as usize);
302            if e.key != Value::TOMBSTONE && e.hash == hash {
303                if e.key == key {
304                    return Probe::Found(ix as usize);
305                }
306                check.push(ix as usize);
307            }
308            perturb >>= 5;
309            i = (i.wrapping_mul(5).wrapping_add(1).wrapping_add(perturb as usize)) & mask;
310        }
311        if check.is_empty() {
312            Probe::Absent
313        } else {
314            Probe::Check(check)
315        }
316    }
317
318    /// Runs the read `f` without the lock, retrying while writes overlap it. `None` means it kept
319    /// colliding with writers; the caller should take the lock. `f` must only read the table.
320    pub fn read_optimistic<R>(&self, f: &impl Fn(&DictTable) -> R) -> Option<R> {
321        for _ in 0..4 {
322            let s1 = self.seq.load(Ordering::Acquire);
323            if s1 & 1 == 1 {
324                std::hint::spin_loop();
325                continue;
326            }
327            let r = f(self);
328            fence(Ordering::Acquire);
329            if self.seq.load(Ordering::Relaxed) == s1 {
330                return Some(r);
331            }
332        }
333        None
334    }
335
336    // ---- writes -----------------------------------------------------------------------------
337    // Each requires write access per the ownership rules: the owning thread, or the holder of a
338    // shared dict's lock. `access` says which.
339
340    fn begin(&self, access: Access) -> u64 {
341        let s = self.seq.load(Ordering::Relaxed);
342        self.seq.store(s + 1, Ordering::Relaxed);
343        if access == Access::Shared {
344            fence(Ordering::Release);
345        }
346        s
347    }
348
349    fn end(&self, s: u64) {
350        self.seq.store(s + 2, Ordering::Release);
351    }
352
353    /// # Safety
354    /// The caller has write access (see above).
355    pub unsafe fn set_value_at(&self, idx: usize, v: Value, access: Access) {
356        let s = self.begin(access);
357        let slot = if self.shape().is_some() { idx } else { idx * W + 2 };
358        if let Some(w) = self.ents().get(slot) {
359            w.store(v.0, Ordering::Relaxed);
360        }
361        self.end(s);
362    }
363
364    /// Appends a new entry (the caller has established the key is absent).
365    ///
366    /// # Safety
367    /// The caller has write access.
368    pub unsafe fn insert_new(&self, hash: u64, key: Value, value: Value, access: Access) {
369        let s = self.begin(access);
370        if let Some(shape) = self.shape() {
371            if shape.len() < MAX_SHAPE_KEYS {
372                if let Some(next) = shape.add(hash, key) {
373                    let n = self.raw_len();
374                    let cap = WordBuf::cap(self.entries.load(Ordering::Relaxed));
375                    if n == cap {
376                        let buf = WordBuf::alloc((cap * 2).max(4), Value::NIL.0);
377                        for (d, w) in WordBuf::slots(buf).iter().zip(self.ents()) {
378                            d.store(w.load(Ordering::Relaxed), Ordering::Relaxed);
379                        }
380                        release(self.entries.swap(buf, Ordering::AcqRel), access);
381                    }
382                    self.ents()[n].store(value.0, Ordering::Relaxed);
383                    self.shape.store(next as *const Shape as *mut Shape, Ordering::Release);
384                    self.used.store(n + 1, Ordering::Relaxed);
385                    self.live.fetch_add(1, Ordering::Relaxed);
386                    self.end(s);
387                    return;
388                }
389            }
390            self.leave_shape_mode(access);
391        }
392        let n = self.raw_len();
393        let icap = WordBuf::cap(self.indices.load(Ordering::Relaxed));
394        let ecap = WordBuf::cap(self.entries.load(Ordering::Relaxed)) / W;
395        if (n + 1) * 3 >= icap * 2 || n == ecap {
396            self.rebuild(self.len() + 1, access);
397        }
398        let n = self.raw_len();
399        let w = &self.ents()[n * W..n * W + W];
400        w[0].store(hash, Ordering::Relaxed);
401        w[1].store(key.0, Ordering::Relaxed);
402        w[2].store(value.0, Ordering::Relaxed);
403        Self::place(WordBuf::slots(self.indices.load(Ordering::Relaxed)), hash, n);
404        self.used.store(n + 1, Ordering::Relaxed);
405        self.live.fetch_add(1, Ordering::Relaxed);
406        self.end(s);
407    }
408
409    fn place(idx: &[AtomicU64], hash: u64, at: usize) {
410        let mask = idx.len() - 1;
411        let mut i = (hash as usize) & mask;
412        let mut perturb = hash;
413        while idx[i].load(Ordering::Relaxed) != EMPTY_IX {
414            perturb >>= 5;
415            i = (i.wrapping_mul(5).wrapping_add(1).wrapping_add(perturb as usize)) & mask;
416        }
417        idx[i].store(at as u64, Ordering::Relaxed);
418    }
419
420    /// Rebuilds the table layout with room for `want` live entries, dropping tombstones.
421    /// Inside a write section.
422    unsafe fn rebuild(&self, want: usize, access: Access) {
423        let live: Vec<Entry> = self.iter().collect();
424        let ents = WordBuf::alloc((want.max(live.len()) * 2).max(4) * W, Value::NIL.0);
425        let idx = WordBuf::alloc(index_cap(want.max(live.len()) * 2), EMPTY_IX);
426        let (ew, iw) = (WordBuf::slots(ents), WordBuf::slots(idx));
427        for (i, e) in live.iter().enumerate() {
428            ew[i * W].store(e.hash, Ordering::Relaxed);
429            ew[i * W + 1].store(e.key.0, Ordering::Relaxed);
430            ew[i * W + 2].store(e.value.0, Ordering::Relaxed);
431            Self::place(iw, e.hash, i);
432        }
433        release(self.entries.swap(ents, Ordering::AcqRel), access);
434        release(self.indices.swap(idx, Ordering::AcqRel), access);
435        self.used.store(live.len(), Ordering::Relaxed);
436    }
437
438    /// Leaves shape mode, keeping entry indices (entry `i` is slot `i`). Inside a write section.
439    unsafe fn leave_shape_mode(&self, access: Access) {
440        if self.shape().is_none() {
441            return;
442        }
443        let n = self.raw_len();
444        let ents = WordBuf::alloc((n + 1).max(4) * W, Value::NIL.0);
445        let idx = WordBuf::alloc(index_cap(n + 1), EMPTY_IX);
446        let (ew, iw) = (WordBuf::slots(ents), WordBuf::slots(idx));
447        for i in 0..n {
448            let e = self.entry(i);
449            ew[i * W].store(e.hash, Ordering::Relaxed);
450            ew[i * W + 1].store(e.key.0, Ordering::Relaxed);
451            ew[i * W + 2].store(e.value.0, Ordering::Relaxed);
452            Self::place(iw, e.hash, i);
453        }
454        release(self.indices.swap(idx, Ordering::AcqRel), access);
455        let old = self.entries.swap(ents, Ordering::AcqRel);
456        self.shape.store(std::ptr::null_mut(), Ordering::Release);
457        release(old, access);
458    }
459
460    /// Removes the entry at `idx`, returning its value.
461    ///
462    /// # Safety
463    /// The caller has write access.
464    pub unsafe fn remove_at(&self, idx: usize, access: Access) -> Value {
465        let s = self.begin(access);
466        self.leave_shape_mode(access);
467        let mut v = Value::NIL;
468        if let Some(w) = self.ents().get(idx * W..idx * W + W) {
469            v = Value(w[2].load(Ordering::Relaxed));
470            // The index slot keeps pointing at the tombstone so probe chains stay intact.
471            w[1].store(Value::TOMBSTONE.0, Ordering::Relaxed);
472            w[2].store(Value::NIL.0, Ordering::Relaxed);
473            self.live.fetch_sub(1, Ordering::Relaxed);
474        }
475        self.end(s);
476        v
477    }
478
479    /// # Safety
480    /// The caller has write access.
481    pub unsafe fn clear(&self, access: Access) {
482        let s = self.begin(access);
483        if self.shape().is_some() {
484            self.shape.store(*ROOT_SHAPE as *const Shape as *mut Shape, Ordering::Release);
485        }
486        self.used.store(0, Ordering::Relaxed);
487        self.live.store(0, Ordering::Relaxed);
488        release(self.entries.swap(WordBuf::empty(), Ordering::AcqRel), access);
489        release(self.indices.swap(WordBuf::empty(), Ordering::AcqRel), access);
490        self.end(s);
491    }
492}
493
494impl Drop for DictTable {
495    fn drop(&mut self) {
496        unsafe {
497            WordBuf::free(*self.entries.get_mut() as *mut u8);
498            WordBuf::free(*self.indices.get_mut() as *mut u8);
499        }
500    }
501}
502
503impl Clone for DictTable {
504    fn clone(&self) -> Self {
505        let n = self.raw_len();
506        if let Some(shape) = self.shape() {
507            let t = DictTable::record(n);
508            for (d, w) in t.ents().iter().zip(&self.ents()[..n.min(self.ents().len())]) {
509                d.store(w.load(Ordering::Relaxed), Ordering::Relaxed);
510            }
511            t.shape.store(shape as *const Shape as *mut Shape, Ordering::Relaxed);
512            t.used.store(n, Ordering::Relaxed);
513            t.live.store(self.len(), Ordering::Relaxed);
514            return t;
515        }
516        let t = DictTable::with_capacity(self.len());
517        for e in self.iter() {
518            // The clone is unpublished: only this thread can see it.
519            unsafe { t.insert_new(e.hash, e.key, e.value, Access::Local) };
520        }
521        t
522    }
523}