1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
// stet - A PostScript Interpreter
// Copyright (c) 2026 Scott Bowman
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Dual-arena dictionary storage: routes to global or local `DictStore`
//! based on the tag bit in `EntityId`.
use crate::dict::{DictEntry, DictKey, DictStore};
use crate::entity_table::EntityMeta;
use crate::object::{EntityId, PsObject};
/// Dual-arena dict store with separate global and local backing stores.
pub struct DualDictStore {
pub global: DictStore,
pub local: DictStore,
}
impl DualDictStore {
pub fn new() -> Self {
Self {
global: DictStore::new(),
local: DictStore::new(),
}
}
#[inline]
fn store(&self, entity: EntityId) -> &DictStore {
if entity.is_global() {
&self.global
} else {
&self.local
}
}
#[inline]
fn store_mut(&mut self, entity: EntityId) -> &mut DictStore {
if entity.is_global() {
&mut self.global
} else {
&mut self.local
}
}
// --- Allocation ---
/// Allocate a dictionary in local VM, stamped as if no `save` were outstanding.
///
/// The entity is stamped `save_level = 0`, `global = false`,
/// `created_after_save = 0` — it claims to predate every outstanding
/// `save`. That is only true during bootstrap, before any `save` can have
/// happened. Everywhere else prefer the VM-aware helpers in
/// `stet_ops::vm_ops` (`alloc_dict` / `alloc_array` / `alloc_string`, or
/// their `_in` variants): an entity mis-stamped this way is invisible to
/// the `invalidrestore` check while still being released by the matching
/// `restore`.
pub fn allocate_at_level_zero(&mut self, max_length: usize, name: &[u8]) -> EntityId {
self.local.allocate(max_length, name)
}
/// Allocate with a specific save level and global flag.
pub fn allocate_with(
&mut self,
max_length: usize,
name: &[u8],
save_level: u16,
global: bool,
created_after_save: u32,
) -> EntityId {
if global {
self.global
.allocate_with(max_length, name, save_level, global, created_after_save)
} else {
self.local
.allocate_with(max_length, name, save_level, global, created_after_save)
}
}
// --- Access ---
/// Look up a key in a dictionary.
#[inline]
pub fn get(&self, entity: EntityId, key: &DictKey) -> Option<PsObject> {
self.store(entity).get(entity, key)
}
/// Insert or update a key-value pair.
pub fn put(&mut self, entity: EntityId, key: DictKey, value: PsObject) {
self.store_mut(entity).put(entity, key, value);
}
/// Check if a key exists.
pub fn known(&self, entity: EntityId, key: &DictKey) -> bool {
self.store(entity).known(entity, key)
}
/// Get the dict's name.
pub fn get_name(&self, entity: EntityId) -> &[u8] {
self.store(entity).get_name(entity)
}
/// Set the dict's name.
pub fn set_name(&mut self, entity: EntityId, name: &[u8]) {
self.store_mut(entity).set_name(entity, name);
}
/// Current number of entries.
pub fn length(&self, entity: EntityId) -> usize {
self.store(entity).length(entity)
}
/// Maximum capacity.
pub fn max_length(&self, entity: EntityId) -> usize {
self.store(entity).max_length(entity)
}
/// Remove a key.
pub fn remove(&mut self, entity: EntityId, key: &DictKey) {
self.store_mut(entity).remove(entity, key);
}
/// Borrow the dict entry.
pub fn entry(&self, entity: EntityId) -> &DictEntry {
self.store(entity).entry(entity)
}
/// Mutably borrow the dict entry.
pub fn entry_mut(&mut self, entity: EntityId) -> &mut DictEntry {
self.store_mut(entity).entry_mut(entity)
}
/// Iterate over keys of a dictionary.
pub fn keys(&self, entity: EntityId) -> impl Iterator<Item = &DictKey> {
self.store(entity).keys(entity)
}
/// Get the access level of a dictionary.
pub fn access(&self, entity: EntityId) -> u8 {
self.store(entity).access(entity)
}
/// Require read access on a dict.
#[inline]
pub fn require_read(&self, entity: EntityId) -> Result<(), crate::error::PsError> {
self.store(entity).require_read(entity)
}
/// Require write access on a dict.
#[inline]
pub fn require_write(&self, entity: EntityId) -> Result<(), crate::error::PsError> {
self.store(entity).require_write(entity)
}
/// Set the access level of a dictionary.
pub fn set_access(&mut self, entity: EntityId, access: u8) {
self.store_mut(entity).set_access(entity, access);
}
// --- COW ---
/// COW copy (always local — global entities skip COW).
pub fn cow_copy(&mut self, entity: EntityId) -> EntityId {
debug_assert!(!entity.is_global(), "COW copy on global entity");
self.local.cow_copy(entity)
}
/// Swap offsets between two entities (used by restore, always local).
pub fn swap_offsets(&mut self, a: EntityId, b: EntityId) {
debug_assert!(
!a.is_global() && !b.is_global(),
"swap_offsets on global entity"
);
self.local.swap_offsets(a, b);
}
// --- Metadata access ---
/// Get entity metadata (read-only).
pub fn entity_meta(&self, entity: EntityId) -> &EntityMeta {
self.store(entity).entities.get(entity)
}
/// Get mutable entity metadata.
pub fn entity_meta_mut(&mut self, entity: EntityId) -> &mut EntityMeta {
self.store_mut(entity).entities.get_mut(entity)
}
// --- Stats ---
/// Total entity count across both stores.
pub fn entity_count(&self) -> usize {
self.local.entities.len() + self.global.entities.len()
}
/// Reset local VM (for job boundary cleanup).
pub fn reset_local(&mut self) {
self.local = DictStore::new();
}
}
impl Default for DualDictStore {
fn default() -> Self {
Self::new()
}
}