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    pub fn allocate(&mut self, max_length: usize, name: &[u8]) -> EntityId {
57        let index = self.dicts.len() as u32;
58        self.dicts.push(DictEntry {
59            max_length,
60            entries: HashMap::with_capacity_and_hasher(max_length.min(64), Default::default()),
61            access: ObjFlags::ACCESS_UNLIMITED,
62            name: name.to_vec(),
63        });
64        // Entity offset = index into dicts vec
65        self.entities.allocate(index, 0, 0, false, 0)
66    }
67
68    /// Allocate with a specific save level and global flag.
69    pub fn allocate_with(
70        &mut self,
71        max_length: usize,
72        name: &[u8],
73        save_level: u16,
74        global: bool,
75        created_after_save: u32,
76    ) -> EntityId {
77        let index = self.dicts.len() as u32;
78        self.dicts.push(DictEntry {
79            max_length,
80            entries: HashMap::with_capacity_and_hasher(max_length.min(64), Default::default()),
81            access: ObjFlags::ACCESS_UNLIMITED,
82            name: name.to_vec(),
83        });
84        self.entities
85            .allocate(index, 0, save_level, global, created_after_save)
86    }
87
88    /// Resolve entity to dict index.
89    fn dict_index(&self, entity: EntityId) -> usize {
90        self.entities.get(entity).offset as usize
91    }
92
93    /// Look up a key in a dictionary.
94    #[inline]
95    pub fn get(&self, entity: EntityId, key: &DictKey) -> Option<PsObject> {
96        self.dicts[self.dict_index(entity)]
97            .entries
98            .get(key)
99            .copied()
100    }
101
102    /// Insert or update a key-value pair.
103    pub fn put(&mut self, entity: EntityId, key: DictKey, value: PsObject) {
104        let idx = self.dict_index(entity);
105        self.dicts[idx].entries.insert(key, value);
106    }
107
108    /// Check if a key exists.
109    pub fn known(&self, entity: EntityId, key: &DictKey) -> bool {
110        self.dicts[self.dict_index(entity)]
111            .entries
112            .contains_key(key)
113    }
114
115    /// Get the dict's name.
116    pub fn get_name(&self, entity: EntityId) -> &[u8] {
117        &self.dicts[self.dict_index(entity)].name
118    }
119
120    /// Set the dict's name.
121    pub fn set_name(&mut self, entity: EntityId, name: &[u8]) {
122        let idx = self.dict_index(entity);
123        self.dicts[idx].name.clear();
124        self.dicts[idx].name.extend_from_slice(name);
125    }
126
127    /// Current number of entries.
128    pub fn length(&self, entity: EntityId) -> usize {
129        self.dicts[self.dict_index(entity)].entries.len()
130    }
131
132    /// Maximum capacity.
133    pub fn max_length(&self, entity: EntityId) -> usize {
134        self.dicts[self.dict_index(entity)].max_length
135    }
136
137    /// Remove a key.
138    pub fn remove(&mut self, entity: EntityId, key: &DictKey) {
139        let idx = self.dict_index(entity);
140        self.dicts[idx].entries.remove(key);
141    }
142
143    /// Borrow the dict entry.
144    pub fn entry(&self, entity: EntityId) -> &DictEntry {
145        &self.dicts[self.dict_index(entity)]
146    }
147
148    /// Mutably borrow the dict entry.
149    pub fn entry_mut(&mut self, entity: EntityId) -> &mut DictEntry {
150        let idx = self.dict_index(entity);
151        &mut self.dicts[idx]
152    }
153
154    /// Iterate over keys of a dictionary.
155    pub fn keys(&self, entity: EntityId) -> impl Iterator<Item = &DictKey> {
156        self.dicts[self.dict_index(entity)].entries.keys()
157    }
158
159    /// Get the access level of a dictionary.
160    pub fn access(&self, entity: EntityId) -> u8 {
161        self.dicts[self.dict_index(entity)].access
162    }
163
164    /// Require read access on a dict. Returns InvalidAccess if access < READ_ONLY.
165    #[inline]
166    pub fn require_read(&self, entity: EntityId) -> Result<(), crate::error::PsError> {
167        if self.access(entity) >= ObjFlags::ACCESS_READ_ONLY {
168            Ok(())
169        } else {
170            Err(crate::error::PsError::InvalidAccess)
171        }
172    }
173
174    /// Require write access on a dict. Returns InvalidAccess if access < UNLIMITED.
175    #[inline]
176    pub fn require_write(&self, entity: EntityId) -> Result<(), crate::error::PsError> {
177        if self.access(entity) >= ObjFlags::ACCESS_UNLIMITED {
178            Ok(())
179        } else {
180            Err(crate::error::PsError::InvalidAccess)
181        }
182    }
183
184    /// Set the access level of a dictionary.
185    pub fn set_access(&mut self, entity: EntityId, access: u8) {
186        let idx = self.dict_index(entity);
187        self.dicts[idx].access = access;
188    }
189
190    /// Copy a dictionary's contents to a new dict (for COW). Returns the new EntityId.
191    /// The original entity is updated to point at the copy; the returned entity
192    /// points at the original data (the backup).
193    pub fn cow_copy(&mut self, entity: EntityId) -> EntityId {
194        let idx = self.dict_index(entity);
195        let meta = self.entities.get(entity);
196        let save_level = meta.save_level;
197        let is_global = meta.is_global();
198        let created_after_save = meta.created_after_save;
199
200        // Clone the dict
201        let orig = &self.dicts[idx];
202        let copy = DictEntry {
203            max_length: orig.max_length,
204            entries: orig.entries.clone(),
205            access: orig.access,
206            name: orig.name.clone(),
207        };
208
209        let new_index = self.dicts.len() as u32;
210        self.dicts.push(copy);
211
212        // Backup points to original index
213        let backup_id = self.entities.allocate(
214            idx as u32, // original dict index
215            0,
216            save_level,
217            is_global,
218            created_after_save,
219        );
220
221        // Original entity now points to new copy
222        self.entities.get_mut(entity).offset = new_index;
223
224        backup_id
225    }
226
227    /// Swap indices between two entities (used by restore).
228    pub fn swap_offsets(&mut self, a: EntityId, b: EntityId) {
229        let off_a = self.entities.get(a).offset;
230        let off_b = self.entities.get(b).offset;
231        self.entities.get_mut(a).offset = off_b;
232        self.entities.get_mut(b).offset = off_a;
233    }
234}
235
236impl Default for DictStore {
237    fn default() -> Self {
238        Self::new()
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn test_dict_basic() {
248        let mut store = DictStore::new();
249        let d = store.allocate(10, b"testdict");
250        let key = DictKey::Name(NameId(0));
251        assert!(!store.known(d, &key));
252
253        store.put(d, key.clone(), PsObject::int(42));
254        assert!(store.known(d, &key));
255        assert_eq!(store.get(d, &key).unwrap().as_i32(), Some(42));
256        assert_eq!(store.length(d), 1);
257        assert_eq!(store.max_length(d), 10);
258
259        store.remove(d, &key);
260        assert!(!store.known(d, &key));
261    }
262
263    #[test]
264    fn test_multiple_dicts() {
265        let mut store = DictStore::new();
266        let d1 = store.allocate(10, b"dict1");
267        let d2 = store.allocate(10, b"dict2");
268        let key = DictKey::Int(1);
269
270        store.put(d1, key.clone(), PsObject::int(100));
271        store.put(d2, key.clone(), PsObject::int(200));
272
273        assert_eq!(store.get(d1, &key).unwrap().as_i32(), Some(100));
274        assert_eq!(store.get(d2, &key).unwrap().as_i32(), Some(200));
275    }
276
277    #[test]
278    fn test_cow_copy() {
279        let mut store = DictStore::new();
280        let d = store.allocate(10, b"test");
281        let key = DictKey::Name(NameId(0));
282        store.put(d, key.clone(), PsObject::int(42));
283
284        let backup = store.cow_copy(d);
285
286        // Modify original — should not affect backup
287        store.put(d, key.clone(), PsObject::int(99));
288        assert_eq!(store.get(d, &key).unwrap().as_i32(), Some(99));
289        assert_eq!(store.get(backup, &key).unwrap().as_i32(), Some(42));
290    }
291
292    #[test]
293    fn test_swap_offsets() {
294        let mut store = DictStore::new();
295        let d1 = store.allocate(10, b"a");
296        let d2 = store.allocate(10, b"b");
297        let key = DictKey::Int(0);
298        store.put(d1, key.clone(), PsObject::int(1));
299        store.put(d2, key.clone(), PsObject::int(2));
300
301        store.swap_offsets(d1, d2);
302        assert_eq!(store.get(d1, &key).unwrap().as_i32(), Some(2));
303        assert_eq!(store.get(d2, &key).unwrap().as_i32(), Some(1));
304    }
305}