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}