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::candidates`] 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
9use catallaxy_gc::Tracer;
10
11use crate::object::mark_value;
12use crate::value::Value;
13
14#[derive(Copy, Clone)]
15pub struct Entry {
16    pub hash: u64,
17    pub key: Value,
18    pub value: Value,
19}
20
21const EMPTY: u32 = u32::MAX;
22
23#[derive(Default)]
24pub struct DictTable {
25    entries: Vec<Entry>,
26    indices: Vec<u32>,
27    live: usize,
28    version: u64,
29}
30
31/// Result of probing for a key.
32pub enum Probe {
33    /// An entry with a bitwise-identical key (always equal).
34    Found(usize),
35    /// No entry with this hash; nothing to compare.
36    Absent,
37    /// Entries with the same hash that need an equality check, in probe order.
38    Check(Vec<usize>),
39}
40
41impl DictTable {
42    pub fn new() -> Self {
43        DictTable::default()
44    }
45
46    pub fn with_capacity(n: usize) -> Self {
47        let mut t = DictTable::default();
48        if n > 0 {
49            t.entries.reserve(n);
50            t.indices = vec![EMPTY; (n * 3 / 2).next_power_of_two().max(8)];
51        }
52        t
53    }
54
55    pub fn len(&self) -> usize {
56        self.live
57    }
58    pub fn is_empty(&self) -> bool {
59        self.live == 0
60    }
61    pub fn version(&self) -> u64 {
62        self.version
63    }
64
65    pub fn heap_bytes(&self) -> usize {
66        self.entries.capacity() * std::mem::size_of::<Entry>() + self.indices.capacity() * 4
67    }
68
69    pub fn trace(&self, t: &mut Tracer) {
70        for e in &self.entries {
71            if e.key != Value::TOMBSTONE {
72                mark_value(t, e.key);
73                mark_value(t, e.value);
74            }
75        }
76    }
77
78    /// Live entries in insertion order.
79    pub fn iter(&self) -> impl Iterator<Item = &Entry> {
80        self.entries.iter().filter(|e| e.key != Value::TOMBSTONE)
81    }
82
83    pub fn entry(&self, idx: usize) -> &Entry {
84        &self.entries[idx]
85    }
86
87    pub fn set_value_at(&mut self, idx: usize, v: Value) {
88        self.entries[idx].value = v;
89    }
90
91    /// Entry index of the n-th live entry (for indexed iteration that tolerates growth).
92    pub fn raw_len(&self) -> usize {
93        self.entries.len()
94    }
95    pub fn raw_entry(&self, i: usize) -> Option<&Entry> {
96        self.entries.get(i).filter(|e| e.key != Value::TOMBSTONE)
97    }
98
99    pub fn probe(&self, hash: u64, key: Value) -> Probe {
100        if self.indices.is_empty() {
101            return Probe::Absent;
102        }
103        let mask = self.indices.len() - 1;
104        let mut i = (hash as usize) & mask;
105        let mut perturb = hash;
106        let mut check = Vec::new();
107        loop {
108            let ix = self.indices[i];
109            if ix == EMPTY {
110                return if check.is_empty() { Probe::Absent } else { Probe::Check(check) };
111            }
112            let e = &self.entries[ix as usize];
113            if e.key != Value::TOMBSTONE && e.hash == hash {
114                if e.key == key {
115                    return Probe::Found(ix as usize);
116                }
117                check.push(ix as usize);
118            }
119            perturb >>= 5;
120            i = (i.wrapping_mul(5).wrapping_add(1).wrapping_add(perturb as usize)) & mask;
121        }
122    }
123
124    /// Appends a new entry (the caller has established the key is absent).
125    pub fn insert_new(&mut self, hash: u64, key: Value, value: Value) {
126        if (self.entries.len() + 1) * 3 >= self.indices.len() * 2 {
127            self.grow();
128        }
129        let idx = self.entries.len() as u32;
130        self.entries.push(Entry { hash, key, value });
131        self.place(hash, idx);
132        self.live += 1;
133        self.version += 1;
134    }
135
136    fn place(&mut self, hash: u64, idx: u32) {
137        let mask = self.indices.len() - 1;
138        let mut i = (hash as usize) & mask;
139        let mut perturb = hash;
140        while self.indices[i] != EMPTY {
141            perturb >>= 5;
142            i = (i.wrapping_mul(5).wrapping_add(1).wrapping_add(perturb as usize)) & mask;
143        }
144        self.indices[i] = idx;
145    }
146
147    fn grow(&mut self) {
148        // Compact tombstones and rebuild the index.
149        if self.live < self.entries.len() {
150            self.entries.retain(|e| e.key != Value::TOMBSTONE);
151        }
152        let want = ((self.live + 1) * 3).next_power_of_two().max(8);
153        self.indices = vec![EMPTY; want];
154        for i in 0..self.entries.len() {
155            let h = self.entries[i].hash;
156            self.place(h, i as u32);
157        }
158        crate::note_external(want * 4);
159    }
160
161    /// Removes the entry at `idx`, returning its value.
162    pub fn remove_at(&mut self, idx: usize) -> Value {
163        let e = &mut self.entries[idx];
164        let v = e.value;
165        e.key = Value::TOMBSTONE;
166        e.value = Value::NIL;
167        self.live -= 1;
168        self.version += 1;
169        // The index slot keeps pointing at the tombstone so probe chains stay intact.
170        v
171    }
172
173    pub fn clear(&mut self) {
174        self.entries.clear();
175        self.indices.clear();
176        self.live = 0;
177        self.version += 1;
178    }
179
180    pub fn first_live(&self) -> Option<usize> {
181        self.entries.iter().position(|e| e.key != Value::TOMBSTONE)
182    }
183}
184
185impl Clone for DictTable {
186    fn clone(&self) -> Self {
187        let mut t = DictTable::with_capacity(self.live);
188        for e in self.iter() {
189            t.insert_new(e.hash, e.key, e.value);
190        }
191        t
192    }
193}