Skip to main content

stet_core/
dict.rs

1// stet - A PostScript Interpreter
2// Copyright (c) 2026 Scott Bowman
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! PostScript dictionary storage.
6//!
7//! Dictionaries are backed by `HashMap` and identified by `EntityId`.
8//! An entity table provides indirection for save/restore COW semantics.
9
10use rustc_hash::FxHashMap as HashMap;
11
12use crate::entity_table::EntityTable;
13use crate::object::{EntityId, NameId, ObjFlags, PsObject};
14
15/// Key type for PostScript dictionary entries.
16#[derive(Clone, Debug, PartialEq, Eq, Hash)]
17pub enum DictKey {
18    Name(NameId),
19    Int(i32),
20    Real(u64), // f64 bits for hashable comparison
21    Bool(bool),
22    String(Vec<u8>), // String keys are copied (per PLRM)
23    Operator(u16),   // Operator opcode
24    /// Identity key for composite objects (array, packedarray, dict).
25    /// Uses (entity_id, start, len) for arrays, (entity_id, 0, 0) for dicts.
26    Identity(u32, u32, u32),
27}
28
29/// A single dictionary with its metadata.
30pub struct DictEntry {
31    pub max_length: usize,
32    pub entries: HashMap<DictKey, PsObject>,
33    pub access: u8,
34    pub name: Vec<u8>,
35}
36
37/// Storage for all PostScript dictionaries.
38///
39/// The entity table maps EntityId → index into `dicts`. For Phase 2 this is
40/// a 1:1 mapping (entity N → dicts\[N\]), but the indirection enables
41/// save_level tracking and future COW.
42pub struct DictStore {
43    dicts: Vec<DictEntry>,
44    pub entities: EntityTable,
45}
46
47impl DictStore {
48    pub fn new() -> Self {
49        Self {
50            dicts: Vec::new(),
51            entities: EntityTable::new(),
52        }
53    }
54
55    /// Allocate a new dictionary with the given maximum length and name.
56    /// Number of dict slots handed out so far; the store's high-water mark.
57    pub fn dict_slots(&self) -> usize {
58        self.dicts.len()
59    }
60
61    /// Release every dict slot and entity from the given marks onward.
62    ///
63    /// Used by `restore` to reclaim the objects a save level created. See
64    /// [`crate::entity_table::EntityTable::truncate`] for the safety argument
65    /// and the `EntityId`-reuse caveat.
66    pub fn truncate_to(&mut self, dicts_len: usize, entity_len: usize) {
67        self.dicts.truncate(dicts_len);
68        self.entities.truncate(entity_len);
69    }
70
71    pub fn allocate(&mut self, max_length: usize, name: &[u8]) -> EntityId {
72        let index = self.dicts.len() as u32;
73        self.dicts.push(DictEntry {
74            max_length,
75            entries: HashMap::with_capacity_and_hasher(max_length.min(64), Default::default()),
76            access: ObjFlags::ACCESS_UNLIMITED,
77            name: name.to_vec(),
78        });
79        // Entity offset = index into dicts vec
80        self.entities.allocate(index, 0, 0, false, 0)
81    }
82
83    /// Allocate with a specific save level and global flag.
84    pub fn allocate_with(
85        &mut self,
86        max_length: usize,
87        name: &[u8],
88        save_level: u16,
89        global: bool,
90        created_after_save: u32,
91    ) -> EntityId {
92        let index = self.dicts.len() as u32;
93        self.dicts.push(DictEntry {
94            max_length,
95            entries: HashMap::with_capacity_and_hasher(max_length.min(64), Default::default()),
96            access: ObjFlags::ACCESS_UNLIMITED,
97            name: name.to_vec(),
98        });
99        self.entities
100            .allocate(index, 0, save_level, global, created_after_save)
101    }
102
103    /// Resolve entity to dict index.
104    fn dict_index(&self, entity: EntityId) -> usize {
105        self.entities.get(entity).offset as usize
106    }
107
108    /// Iterate every allocated dictionary in this store as
109    /// `(EntityId, &DictEntry)`.
110    ///
111    /// Used by the VM audit (see [`crate::vm_audit`]) to sweep global VM for
112    /// PLRM 3.7.2 violations. Iterating entities rather than following
113    /// references from a root set is deliberate: it catches dictionaries that
114    /// nothing reachable points at, and it does not depend on writes having
115    /// gone through [`DictStore::put`].
116    pub fn iter_entities(&self) -> impl Iterator<Item = (EntityId, &DictEntry)> {
117        (0..self.entities.len()).map(|i| {
118            let id = if self.entities.get_by_index(i).is_global() {
119                EntityId::global(i as u32)
120            } else {
121                EntityId::local(i as u32)
122            };
123            (id, &self.dicts[self.dict_index(id)])
124        })
125    }
126
127    /// Look up a key in a dictionary.
128    #[inline]
129    pub fn get(&self, entity: EntityId, key: &DictKey) -> Option<PsObject> {
130        self.dicts[self.dict_index(entity)]
131            .entries
132            .get(key)
133            .copied()
134    }
135
136    /// Insert or update a key-value pair.
137    pub fn put(&mut self, entity: EntityId, key: DictKey, value: PsObject) {
138        let idx = self.dict_index(entity);
139        self.dicts[idx].entries.insert(key, value);
140    }
141
142    /// Check if a key exists.
143    pub fn known(&self, entity: EntityId, key: &DictKey) -> bool {
144        self.dicts[self.dict_index(entity)]
145            .entries
146            .contains_key(key)
147    }
148
149    /// Get the dict's name.
150    pub fn get_name(&self, entity: EntityId) -> &[u8] {
151        &self.dicts[self.dict_index(entity)].name
152    }
153
154    /// Set the dict's name.
155    pub fn set_name(&mut self, entity: EntityId, name: &[u8]) {
156        let idx = self.dict_index(entity);
157        self.dicts[idx].name.clear();
158        self.dicts[idx].name.extend_from_slice(name);
159    }
160
161    /// Current number of entries.
162    pub fn length(&self, entity: EntityId) -> usize {
163        self.dicts[self.dict_index(entity)].entries.len()
164    }
165
166    /// Maximum capacity.
167    pub fn max_length(&self, entity: EntityId) -> usize {
168        self.dicts[self.dict_index(entity)].max_length
169    }
170
171    /// Remove a key.
172    pub fn remove(&mut self, entity: EntityId, key: &DictKey) {
173        let idx = self.dict_index(entity);
174        self.dicts[idx].entries.remove(key);
175    }
176
177    /// Borrow the dict entry.
178    pub fn entry(&self, entity: EntityId) -> &DictEntry {
179        &self.dicts[self.dict_index(entity)]
180    }
181
182    /// Mutably borrow the dict entry.
183    pub fn entry_mut(&mut self, entity: EntityId) -> &mut DictEntry {
184        let idx = self.dict_index(entity);
185        &mut self.dicts[idx]
186    }
187
188    /// Iterate over keys of a dictionary.
189    pub fn keys(&self, entity: EntityId) -> impl Iterator<Item = &DictKey> {
190        self.dicts[self.dict_index(entity)].entries.keys()
191    }
192
193    /// Get the access level of a dictionary.
194    pub fn access(&self, entity: EntityId) -> u8 {
195        self.dicts[self.dict_index(entity)].access
196    }
197
198    /// Require read access on a dict. Returns InvalidAccess if access < READ_ONLY.
199    #[inline]
200    pub fn require_read(&self, entity: EntityId) -> Result<(), crate::error::PsError> {
201        if self.access(entity) >= ObjFlags::ACCESS_READ_ONLY {
202            Ok(())
203        } else {
204            Err(crate::error::PsError::InvalidAccess)
205        }
206    }
207
208    /// Require write access on a dict. Returns InvalidAccess if access < UNLIMITED.
209    #[inline]
210    pub fn require_write(&self, entity: EntityId) -> Result<(), crate::error::PsError> {
211        if self.access(entity) >= ObjFlags::ACCESS_UNLIMITED {
212            Ok(())
213        } else {
214            Err(crate::error::PsError::InvalidAccess)
215        }
216    }
217
218    /// Set the access level of a dictionary.
219    pub fn set_access(&mut self, entity: EntityId, access: u8) {
220        let idx = self.dict_index(entity);
221        self.dicts[idx].access = access;
222    }
223
224    /// Copy a dictionary's contents to a new dict (for COW). Returns the new EntityId.
225    /// The original entity is updated to point at the copy; the returned entity
226    /// points at the original data (the backup).
227    pub fn cow_copy(&mut self, entity: EntityId) -> EntityId {
228        let idx = self.dict_index(entity);
229        let meta = self.entities.get(entity);
230        let save_level = meta.save_level;
231        let is_global = meta.is_global();
232        let created_after_save = meta.created_after_save;
233
234        // Clone the dict
235        let orig = &self.dicts[idx];
236        let copy = DictEntry {
237            max_length: orig.max_length,
238            entries: orig.entries.clone(),
239            access: orig.access,
240            name: orig.name.clone(),
241        };
242
243        let new_index = self.dicts.len() as u32;
244        self.dicts.push(copy);
245
246        // Backup points to original index
247        let backup_id = self.entities.allocate(
248            idx as u32, // original dict index
249            0,
250            save_level,
251            is_global,
252            created_after_save,
253        );
254
255        self.entities.get_mut(backup_id).set_cow_backup();
256
257        // Original entity now points to new copy
258        self.entities.get_mut(entity).offset = new_index;
259
260        backup_id
261    }
262
263    /// Swap indices between two entities (used by restore).
264    pub fn swap_offsets(&mut self, a: EntityId, b: EntityId) {
265        let off_a = self.entities.get(a).offset;
266        let off_b = self.entities.get(b).offset;
267        self.entities.get_mut(a).offset = off_b;
268        self.entities.get_mut(b).offset = off_a;
269    }
270}
271
272impl Default for DictStore {
273    fn default() -> Self {
274        Self::new()
275    }
276}
277
278#[cfg(test)]
279mod tests {
280    use super::*;
281
282    #[test]
283    fn test_dict_basic() {
284        let mut store = DictStore::new();
285        let d = store.allocate(10, b"testdict");
286        let key = DictKey::Name(NameId(0));
287        assert!(!store.known(d, &key));
288
289        store.put(d, key.clone(), PsObject::int(42));
290        assert!(store.known(d, &key));
291        assert_eq!(store.get(d, &key).unwrap().as_i32(), Some(42));
292        assert_eq!(store.length(d), 1);
293        assert_eq!(store.max_length(d), 10);
294
295        store.remove(d, &key);
296        assert!(!store.known(d, &key));
297    }
298
299    #[test]
300    fn test_multiple_dicts() {
301        let mut store = DictStore::new();
302        let d1 = store.allocate(10, b"dict1");
303        let d2 = store.allocate(10, b"dict2");
304        let key = DictKey::Int(1);
305
306        store.put(d1, key.clone(), PsObject::int(100));
307        store.put(d2, key.clone(), PsObject::int(200));
308
309        assert_eq!(store.get(d1, &key).unwrap().as_i32(), Some(100));
310        assert_eq!(store.get(d2, &key).unwrap().as_i32(), Some(200));
311    }
312
313    #[test]
314    fn test_cow_copy() {
315        let mut store = DictStore::new();
316        let d = store.allocate(10, b"test");
317        let key = DictKey::Name(NameId(0));
318        store.put(d, key.clone(), PsObject::int(42));
319
320        let backup = store.cow_copy(d);
321
322        // Modify original — should not affect backup
323        store.put(d, key.clone(), PsObject::int(99));
324        assert_eq!(store.get(d, &key).unwrap().as_i32(), Some(99));
325        assert_eq!(store.get(backup, &key).unwrap().as_i32(), Some(42));
326    }
327
328    #[test]
329    fn test_swap_offsets() {
330        let mut store = DictStore::new();
331        let d1 = store.allocate(10, b"a");
332        let d2 = store.allocate(10, b"b");
333        let key = DictKey::Int(0);
334        store.put(d1, key.clone(), PsObject::int(1));
335        store.put(d2, key.clone(), PsObject::int(2));
336
337        store.swap_offsets(d1, d2);
338        assert_eq!(store.get(d1, &key).unwrap().as_i32(), Some(2));
339        assert_eq!(store.get(d2, &key).unwrap().as_i32(), Some(1));
340    }
341}