vj_sim/state/frame.rs
1//! Task 17 — The STATE-owned frame arena for `process::ActivationRecord`
2//! (ledger ST-103, ST-104, ST-110).
3//!
4//! Three separate flat backing stores, one per [`crate::process::ArenaRange`]
5//! -typed field `process::ActivationRecord` embeds (`frame`,
6//! `WaitSpecStorage::EdgeList`, `ForkFrame::children` respectively —
7//! `process/mod.rs`'s own doc comment on `ArenaRange` names all three): this
8//! is where each of the three actually gets STATE-owned backing storage, not
9//! just the pointer-free reference *shape* `process/mod.rs` already defines.
10//! Same bump-allocation discipline as [`super::arena::Arena`] (Task 2/7):
11//! append-only, never shrink/reuse — a range, once handed out, stays valid
12//! and non-aliased for the life of the arena.
13//!
14//! `new_activation_record` (ST-103, ST-110) is `StateEngine`'s one entry
15//! point for minting a real `process::ActivationRecord`: STATE owns both the
16//! frame-word allocation *and* the PC storage (`ActivationRecord.pc` is
17//! itself the resume-point storage ST-110 asks for — re-reading `sched`/
18//! `process`'s own contract types confirms no separate scheduler-owned PC
19//! field exists anywhere in this crate, so this method, not a new field
20//! elsewhere, is the concrete fulfillment).
21
22use crate::process::{ArenaRange, PredicateFn};
23use crate::sched::ProcessId;
24
25use super::EdgeKind;
26use super::StateCellId;
27use super::instance::StateEngine;
28
29/// The STATE-owned backing storage for every `process::ActivationRecord`
30/// this crate hands out (ledger ST-103, ST-104, ST-110). Exactly three flat
31/// `Vec`s, one per `ArenaRange`-typed field an `ActivationRecord` can embed —
32/// see this module's doc comment for why there are exactly three and not,
33/// say, one shared buffer.
34pub struct FrameArena {
35 words: Vec<u64>,
36 edge_terms: Vec<(EdgeKind, StateCellId, Option<PredicateFn>)>,
37 fork_children: Vec<ProcessId>,
38}
39
40impl Default for FrameArena {
41 fn default() -> Self {
42 Self::new()
43 }
44}
45
46impl FrameArena {
47 pub fn new() -> Self {
48 FrameArena {
49 words: Vec::new(),
50 edge_terms: Vec::new(),
51 fork_children: Vec::new(),
52 }
53 }
54
55 /// Bump-allocates `n` new frame words, returning the shared start offset
56 /// as an [`ArenaRange`] — the backing storage for one activation's
57 /// `ActivationRecord.frame`. `n == 0` is a legal no-op that still returns
58 /// a well-defined (empty) range at the current length, matching
59 /// `Arena::alloc_inline_words`' own zero-is-a-no-op contract.
60 pub fn alloc_frame_words(&mut self, n: u32) -> ArenaRange {
61 let base = self.words.len() as u32;
62 self.words.resize(self.words.len() + n as usize, 0);
63 ArenaRange { base, count: n }
64 }
65
66 /// Bump-allocates a copy of `terms` into the edge-term table, returning
67 /// the shared start offset as an [`ArenaRange`] — the backing storage for
68 /// `WaitSpecStorage::EdgeList`. The concrete `StateCellId`s for a given
69 /// suspend point are fixed once elaboration resolves the instance's
70 /// signals and never change for the life of the run, so this table is a
71 /// read-only lookup once written, never a rollback hazard (see this
72 /// module's doc comment / `WaitSpecStorage::EdgeList`'s own doc comment).
73 pub fn alloc_edge_terms(
74 &mut self,
75 terms: &[(EdgeKind, StateCellId, Option<PredicateFn>)],
76 ) -> ArenaRange {
77 let base = self.edge_terms.len() as u32;
78 self.edge_terms.extend_from_slice(terms);
79 ArenaRange {
80 base,
81 count: terms.len() as u32,
82 }
83 }
84
85 /// Bump-allocates `n` reserved fork-child slots, returning the shared
86 /// start offset as an [`ArenaRange`] — the backing storage for
87 /// `ForkFrame::children`. Reserved, not populated: per Resolved design
88 /// decision D1 (`docs/CONTRACTS.md`), concrete child `ProcessId`s are
89 /// assigned dynamically by the scheduler as children are actually
90 /// spawned, so this only reserves the range; the placeholder `0` values
91 /// written here are overwritten by the caller once each child's real
92 /// `ProcessId` is known.
93 pub fn alloc_fork_children(&mut self, n: u32) -> ArenaRange {
94 let base = self.fork_children.len() as u32;
95 self.fork_children
96 .resize(self.fork_children.len() + n as usize, 0);
97 ArenaRange { base, count: n }
98 }
99}
100
101impl StateEngine {
102 /// STATE's one constructor for a real (non-hand-literal)
103 /// `process::ActivationRecord` (ST-103, ST-110): allocates
104 /// `frame_word_count` words from this engine's own `FrameArena` and
105 /// returns a fresh record whose `pc`/`frame` are both STATE-owned
106 /// storage. `pending_wait`/`fork_state` start at their empty defaults
107 /// (`WaitSpecStorage::None`/`None`) — populating them (via
108 /// `alloc_edge_terms`/`alloc_fork_children` above) is the caller's job
109 /// once a suspend point or fork actually occurs, not this constructor's.
110 pub fn new_activation_record(
111 &mut self,
112 pc: u32,
113 frame_word_count: u32,
114 ) -> crate::process::ActivationRecord {
115 let frame = self.frame.alloc_frame_words(frame_word_count);
116 crate::process::ActivationRecord {
117 pc,
118 frame,
119 pending_wait: crate::process::WaitSpecStorage::None,
120 fork_state: None,
121 }
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::super::instance::StateEngine;
128 use super::super::instance::TemplateRegistry;
129 use super::*;
130 use crate::process::ArenaRange;
131
132 #[test]
133 fn activation_records_are_allocated_by_state_not_constructed_ad_hoc() {
134 let mut engine = StateEngine::new(TemplateRegistry::new());
135 let ar1 = engine.new_activation_record(0, 4);
136 let ar2 = engine.new_activation_record(0, 4);
137 assert_eq!(ar2.frame.base, ar1.frame.base + 4);
138 assert_eq!(ar1.frame.count, 4);
139 assert_eq!(ar2.frame.count, 4);
140 }
141
142 #[test]
143 fn activation_record_is_still_pod_after_going_through_state_allocation() {
144 let mut engine = StateEngine::new(TemplateRegistry::new());
145 let ar = engine.new_activation_record(0, 4);
146 let copy = ar;
147 engine.frame.words[ar.frame.base as usize] = 0xDEAD_BEEF;
148 assert_eq!(copy.frame.base, ar.frame.base);
149 assert_eq!(copy.frame.count, ar.frame.count);
150 assert_eq!(engine.frame.words[copy.frame.base as usize], 0xDEAD_BEEF);
151 assert_eq!(engine.frame.words[ar.frame.base as usize], 0xDEAD_BEEF);
152 }
153
154 #[test]
155 fn edge_list_and_fork_children_get_independent_arena_ranges() {
156 use crate::state::{EdgeKind, StateCellId};
157 let mut fa = FrameArena::new();
158 let edge_range = fa.alloc_edge_terms(&[
159 (EdgeKind::Posedge, StateCellId(0), None),
160 (EdgeKind::Negedge, StateCellId(1), None),
161 (EdgeKind::BothEdges, StateCellId(2), None),
162 ]);
163 let fork_range = fa.alloc_fork_children(2);
164
165 assert_eq!(edge_range, ArenaRange { base: 0, count: 3 });
166 assert_eq!(fork_range, ArenaRange { base: 0, count: 2 });
167
168 // Independent backing stores: writing/reading through each does not
169 // alias the other.
170 fa.fork_children[fork_range.base as usize] = 42;
171 fa.fork_children[fork_range.base as usize + 1] = 43;
172 assert_eq!(fa.edge_terms.len(), 3);
173 assert_eq!(fa.edge_terms[0].1, StateCellId(0));
174 assert_eq!(fa.fork_children[0], 42);
175 assert_eq!(fa.fork_children[1], 43);
176 }
177
178 #[test]
179 fn resume_pc_lives_on_the_activation_record_state_owns_no_separate_pc_table() {
180 let mut engine = StateEngine::new(TemplateRegistry::new());
181 let mut ar1 = engine.new_activation_record(10, 0);
182 let ar2 = engine.new_activation_record(20, 0);
183 assert_eq!(ar1.pc, 10);
184 assert_eq!(ar2.pc, 20);
185
186 ar1.pc = 99;
187 assert_eq!(ar1.pc, 99);
188 assert_eq!(
189 ar2.pc, 20,
190 "mutating one record's pc must never affect another's"
191 );
192 }
193}