vj_sim/state/write.rs
1//! Task 8 — `StateCellId` allocation and the one write primitive
2//! (`commit_write`) (ledger ST-031, ST-079, ST-082, ST-091, ST-109, ST-155).
3//!
4//! This is the single low-level primitive every later write path (the
5//! accessor ABI, `resolve_net_step`, NBA/blocking commits) goes through —
6//! never reimplemented per call site (see this file's own `Acceptance`
7//! grep-count discipline in the plan).
8//!
9//! **Deviations from the plan's literal text (documented once here, applying
10//! to everything below):**
11//!
12//! 1. **`StateEngine`'s new fields are `pub(crate)`, not module-private.**
13//! The plan's own Files list splits `StateEngine`'s field *declarations*
14//! (`cells`, `current_time`, `current_region`, `worker_buffers`) into
15//! `state/instance.rs`, while this file's `commit_write`/`alloc_cell`
16//! (and `instance.rs`'s own `register_worker_thread`/`WorkerSlot`) need
17//! to read/write them from the *sibling* `state::write` module. Rust's
18//! privacy model has no "private to a set of sibling modules" tier
19//! between "private to the declaring module (and its descendants)" and
20//! "visible everywhere `pub` reaches" other than `pub(crate)`, so the
21//! fields (and `WorkerSlot`'s inner index) are `pub(crate)`: still fully
22//! outside `vj-sim`'s public API surface (the encapsulation the plan
23//! actually cares about), just visible across this one crate's own
24//! sibling modules.
25//! 2. **A fourth, new `engine_id: u64` field on `StateEngine`** (assigned
26//! once, monotonically, from a process-wide atomic counter, at
27//! `StateEngine::new`). The plan's own thread-local self-registration
28//! cache is keyed purely by "is this thread's cache populated at all",
29//! which is sound only if at most one `StateEngine` is ever built per OS
30//! thread for that thread's entire lifetime — true for a real
31//! simulation run's worker threads, but not for this crate's own test
32//! suite (many short-lived `StateEngine`s; Rust's default test harness
33//! does not guarantee a fresh OS thread per `#[test]` function). Without
34//! `engine_id`, a second `StateEngine` built on a thread that already
35//! populated the cache for a *different*, since-dropped engine would
36//! reuse that engine's dangling raw pointer — a genuine use-after-free,
37//! not merely a wrong-value bug. Keying the cache by `engine_id` instead
38//! closes this while keeping the plan's "no further registry lookup
39//! once cached" cost profile and leaving every call site's own
40//! signature/behavior unchanged.
41//! 3. **`VcRecord` derives `Debug, Clone` only, not `PartialEq`; `ChangeResult`
42//! derives `Debug` only, no `PartialEq`.** Both embed [`FourStateValue`],
43//! which this crate's Task 1 deliberately never gives a `PartialEq` impl
44//! (see `state::value`'s/`SlotDescriptor`'s doc comments and this task's
45//! own ST-155 discussion below) precisely so no shortcut/value-only
46//! equality check can silently exist — `#[derive(PartialEq)]` cannot be
47//! added to a struct one of whose fields has no `PartialEq` impl, so the
48//! plan's literal derive lists for these two types do not compile as
49//! written. Tests that would have used `assert_eq!` on a whole
50//! `ChangeResult`/`VcRecord` instead assert on `.changed`/`.cell`/`.at`
51//! directly and pattern-match the `FourStateValue` fields.
52//!
53//! **Task 9 retrofit (ST-013, ST-020..ST-023 — not in the plan's Files list
54//! for Task 9, which names only `accessor_impl.rs` (new) and `mod.rs`
55//! (edit)):** the plan's header design decision makes `SimHandle` point at a
56//! bare `InstanceHandle{base, template, arena, layout}` with no back
57//! -reference to the `StateEngine` that minted it, yet also requires
58//! `sv_set_scalar`/`sv_set_wide` to "call Task 8's `StateEngine::commit_write`
59//! (not a second, parallel write path)". Those two requirements are only
60//! reconcilable if whatever `commit_write` needs beyond `Arena` (itself
61//! already `Rc`-shared and reachable with no `StateEngine` back-reference,
62//! per Task 7 r2) is *also* reachable without one. [`VcState`] below pulls
63//! `current_time`/`current_region`/`worker_buffers`/`engine_id` out of
64//! `StateEngine` (`state::instance`, which now holds `vc: Rc<VcState>`
65//! instead of those four fields) into their own `Rc`-shared, interior
66//! -mutable type, mirroring the exact `Rc`+`UnsafeCell`/`&self`-accessor
67//! pattern Task 7 r1/r2 already established for `Arena`/`TemplateRegistry`,
68//! for the identical reason: `InstanceHandle` (Task 9 retrofit,
69//! `state::instance`) clones this same `Rc<VcState>` at `allocate_instance`
70//! time, so the accessor ABI (`accessor_impl.rs`) can call
71//! [`commit_write_at`] — the same diff-and-append primitive
72//! `StateEngine::commit_write` now merely forwards to — directly, with no
73//! `&mut StateEngine` in scope. `StateEngine`'s own public methods
74//! (`set_current_time`, `set_current_region`, `register_worker_thread`,
75//! `commit_write`) keep their exact pre-Task-9 signatures; only their bodies
76//! now forward through `self.vc`.
77
78use std::cell::{Cell, RefCell, UnsafeCell};
79
80use vj_abi::{Region, SimTime};
81
82use super::arena::{Arena, BuildMode};
83use super::instance::StateEngine;
84use super::{AliasWindow, EdgeKind, FourStateValue, SlotClass, StateCellId};
85
86/// The already-*absolute* (base already added) address [`StateEngine::commit_write`]
87/// reads/writes at; both `InstanceHandle`-relative accessor calls and
88/// `StateCellId`-keyed calls resolve down to this shape before calling in.
89///
90/// **Task 11 (ST-137, ST-138) adds `row_stride_words`/`num_rows`,** mirroring
91/// [`super::SlotOffset`]'s own field suffix (ST-144) — a `SlotClass::Memory`
92/// slot's own per-row word stride and total row count, both stable
93/// template constants copied straight through from the owning `SlotOffset`
94/// (never recomputed here). Every non-memory `SlotLocation` this crate builds
95/// sets both to `0` (mechanical ripple, matching `SlotOffset`'s own
96/// established convention for a non-memory slot).
97#[derive(Clone, Copy, PartialEq, Eq, Debug)]
98pub struct SlotLocation {
99 pub class: SlotClass,
100 pub word_offset: u32,
101 pub bit_width: u32,
102 pub elem_word_count: u32,
103 pub row_stride_words: u32,
104 pub num_rows: u32,
105}
106
107/// Task 11 (ST-138): state.md §3's pinned formula, verbatim: `word_offset' =
108/// word_offset + row_stride_words * row_index`, `row_index` a *runtime*
109/// value. Panics (never a silent OOB read/write) if `loc.class !=
110/// SlotClass::Memory` or `row_index >= loc.num_rows as u64` — ARCHITECTURE §7
111/// item 9's "loud, never silent" discipline. The returned `SlotLocation`
112/// still has `class: SlotClass::Memory` and the SAME `elem_word_count` (the
113/// layout builder already sets `elem_word_count == row_stride_words` for
114/// every real `Memory` slot — one row's own word count — so the resolved
115/// location slots directly into the existing `read_slot`/`write_slot`/
116/// `commit_write_at` primitives with zero further change).
117///
118/// **Task 11 deviation:** `pub`, not `pub(crate)` — see [`read_slot`]'s own
119/// doc comment for why.
120pub fn resolve_mem_row(loc: SlotLocation, row_index: u64) -> SlotLocation {
121 assert!(
122 loc.class == SlotClass::Memory,
123 "resolve_mem_row: SlotLocation class {:?} is not SlotClass::Memory -- \
124 row addressing only ever applies to a memory slot",
125 loc.class
126 );
127 assert!(
128 row_index < loc.num_rows as u64,
129 "resolve_mem_row: row index {row_index} is out of bounds for a {}-row memory",
130 loc.num_rows
131 );
132 let word_offset = loc.word_offset as u64 + loc.row_stride_words as u64 * row_index;
133 SlotLocation {
134 word_offset: u32::try_from(word_offset)
135 .expect("resolve_mem_row: resolved word_offset overflows u32"),
136 ..loc
137 }
138}
139
140/// ST-109's exact shape: `commit_write`'s return value.
141#[derive(Debug)]
142pub struct ChangeResult {
143 pub changed: bool,
144 pub old: FourStateValue,
145 pub new: FourStateValue,
146}
147
148/// The VC-buffer's one record shape (ST-079, ST-109) — later `drain_committed_changes`
149/// consumes this type as-is, never redefined.
150#[derive(Debug, Clone)]
151pub struct VcRecord {
152 pub cell: StateCellId,
153 pub old: FourStateValue,
154 pub new: FourStateValue,
155 pub at: SimTime,
156}
157
158/// Opaque handle to one thread's registered speculative VC buffer, returned
159/// by [`StateEngine::register_worker_thread`]. `pub(crate)` inner index: see
160/// this module's top doc comment, deviation 1.
161#[derive(Clone, Copy, PartialEq, Eq, Debug)]
162pub struct WorkerSlot(pub(crate) usize);
163
164/// Task 9 retrofit (see this module's top doc comment): the subset of
165/// `StateEngine`'s own state `commit_write_at` needs beyond `Arena`, pulled
166/// into its own `Rc`-shared, interior-mutable type so both `StateEngine`
167/// (`state::instance`) and every `InstanceHandle` minted against it can call
168/// the same primitive without a `&mut StateEngine` back-reference.
169pub(crate) struct VcState {
170 current_time: Cell<SimTime>,
171 current_region: Cell<Region>,
172 // See `StateEngine`'s pre-Task-9 identical field for why this is boxed
173 // per-entry (`clippy::vec_box` false positive, same reasoning).
174 #[allow(clippy::vec_box)]
175 worker_buffers: UnsafeCell<Vec<Box<RefCell<Vec<VcRecord>>>>>,
176 engine_id: u64,
177 /// Task 12 (ST-081): the last value `StateEngine::advance_gvt` stored —
178 /// `advance_gvt` itself is a pure setter (`self.vc.set_gvt_watermark(gvt)`),
179 /// with no dependency on any scheduler-internal type. `drain_committed_changes`
180 /// (below) is given its own `up_to: SimTime` parameter directly by the
181 /// caller rather than reading this field back internally (matching this
182 /// task's pinned `drain_committed_changes(&mut self, up_to: SimTime)`
183 /// signature) — this field exists so `advance_gvt` has real, observable
184 /// state to set (proven by the `#[cfg(test)]` getter below), not so
185 /// `drain_committed_changes` consults it.
186 gvt_watermark: Cell<SimTime>,
187 /// `force`/`release` (`state::force`): this engine's forced cells.
188 pub(crate) force: RefCell<super::force::ForceTable>,
189 /// Whether `commit_write_at` appends a `VcRecord` per change (ST-124).
190 /// Only `drain_committed_changes` consumes those records, so a run with
191 /// no such consumer turns this off (`StateEngine::set_change_log`):
192 /// otherwise every value change of the whole run stays buffered.
193 record_changes: Cell<bool>,
194 /// Window aliases of ref ports bound to a select (`state::alias`).
195 pub(crate) views: super::alias::Views,
196}
197
198impl VcState {
199 pub(crate) fn new(engine_id: u64) -> Self {
200 VcState {
201 current_time: Cell::new(SimTime {
202 sim_units: 0,
203 delta: 0,
204 }),
205 current_region: Cell::new(Region::Active),
206 worker_buffers: UnsafeCell::new(Vec::new()),
207 engine_id,
208 gvt_watermark: Cell::new(SimTime {
209 sim_units: 0,
210 delta: 0,
211 }),
212 force: RefCell::new(super::force::ForceTable::default()),
213 record_changes: Cell::new(true),
214 views: super::alias::Views::default(),
215 }
216 }
217
218 pub(crate) fn records_changes(&self) -> bool {
219 self.record_changes.get()
220 }
221
222 pub(crate) fn set_record_changes(&self, on: bool) {
223 self.record_changes.set(on);
224 }
225
226 pub(crate) fn set_current_time(&self, at: SimTime) {
227 self.current_time.set(at);
228 }
229
230 pub(crate) fn set_current_region(&self, region: Region) {
231 self.current_region.set(region);
232 }
233
234 /// Task 12: `commit_write` reads this back to stamp `notify_scheduler`'s
235 /// `at`/`region` arguments with the same `SimTime`/`Region` `VcRecord`
236 /// appends use (this plan's header "how STATE learns now" decision).
237 pub(crate) fn current_time(&self) -> SimTime {
238 self.current_time.get()
239 }
240
241 pub(crate) fn current_region(&self) -> Region {
242 self.current_region.get()
243 }
244
245 /// Task 12 (ST-081): see this type's `gvt_watermark` field doc comment.
246 pub(crate) fn set_gvt_watermark(&self, gvt: SimTime) {
247 self.gvt_watermark.set(gvt);
248 }
249
250 #[cfg(test)]
251 pub(crate) fn gvt_watermark(&self) -> SimTime {
252 self.gvt_watermark.get()
253 }
254
255 /// Task 8's "cross-thread visibility of the VC speculative buffer"
256 /// header decision, now `&self`-based (see this type's doc comment):
257 /// pushes a fresh, individually-boxed `RefCell<Vec<VcRecord>>` onto
258 /// `worker_buffers` and returns its index -- each entry's own heap
259 /// allocation has a stable address regardless of how many further times
260 /// `worker_buffers` itself grows (the same box-per-entry stability
261 /// argument `TemplateRegistry` already relies on for `StateLayout`).
262 ///
263 /// # Safety
264 /// The caller must ensure no other live borrow of `worker_buffers` (in
265 /// particular, no live dereference of a pointer from a previous
266 /// [`Self::buffer_ptr`] call) is held across this call, and that at most
267 /// one logical writer grows this list at a time -- the same
268 /// single-logical-writer discipline `Arena`'s own growing accessors
269 /// document (see `state::arena`'s module doc comment). `&self`, not
270 /// `&mut self`, so this is callable through an `Rc<VcState>` without ever
271 /// forming a `&mut VcState`.
272 pub(crate) unsafe fn register_worker_thread(&self) -> WorkerSlot {
273 // SAFETY: forwarded to this function's own `# Safety` section above.
274 unsafe {
275 let buffers = &mut *self.worker_buffers.get();
276 buffers.push(Box::new(RefCell::new(Vec::new())));
277 WorkerSlot(buffers.len() - 1)
278 }
279 }
280
281 /// Raw pointer to the (stably-heap-addressed) buffer registered at
282 /// `slot` -- see [`Self::register_worker_thread`]'s doc comment for why
283 /// growing `worker_buffers` later never invalidates this pointer.
284 ///
285 /// # Safety
286 /// `slot` must have been returned by a prior `register_worker_thread`
287 /// call on this same `VcState`, and no concurrent `register_worker_thread`
288 /// call may race this read.
289 unsafe fn buffer_ptr(&self, slot: WorkerSlot) -> *const RefCell<Vec<VcRecord>> {
290 // SAFETY: forwarded to this function's own `# Safety` section above.
291 let buffers: &Vec<Box<RefCell<Vec<VcRecord>>>> = unsafe { &*self.worker_buffers.get() };
292 &*buffers[slot.0]
293 }
294
295 /// Task 12 (ST-078, ST-081; review fix — this plan's header "cross-thread
296 /// visibility" decision): drains **every** registered worker thread's
297 /// buffer (not only the calling thread's) of records with `at <= up_to`,
298 /// merging the removed records into one `Vec<VcRecord>` ordered by
299 /// `(at, registration index)`. With exactly 1 registered worker this
300 /// degenerates to the old single-buffer behavior; with N it is correct
301 /// for all N — Waveform/VPI/`$display` observe exclusively through this
302 /// method (`StateEngine::drain_committed_changes`, `state::gvt`), never
303 /// through `Dispatch::notify_waveform_and_vpi` eagerly.
304 ///
305 /// Each registered buffer is walked in registration order and its
306 /// matching records are `Vec::remove`d in place (so a record already
307 /// drained never appears again on a later call, and a record with
308 /// `at > up_to` is left in the buffer for a future call once the
309 /// watermark passes it); the concatenated result is then stable-sorted
310 /// by `at` alone — stable sort preserves the `(buffer-registration-order,
311 /// within-buffer-append-order)` relative order of any two records that
312 /// tie on `at`, which is exactly "registration index" as a secondary key.
313 ///
314 /// # Safety precondition (documented, not enforced by the type system)
315 /// May run only when no worker thread holds a live borrow of its own
316 /// `RefCell<Vec<VcRecord>>` (i.e. is not concurrently inside a
317 /// `commit_write_at` call) — the same WOCB-barrier precondition
318 /// documented on [`Self::register_worker_thread`]. `&self`, not `&mut
319 /// self`/`&mut StateEngine`, only for symmetry with this type's other
320 /// methods; `StateEngine::drain_committed_changes` (`state::gvt`) still
321 /// takes `&mut self` itself, which is what actually enforces
322 /// non-reentrancy from a single caller's own perspective.
323 pub(crate) fn drain_committed_changes(&self, up_to: SimTime) -> Vec<VcRecord> {
324 // SAFETY: `worker_buffers` only ever grows by `push` (never
325 // reallocates/moves an existing `Box`'s heap allocation, per
326 // `register_worker_thread`'s own doc comment), and this call's own
327 // `# Safety precondition` above rules out a concurrent writer
328 // borrowing any entry while this runs.
329 let buffers: &Vec<Box<RefCell<Vec<VcRecord>>>> = unsafe { &*self.worker_buffers.get() };
330
331 let mut merged: Vec<VcRecord> = Vec::new();
332 for buf in buffers.iter() {
333 let mut records = buf.borrow_mut();
334 let mut i = 0;
335 while i < records.len() {
336 if sim_time_le(records[i].at, up_to) {
337 merged.push(records.remove(i));
338 } else {
339 i += 1;
340 }
341 }
342 }
343 // Stable sort: see this method's own doc comment for why this alone
344 // realizes the "(at, registration index)" ordering.
345 merged.sort_by_key(|a| (a.at.sim_units, a.at.delta));
346 merged
347 }
348}
349
350/// Total order over `SimTime` (`sim_units` then `delta`) this crate needs for
351/// GVT-watermark comparisons — `vj_abi::SimTime` itself derives no
352/// `PartialOrd`/`Ord` (see `handle.rs`; out of this track's contract to add
353/// one to another crate's type), so this is a local helper, not a trait impl.
354///
355/// `pub(crate)` (Task 16 deviation): `state::rollback::UndoLog::fossil_collect`
356/// needs the exact same total order to compare an entry's own `at` against a
357/// `gvt` watermark — reusing this rather than a second, independently
358/// hand-rolled comparator (the same "one place" discipline `drain_committed_changes`
359/// above already follows for its own `at <= up_to` gating).
360pub(crate) fn sim_time_le(a: SimTime, b: SimTime) -> bool {
361 (a.sim_units, a.delta) <= (b.sim_units, b.delta)
362}
363
364/// The ST-091/ST-031 "hook present in the calling convention from the first
365/// freeze" requirement made concrete now: `commit_write` takes `hook: &mut
366/// dyn PreWriteHook` as a required parameter, not an optional add-on, so a
367/// later real undo-log `PreWriteHook` implementer slots in without touching
368/// this signature or any of its call sites.
369///
370/// **Deviation from the plan's literal signature** (binding per this task's
371/// own unresolved-review-findings list): `before_write` also receives
372/// `cell: StateCellId`, not just `loc`/`old`/`new_incoming` — `commit_write`
373/// already has `cell` in scope at the call site, and a later undo-log
374/// implementer needs the `StateCellId` (not merely a `SlotLocation`) to
375/// replay a rollback via `commit_write_no_hook(cell, ...)`; no
376/// `SlotLocation -> StateCellId` reverse lookup exists anywhere in this
377/// crate, so `cell` must be threaded through here directly.
378///
379/// **Second deviation (Task 16):** `before_write` also receives
380/// `at: SimTime`. Task 16's `UndoLog` (`state::rollback`) implements this
381/// trait and must tag every entry it records with the `SimTime` the write
382/// happened at — `fossil_collect(gvt)`'s own acceptance text ("trims entries
383/// the caller asserts are provably before `gvt`") is only meaningful if the
384/// log's entries actually carry a timestamp to compare against `gvt`, and
385/// nothing else in this calling convention threads one to the hook. The one
386/// call site (`commit_write_at`, below) already reads `vc.current_time()`
387/// for its own `VcRecord.at` field, so passing that same value to the hook
388/// costs nothing extra and keeps both timestamps (the VC record's and the
389/// undo entry's) identically sourced.
390pub trait PreWriteHook {
391 fn before_write(
392 &mut self,
393 cell: StateCellId,
394 loc: SlotLocation,
395 old: &FourStateValue,
396 new_incoming: &FourStateValue,
397 at: SimTime,
398 );
399}
400
401/// The no-op default: v1 (`LOOKAHEAD_CYCLES = 0`) never exercises rollback,
402/// so every call site that doesn't care passes `&mut ()`.
403impl PreWriteHook for () {
404 fn before_write(
405 &mut self,
406 _cell: StateCellId,
407 _loc: SlotLocation,
408 _old: &FourStateValue,
409 _new_incoming: &FourStateValue,
410 _at: SimTime,
411 ) {
412 }
413}
414
415/// One thread's cached self-registration: which `StateEngine` (`engine_id`,
416/// deviation 2 above) this thread last registered against, the `WorkerSlot`
417/// that registration returned, and a raw pointer straight at that boxed
418/// entry's `RefCell` (stable heap address — see `register_worker_thread`'s
419/// doc comment), so every subsequent `commit_write` on this thread against
420/// the *same* engine appends with no further registry lookup.
421struct WorkerCacheEntry {
422 engine_id: u64,
423 #[allow(dead_code)]
424 // kept for debugging/symmetry with the plan's named field; `buffer` is what every call site actually uses.
425 slot: WorkerSlot,
426 buffer: *const RefCell<Vec<VcRecord>>,
427}
428
429thread_local! {
430 static WORKER_CACHE: RefCell<Option<WorkerCacheEntry>> = const { RefCell::new(None) };
431}
432
433/// ST-155, hand-written (see this module's top doc comment, deviation 3, for
434/// why `FourStateValue` cannot derive `PartialEq`): the one place that diffs
435/// old-vs-new, independently comparing **both** the value/aval half **and**
436/// the mask/unknown/bval half for **both** storage tiers — never a
437/// value-only or single-plane shortcut. `Narrow`'s low 64 bits are the value
438/// half, the high 64 bits are the mask half (state::value's fixed accessor-ABI
439/// packing); `Wide`'s `aval`/`bval` planes are compared element-wise,
440/// independently of each other.
441pub(crate) fn four_state_changed(old: &FourStateValue, new: &FourStateValue) -> bool {
442 match (old, new) {
443 (FourStateValue::Narrow(o), FourStateValue::Narrow(n)) => {
444 let o_val = *o as u64;
445 let o_unk = (*o >> 64) as u64;
446 let n_val = *n as u64;
447 let n_unk = (*n >> 64) as u64;
448 o_val != n_val || o_unk != n_unk
449 }
450 (
451 FourStateValue::Wide { aval: oa, bval: ob },
452 FourStateValue::Wide { aval: na, bval: nb },
453 ) => oa != na || ob != nb,
454 (FourStateValue::Narrow(_), FourStateValue::Wide { .. })
455 | (FourStateValue::Wide { .. }, FourStateValue::Narrow(_)) => {
456 // A cell's `SlotLocation::class` fixes which `FourStateValue`
457 // shape it ever stores (`Inline` <-> `Narrow`, `Wide`/`Memory`
458 // <-> `Wide`); a caller reaching this arm passed a value whose
459 // shape doesn't match the cell's own storage class, which is a
460 // programming error at the call site, not a normal divergence
461 // this primitive is meant to tolerate silently.
462 panic!(
463 "commit_write: old/new FourStateValue shape mismatch (Narrow vs Wide) for the \
464 same cell -- the caller's `new` value does not match this cell's storage class"
465 );
466 }
467 }
468}
469
470/// Reads the current word(s) stored at `loc`, without any diffing —
471/// `commit_write_at`'s own `old` read (Task 9 retrofit: a free function over
472/// `&Arena`, not a `StateEngine` method, so it is callable from the accessor
473/// ABI's `InstanceHandle`-only call sites too — see this module's top doc
474/// comment).
475///
476/// **Task 11 deviation (widened from `pub(crate)` to `pub`):** the plan's own
477/// `mem_row_addressing_uses_real_frontend_layout.rs` acceptance test (ST-138)
478/// is a genuine external integration test (`crates/vj-sim/tests/`, a
479/// separate compilation unit that sees only this crate's `pub` surface) and
480/// needs to read back a resolved memory row's words directly, through the
481/// same no-second-path primitive `commit_write_at` itself uses, to prove
482/// `resolve_mem_row`'s output slots into it correctly on the real,
483/// frontend-derived layout — not a hand-rolled second reader. Widening this
484/// (and `write_slot`/`resolve_mem_row` below) to `pub` only enlarges this
485/// crate's public surface to match the already-`pub` sibling 2-state family
486/// (`read_inline_2s`/`write_inline_2s`/`read_wide_2s`/`write_wide_2s`, this
487/// same file, Task 13) that already sits at this exact visibility for the
488/// identical reason — it does not change behavior or the single-writer
489/// discipline either family already documents.
490pub fn read_slot(arena: &Arena, loc: SlotLocation) -> FourStateValue {
491 match loc.class {
492 SlotClass::Inline => FourStateValue::Narrow(arena.inline()[loc.word_offset as usize]),
493 // `Memory` shares the wide planes with `Wide` (arena.rs's own
494 // module doc comment: "wide_aval/wide_bval hold every wider
495 // signal and every memory element"); by the time a `SlotLocation`
496 // exists, any per-row addressing a memory access needed has
497 // already resolved down to a plain word_offset/elem_word_count
498 // range in those planes, same as an ordinary wide signal.
499 SlotClass::Wide | SlotClass::Memory => {
500 let start = loc.word_offset as usize;
501 let end = start + loc.elem_word_count as usize;
502 FourStateValue::Wide {
503 aval: arena.wide_aval()[start..end].to_vec(),
504 bval: arena.wide_bval()[start..end].to_vec(),
505 }
506 }
507 // Review fix (Task 14 r2, blocking finding 2): `force_driver`'s
508 // Wide/Memory-class contribution cells live in `Arena`'s separate,
509 // never-sealed `force_wide_aval`/`force_wide_bval` planes, not the
510 // ordinary wide planes — see `SlotClass::ForceWide`'s own doc
511 // comment.
512 SlotClass::ForceWide => {
513 let start = loc.word_offset as usize;
514 let end = start + loc.elem_word_count as usize;
515 FourStateValue::Wide {
516 aval: arena.force_wide_aval()[start..end].to_vec(),
517 bval: arena.force_wide_bval()[start..end].to_vec(),
518 }
519 }
520 }
521}
522
523/// Stores `new` at `loc`. Only `commit_write_at` (below) calls this in the
524/// ordinary write path, and only after `four_state_changed` has already
525/// confirmed a real change — see this function's own SAFETY comments for the
526/// single-logical-writer argument that makes the two `unsafe` `Arena`
527/// accessor calls sound.
528///
529/// `pub(crate)` (Task 16 deviation, not in that task's own Files list):
530/// `state::rollback`'s `StateEngine::commit_write_no_hook` (the undo-log's
531/// own reverse-replay primitive) and `StateEngine::reset_to_initial`/
532/// `restore` need this exact same raw, no-diff, no-hook, no-VC-record store
533/// — reusing it rather than a second hand-rolled copy of the same
534/// class-dispatch (ST-155's "one place" discipline applied to writes, not
535/// just diffing).
536///
537/// **Task 11 deviation:** widened `pub(crate)` -> `pub` for the same reason,
538/// and by the same precedent, as [`read_slot`]'s own doc comment above.
539pub fn write_slot(arena: &Arena, loc: SlotLocation, new: &FourStateValue) {
540 match (loc.class, new) {
541 (SlotClass::Inline, FourStateValue::Narrow(word)) => {
542 // SAFETY: every caller of `commit_write_at` upholds the
543 // single-logical-writer discipline `Arena`'s own mutating
544 // accessors document (`StateEngine::commit_write` under its own
545 // `&mut self`; the accessor ABI's `sv_set_scalar`/`sv_set_wide`
546 // under the accessor-ABI's documented single-writer-per-slot
547 // calling convention) -- nothing in this crate holds a live
548 // `&[u128]`/`&mut [u128]` borrow of the inline plane across this
549 // call (mirrors Task 7's `allocate_instance` SAFETY argument for
550 // the same `Arena`).
551 unsafe {
552 arena.inline_mut()[loc.word_offset as usize] = *word;
553 }
554 }
555 (SlotClass::Wide | SlotClass::Memory, FourStateValue::Wide { aval, bval }) => {
556 let start = loc.word_offset as usize;
557 // A huge signal's mirror must not outlive a write it did not make
558 // (`state::huge_mirror`); a plain compare when no mirror exists.
559 arena.huge.invalidate_range(start, aval.len());
560 // SAFETY: same single-logical-writer argument as the Inline arm
561 // above, applied to both wide planes; `aval.len() == bval.len()
562 // == loc.elem_word_count` by construction of every
563 // `FourStateValue::Wide` this crate ever builds for a
564 // `Wide`/`Memory`-class cell (`read_slot` above is the only
565 // producer of a `Wide` for such a cell via `commit_write_at`,
566 // and always slices exactly `elem_word_count` words from each
567 // plane; the accessor ABI's `sv_set_wide` builds its own
568 // `Wide` the same way, from the caller-supplied `WideView`'s
569 // `word_count` words).
570 unsafe {
571 arena.wide_aval_mut()[start..start + aval.len()].copy_from_slice(aval);
572 arena.wide_bval_mut()[start..start + bval.len()].copy_from_slice(bval);
573 }
574 }
575 // Review fix (Task 14 r2, blocking finding 2): same shape as the
576 // `Wide | Memory` arm above, through the separate never-sealed
577 // force-only planes — see `SlotClass::ForceWide`'s own doc comment.
578 (SlotClass::ForceWide, FourStateValue::Wide { aval, bval }) => {
579 let start = loc.word_offset as usize;
580 // SAFETY: same single-logical-writer argument as the `Wide |
581 // Memory` arm above, applied to the force-only wide planes;
582 // `aval.len() == bval.len() == loc.elem_word_count` by the same
583 // construction argument (this crate's only producer of a `Wide`
584 // shape for a `ForceWide`-class cell is `read_slot` above, via
585 // `force_driver`'s own `commit_write` seed and later
586 // `resolve_net_step` reads).
587 unsafe {
588 arena.force_wide_aval_mut()[start..start + aval.len()].copy_from_slice(aval);
589 arena.force_wide_bval_mut()[start..start + bval.len()].copy_from_slice(bval);
590 }
591 }
592 (class, value) => panic!(
593 "commit_write: SlotClass {class:?} does not match the FourStateValue shape \
594 being written ({value:?}) -- a caller passed a value shape that doesn't match \
595 this cell's own storage class"
596 ),
597 }
598}
599
600/// Returns this thread's cached raw pointer into `vc`'s own registered
601/// worker buffer, calling [`VcState::register_worker_thread`] itself, at
602/// most once per (thread, engine) pair — see this module's top doc comment,
603/// deviation 2, for why the cache is keyed by `engine_id` and not merely "is
604/// this thread's cache populated".
605fn thread_local_buffer(vc: &VcState) -> *const RefCell<Vec<VcRecord>> {
606 let cached = WORKER_CACHE.with(|cache| {
607 cache
608 .borrow()
609 .as_ref()
610 .filter(|entry| entry.engine_id == vc.engine_id)
611 .map(|entry| entry.buffer)
612 });
613 if let Some(ptr) = cached {
614 return ptr;
615 }
616
617 // SAFETY: the cache check above ensures this runs at most once per
618 // (thread, `vc`) pair; no other live borrow of `vc`'s `worker_buffers`
619 // is held across this call.
620 let slot = unsafe { vc.register_worker_thread() };
621 // SAFETY: `slot` was just returned by `register_worker_thread` above, on
622 // this same `vc`; per that method's own doc comment, its heap allocation
623 // stays stable regardless of later registrations.
624 let ptr = unsafe { vc.buffer_ptr(slot) };
625 WORKER_CACHE.with(|cache| {
626 *cache.borrow_mut() = Some(WorkerCacheEntry {
627 engine_id: vc.engine_id,
628 slot,
629 buffer: ptr,
630 });
631 });
632 ptr
633}
634
635/// The one write primitive (ST-109, ST-079, ST-082, ST-091, ST-155; Task 9
636/// retrofit — see this module's top doc comment): reads the current value,
637/// calls `hook.before_write` *before* mutating (ST-091), diffs old-vs-new on
638/// both halves independently (ST-155); if (and only if) different, stores
639/// `new` and appends one `VcRecord` (stamped with `vc`'s own `current_time`)
640/// to the calling thread's own registered speculative buffer (ST-079,
641/// ST-082). A free function over `&Arena`/`&VcState` (not a `StateEngine`
642/// method) so both `StateEngine::commit_write` (keyed by `StateCellId`,
643/// resolved via `self.cells`) and the accessor ABI (Task 9, keyed directly
644/// by an already-absolute `SlotLocation` resolved via `InstanceHandle`) call
645/// this exact same primitive — never a second, parallel write path.
646pub(crate) fn commit_write_at(
647 arena: &Arena,
648 vc: &VcState,
649 loc: SlotLocation,
650 cell: StateCellId,
651 new: FourStateValue,
652 hook: &mut dyn PreWriteHook,
653) -> ChangeResult {
654 // A forced cell keeps its forced bits (`state::force`).
655 let new = {
656 let mut forced = vc.force.borrow_mut();
657 if forced.is_empty() {
658 new
659 } else {
660 if loc.class == SlotClass::Memory {
661 forced.filter_row(cell, loc.word_offset, new)
662 } else {
663 forced.filter(cell, new)
664 }
665 }
666 };
667 let old = read_slot(arena, loc);
668 // Task 16 deviation: computed once here (rather than re-read separately
669 // by the hook call below and the `VcRecord` construction further down)
670 // so both timestamps are identically sourced — see `PreWriteHook`'s own
671 // doc comment for why the hook needs this at all.
672 let at = vc.current_time.get();
673 hook.before_write(cell, loc, &old, &new, at);
674 let changed = four_state_changed(&old, &new);
675 if changed {
676 write_slot(arena, loc, &new);
677 }
678 if changed && vc.record_changes.get() {
679 let record = VcRecord {
680 cell,
681 old: old.clone(),
682 new: new.clone(),
683 at,
684 };
685 let buf_ptr = thread_local_buffer(vc);
686 // SAFETY: `buf_ptr` was derived (and cached) by `thread_local_buffer`
687 // from this same `vc`'s own `worker_buffers` boxed entry; per this
688 // plan's own WOCB-barrier precondition (documented on
689 // `VcState::register_worker_thread`), no other thread borrows this
690 // buffer concurrently with a `commit_write_at` call, and this
691 // function itself has not borrowed it before this line, so this is
692 // the only live borrow.
693 unsafe {
694 (*buf_ptr).borrow_mut().push(record);
695 }
696 }
697 ChangeResult { changed, old, new }
698}
699
700/// Task 13 — 2-state build-mode arena and read/write family (ledger ST-100,
701/// ST-101, ST-102, ST-139).
702///
703/// A structurally separate function family from `commit_write`/`read_slot`/
704/// `write_slot` above: plain `u64`, no mask half, no `FourStateValue`
705/// anywhere in these signatures, and no shared code path with the 4-state
706/// family except `Arena` itself (ST-139) -- which build-mode-specific
707/// function family a compiled kernel calls is fixed once at CODEGEN-compile
708/// time by which family CODEGEN emits calls to, never a runtime branch
709/// inside either family (ST-100, ST-101).
710///
711/// **Deviation from the plan's literal `&mut Arena` signatures for
712/// `write_inline_2s`/`write_wide_2s`:** Task 7 r1's `Arena` review fix (see
713/// `state::arena`'s module doc comment) made every growing/writing `Arena`
714/// accessor `&self`-based specifically so forming a fresh `&mut Arena` never
715/// invalidates a previously-minted `InstanceHandle`'s raw pointer under
716/// Stacked/Tree Borrows; `read_slot`/`write_slot`/`commit_write_at` above
717/// already follow this discipline (`&Arena`, never `&mut Arena`). This
718/// family follows the same already-established pattern for the identical
719/// reason -- a `&mut Arena` parameter here would be the one Arena-mutating
720/// call path in this crate that still forces a fresh `&mut Arena` into
721/// existence, reintroducing exactly the hazard Task 7 r1 fixed. The
722/// "changed" `bool` return value the plan's own prose describes is kept
723/// exactly as specified.
724///
725/// Reads the current 2-state word at `word_offset`, no diffing.
726pub fn read_inline_2s(arena: &Arena, word_offset: u32) -> u64 {
727 arena.inline_2s()[word_offset as usize]
728}
729
730/// Writes `value` at `word_offset` iff it differs from the currently-stored
731/// value (plain `u64` equality -- ST-102's "without any mask comparison":
732/// there is no mask half in this family to compare). Returns whether a real
733/// change occurred.
734pub fn write_inline_2s(arena: &Arena, word_offset: u32, value: u64) -> bool {
735 let idx = word_offset as usize;
736 let old = arena.inline_2s()[idx];
737 let changed = old != value;
738 if changed {
739 // SAFETY: every caller of this crate's write primitives upholds the
740 // single-logical-writer discipline `Arena`'s own mutating accessors
741 // document (see `state::arena`'s module doc comment); nothing in
742 // this crate holds a live `&[u64]`/`&mut [u64]` borrow of the
743 // `inline_2s` plane across this call.
744 unsafe {
745 arena.inline_2s_mut()[idx] = value;
746 }
747 }
748 changed
749}
750
751/// Reads `elem_word_count` words starting at `word_offset` from the wide
752/// 2-state plane, no diffing.
753pub fn read_wide_2s(arena: &Arena, word_offset: u32, elem_word_count: u32) -> &[u64] {
754 let start = word_offset as usize;
755 let end = start + elem_word_count as usize;
756 &arena.wide_2s()[start..end]
757}
758
759/// Writes `values` starting at `word_offset` in the wide 2-state plane iff
760/// they differ from what's currently stored (element-wise `u64` equality,
761/// no mask half). Returns whether a real change occurred.
762pub fn write_wide_2s(arena: &Arena, word_offset: u32, values: &[u64]) -> bool {
763 let start = word_offset as usize;
764 let end = start + values.len();
765 let changed = arena.wide_2s()[start..end] != *values;
766 if changed {
767 // SAFETY: same single-logical-writer argument as `write_inline_2s`
768 // above, applied to the wide 2-state plane.
769 unsafe {
770 arena.wide_2s_mut()[start..end].copy_from_slice(values);
771 }
772 }
773 changed
774}
775
776impl StateEngine {
777 /// `StateCellId` allocation (ST-109): appends `loc` to a private
778 /// `Vec<SlotLocation>` and returns its dense index as the new
779 /// `StateCellId`, so `StateCellId(i)` and `cells[i]` are always in
780 /// lockstep (no reuse/free in v1, matching the arena's own bump-allocator
781 /// discipline).
782 pub fn alloc_cell(&mut self, loc: SlotLocation) -> StateCellId {
783 let id = self.cells.len() as u64;
784 self.cells.push(loc);
785 // Task 16 deviation (not in that task's own Files list, `state/
786 // rollback.rs` (new) and `state/mod.rs`): `cell_reset` must stay in
787 // exact lockstep with `cells` regardless of which call site
788 // allocates a cell (`allocate_instance`'s real template slots,
789 // `force_driver`'s synthetic contribution cells, or this crate's own
790 // test helpers) — pushing the conservative `CellReset::AllX` default
791 // here, unconditionally, is what guarantees that invariant; the one
792 // call site that actually knows a slot's real `initial`/`net_info`
793 // (`instance::StateEngine::allocate_instance`) overwrites it
794 // afterwards via a direct `self.cell_reset[id] = ...` write.
795 self.cell_reset.push(super::rollback::CellReset::AllX);
796 StateCellId(id)
797 }
798
799 /// The one write primitive (ST-109, ST-079, ST-082, ST-091, ST-155):
800 /// looks up `cell`'s already-allocated `SlotLocation` and forwards to
801 /// [`commit_write_at`] — see that function's doc comment for the full
802 /// algorithm. Task 12 (ST-075): on a real change, also makes exactly one
803 /// call to `Dispatch::notify_scheduler` — the *only* call site
804 /// `notify_scheduler` has anywhere in this crate — supplying `self.vc`'s
805 /// own `current_time`/`current_region` (this plan's header "how STATE
806 /// learns now" decision; never a parameter this method's own signature
807 /// carries). `Dispatch::notify_waveform_and_vpi` is deliberately **not**
808 /// called here: Waveform/VPI observe only through
809 /// `StateEngine::drain_committed_changes` (`state::gvt`), reading the
810 /// `VcRecord` [`commit_write_at`] already appended above. Scheduler
811 /// wakeup is never GVT-gated — see `Dispatch::notify_scheduler`'s own
812 /// doc comment for why.
813 pub fn commit_write(
814 &mut self,
815 cell: StateCellId,
816 new: FourStateValue,
817 hook: &mut dyn PreWriteHook,
818 ) -> ChangeResult {
819 let loc = self.cells[cell.0 as usize];
820 let result = commit_write_at(&self.arena, &self.vc, loc, cell, new, hook);
821 self.vc.views.note_change(cell, None, &result);
822 if result.changed {
823 self.dispatch.notify_scheduler(
824 cell,
825 EdgeKind::None,
826 self.vc.current_time(),
827 self.vc.current_region(),
828 );
829 }
830 result
831 }
832
833 /// The view cell of `window` of `root` (`state::alias`): a cell that
834 /// owns no storage, whose value is that window of `root`'s. One cell per
835 /// distinct `(root, window)`. Never reset (`root` is).
836 pub fn alloc_view_cell(&mut self, root: StateCellId, window: AliasWindow) -> StateCellId {
837 debug_assert!(
838 self.vc.views.info(root).is_none(),
839 "a window alias's root is a real cell, never another view"
840 );
841 let existing = self.vc.views.table().existing(root, window);
842 if let Some(view) = existing {
843 return view;
844 }
845 let loc = self.cells[root.0 as usize];
846 let view = self.alloc_cell(loc);
847 self.cell_reset[view.0 as usize] = super::rollback::CellReset::ForceContribution;
848 self.vc.views.table().register(view, root, window);
849 self.vc.views.mark_any();
850 view
851 }
852
853 /// Task 13 (ST-101): read-only accessor so a test (or, later, RUNTIME)
854 /// can confirm which mode a given `StateEngine` was constructed in. No
855 /// `set_build_mode` method exists anywhere in this crate's public API --
856 /// `BuildMode` is fixed once at `Arena::new` (via
857 /// `StateEngine::new_with_mode`, `state::instance`) and never mutated
858 /// after, which is the structural enforcement of "no design or
859 /// implementation makes 4-state and 2-state arenas coexist at runtime".
860 pub fn build_mode(&self) -> BuildMode {
861 self.arena.mode()
862 }
863}
864
865/// A `#[cfg(test)]`-visible peek at the *calling thread's own* registered
866/// buffer's most recent entry for `cell` (single-threaded in every test that
867/// uses it in this task). No production code should ever need to *query*
868/// the buffer this way -- a later `drain_committed_changes` owns draining it
869/// for real.
870#[cfg(test)]
871pub fn last_vc_record_for(cell: StateCellId) -> Option<VcRecord> {
872 WORKER_CACHE.with(|cache| {
873 let buffer_ptr = cache.borrow().as_ref().map(|entry| entry.buffer)?;
874 // SAFETY: `buffer_ptr` was derived (and is kept alive) exactly as
875 // `commit_write`'s own SAFETY comment above argues; this function
876 // only ever runs on the same thread that populated the cache entry,
877 // single-threaded in every test that calls it.
878 let buffer = unsafe { &*buffer_ptr };
879 buffer
880 .borrow()
881 .iter()
882 .rev()
883 .find(|record| record.cell == cell)
884 .cloned()
885 })
886}
887
888#[cfg(test)]
889mod tests {
890 use super::*;
891 use crate::state::instance::TemplateRegistry;
892 use crate::state::{ListenerRole, SignalAccess, ValueChangeSink};
893 use std::sync::{Arc, Mutex};
894 use vj_abi::Region;
895
896 fn fresh_engine() -> StateEngine {
897 StateEngine::new(TemplateRegistry::new())
898 }
899
900 /// Records every `notify_change` call it receives into a shared log
901 /// (mirrors `state::gvt::tests::SpySink`, duplicated locally per this
902 /// crate's existing per-task-module test helper convention).
903 struct SpySink {
904 log: Arc<Mutex<Vec<StateCellId>>>,
905 }
906
907 impl ValueChangeSink for SpySink {
908 fn notify_change(
909 &mut self,
910 cell: StateCellId,
911 _edge: EdgeKind,
912 _at: SimTime,
913 _region: Region,
914 ) {
915 self.log.lock().unwrap().push(cell);
916 }
917 }
918
919 /// Allocates one inline cell (a real, valid `word_offset` into a fresh
920 /// engine's own arena) and returns `(engine, cell, loc)`.
921 fn inline_cell(engine: &mut StateEngine) -> (StateCellId, SlotLocation) {
922 // SAFETY: `engine` is a local, single-owned value with no other live
923 // borrow of the inline plane, and this single-threaded test performs
924 // no concurrent allocation.
925 let word_offset = unsafe { engine.arena.alloc_inline_word() };
926 let loc = SlotLocation {
927 class: SlotClass::Inline,
928 word_offset,
929 bit_width: 8,
930 elem_word_count: 1,
931 row_stride_words: 0,
932 num_rows: 0,
933 };
934 let cell = engine.alloc_cell(loc);
935 (cell, loc)
936 }
937
938 /// Allocates one wide cell of `elem_word_count` words.
939 fn wide_cell(engine: &mut StateEngine, elem_word_count: u32) -> (StateCellId, SlotLocation) {
940 // SAFETY: same reasoning as `inline_cell` above, applied to the wide
941 // planes.
942 let word_offset = unsafe { engine.arena.alloc_wide_words(elem_word_count) };
943 let loc = SlotLocation {
944 class: SlotClass::Wide,
945 word_offset,
946 bit_width: elem_word_count * 64,
947 elem_word_count,
948 row_stride_words: 0,
949 num_rows: 0,
950 };
951 let cell = engine.alloc_cell(loc);
952 (cell, loc)
953 }
954
955 fn narrow_word(val: u64, unk: u64) -> u128 {
956 (val as u128) | ((unk as u128) << 64)
957 }
958
959 /// ST-109: `alloc_cell` 3x; the returned ids are dense and in lockstep
960 /// with the order allocated, never reused/coalesced.
961 #[test]
962 fn alloc_cell_returns_dense_lockstep_indices() {
963 let mut engine = fresh_engine();
964 let (c0, _) = inline_cell(&mut engine);
965 let (c1, _) = inline_cell(&mut engine);
966 let (c2, _) = inline_cell(&mut engine);
967 assert_eq!(c0, StateCellId(0));
968 assert_eq!(c1, StateCellId(1));
969 assert_eq!(c2, StateCellId(2));
970 }
971
972 /// ST-082: a write that does not change the already-held value produces
973 /// zero VC records. The cell is pre-seeded to `Narrow(5)` via a *real*
974 /// `commit_write` call (which itself appends exactly one record, going
975 /// from the zero-initialized default to 5); the second, no-op
976 /// `commit_write` with the same value must append nothing, leaving
977 /// `last_vc_record_for` reflecting only the first write. Fails on a
978 /// primitive that stores-then-diffs (always "changing" because it
979 /// compares the just-written value against itself) or that skips the
980 /// compare entirely (always reporting `changed: true`).
981 #[test]
982 fn commit_write_on_unchanged_value_produces_zero_vc_records() {
983 let mut engine = fresh_engine();
984 let (cell, _) = inline_cell(&mut engine);
985
986 let seed = engine.commit_write(cell, FourStateValue::Narrow(narrow_word(5, 0)), &mut ());
987 assert!(
988 seed.changed,
989 "seeding from the zero default to 5 is a real change"
990 );
991
992 let repeat = engine.commit_write(cell, FourStateValue::Narrow(narrow_word(5, 0)), &mut ());
993 assert!(
994 !repeat.changed,
995 "writing the already-held value must not report a change"
996 );
997
998 let last = last_vc_record_for(cell).expect("the seed write appended a record");
999 match last.old {
1000 FourStateValue::Narrow(w) => assert_eq!(w, narrow_word(0, 0)),
1001 FourStateValue::Wide { .. } => panic!("expected Narrow"),
1002 }
1003 match last.new {
1004 FourStateValue::Narrow(w) => assert_eq!(w, narrow_word(5, 0)),
1005 FourStateValue::Wide { .. } => panic!("expected Narrow"),
1006 }
1007 }
1008
1009 /// Mutation regression (survived the mutation-testing pass): Task 12's
1010 /// (ST-075) documented guarantee that `StateEngine::commit_write` calls
1011 /// `Dispatch::notify_scheduler` "on a real change" -- guarded by `if
1012 /// result.changed`, never unconditionally -- was previously asserted
1013 /// only in this method's own doc comment, with no test exercising the
1014 /// no-op-write case through a real registered `Scheduler` sink. Seeds
1015 /// the cell to `5` (a real change, one notification), then repeats the
1016 /// identical `commit_write(cell, Narrow(5))` (no change): the spy's log
1017 /// must still hold exactly the one notification from the seed write.
1018 /// Fails if `commit_write` notified the scheduler on every call
1019 /// regardless of `result.changed`.
1020 #[test]
1021 fn commit_write_notifies_the_scheduler_only_on_a_real_change_never_on_a_no_op_write() {
1022 let mut engine = fresh_engine();
1023 let (cell, _) = inline_cell(&mut engine);
1024
1025 let log = Arc::new(Mutex::new(Vec::new()));
1026 engine.subscribe(
1027 cell,
1028 ListenerRole::Scheduler,
1029 Box::new(SpySink { log: log.clone() }),
1030 );
1031
1032 let seed = engine.commit_write(cell, FourStateValue::Narrow(narrow_word(5, 0)), &mut ());
1033 assert!(
1034 seed.changed,
1035 "seeding from the zero default to 5 is a real change"
1036 );
1037 assert_eq!(
1038 log.lock().unwrap().len(),
1039 1,
1040 "the seed write is a real change and must notify the scheduler exactly once"
1041 );
1042
1043 let repeat = engine.commit_write(cell, FourStateValue::Narrow(narrow_word(5, 0)), &mut ());
1044 assert!(
1045 !repeat.changed,
1046 "writing the already-held value must not report a change"
1047 );
1048 assert_eq!(
1049 log.lock().unwrap().len(),
1050 1,
1051 "a no-op write (no real value change) must never notify the scheduler again"
1052 );
1053 }
1054
1055 /// ST-082, ST-079: a write that does change the value produces exactly
1056 /// one VC record for that write. The cell is pre-seeded to `Narrow(5)`
1057 /// via a direct, low-level arena write (bypassing `commit_write`
1058 /// entirely, exactly the pattern Task 7's own tests use), so this test's
1059 /// one real `commit_write(cell, Narrow(6))` call is the *only* thing
1060 /// that can append to the counted buffer -- excluding the seed from the
1061 /// count by construction, not by coincidence.
1062 #[test]
1063 fn commit_write_on_changed_value_produces_exactly_one_vc_record() {
1064 let mut engine = fresh_engine();
1065 let (cell, loc) = inline_cell(&mut engine);
1066
1067 // SAFETY: local, single-owned engine; no other live borrow of the
1068 // inline plane is held across this write, and this single-threaded
1069 // test performs no concurrent write. Bypasses `commit_write` on
1070 // purpose -- see this test's doc comment.
1071 unsafe {
1072 engine.arena.inline_mut()[loc.word_offset as usize] = narrow_word(5, 0);
1073 }
1074
1075 let result = engine.commit_write(cell, FourStateValue::Narrow(narrow_word(6, 0)), &mut ());
1076 assert!(result.changed);
1077 match result.old {
1078 FourStateValue::Narrow(w) => assert_eq!(w, narrow_word(5, 0)),
1079 FourStateValue::Wide { .. } => panic!("expected Narrow"),
1080 }
1081 match result.new {
1082 FourStateValue::Narrow(w) => assert_eq!(w, narrow_word(6, 0)),
1083 FourStateValue::Wide { .. } => panic!("expected Narrow"),
1084 }
1085
1086 let count = WORKER_CACHE.with(|cache| {
1087 let buffer_ptr = cache.borrow().as_ref().unwrap().buffer;
1088 // SAFETY: see `last_vc_record_for`'s identical SAFETY comment.
1089 let buffer = unsafe { &*buffer_ptr };
1090 buffer.borrow().iter().filter(|r| r.cell == cell).count()
1091 });
1092 assert_eq!(count, 1, "the bypassed seed write must not be counted");
1093 }
1094
1095 /// ST-155 (the literal debugger.md-flagged edge case): a driven-`1`
1096 /// narrow cell (`val=1,unk=0`) written to driven-`Z` (`val=1,unk=1` per
1097 /// `Bit4::to_val_unk` -- value bit unchanged, mask bit flips) must
1098 /// report `changed: true`. A value-only comparison (`old as u64 == new
1099 /// as u64`, ignoring the high 64 bits) passes this exact transition
1100 /// unchanged and would fail this test.
1101 #[test]
1102 fn commit_write_diffs_the_mask_half_not_just_the_value_half() {
1103 let mut engine = fresh_engine();
1104 let (cell, loc) = inline_cell(&mut engine);
1105 // SAFETY: same as the previous test's seed write.
1106 unsafe {
1107 engine.arena.inline_mut()[loc.word_offset as usize] = narrow_word(1, 0);
1108 }
1109
1110 let result = engine.commit_write(cell, FourStateValue::Narrow(narrow_word(1, 1)), &mut ());
1111 assert!(
1112 result.changed,
1113 "a driven-1 -> driven-Z transition (value bit constant, mask bit flips) must register as changed"
1114 );
1115 }
1116
1117 /// ST-155, wide-tier sibling (review fix): the same X/Z-only-transition
1118 /// edge case, on a `Wide{aval,bval}` cell -- `aval` unchanged, `bval`
1119 /// flips 0->1. A hand-rolled comparator that diffs only `aval`
1120 /// element-wise (forgetting `bval`) would wrongly report `changed:
1121 /// false` and fail this test.
1122 #[test]
1123 fn commit_write_diffs_the_mask_half_not_just_the_value_half_on_a_wide_cell() {
1124 let mut engine = fresh_engine();
1125 let (cell, loc) = wide_cell(&mut engine, 1);
1126 // SAFETY: local, single-owned engine; no other live borrow of the
1127 // wide planes across this write; bypasses `commit_write` for the
1128 // seed, same reasoning as the narrow seed tests above.
1129 unsafe {
1130 engine.arena.wide_aval_mut()[loc.word_offset as usize] = 1;
1131 engine.arena.wide_bval_mut()[loc.word_offset as usize] = 0;
1132 }
1133
1134 let result = engine.commit_write(
1135 cell,
1136 FourStateValue::Wide {
1137 aval: vec![1],
1138 bval: vec![1],
1139 },
1140 &mut (),
1141 );
1142 assert!(
1143 result.changed,
1144 "aval unchanged / bval flips 0->1 must still register as changed on the wide tier"
1145 );
1146 }
1147
1148 /// ST-091, ST-031: the hook fires exactly once per `commit_write`, and
1149 /// strictly *before* the store -- proven by asserting, from *inside* the
1150 /// hook callback itself, that the cell's currently-stored value still
1151 /// equals `old`.
1152 #[test]
1153 fn before_write_hook_is_called_exactly_once_per_commit_write_before_the_store() {
1154 struct RecordingHook {
1155 arena: std::rc::Rc<super::super::arena::Arena>,
1156 calls: Vec<(StateCellId, SlotLocation, u128, u128)>,
1157 }
1158 impl PreWriteHook for RecordingHook {
1159 fn before_write(
1160 &mut self,
1161 cell: StateCellId,
1162 loc: SlotLocation,
1163 old: &FourStateValue,
1164 new_incoming: &FourStateValue,
1165 _at: SimTime,
1166 ) {
1167 let current = self.arena.inline()[loc.word_offset as usize];
1168 let old_word = match old {
1169 FourStateValue::Narrow(w) => *w,
1170 FourStateValue::Wide { .. } => panic!("expected Narrow"),
1171 };
1172 assert_eq!(
1173 current, old_word,
1174 "the stored value must still equal `old` inside the hook -- it fires before the mutation"
1175 );
1176 let new_word = match new_incoming {
1177 FourStateValue::Narrow(w) => *w,
1178 FourStateValue::Wide { .. } => panic!("expected Narrow"),
1179 };
1180 self.calls.push((cell, loc, old_word, new_word));
1181 }
1182 }
1183
1184 let mut engine = fresh_engine();
1185 let (cell, loc) = inline_cell(&mut engine);
1186 let mut hook = RecordingHook {
1187 arena: std::rc::Rc::clone(&engine.arena),
1188 calls: Vec::new(),
1189 };
1190
1191 engine.commit_write(cell, FourStateValue::Narrow(narrow_word(9, 0)), &mut hook);
1192
1193 assert_eq!(hook.calls.len(), 1);
1194 let (called_cell, called_loc, old_word, new_word) = hook.calls[0];
1195 assert_eq!(called_cell, cell);
1196 assert_eq!(called_loc, loc);
1197 assert_eq!(old_word, narrow_word(0, 0));
1198 assert_eq!(new_word, narrow_word(9, 0));
1199 }
1200
1201 /// Review fix: two explicit `register_worker_thread` calls on one engine
1202 /// return distinct, increasing slots; separately, a *fresh* engine on
1203 /// which `commit_write` is called directly (no explicit
1204 /// `register_worker_thread` call first) still produces a retrievable
1205 /// `last_vc_record_for` entry -- proving `commit_write` self-registers
1206 /// on first use.
1207 #[test]
1208 fn register_worker_thread_returns_distinct_increasing_slots_and_commit_write_self_registers() {
1209 let mut engine = fresh_engine();
1210 let slot0 = engine.register_worker_thread();
1211 let slot1 = engine.register_worker_thread();
1212 assert_eq!(slot0, WorkerSlot(0));
1213 assert_eq!(slot1, WorkerSlot(1));
1214
1215 let mut fresh = fresh_engine();
1216 let (cell, _) = inline_cell(&mut fresh);
1217 fresh.commit_write(cell, FourStateValue::Narrow(narrow_word(1, 0)), &mut ());
1218
1219 let record = last_vc_record_for(cell);
1220 assert!(
1221 record.is_some(),
1222 "commit_write must self-register this thread against the fresh engine, not require an explicit register_worker_thread call first"
1223 );
1224 }
1225
1226 // -- Task 13: 2-state build-mode arena and read/write family
1227 // (ST-100, ST-101, ST-102, ST-139) --
1228
1229 /// ST-102: the 2-state analogue of `commit_write_on_unchanged_value_produces_zero_vc_records`'s
1230 /// change-detection compare, using plain `u64` equality (no mask half
1231 /// exists in this family to compare). Fails on a primitive that
1232 /// stores-then-diffs (always "changing") or that skips the compare
1233 /// entirely (always reporting `changed: true`), and would also fail if
1234 /// this family accidentally routed through the 4-state `four_state_changed`
1235 /// mask-aware comparator instead of a plain scalar compare.
1236 #[test]
1237 fn two_state_write_changes_are_detected_without_any_mask_comparison() {
1238 let arena = Arena::new(BuildMode::TwoState);
1239 // SAFETY: `arena` is a local, single-owned value; no other live
1240 // borrow of the inline_2s plane across these sequential calls.
1241 let off = unsafe { arena.alloc_inline_2s_words(1) };
1242
1243 assert!(
1244 write_inline_2s(&arena, off, 5),
1245 "uninitialized zero -> 5 is a real change"
1246 );
1247 assert!(
1248 !write_inline_2s(&arena, off, 5),
1249 "writing the already-held value must not report a change"
1250 );
1251 assert!(write_inline_2s(&arena, off, 6), "5 -> 6 is a real change");
1252 assert_eq!(read_inline_2s(&arena, off), 6);
1253 }
1254
1255 /// ST-100, ST-102: proves the FFI view's wide planes stay null in
1256 /// `BuildMode::TwoState` even after a real wide signal has been allocated
1257 /// through the full `StateEngine::allocate_instance` path (Task 2's own
1258 /// `ffi_view_wide_planes_null_only_in_two_state_mode` test already proved
1259 /// this at the empty-arena level; this test proves it still holds once a
1260 /// 128-bit signal has actually been allocated and written through
1261 /// `write_wide_2s`). Fails if the FFI view always returns the (non-null)
1262 /// `Vec`'s pointer regardless of mode once real allocation has occurred.
1263 #[test]
1264 fn two_state_binary_never_allocates_a_wide_bval_word() {
1265 use crate::netlist::{SymbolKey, TemplateKey};
1266 use crate::state::{SlotDescriptor, SlotOffset, StateLayout};
1267 use std::rc::Rc;
1268
1269 let templates = TemplateRegistry::new();
1270 let tmpl = TemplateKey {
1271 definition: 0,
1272 params_hash: 0,
1273 };
1274 // SAFETY: `templates` is a local, single-owned value with no other
1275 // live borrow and no concurrent caller.
1276 unsafe {
1277 templates.register(
1278 tmpl,
1279 StateLayout {
1280 slots: vec![SlotDescriptor {
1281 key: SymbolKey(0),
1282 offset: SlotOffset {
1283 class: SlotClass::Wide,
1284 word_offset: 0,
1285 bit_width: 128,
1286 elem_word_count: 2,
1287 row_stride_words: 0,
1288 num_rows: 0,
1289 },
1290 initial: None,
1291 net_info: None,
1292 }],
1293 inline_words: 0,
1294 wide_words: 2,
1295 },
1296 );
1297 }
1298
1299 let mut engine = StateEngine::new_with_mode(templates, BuildMode::TwoState);
1300 assert_eq!(engine.build_mode(), BuildMode::TwoState);
1301
1302 let handle = engine
1303 .allocate_instance(tmpl)
1304 .expect("template is registered");
1305 let changed = write_wide_2s(
1306 &engine.arena,
1307 handle.base.wide_base,
1308 &[0xAAAA_AAAA_AAAA_AAAA, 0xBBBB_BBBB_BBBB_BBBB],
1309 );
1310 assert!(
1311 changed,
1312 "writing a nonzero pattern to a fresh zero-initialized cell must report changed"
1313 );
1314 assert_eq!(
1315 read_wide_2s(&engine.arena, handle.base.wide_base, 2),
1316 &[0xAAAA_AAAA_AAAA_AAAA, 0xBBBB_BBBB_BBBB_BBBB]
1317 );
1318 drop(handle);
1319
1320 let arena = Rc::get_mut(&mut engine.arena)
1321 .expect("no other Rc<Arena> clone outstanding after dropping the handle");
1322 let view = arena.ffi_view();
1323 assert!(view.wide_aval.is_null());
1324 assert!(view.wide_bval.is_null());
1325 }
1326
1327 /// ST-139: a `grep`-free structural companion -- `write_inline_2s`/
1328 /// `read_inline_2s` only ever index `arena.inline_2s` (their own function
1329 /// bodies take no `&arena.inline`/`&arena.wide_aval`/`&arena.wide_bval`
1330 /// argument, so there is no code path through which they could touch the
1331 /// 4-state planes). This test proves it observably: after a 2-state-only
1332 /// write, the 4-state inline plane (grown in lockstep by
1333 /// `alloc_inline_2s_words`, per its own deviation note in `arena.rs`, but
1334 /// never written to by anything in this test) is still all-zero.
1335 #[test]
1336 fn two_state_functions_never_touch_the_4state_planes() {
1337 let arena = Arena::new(BuildMode::TwoState);
1338 // SAFETY: `arena` is a local, single-owned value; no other live
1339 // borrow of either plane across these sequential calls.
1340 let off = unsafe { arena.alloc_inline_2s_words(1) };
1341
1342 write_inline_2s(&arena, off, 0xDEAD_BEEF);
1343
1344 assert!(
1345 arena.inline().iter().all(|&w| w == 0),
1346 "a 2-state-only write must never touch the 4-state inline plane"
1347 );
1348 }
1349
1350 /// ST-101: constructing a `StateEngine` in `FourState` mode reports
1351 /// `FourState`; the absence of any `set_build_mode`/`&mut
1352 /// BuildMode`-returning method anywhere in `StateEngine`'s public API is
1353 /// the structural enforcement (documented, not independently
1354 /// runtime-tested beyond this getter returning a plain `Copy` value, not
1355 /// a mutable reference).
1356 #[test]
1357 fn build_mode_is_fixed_at_construction_no_setter_exists() {
1358 let engine = fresh_engine();
1359 assert_eq!(engine.build_mode(), BuildMode::FourState);
1360
1361 let two_state = StateEngine::new_with_mode(TemplateRegistry::new(), BuildMode::TwoState);
1362 assert_eq!(two_state.build_mode(), BuildMode::TwoState);
1363 }
1364
1365 // -- Task 11: wide/memory word-level read-write shape (ST-137, ST-138) --
1366
1367 /// ST-138: a hand-built `SlotLocation` deliberately with `row_stride_words
1368 /// (3) != elem_word_count (2)` -- a case the real layout builder never
1369 /// actually produces (it always sets them equal), but which the
1370 /// *documented formula* must still get right, proving `resolve_mem_row`
1371 /// implements state.md §3's literal formula rather than one that happens
1372 /// to work only because the two fields are always equal in practice.
1373 #[test]
1374 fn resolve_mem_row_computes_word_offset_plus_row_stride_times_row_index() {
1375 let loc = SlotLocation {
1376 class: SlotClass::Memory,
1377 word_offset: 100,
1378 bit_width: 32,
1379 elem_word_count: 2,
1380 row_stride_words: 3,
1381 num_rows: 8,
1382 };
1383 assert_eq!(resolve_mem_row(loc, 0).word_offset, 100);
1384 assert_eq!(resolve_mem_row(loc, 3).word_offset, 109);
1385 assert_eq!(resolve_mem_row(loc, 7).word_offset, 121);
1386 }
1387
1388 /// ST-138: an out-of-bounds row index must panic loudly, never compute a
1389 /// bogus in-range-looking offset that silently aliases whatever the arena
1390 /// allocated next.
1391 #[test]
1392 #[should_panic(expected = "row index")]
1393 fn resolve_mem_row_panics_on_an_out_of_bounds_row_index() {
1394 let loc = SlotLocation {
1395 class: SlotClass::Memory,
1396 word_offset: 100,
1397 bit_width: 32,
1398 elem_word_count: 2,
1399 row_stride_words: 3,
1400 num_rows: 8,
1401 };
1402 resolve_mem_row(loc, 8);
1403 }
1404
1405 /// ST-138: row addressing must never silently apply to a non-memory slot.
1406 #[test]
1407 #[should_panic]
1408 fn resolve_mem_row_rejects_a_non_memory_slot_location() {
1409 let loc = SlotLocation {
1410 class: SlotClass::Wide,
1411 word_offset: 0,
1412 bit_width: 64,
1413 elem_word_count: 1,
1414 row_stride_words: 0,
1415 num_rows: 0,
1416 };
1417 resolve_mem_row(loc, 0);
1418 }
1419
1420 /// ST-137: a 200-bit (4-word) wide signal's write must compare all
1421 /// `ceil(200/64) == 4` words before deciding change/no-change. The second
1422 /// `commit_write` differs from the first ONLY in word 3 (the last word) --
1423 /// exactly the shape that catches a truncated/prefix-only compare (e.g. a
1424 /// buggy `four_state_changed` that only ever looked at word 0) while a
1425 /// first-word-difference case would not. A third `commit_write`,
1426 /// byte-identical to the second across all 4 words, must then report no
1427 /// change at all.
1428 #[test]
1429 fn wide_write_detects_a_change_confined_to_only_the_last_of_four_words() {
1430 let mut engine = fresh_engine();
1431 let (cell, _) = wide_cell(&mut engine, 4);
1432
1433 let first = engine.commit_write(
1434 cell,
1435 FourStateValue::Wide {
1436 aval: vec![1, 2, 3, 4],
1437 bval: vec![0, 0, 0, 0],
1438 },
1439 &mut (),
1440 );
1441 assert!(
1442 first.changed,
1443 "seeding from the zero default is a real change"
1444 );
1445
1446 // Differs from `first` ONLY in the last (4th) word.
1447 let second = engine.commit_write(
1448 cell,
1449 FourStateValue::Wide {
1450 aval: vec![1, 2, 3, 999],
1451 bval: vec![0, 0, 0, 0],
1452 },
1453 &mut (),
1454 );
1455 assert!(
1456 second.changed,
1457 "a difference confined to the last of 4 words must still register as changed \
1458 -- a truncated/prefix-only word compare would miss this"
1459 );
1460
1461 // Byte-identical to `second` -- the no-op case, exercised across all
1462 // 4 words, not just word 0.
1463 let third = engine.commit_write(
1464 cell,
1465 FourStateValue::Wide {
1466 aval: vec![1, 2, 3, 999],
1467 bval: vec![0, 0, 0, 0],
1468 },
1469 &mut (),
1470 );
1471 assert!(
1472 !third.changed,
1473 "writing the already-held 4-word value must not report a change"
1474 );
1475 }
1476}