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    /// Allocate `len` null-filled slots, returning an `EntityId`.
30    pub fn allocate(&mut self, len: usize) -> EntityId {
31        let offset = self.data.len() as u32;
32        self.data.resize(self.data.len() + len, PsObject::null());
33        self.entities.allocate(offset, len as u32, 0, false, 0)
34    }
35
36    /// Allocate and copy `items` into the store.
37    pub fn allocate_from(&mut self, items: &[PsObject]) -> EntityId {
38        let offset = self.data.len() as u32;
39        self.data.extend_from_slice(items);
40        self.entities
41            .allocate(offset, items.len() as u32, 0, false, 0)
42    }
43
44    /// Allocate and copy `items` with a specific save level and global flag.
45    pub fn allocate_from_with(
46        &mut self,
47        items: &[PsObject],
48        save_level: u16,
49        global: bool,
50        created_after_save: u32,
51    ) -> EntityId {
52        let offset = self.data.len() as u32;
53        self.data.extend_from_slice(items);
54        self.entities.allocate(
55            offset,
56            items.len() as u32,
57            save_level,
58            global,
59            created_after_save,
60        )
61    }
62
63    /// Allocate with a specific save level and global flag.
64    pub fn allocate_with(
65        &mut self,
66        len: usize,
67        save_level: u16,
68        global: bool,
69        created_after_save: u32,
70    ) -> EntityId {
71        let offset = self.data.len() as u32;
72        self.data.resize(self.data.len() + len, PsObject::null());
73        self.entities
74            .allocate(offset, len as u32, save_level, global, created_after_save)
75    }
76
77    /// Get a slice of array elements via entity table indirection.
78    pub fn get(&self, entity: EntityId, start: u32, len: u32) -> &[PsObject] {
79        let base = self.entities.get(entity).offset as usize + start as usize;
80        &self.data[base..base + len as usize]
81    }
82
83    /// Get a mutable slice of array elements via entity table indirection.
84    pub fn get_mut(&mut self, entity: EntityId, start: u32, len: u32) -> &mut [PsObject] {
85        let base = self.entities.get(entity).offset as usize + start as usize;
86        &mut self.data[base..base + len as usize]
87    }
88
89    /// Get a single element.
90    #[inline]
91    pub fn get_element(&self, entity: EntityId, index: u32) -> PsObject {
92        let base = self.entities.get(entity).offset as usize;
93        self.data[base + index as usize]
94    }
95
96    /// Set a single element.
97    pub fn set_element(&mut self, entity: EntityId, index: u32, obj: PsObject) {
98        let base = self.entities.get(entity).offset as usize;
99        self.data[base + index as usize] = obj;
100    }
101
102    /// Copy entity data to a new region (for COW). Returns the new EntityId
103    /// pointing at the backup. The original entity's offset is updated to
104    /// point at the fresh copy.
105    pub fn cow_copy(&mut self, entity: EntityId) -> EntityId {
106        let meta = self.entities.get(entity);
107        let old_offset = meta.offset as usize;
108        let len = meta.len;
109        let save_level = meta.save_level;
110        let is_global = meta.is_global();
111        let created_after_save = meta.created_after_save;
112
113        let temp: Vec<PsObject> = self.data[old_offset..old_offset + len as usize].to_vec();
114        let new_offset = self.data.len() as u32;
115        self.data.extend_from_slice(&temp);
116
117        // Backup entity points to original data
118        let copy_id = self.entities.allocate(
119            meta.offset, // original offset
120            len,
121            save_level,
122            is_global,
123            created_after_save,
124        );
125
126        // Original entity now points to new copy
127        self.entities.get_mut(entity).offset = new_offset;
128
129        copy_id
130    }
131
132    /// Swap offsets between two entities (used by restore).
133    pub fn swap_offsets(&mut self, a: EntityId, b: EntityId) {
134        let off_a = self.entities.get(a).offset;
135        let off_b = self.entities.get(b).offset;
136        self.entities.get_mut(a).offset = off_b;
137        self.entities.get_mut(b).offset = off_a;
138    }
139}
140
141impl Default for ArrayStore {
142    fn default() -> Self {
143        Self::new()
144    }
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150    use crate::object::PsValue;
151
152    #[test]
153    fn test_allocate_from() {
154        let mut store = ArrayStore::new();
155        let items = [PsObject::int(1), PsObject::int(2), PsObject::int(3)];
156        let id = store.allocate_from(&items);
157        assert_eq!(store.get_element(id, 0).as_i32(), Some(1));
158        assert_eq!(store.get_element(id, 1).as_i32(), Some(2));
159        assert_eq!(store.get_element(id, 2).as_i32(), Some(3));
160    }
161
162    #[test]
163    fn test_allocate_null_filled() {
164        let mut store = ArrayStore::new();
165        let id = store.allocate(3);
166        for i in 0..3 {
167            assert!(matches!(store.get_element(id, i).value, PsValue::Null));
168        }
169    }
170
171    #[test]
172    fn test_set_element() {
173        let mut store = ArrayStore::new();
174        let id = store.allocate(2);
175        store.set_element(id, 0, PsObject::int(42));
176        assert_eq!(store.get_element(id, 0).as_i32(), Some(42));
177    }
178
179    #[test]
180    fn test_subarray_view() {
181        let mut store = ArrayStore::new();
182        let items = [
183            PsObject::int(10),
184            PsObject::int(20),
185            PsObject::int(30),
186            PsObject::int(40),
187        ];
188        let id = store.allocate_from(&items);
189        // Subarray starting at index 1, length 2
190        let sub = store.get(id, 1, 2);
191        assert_eq!(sub[0].as_i32(), Some(20));
192        assert_eq!(sub[1].as_i32(), Some(30));
193    }
194
195    #[test]
196    fn test_cow_copy() {
197        let mut store = ArrayStore::new();
198        let items = [PsObject::int(1), PsObject::int(2), PsObject::int(3)];
199        let id = store.allocate_from(&items);
200
201        let backup = store.cow_copy(id);
202
203        // Modify original — should not affect backup
204        store.set_element(id, 0, PsObject::int(99));
205        assert_eq!(store.get_element(id, 0).as_i32(), Some(99));
206        assert_eq!(store.get_element(backup, 0).as_i32(), Some(1));
207    }
208
209    #[test]
210    fn test_swap_offsets() {
211        let mut store = ArrayStore::new();
212        let id1 = store.allocate_from(&[PsObject::int(1)]);
213        let id2 = store.allocate_from(&[PsObject::int(2)]);
214
215        store.swap_offsets(id1, id2);
216        assert_eq!(store.get_element(id1, 0).as_i32(), Some(2));
217        assert_eq!(store.get_element(id2, 0).as_i32(), Some(1));
218    }
219}