Skip to main content

stet_core/
entity_table.rs

1// stet - A PostScript Interpreter
2// Copyright (c) 2026 Scott Bowman
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! Entity table: indirection layer for arena stores.
6//!
7//! Each composite object (string, array, dict) is identified by an `EntityId`.
8//! The entity table maps `EntityId → EntityMeta`, which records the offset
9//! into the backing store's data vec, the allocated length, the save level
10//! at creation/last COW copy, and flags (global, gc_mark).
11
12use crate::object::EntityId;
13
14/// Metadata for one entity in an arena store.
15#[derive(Clone, Debug)]
16pub struct EntityMeta {
17    /// Offset into the backing store's data vec.
18    pub offset: u32,
19    /// Allocated capacity (number of elements/bytes).
20    pub len: u32,
21    /// Save level when created or last COW-copied.
22    pub save_level: u16,
23    /// Bit 0: is_global, Bit 1: gc_mark (reserved for future use).
24    pub flags: u8,
25    /// Save ID that was active when this entity was created (0 = before any save).
26    /// Used for invalidrestore: entities with created_after_save >= target_save_id
27    /// are "newer than the snapshot being restored."
28    pub created_after_save: u32,
29}
30
31impl EntityMeta {
32    const FLAG_GLOBAL: u8 = 1;
33
34    /// Check if this entity is in global VM.
35    pub fn is_global(&self) -> bool {
36        self.flags & Self::FLAG_GLOBAL != 0
37    }
38
39    /// Set the global flag.
40    pub fn set_global(&mut self, global: bool) {
41        if global {
42            self.flags |= Self::FLAG_GLOBAL;
43        } else {
44            self.flags &= !Self::FLAG_GLOBAL;
45        }
46    }
47}
48
49/// Indirection table mapping `EntityId` to metadata about stored data.
50pub struct EntityTable {
51    entries: Vec<EntityMeta>,
52}
53
54impl EntityTable {
55    /// Create an empty entity table.
56    pub fn new() -> Self {
57        Self {
58            entries: Vec::new(),
59        }
60    }
61
62    /// Allocate a new entity, returning its `EntityId`.
63    /// The returned EntityId is tagged with the global bit based on the `global` param.
64    pub fn allocate(
65        &mut self,
66        offset: u32,
67        len: u32,
68        save_level: u16,
69        global: bool,
70        created_after_save: u32,
71    ) -> EntityId {
72        let index = self.entries.len() as u32;
73        let id = if global {
74            EntityId::global(index)
75        } else {
76            EntityId::local(index)
77        };
78        let mut flags = 0u8;
79        if global {
80            flags |= EntityMeta::FLAG_GLOBAL;
81        }
82        self.entries.push(EntityMeta {
83            offset,
84            len,
85            save_level,
86            flags,
87            created_after_save,
88        });
89        id
90    }
91
92    /// Get metadata for an entity (read-only).
93    #[inline]
94    pub fn get(&self, id: EntityId) -> &EntityMeta {
95        &self.entries[id.raw_index()]
96    }
97
98    /// Get mutable metadata for an entity.
99    pub fn get_mut(&mut self, id: EntityId) -> &mut EntityMeta {
100        &mut self.entries[id.raw_index()]
101    }
102
103    /// Number of entities allocated.
104    pub fn len(&self) -> usize {
105        self.entries.len()
106    }
107
108    /// Whether the table is empty.
109    pub fn is_empty(&self) -> bool {
110        self.entries.is_empty()
111    }
112}
113
114impl Default for EntityTable {
115    fn default() -> Self {
116        Self::new()
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn test_allocate_and_get() {
126        let mut table = EntityTable::new();
127        let id = table.allocate(0, 10, 0, false, 0);
128        assert_eq!(id, EntityId::local(0));
129        assert!(!id.is_global());
130        let meta = table.get(id);
131        assert_eq!(meta.offset, 0);
132        assert_eq!(meta.len, 10);
133        assert_eq!(meta.save_level, 0);
134        assert!(!meta.is_global());
135    }
136
137    #[test]
138    fn test_multiple_allocations() {
139        let mut table = EntityTable::new();
140        let id0 = table.allocate(0, 5, 0, false, 0);
141        let id1 = table.allocate(5, 10, 0, true, 0);
142        assert_eq!(id0, EntityId::local(0));
143        assert_eq!(id1, EntityId::global(1));
144        assert_eq!(table.len(), 2);
145        assert!(!id0.is_global());
146        assert!(id1.is_global());
147    }
148
149    #[test]
150    fn test_get_mut() {
151        let mut table = EntityTable::new();
152        let id = table.allocate(0, 5, 0, false, 0);
153        table.get_mut(id).offset = 100;
154        assert_eq!(table.get(id).offset, 100);
155    }
156
157    #[test]
158    fn test_global_flag() {
159        let mut table = EntityTable::new();
160        let id = table.allocate(0, 5, 0, false, 0);
161        assert!(!table.get(id).is_global());
162        table.get_mut(id).set_global(true);
163        assert!(table.get(id).is_global());
164        table.get_mut(id).set_global(false);
165        assert!(!table.get(id).is_global());
166    }
167
168    #[test]
169    fn test_save_level_tracking() {
170        let mut table = EntityTable::new();
171        let id = table.allocate(0, 5, 1, false, 0);
172        assert_eq!(table.get(id).save_level, 1);
173        table.get_mut(id).save_level = 2;
174        assert_eq!(table.get(id).save_level, 2);
175    }
176
177    #[test]
178    fn test_empty_table() {
179        let table = EntityTable::new();
180        assert_eq!(table.len(), 0);
181        assert!(table.is_empty());
182    }
183
184    #[test]
185    fn test_default() {
186        let table = EntityTable::default();
187        assert!(table.is_empty());
188    }
189
190    #[test]
191    fn test_len_after_allocations() {
192        let mut table = EntityTable::new();
193        table.allocate(0, 1, 0, false, 0);
194        table.allocate(1, 2, 0, false, 0);
195        table.allocate(3, 3, 0, false, 0);
196        assert_eq!(table.len(), 3);
197        assert!(!table.is_empty());
198    }
199}