Skip to main content

stet_core/
dual_string_store.rs

1// stet - A PostScript Interpreter
2// Copyright (c) 2026 Scott Bowman
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! Dual-arena string storage: routes to global or local `StringStore`
6//! based on the tag bit in `EntityId`.
7
8use crate::entity_table::EntityMeta;
9use crate::object::EntityId;
10use crate::string_store::StringStore;
11
12/// Dual-arena string store with separate global and local backing stores.
13pub struct DualStringStore {
14    pub global: StringStore,
15    pub local: StringStore,
16}
17
18impl DualStringStore {
19    pub fn new() -> Self {
20        Self {
21            global: StringStore::new(),
22            local: StringStore::new(),
23        }
24    }
25
26    /// Select the store for a given entity.
27    #[inline]
28    fn store(&self, entity: EntityId) -> &StringStore {
29        if entity.is_global() {
30            &self.global
31        } else {
32            &self.local
33        }
34    }
35
36    /// Select the mutable store for a given entity.
37    #[inline]
38    fn store_mut(&mut self, entity: EntityId) -> &mut StringStore {
39        if entity.is_global() {
40            &mut self.global
41        } else {
42            &mut self.local
43        }
44    }
45
46    // --- Allocation (defaults to local) ---
47
48    /// Allocate `len` zero-filled bytes in local VM, stamped as if no `save` were outstanding.
49    ///
50    /// The entity is stamped `save_level = 0`, `global = false`,
51    /// `created_after_save = 0` — it claims to predate every outstanding
52    /// `save`. That is only true during bootstrap, before any `save` can have
53    /// happened. Everywhere else prefer the VM-aware helpers in
54    /// `stet_ops::vm_ops` (`alloc_dict` / `alloc_array` / `alloc_string`, or
55    /// their `_in` variants): an entity mis-stamped this way is invisible to
56    /// the `invalidrestore` check while still being released by the matching
57    /// `restore`.
58    pub fn allocate_at_level_zero(&mut self, len: usize) -> EntityId {
59        self.local.allocate(len)
60    }
61
62    /// Allocate and copy `bytes` into local VM, stamped as if no `save` were outstanding.
63    ///
64    /// The entity is stamped `save_level = 0`, `global = false`,
65    /// `created_after_save = 0` — it claims to predate every outstanding
66    /// `save`. That is only true during bootstrap, before any `save` can have
67    /// happened. Everywhere else prefer the VM-aware helpers in
68    /// `stet_ops::vm_ops` (`alloc_dict` / `alloc_array` / `alloc_string`, or
69    /// their `_in` variants): an entity mis-stamped this way is invisible to
70    /// the `invalidrestore` check while still being released by the matching
71    /// `restore`.
72    pub fn allocate_from_at_level_zero(&mut self, bytes: &[u8]) -> EntityId {
73        self.local.allocate_from(bytes)
74    }
75
76    /// Allocate and copy `bytes` with a specific save level and global flag.
77    pub fn allocate_from_with(
78        &mut self,
79        bytes: &[u8],
80        save_level: u16,
81        global: bool,
82        created_after_save: u32,
83    ) -> EntityId {
84        if global {
85            self.global
86                .allocate_from_with(bytes, save_level, global, created_after_save)
87        } else {
88            self.local
89                .allocate_from_with(bytes, save_level, global, created_after_save)
90        }
91    }
92
93    /// Allocate with a specific save level and global flag.
94    pub fn allocate_with(
95        &mut self,
96        len: usize,
97        save_level: u16,
98        global: bool,
99        created_after_save: u32,
100    ) -> EntityId {
101        if global {
102            self.global
103                .allocate_with(len, save_level, global, created_after_save)
104        } else {
105            self.local
106                .allocate_with(len, save_level, global, created_after_save)
107        }
108    }
109
110    // --- Access ---
111
112    /// Get a slice of the string data.
113    pub fn get(&self, entity: EntityId, start: u32, len: u32) -> &[u8] {
114        self.store(entity).get(entity, start, len)
115    }
116
117    /// Get a mutable slice of the string data.
118    pub fn get_mut(&mut self, entity: EntityId, start: u32, len: u32) -> &mut [u8] {
119        self.store_mut(entity).get_mut(entity, start, len)
120    }
121
122    /// Set a single byte.
123    pub fn put_byte(&mut self, entity: EntityId, offset: u32, byte: u8) {
124        self.store_mut(entity).put_byte(entity, offset, byte);
125    }
126
127    /// Get a single byte.
128    pub fn get_byte(&self, entity: EntityId, offset: u32) -> u8 {
129        self.store(entity).get_byte(entity, offset)
130    }
131
132    // --- COW ---
133
134    /// COW copy (always local — global entities skip COW).
135    pub fn cow_copy(&mut self, entity: EntityId) -> EntityId {
136        debug_assert!(!entity.is_global(), "COW copy on global entity");
137        self.local.cow_copy(entity)
138    }
139
140    /// Swap offsets between two entities (used by restore, always local).
141    pub fn swap_offsets(&mut self, a: EntityId, b: EntityId) {
142        debug_assert!(
143            !a.is_global() && !b.is_global(),
144            "swap_offsets on global entity"
145        );
146        self.local.swap_offsets(a, b);
147    }
148
149    // --- Metadata access ---
150
151    /// Get entity metadata (read-only).
152    pub fn entity_meta(&self, entity: EntityId) -> &EntityMeta {
153        self.store(entity).entities.get(entity)
154    }
155
156    /// Get mutable entity metadata.
157    pub fn entity_meta_mut(&mut self, entity: EntityId) -> &mut EntityMeta {
158        self.store_mut(entity).entities.get_mut(entity)
159    }
160
161    // --- Stats ---
162
163    /// Access local backing data (for vmstatus approximation).
164    pub fn data(&self) -> &[u8] {
165        self.local.data()
166    }
167
168    /// Total entity count across both stores.
169    pub fn entity_count(&self) -> usize {
170        self.local.entities.len() + self.global.entities.len()
171    }
172
173    /// Reset local VM (for job boundary cleanup).
174    pub fn reset_local(&mut self) {
175        self.local = StringStore::new();
176    }
177}
178
179impl Default for DualStringStore {
180    fn default() -> Self {
181        Self::new()
182    }
183}