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    pub fn truncate_to(&mut self, data_len: usize, entity_len: usize) {
45        self.data.truncate(data_len);
46        self.entities.truncate(entity_len);
47    }
48
49    /// Allocate `len` null-filled slots, returning an `EntityId`.
50    pub fn allocate(&mut self, len: usize) -> EntityId {
51        let offset = self.data.len() as u32;
52        self.data.resize(self.data.len() + len, PsObject::null());
53        self.entities.allocate(offset, len as u32, 0, false, 0)
54    }
55
56    /// Allocate and copy `items` into the store.
57    pub fn allocate_from(&mut self, items: &[PsObject]) -> EntityId {
58        let offset = self.data.len() as u32;
59        self.data.extend_from_slice(items);
60        self.entities
61            .allocate(offset, items.len() as u32, 0, false, 0)
62    }
63
64    /// Allocate and copy `items` with a specific save level and global flag.
65    pub fn allocate_from_with(
66        &mut self,
67        items: &[PsObject],
68        save_level: u16,
69        global: bool,
70        created_after_save: u32,
71    ) -> EntityId {
72        let offset = self.data.len() as u32;
73        self.data.extend_from_slice(items);
74        self.entities.allocate(
75            offset,
76            items.len() as u32,
77            save_level,
78            global,
79            created_after_save,
80        )
81    }
82
83    /// Allocate with a specific save level and global flag.
84    pub fn allocate_with(
85        &mut self,
86        len: usize,
87        save_level: u16,
88        global: bool,
89        created_after_save: u32,
90    ) -> EntityId {
91        let offset = self.data.len() as u32;
92        self.data.resize(self.data.len() + len, PsObject::null());
93        self.entities
94            .allocate(offset, len as u32, save_level, global, created_after_save)
95    }
96
97    /// Iterate every allocated array in this store as `(EntityId, elements)`.
98    ///
99    /// Used by the VM audit (see [`crate::vm_audit`]) to sweep global VM for
100    /// PLRM 3.7.2 violations. Sweeping the entity table rather than following
101    /// references catches arrays written through [`ArrayStore::get_mut`], which
102    /// hands out `&mut [PsObject]` and so bypasses any per-store checkpoint.
103    pub fn iter_entities(&self) -> impl Iterator<Item = (EntityId, &[PsObject])> {
104        (0..self.entities.len()).map(|i| {
105            let meta = self.entities.get_by_index(i);
106            let id = if meta.is_global() {
107                EntityId::global(i as u32)
108            } else {
109                EntityId::local(i as u32)
110            };
111            let base = meta.offset as usize;
112            (id, &self.data[base..base + meta.len as usize])
113        })
114    }
115
116    /// Get a slice of array elements via entity table indirection.
117    pub fn get(&self, entity: EntityId, start: u32, len: u32) -> &[PsObject] {
118        let base = self.entities.get(entity).offset as usize + start as usize;
119        &self.data[base..base + len as usize]
120    }
121
122    /// Get a mutable slice of array elements via entity table indirection.
123    pub fn get_mut(&mut self, entity: EntityId, start: u32, len: u32) -> &mut [PsObject] {
124        let base = self.entities.get(entity).offset as usize + start as usize;
125        &mut self.data[base..base + len as usize]
126    }
127
128    /// Get a single element.
129    #[inline]
130    pub fn get_element(&self, entity: EntityId, index: u32) -> PsObject {
131        let base = self.entities.get(entity).offset as usize;
132        self.data[base + index as usize]
133    }
134
135    /// Set a single element.
136    pub fn set_element(&mut self, entity: EntityId, index: u32, obj: PsObject) {
137        let base = self.entities.get(entity).offset as usize;
138        self.data[base + index as usize] = obj;
139    }
140
141    /// Copy entity data to a new region (for COW). Returns the new EntityId
142    /// pointing at the backup. The original entity's offset is updated to
143    /// point at the fresh copy.
144    pub fn cow_copy(&mut self, entity: EntityId) -> EntityId {
145        let meta = self.entities.get(entity);
146        let old_offset = meta.offset as usize;
147        let len = meta.len;
148        let save_level = meta.save_level;
149        let is_global = meta.is_global();
150        let created_after_save = meta.created_after_save;
151
152        let temp: Vec<PsObject> = self.data[old_offset..old_offset + len as usize].to_vec();
153        let new_offset = self.data.len() as u32;
154        self.data.extend_from_slice(&temp);
155
156        // Backup entity points to original data
157        let copy_id = self.entities.allocate(
158            meta.offset, // original offset
159            len,
160            save_level,
161            is_global,
162            created_after_save,
163        );
164
165        self.entities.get_mut(copy_id).set_cow_backup();
166
167        // Original entity now points to new copy
168        self.entities.get_mut(entity).offset = new_offset;
169
170        copy_id
171    }
172
173    /// Swap offsets between two entities (used by restore).
174    pub fn swap_offsets(&mut self, a: EntityId, b: EntityId) {
175        let off_a = self.entities.get(a).offset;
176        let off_b = self.entities.get(b).offset;
177        self.entities.get_mut(a).offset = off_b;
178        self.entities.get_mut(b).offset = off_a;
179    }
180}
181
182impl Default for ArrayStore {
183    fn default() -> Self {
184        Self::new()
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use crate::object::PsValue;
192
193    #[test]
194    fn test_allocate_from() {
195        let mut store = ArrayStore::new();
196        let items = [PsObject::int(1), PsObject::int(2), PsObject::int(3)];
197        let id = store.allocate_from(&items);
198        assert_eq!(store.get_element(id, 0).as_i32(), Some(1));
199        assert_eq!(store.get_element(id, 1).as_i32(), Some(2));
200        assert_eq!(store.get_element(id, 2).as_i32(), Some(3));
201    }
202
203    #[test]
204    fn test_allocate_null_filled() {
205        let mut store = ArrayStore::new();
206        let id = store.allocate(3);
207        for i in 0..3 {
208            assert!(matches!(store.get_element(id, i).value, PsValue::Null));
209        }
210    }
211
212    #[test]
213    fn test_set_element() {
214        let mut store = ArrayStore::new();
215        let id = store.allocate(2);
216        store.set_element(id, 0, PsObject::int(42));
217        assert_eq!(store.get_element(id, 0).as_i32(), Some(42));
218    }
219
220    #[test]
221    fn test_subarray_view() {
222        let mut store = ArrayStore::new();
223        let items = [
224            PsObject::int(10),
225            PsObject::int(20),
226            PsObject::int(30),
227            PsObject::int(40),
228        ];
229        let id = store.allocate_from(&items);
230        // Subarray starting at index 1, length 2
231        let sub = store.get(id, 1, 2);
232        assert_eq!(sub[0].as_i32(), Some(20));
233        assert_eq!(sub[1].as_i32(), Some(30));
234    }
235
236    #[test]
237    fn test_cow_copy() {
238        let mut store = ArrayStore::new();
239        let items = [PsObject::int(1), PsObject::int(2), PsObject::int(3)];
240        let id = store.allocate_from(&items);
241
242        let backup = store.cow_copy(id);
243
244        // Modify original — should not affect backup
245        store.set_element(id, 0, PsObject::int(99));
246        assert_eq!(store.get_element(id, 0).as_i32(), Some(99));
247        assert_eq!(store.get_element(backup, 0).as_i32(), Some(1));
248    }
249
250    #[test]
251    fn test_swap_offsets() {
252        let mut store = ArrayStore::new();
253        let id1 = store.allocate_from(&[PsObject::int(1)]);
254        let id2 = store.allocate_from(&[PsObject::int(2)]);
255
256        store.swap_offsets(id1, id2);
257        assert_eq!(store.get_element(id1, 0).as_i32(), Some(2));
258        assert_eq!(store.get_element(id2, 0).as_i32(), Some(1));
259    }
260}