Skip to main content

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}