Skip to main content

stet_core/
array_store.rs

1// stet - A PostScript Interpreter
2// Copyright (c) 2026 Scott Bowman
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! Storage for PostScript array contents.
6//!
7//! Each array is a contiguous range of `PsObject` values. The `EntityId`
8//! indexes into the entity table, which provides the base offset into the
9//! flat data vector. Subarrays use `start` and `len` fields in the
10//! `PsObject` for view support.
11
12use crate::entity_table::EntityTable;
13use crate::object::{EntityId, PsObject};
14
15/// Storage for PostScript array element data.
16pub struct ArrayStore {
17    data: Vec<PsObject>,
18    pub entities: EntityTable,
19}
20
21impl ArrayStore {
22    pub fn new() -> Self {
23        Self {
24            data: Vec::new(),
25            entities: EntityTable::new(),
26        }
27    }
28
29    /// Number of object slots handed out so far.
30    ///
31    /// The backing arena only ever grows — nothing is reclaimed without a
32    /// `restore` — so this doubles as the store's high-water mark. Useful for
33    /// asserting that a code path which evaluates PostScript repeatedly isn't
34    /// allocating a fresh array per evaluation.
35    pub fn allocated_objects(&self) -> usize {
36        self.data.len()
37    }
38
39    /// Release every object slot and entity from the given marks onward.
40    ///
41    /// Used by `restore` to reclaim the objects a save level created. See
42    /// [`crate::entity_table::EntityTable::truncate`] for the safety argument
43    /// and the `EntityId`-reuse caveat.
44    /// Number of `PsObject` slots currently held.
45    ///
46    /// Used for VM accounting; see [`crate::context::Context::vm_bytes`].
47    pub fn data_len(&self) -> usize {
48        self.data.len()
49    }
50
51    /// Slots currently *reserved*, which is what the allocator actually holds.
52    ///
53    /// VM accounting has to use this rather than the length. The backing
54    /// `Vec` grows geometrically, so a store holding just under the ceiling by
55    /// length asks the allocator for roughly twice that on its next growth —
56    /// which is how `{ 1000000 string pop } loop` still aborted with a 16 GB
57    /// request after a length-based check had passed at 8 GB.
58    pub fn data_capacity(&self) -> usize {
59        self.data.capacity()
60    }
61
62    pub fn truncate_to(&mut self, data_len: usize, entity_len: usize) {
63        self.data.truncate(data_len);
64        self.entities.truncate(entity_len);
65    }
66
67    /// Allocate `len` null-filled slots, returning an `EntityId`.
68    pub fn allocate(&mut self, len: usize) -> EntityId {
69        let offset = self.data.len() as u32;
70        self.data.resize(self.data.len() + len, PsObject::null());
71        self.entities.allocate(offset, len as u32, 0, false, 0)
72    }
73
74    /// Allocate and copy `items` into the store.
75    pub fn allocate_from(&mut self, items: &[PsObject]) -> EntityId {
76        let offset = self.data.len() as u32;
77        self.data.extend_from_slice(items);
78        self.entities
79            .allocate(offset, items.len() as u32, 0, false, 0)
80    }
81
82    /// Allocate and copy `items` with a specific save level and global flag.
83    pub fn allocate_from_with(
84        &mut self,
85        items: &[PsObject],
86        save_level: u16,
87        global: bool,
88        created_after_save: u32,
89    ) -> EntityId {
90        let offset = self.data.len() as u32;
91        self.data.extend_from_slice(items);
92        self.entities.allocate(
93            offset,
94            items.len() as u32,
95            save_level,
96            global,
97            created_after_save,
98        )
99    }
100
101    /// Allocate with a specific save level and global flag.
102    pub fn allocate_with(
103        &mut self,
104        len: usize,
105        save_level: u16,
106        global: bool,
107        created_after_save: u32,
108    ) -> EntityId {
109        let offset = self.data.len() as u32;
110        self.data.resize(self.data.len() + len, PsObject::null());
111        self.entities
112            .allocate(offset, len as u32, save_level, global, created_after_save)
113    }
114
115    /// Iterate every allocated array in this store as `(EntityId, elements)`.
116    ///
117    /// Used by the VM audit (see [`crate::vm_audit`]) to sweep global VM for
118    /// PLRM 3.7.2 violations. Sweeping the entity table rather than following
119    /// references catches arrays written through [`ArrayStore::get_mut`], which
120    /// hands out `&mut [PsObject]` and so bypasses any per-store checkpoint.
121    pub fn iter_entities(&self) -> impl Iterator<Item = (EntityId, &[PsObject])> {
122        (0..self.entities.len()).map(|i| {
123            let meta = self.entities.get_by_index(i);
124            let id = if meta.is_global() {
125                EntityId::global(i as u32)
126            } else {
127                EntityId::local(i as u32)
128            };
129            let base = meta.offset as usize;
130            (id, &self.data[base..base + meta.len as usize])
131        })
132    }
133
134    /// Get a slice of array elements via entity table indirection.
135    pub fn get(&self, entity: EntityId, start: u32, len: u32) -> &[PsObject] {
136        let base = self.entities.get(entity).offset as usize + start as usize;
137        &self.data[base..base + len as usize]
138    }
139
140    /// Get a mutable slice of array elements via entity table indirection.
141    pub fn get_mut(&mut self, entity: EntityId, start: u32, len: u32) -> &mut [PsObject] {
142        let base = self.entities.get(entity).offset as usize + start as usize;
143        &mut self.data[base..base + len as usize]
144    }
145
146    /// Get a single element.
147    #[inline]
148    pub fn get_element(&self, entity: EntityId, index: u32) -> PsObject {
149        let base = self.entities.get(entity).offset as usize;
150        self.data[base + index as usize]
151    }
152
153    /// Set a single element.
154    pub fn set_element(&mut self, entity: EntityId, index: u32, obj: PsObject) {
155        let base = self.entities.get(entity).offset as usize;
156        self.data[base + index as usize] = obj;
157    }
158
159    /// Copy entity data to a new region (for COW). Returns the new EntityId
160    /// pointing at the backup. The original entity's offset is updated to
161    /// point at the fresh copy.
162    pub fn cow_copy(&mut self, entity: EntityId) -> EntityId {
163        let meta = self.entities.get(entity);
164        let old_offset = meta.offset as usize;
165        let len = meta.len;
166        let save_level = meta.save_level;
167        let is_global = meta.is_global();
168        let created_after_save = meta.created_after_save;
169
170        let temp: Vec<PsObject> = self.data[old_offset..old_offset + len as usize].to_vec();
171        let new_offset = self.data.len() as u32;
172        self.data.extend_from_slice(&temp);
173
174        // Backup entity points to original data
175        let copy_id = self.entities.allocate(
176            meta.offset, // original offset
177            len,
178            save_level,
179            is_global,
180            created_after_save,
181        );
182
183        self.entities.get_mut(copy_id).set_cow_backup();
184
185        // Original entity now points to new copy
186        self.entities.get_mut(entity).offset = new_offset;
187
188        copy_id
189    }
190
191    /// Swap offsets between two entities (used by restore).
192    pub fn swap_offsets(&mut self, a: EntityId, b: EntityId) {
193        let off_a = self.entities.get(a).offset;
194        let off_b = self.entities.get(b).offset;
195        self.entities.get_mut(a).offset = off_b;
196        self.entities.get_mut(b).offset = off_a;
197    }
198}
199
200impl Default for ArrayStore {
201    fn default() -> Self {
202        Self::new()
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use crate::object::PsValue;
210
211    #[test]
212    fn test_allocate_from() {
213        let mut store = ArrayStore::new();
214        let items = [PsObject::int(1), PsObject::int(2), PsObject::int(3)];
215        let id = store.allocate_from(&items);
216        assert_eq!(store.get_element(id, 0).as_i32(), Some(1));
217        assert_eq!(store.get_element(id, 1).as_i32(), Some(2));
218        assert_eq!(store.get_element(id, 2).as_i32(), Some(3));
219    }
220
221    #[test]
222    fn test_allocate_null_filled() {
223        let mut store = ArrayStore::new();
224        let id = store.allocate(3);
225        for i in 0..3 {
226            assert!(matches!(store.get_element(id, i).value, PsValue::Null));
227        }
228    }
229
230    #[test]
231    fn test_set_element() {
232        let mut store = ArrayStore::new();
233        let id = store.allocate(2);
234        store.set_element(id, 0, PsObject::int(42));
235        assert_eq!(store.get_element(id, 0).as_i32(), Some(42));
236    }
237
238    #[test]
239    fn test_subarray_view() {
240        let mut store = ArrayStore::new();
241        let items = [
242            PsObject::int(10),
243            PsObject::int(20),
244            PsObject::int(30),
245            PsObject::int(40),
246        ];
247        let id = store.allocate_from(&items);
248        // Subarray starting at index 1, length 2
249        let sub = store.get(id, 1, 2);
250        assert_eq!(sub[0].as_i32(), Some(20));
251        assert_eq!(sub[1].as_i32(), Some(30));
252    }
253
254    #[test]
255    fn test_cow_copy() {
256        let mut store = ArrayStore::new();
257        let items = [PsObject::int(1), PsObject::int(2), PsObject::int(3)];
258        let id = store.allocate_from(&items);
259
260        let backup = store.cow_copy(id);
261
262        // Modify original — should not affect backup
263        store.set_element(id, 0, PsObject::int(99));
264        assert_eq!(store.get_element(id, 0).as_i32(), Some(99));
265        assert_eq!(store.get_element(backup, 0).as_i32(), Some(1));
266    }
267
268    #[test]
269    fn test_swap_offsets() {
270        let mut store = ArrayStore::new();
271        let id1 = store.allocate_from(&[PsObject::int(1)]);
272        let id2 = store.allocate_from(&[PsObject::int(2)]);
273
274        store.swap_offsets(id1, id2);
275        assert_eq!(store.get_element(id1, 0).as_i32(), Some(2));
276        assert_eq!(store.get_element(id2, 0).as_i32(), Some(1));
277    }
278}