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
// stet - A PostScript Interpreter
// Copyright (c) 2026 Scott Bowman
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Dual-arena array storage: routes to global or local `ArrayStore`
//! based on the tag bit in `EntityId`.
use crate::array_store::ArrayStore;
use crate::entity_table::EntityMeta;
use crate::object::{EntityId, PsObject};
/// Dual-arena array store with separate global and local backing stores.
pub struct DualArrayStore {
pub global: ArrayStore,
pub local: ArrayStore,
}
impl DualArrayStore {
pub fn new() -> Self {
Self {
global: ArrayStore::new(),
local: ArrayStore::new(),
}
}
/// Combined high-water mark of both arenas, in object slots.
/// See [`ArrayStore::allocated_objects`].
pub fn allocated_objects(&self) -> usize {
self.global.allocated_objects() + self.local.allocated_objects()
}
#[inline]
fn store(&self, entity: EntityId) -> &ArrayStore {
if entity.is_global() {
&self.global
} else {
&self.local
}
}
#[inline]
fn store_mut(&mut self, entity: EntityId) -> &mut ArrayStore {
if entity.is_global() {
&mut self.global
} else {
&mut self.local
}
}
// --- Allocation ---
/// Allocate `len` null-filled slots 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, len: usize) -> EntityId {
self.local.allocate(len)
}
/// Allocate and copy `items` into 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_from_at_level_zero(&mut self, items: &[PsObject]) -> EntityId {
self.local.allocate_from(items)
}
/// Allocate and copy `items` with a specific save level and global flag.
pub fn allocate_from_with(
&mut self,
items: &[PsObject],
save_level: u16,
global: bool,
created_after_save: u32,
) -> EntityId {
if global {
self.global
.allocate_from_with(items, save_level, global, created_after_save)
} else {
self.local
.allocate_from_with(items, save_level, global, created_after_save)
}
}
/// Allocate with a specific save level and global flag.
pub fn allocate_with(
&mut self,
len: usize,
save_level: u16,
global: bool,
created_after_save: u32,
) -> EntityId {
if global {
self.global
.allocate_with(len, save_level, global, created_after_save)
} else {
self.local
.allocate_with(len, save_level, global, created_after_save)
}
}
// --- Access ---
/// Get a slice of array elements.
pub fn get(&self, entity: EntityId, start: u32, len: u32) -> &[PsObject] {
self.store(entity).get(entity, start, len)
}
/// Get a mutable slice of array elements.
pub fn get_mut(&mut self, entity: EntityId, start: u32, len: u32) -> &mut [PsObject] {
self.store_mut(entity).get_mut(entity, start, len)
}
/// Get a single element.
#[inline]
pub fn get_element(&self, entity: EntityId, index: u32) -> PsObject {
self.store(entity).get_element(entity, index)
}
/// Set a single element.
pub fn set_element(&mut self, entity: EntityId, index: u32, obj: PsObject) {
self.store_mut(entity).set_element(entity, index, obj);
}
// --- 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 = ArrayStore::new();
}
}
impl Default for DualArrayStore {
fn default() -> Self {
Self::new()
}
}