Skip to main content

sv_sim_sim/state/
rollback.rs

1//! Task 16 — Undo-log rollback and the one generic coarse snapshot/restore
2//! primitive (ledger ST-032, ST-036, ST-098).
3//!
4//! Two independent primitives, both documented in ARCHITECTURE §6.2/§7 item 5
5//! as STATE's checkpoint machinery:
6//! - [`UndoLog`] (ST-036): a per-thread-owned reverse log implementing Task
7//!   8's [`super::PreWriteHook`], giving that hook its first real body.
8//!   `O(1)`-amortized append via `before_write`, `O(distance)` undo via
9//!   [`UndoLog::rollback_to`] — the checkpoint cost model ST-032 asks be
10//!   "referenced by the accessor-ABI calling convention" (Task 8's
11//!   `commit_write(&mut self, cell, new, hook: &mut dyn PreWriteHook)`,
12//!   frozen since that task).
13//! - [`StateEngine::snapshot`]/[`StateEngine::restore`]/
14//!   [`StateEngine::reset_to_initial`] (ST-098): the one generic
15//!   slice-granular checkpoint/restore primitive — an optional
16//!   `(SlotClass, start_word, word_count)` range, `None` meaning "the whole
17//!   arena" — WOCB's inner-window rollback, a top-level `$save`/`$restart`,
18//!   and VPI/debugger state snapshotting all call this *same* function with
19//!   different `range` arguments, never three separate serialization
20//!   implementations.
21//!
22//! **Deviations from the plan's literal text** (documented once here,
23//! applying to everything below — the first four are related: each exists
24//! because the plan's own pinned signatures, taken literally, are either
25//! under-specified or structurally unimplementable as written; the fifth is
26//! a review fix, not a plan-signature gap):
27//!
28//! 1. **`checkpoint_marker`/`rollback_to`/`fossil_collect` live on
29//!    [`UndoLog`], not on `StateEngine`.** The plan's prose puts them in
30//!    `impl StateEngine`, but `StateEngine` cannot *also* own the log as a
31//!    field and have `commit_write`'s existing, frozen
32//!    `hook: &mut dyn PreWriteHook` parameter be `&mut self.undo_log` at the
33//!    one call site that matters — `engine.commit_write(cell, new, &mut
34//!    engine.undo_log)` is two overlapping mutable borrows of `self` through
35//!    one method call, which Rust refuses. Keeping [`UndoLog`] a plain,
36//!    caller-owned value — exactly mirroring `()`'s existing role as the
37//!    no-op hook — sidesteps that conflict entirely and needs no new
38//!    `StateEngine` field: a caller passes `&mut log` to `commit_write`
39//!    exactly like `&mut ()`, and later calls `log.rollback_to(&mut engine,
40//!    mark)` (which *does* need `&mut StateEngine`, to replay into it — its
41//!    only extra parameter versus the plan's pinned prose) or
42//!    `log.fossil_collect(gvt)`/`log.checkpoint_marker()` (which don't).
43//! 2. **`rollback_to` returns `Result<(), sched::RewindError>`, not `()`**
44//!    (binding per this task's own unresolved-review-findings list, findings
45//!    3/4): the plan's own `fossil_collect` test requires that a
46//!    `rollback_to` call targeting a marker before the fossil-collected
47//!    horizon "returns a documented `RewindError`-shaped failure rather than
48//!    silently under-rolling-back," which a unit-returning function
49//!    structurally cannot do. `sched::RewindError` (the crate's own
50//!    contract stub, `crate::sched::mod.rs`) already declares exactly the
51//!    two variants this module needs — `PastFossilHorizon` (reused here, per
52//!    the plan's own test prose) and `LayoutMismatch` (reused by
53//!    [`StateEngine::restore`] below, for the identical reason: no new local
54//!    error type duplicates what the scheduler contract already names).
55//! 3. **`PreWriteHook::before_write` gained a `SimTime` parameter**
56//!    (`state::write`, this module's one caller of the trait): `fossil_collect
57//!    (gvt)`'s "trims entries provably before `gvt`" is only meaningful if
58//!    [`UndoLog`]'s entries carry a real timestamp to compare, and nothing
59//!    else in the calling convention threads one to the hook — see
60//!    `state::write`'s own doc comment on this second, smaller deviation.
61//! 4. **[`ArenaSnapshot`] carries a fifth field, `range`,** beyond the plan's
62//!    literal `{layout_hash, inline, wide_aval, wide_bval}` list: `restore`'s
63//!    own pinned signature (`&mut self, snap: &ArenaSnapshot`) takes no
64//!    `range` parameter of its own, so a snapshot that only stored the four
65//!    plan-listed fields would have no way to tell `restore` *where* a
66//!    partial-range snapshot's data belongs — seeing this field is the
67//!    minimal addition that makes the plan's own pinned `restore` signature
68//!    actually implementable for the partial-range case its own test
69//!    requires.
70//! 5. **`ArenaSnapshot` carries two further fields, `build_mode` and a
71//!    2-state pair (`inline_2s`, `wide_2s`), and `snapshot`/`restore`/
72//!    `reset_to_initial` are all build-mode-aware** (Task 16 r1, blocking
73//!    review finding 1): the plan's own prose pins `reset_to_initial`'s
74//!    contract as "uninitialized 4-state variables ... to all-X ..., 2-state
75//!    to 0" — i.e. this primitive is specified to handle a
76//!    `BuildMode::TwoState` engine, not just the default `BuildMode::FourState`
77//!    one this module's first revision exclusively handled. Task 13's own
78//!    `alloc_inline_words`/`alloc_wide_words` grow the 4-state and 2-state
79//!    plane-pairs in lockstep at identical word offsets (`state::arena`'s own
80//!    module doc comment), so a `TwoState` engine's real live data lives in
81//!    `inline_2s`/`wide_2s` at the exact same offsets this module's
82//!    4-state-only primitives were reading/writing instead — silently
83//!    touching only the inert, always-zero 4-state planes and leaving the
84//!    real data completely untouched, with no panic or error of any kind.
85//!    `snapshot` now branches on `self.build_mode()` once per call and
86//!    captures whichever plane-pair is actually live for that range (never
87//!    both — the other pair's fields stay empty `Vec`s), and stamps the
88//!    captured `BuildMode` onto the snapshot; `restore` rejects
89//!    (`Err(RewindError::LayoutMismatch)`) a snapshot whose stored
90//!    `build_mode` doesn't match `self`'s own, before touching any plane —
91//!    the same "no ArenaSnapshot with a ForceWide range can exist to reach
92//!    that arm" style of making a cross-mode restore structurally impossible
93//!    to mis-apply, rather than merely undocumented. This does not conflict
94//!    with `state::write`'s own "never a runtime branch inside either
95//!    [4-state/2-state] family" design principle: that principle is about
96//!    per-write dispatch inside a compiled kernel's hot path, never applying
97//!    to this module's coarse, call-once-per-checkpoint administrative bulk
98//!    operations, which already branch once per call on `SlotClass` in
99//!    exactly this same way.
100
101use sv_sim_abi::SimTime;
102
103use super::arena::{Arena, BuildMode};
104use super::instance::StateEngine;
105use super::write::{PreWriteHook, SlotLocation, write_inline_2s, write_slot, write_wide_2s};
106use super::{FourStateValue, SlotClass, StateCellId};
107use crate::sched::RewindError;
108
109/// An opaque log-length marker (ST-036) — see [`UndoLog::checkpoint_marker`].
110/// The wrapped index is absolute over the log's *entire* lifetime (it
111/// includes entries [`UndoLog::fossil_collect`] has already trimmed), never
112/// merely an index into the log's *currently retained* `Vec` — see
113/// [`UndoLog::rollback_to`]'s own doc comment for why that distinction is
114/// what makes the fossil-horizon check correct.
115#[derive(Clone, Copy, PartialEq, Eq, Debug)]
116pub struct RollbackMark(usize);
117
118/// Task 16 (ST-098): per-cell reset behavior [`StateEngine::reset_to_initial`]
119/// consults, populated in lockstep with `StateEngine.cells` — see
120/// `state::instance`'s `StateEngine.cell_reset` field doc comment for how
121/// each cell gets its real entry.
122#[derive(Debug)]
123pub(crate) enum CellReset {
124    /// An uninitialized non-net variable/port: 4-state all-X (`val=0,
125    /// mask=all-1`), sized from the cell's own `SlotLocation` — the
126    /// conservative default `write::StateEngine::alloc_cell` pushes for
127    /// *every* cell (including force_driver's synthetic contribution cells
128    /// and this crate's own test helpers), never overwritten for those.
129    AllX,
130    /// A variable/port with a declared initializer (Task 6): the exact
131    /// `FourStateValue` to re-blit, already converted from the slot's
132    /// `sv_lang::OwnedSVInt` at `allocate_instance` time.
133    Initial(FourStateValue),
134    /// A net-backed cell (`SlotDescriptor.net_info.is_some()`): never
135    /// touched by `reset_to_initial` — its reset state is the emergent `Z`
136    /// `driver::resolve_net_step` folds to over zero active drivers (Task
137    /// 14), not a stored arena default.
138    Net,
139    /// **Review fix (Task 16 r1, blocking finding 2):** one of
140    /// `driver::force_driver`'s synthetic, caller-invisible contribution
141    /// cells (both its `SlotClass::Inline` arm, which shares the ordinary
142    /// inline plane with real variables and is otherwise indistinguishable
143    /// from one once `alloc_cell` has run, and its `SlotClass::ForceWide`
144    /// arm) — never touched by `reset_to_initial`, for the identical reason
145    /// [`StateEngine::snapshot`] already refuses a `SlotClass::ForceWide`
146    /// range outright: these cells are never part of a real elaborated
147    /// signal's storage, so blitting all-X (or any reset value) over one
148    /// would silently corrupt a live `force` contribution with no error,
149    /// exactly the "silent corruption" class `snapshot`'s own `ForceWide`
150    /// panic exists to prevent for the snapshot/restore half of this same
151    /// primitive family. `force_driver` (`state::driver`) sets this on its
152    /// synthetic cell immediately after `StateEngine::alloc_cell`, the same
153    /// way `allocate_instance` overwrites `alloc_cell`'s conservative
154    /// default for its own real slots.
155    ForceContribution,
156}
157
158/// ST-036: the per-thread undo/reverse log — see this module's top doc
159/// comment for why it is a plain, caller-owned value (like `()`) rather than
160/// a `StateEngine`-owned field. Implements [`PreWriteHook`]: every
161/// `commit_write(cell, new, &mut log)` call appends `(cell, old, at)` here
162/// *before* the store happens (Task 8/ST-091's own ordering guarantee),
163/// giving that hook its first real body.
164#[derive(Default)]
165pub struct UndoLog {
166    entries: Vec<(StateCellId, FourStateValue, SimTime)>,
167    /// The absolute count of entries [`Self::fossil_collect`] has ever
168    /// trimmed from the front over this log's lifetime — see
169    /// [`RollbackMark`]'s own doc comment for why marks are absolute indices
170    /// over this running total, not indices into `entries` alone.
171    collected: usize,
172}
173
174impl UndoLog {
175    pub fn new() -> Self {
176        UndoLog::default()
177    }
178
179    /// ST-036: an opaque marker for the log's current length — "current"
180    /// meaning the absolute count of entries ever appended so far (already
181    /// including any [`Self::fossil_collect`] has trimmed), so a mark taken
182    /// before a trim and a mark taken after it are still comparable on the
183    /// same absolute scale.
184    pub fn checkpoint_marker(&self) -> RollbackMark {
185        RollbackMark(self.collected + self.entries.len())
186    }
187
188    /// ST-036: pops entries back to `mark`, replaying each popped entry's
189    /// `old` value into `engine` via [`StateEngine::commit_write_no_hook`]
190    /// (a *second*, hook-free write primitive, so unwinding the log never
191    /// re-appends to the very log it's unwinding) in reverse order — `Vec::
192    /// pop` naturally visits the most-recently-appended entry first, which
193    /// is what makes a cell written twice since `mark` end up back at its
194    /// original (not its intermediate) value.
195    ///
196    /// `Err(RewindError::PastFossilHorizon)` — never a silent
197    /// under-rollback — when `mark` refers to a position at or before
198    /// entries [`Self::fossil_collect`] has already, irreversibly discarded;
199    /// the log is left completely untouched in that case (checked before any
200    /// entry is popped).
201    pub fn rollback_to(
202        &mut self,
203        engine: &mut StateEngine,
204        mark: RollbackMark,
205    ) -> Result<(), RewindError> {
206        if mark.0 < self.collected {
207            return Err(RewindError::PastFossilHorizon);
208        }
209        let target_local = mark.0 - self.collected;
210        while self.entries.len() > target_local {
211            let (cell, old, _at) = self
212                .entries
213                .pop()
214                .expect("entries.len() > target_local, checked by the loop condition, guarantees pop succeeds");
215            engine.commit_write_no_hook(cell, old);
216        }
217        Ok(())
218    }
219
220    /// Trims every entry with `at <= gvt` from the *front* of the log
221    /// (append-only, so the front holds the oldest entries) — "at or before
222    /// `gvt`, never past it": the scan stops at the first entry whose `at`
223    /// is strictly after `gvt`, so nothing newer is ever removed even if a
224    /// later entry happened to compare `<=` some other way.
225    pub fn fossil_collect(&mut self, gvt: SimTime) {
226        let mut cut = 0;
227        while cut < self.entries.len() && super::write::sim_time_le(self.entries[cut].2, gvt) {
228            cut += 1;
229        }
230        if cut > 0 {
231            self.entries.drain(0..cut);
232            self.collected += cut;
233        }
234    }
235
236    /// Test-only peek at how many entries are currently retained (distinct
237    /// from `checkpoint_marker`'s absolute, ever-appended count) — lets a
238    /// test assert `fossil_collect` actually removed entries without
239    /// depending on `rollback_to`'s own (separately tested) replay behavior.
240    #[cfg(test)]
241    fn retained_len(&self) -> usize {
242        self.entries.len()
243    }
244}
245
246impl PreWriteHook for UndoLog {
247    fn before_write(
248        &mut self,
249        cell: StateCellId,
250        _loc: SlotLocation,
251        old: &FourStateValue,
252        _new_incoming: &FourStateValue,
253        at: SimTime,
254    ) {
255        self.entries.push((cell, old.clone(), at));
256    }
257}
258
259/// ST-098: the one generic slice-granular checkpoint/restore primitive's
260/// data — see this module's top doc comment, deviation 4, for why `range` is
261/// a fifth field beyond the plan's literal list, and deviation 5 for why
262/// `build_mode`/`inline_2s`/`wide_2s` exist beyond that (only one of
263/// `{inline, wide_aval, wide_bval}`/`{inline_2s, wide_2s}` is ever populated
264/// for a given snapshot — whichever plane-pair `build_mode` says is actually
265/// live — the other stays empty `Vec`s).
266pub struct ArenaSnapshot {
267    layout_hash: u64,
268    range: Option<(SlotClass, u32, u32)>,
269    build_mode: BuildMode,
270    inline: Vec<u128>,
271    wide_aval: Vec<u64>,
272    wide_bval: Vec<u64>,
273    inline_2s: Vec<u64>,
274    wide_2s: Vec<u64>,
275}
276
277/// Builds the all-X [`FourStateValue`] for an uninitialized cell's own
278/// `SlotLocation` shape — reuses [`bits_to_four_state`] for the `Inline`
279/// case (always `<= 64` bits by construction, so it always yields `Narrow`,
280/// never ambiguous) rather than hand-deriving the val/unk bit pattern; the
281/// `Wide`/`Memory`/`ForceWide` case constructs the per-word all-ones
282/// `aval`/`bval` planes directly from the already-established
283/// `Bit4::to_aval_bval(X) == (true, true)` table (`state::value`'s own test,
284/// `bits_to_four_state_wide_uses_bit4_aval_bval_table`, is what pins that
285/// exact constant) rather than through `bits_to_four_state` itself, because
286/// a `Wide`-class cell can legitimately be exactly 64 bits (one word) —
287/// `bits_to_four_state`'s own `<= 64` branch would then return `Narrow`, a
288/// shape `write_slot`'s `Wide`/`Memory` match arm rejects outright.
289fn all_x_value_for(loc: SlotLocation) -> FourStateValue {
290    match loc.class {
291        SlotClass::Inline => {
292            // Narrow lanes are `2*unk+val` with val in the low 64 bits and
293            // unk in the high 64; X is (val 0, unk 1) per
294            // `Bit4::to_val_unk`, so all-X is the width mask in the unk
295            // plane. (Built directly, not through the frontend-only
296            // `bits_to_four_state`, so this module builds without the
297            // `frontend` feature -- CONTRACTS D4.)
298            let (x_val, x_unk) = sv_sim_abi::Bit4::X.to_val_unk();
299            let mask: u64 = if loc.bit_width >= 64 {
300                u64::MAX
301            } else {
302                (1u64 << loc.bit_width) - 1
303            };
304            let val = if x_val { mask } else { 0 };
305            let unk = if x_unk { mask } else { 0 };
306            FourStateValue::Narrow((val as u128) | ((unk as u128) << 64))
307        }
308        SlotClass::Wide | SlotClass::Memory | SlotClass::ForceWide => {
309            let n = loc.elem_word_count as usize;
310            FourStateValue::Wide {
311                aval: vec![u64::MAX; n],
312                bval: vec![u64::MAX; n],
313            }
314        }
315    }
316}
317
318/// **Review fix (Task 16 r1, blocking finding 1):** the `BuildMode::TwoState`
319/// counterpart of [`all_x_value_for`] — an uninitialized 2-state cell resets
320/// to plain `0` (the plan's own `reset_to_initial` contract: "... 2-state to
321/// `0`"), never all-ones, since there is no mask/unknown half in the 2-state
322/// plane to set at all.
323///
324/// # Panics
325/// If `loc.class` is `SlotClass::ForceWide` — unreachable in practice, since
326/// [`StateEngine::reset_to_initial`] filters out every
327/// [`CellReset::ForceContribution`] cell (which is what every `ForceWide`
328/// cell's own `CellReset` entry is tagged as, per that variant's own doc
329/// comment) before this helper is ever called.
330fn reset_2s_cell_to_zero(arena: &Arena, loc: SlotLocation) {
331    match loc.class {
332        SlotClass::Inline => {
333            write_inline_2s(arena, loc.word_offset, 0);
334        }
335        SlotClass::Wide | SlotClass::Memory => {
336            let zeros = vec![0u64; loc.elem_word_count as usize];
337            write_wide_2s(arena, loc.word_offset, &zeros);
338        }
339        SlotClass::ForceWide => unreachable!(
340            "reset_2s_cell_to_zero: force_driver's contribution cells are tagged \
341             CellReset::ForceContribution and are filtered out by reset_to_initial \
342             before this helper is ever reached for one"
343        ),
344    }
345}
346
347/// **Review fix (Task 16 r1, blocking finding 1):** the `BuildMode::TwoState`
348/// counterpart of re-blitting a [`CellReset::Initial`] value — projects the
349/// 4-state [`FourStateValue`] a declared initializer was already converted
350/// to (Task 6, `value::owned_svint_to_four_state`) down to just its known
351/// "value" half (`val` for `Narrow`, `aval` for `Wide` — `state::value`'s own
352/// val/unk and aval/bval encoding, where the first half is always the plain
353/// value bits and the second is the unknown-ness mask), since the 2-state
354/// plane has no mask half to store an unknown bit in at all.
355///
356/// # Panics
357/// If `loc`'s `SlotClass` doesn't match `fv`'s own `FourStateValue` shape
358/// (`Inline`/`Narrow` or `Wide|Memory`/`Wide`) — the same class of
359/// caller-contract violation `write_slot`'s own shape-mismatch arm already
360/// panics on elsewhere in this crate; every real `CellReset::Initial` this
361/// crate ever constructs (`allocate_instance`, the one call site) is built
362/// from the same slot's own `SlotLocation`, so reaching this arm indicates a
363/// lockstep-invariant violation elsewhere, never a normal case to tolerate
364/// silently.
365fn write_2s_cell_from_initial(arena: &Arena, loc: SlotLocation, fv: &FourStateValue) {
366    match (loc.class, fv) {
367        (SlotClass::Inline, FourStateValue::Narrow(word)) => {
368            write_inline_2s(arena, loc.word_offset, *word as u64);
369        }
370        (SlotClass::Wide | SlotClass::Memory, FourStateValue::Wide { aval, .. }) => {
371            write_wide_2s(arena, loc.word_offset, aval);
372        }
373        (class, value) => panic!(
374            "write_2s_cell_from_initial: SlotClass {class:?} does not match the \
375             FourStateValue shape of this cell's own CellReset::Initial ({value:?}) -- \
376             a CellReset::Initial is only ever built by allocate_instance from the same \
377             slot's own SlotLocation, so this indicates a lockstep-invariant violation \
378             elsewhere in this crate, not a normal case this helper tolerates silently"
379        ),
380    }
381}
382
383impl StateEngine {
384    /// ST-036: writes `value` at `cell`'s already-allocated `SlotLocation`
385    /// with no [`PreWriteHook`] call and no `VcRecord` append — used
386    /// exclusively by [`UndoLog::rollback_to`]'s own reverse replay, so
387    /// unwinding the log never re-appends to the very log it is unwinding,
388    /// and a rollback (reverting to a value the log already knows was live)
389    /// is never itself treated as a fresh, dispatch-notified "change."
390    pub(crate) fn commit_write_no_hook(&mut self, cell: StateCellId, value: FourStateValue) {
391        let loc = self.cells[cell.0 as usize];
392        write_slot(&self.arena, loc, &value);
393    }
394
395    /// ST-098: takes a snapshot of `range` (`None` = the whole arena) —
396    /// `layout_hash` is `self.templates.signature_hash()` at the moment of
397    /// the call, guarding a later [`Self::restore`] against an incompatible
398    /// `StateEngine`. **Review fix (Task 16 r1, blocking finding 1):**
399    /// build-mode-aware — branches once on `self.build_mode()` and captures
400    /// whichever plane-pair (`{inline, wide_aval, wide_bval}` for
401    /// `FourState`, `{inline_2s, wide_2s}` for `TwoState`) is actually the
402    /// engine's live data at `range`'s word offsets (Task 13's
403    /// `alloc_inline_words`/`alloc_wide_words` grow both plane-pairs in
404    /// lockstep at identical offsets, `state::arena`'s own module doc
405    /// comment, so the offsets are shared — only which plane-pair holds the
406    /// real values differs); the other plane-pair's fields stay empty
407    /// `Vec`s. The captured `build_mode` is stamped onto the returned
408    /// snapshot so [`Self::restore`] can refuse a cross-mode restore.
409    ///
410    /// # Panics
411    /// If `range`'s `SlotClass` is `ForceWide` — `force_driver`'s synthetic,
412    /// caller-invisible contribution cells (see `SlotClass::ForceWide`'s own
413    /// doc comment) are never part of a real elaborated signal's storage and
414    /// are not an intended target of this API (WOCB rollback, `$save`/
415    /// `$restart`, and VPI/debugger snapshotting all operate over ordinary
416    /// `Inline`/`Wide`/`Memory` signals); a caller reaching this is a
417    /// programming error at the call site, the same class of failure
418    /// `write_slot`'s own shape-mismatch panic already documents elsewhere
419    /// in this crate, not a normal case this primitive tolerates silently.
420    pub fn snapshot(&self, range: Option<(SlotClass, u32, u32)>) -> ArenaSnapshot {
421        let layout_hash = self.templates.signature_hash();
422        let build_mode = self.build_mode();
423        let (inline, wide_aval, wide_bval, inline_2s, wide_2s) = match (build_mode, range) {
424            (BuildMode::FourState, None) => (
425                self.arena.inline().to_vec(),
426                self.arena.wide_aval().to_vec(),
427                self.arena.wide_bval().to_vec(),
428                Vec::new(),
429                Vec::new(),
430            ),
431            (BuildMode::FourState, Some((SlotClass::Inline, start, count))) => {
432                let (s, e) = (start as usize, start as usize + count as usize);
433                (
434                    self.arena.inline()[s..e].to_vec(),
435                    Vec::new(),
436                    Vec::new(),
437                    Vec::new(),
438                    Vec::new(),
439                )
440            }
441            (BuildMode::FourState, Some((SlotClass::Wide | SlotClass::Memory, start, count))) => {
442                let (s, e) = (start as usize, start as usize + count as usize);
443                (
444                    Vec::new(),
445                    self.arena.wide_aval()[s..e].to_vec(),
446                    self.arena.wide_bval()[s..e].to_vec(),
447                    Vec::new(),
448                    Vec::new(),
449                )
450            }
451            (BuildMode::TwoState, None) => (
452                Vec::new(),
453                Vec::new(),
454                Vec::new(),
455                self.arena.inline_2s().to_vec(),
456                self.arena.wide_2s().to_vec(),
457            ),
458            (BuildMode::TwoState, Some((SlotClass::Inline, start, count))) => {
459                let (s, e) = (start as usize, start as usize + count as usize);
460                (
461                    Vec::new(),
462                    Vec::new(),
463                    Vec::new(),
464                    self.arena.inline_2s()[s..e].to_vec(),
465                    Vec::new(),
466                )
467            }
468            (BuildMode::TwoState, Some((SlotClass::Wide | SlotClass::Memory, start, count))) => {
469                let (s, e) = (start as usize, start as usize + count as usize);
470                (
471                    Vec::new(),
472                    Vec::new(),
473                    Vec::new(),
474                    Vec::new(),
475                    self.arena.wide_2s()[s..e].to_vec(),
476                )
477            }
478            (_, Some((SlotClass::ForceWide, _, _))) => panic!(
479                "StateEngine::snapshot: SlotClass::ForceWide is never a valid snapshot/restore \
480                 range -- force_driver's synthetic contribution cells are never part of a real \
481                 elaborated signal's storage and are not observable through this API"
482            ),
483        };
484        ArenaSnapshot {
485            layout_hash,
486            range,
487            build_mode,
488            inline,
489            wide_aval,
490            wide_bval,
491            inline_2s,
492            wide_2s,
493        }
494    }
495
496    /// ST-098: restores `snap`'s data back into `self`'s arena at `snap`'s
497    /// own recorded range (or the whole arena, for `None`) — a slice
498    /// operation for `Some(range)`: words outside that range are left
499    /// completely untouched. `Err(RewindError::LayoutMismatch)` — checked
500    /// *before* any write — when `self`'s currently-registered template
501    /// set doesn't hash the same as the one `snap` was taken from, when
502    /// `self`'s `build_mode` doesn't match `snap`'s own (**review fix, Task
503    /// 16 r1, blocking finding 1** — restoring a `FourState` snapshot into a
504    /// `TwoState` engine, or vice versa, would otherwise silently target the
505    /// wrong plane-pair entirely, the identical hazard this fix closes for
506    /// `snapshot`/`reset_to_initial`), and when the target range's own word
507    /// count doesn't match what `snap` actually holds (a
508    /// same-hash-different-instance-count edge case the plan's own
509    /// four-field `ArenaSnapshot` shape doesn't separately name, but which
510    /// this same typed error class covers rather than panicking on a
511    /// length-mismatched `copy_from_slice`); on any mismatch, `self`'s
512    /// arena contents are provably unchanged — no partial corruption on the
513    /// rejected path.
514    pub fn restore(&mut self, snap: &ArenaSnapshot) -> Result<(), RewindError> {
515        if self.templates.signature_hash() != snap.layout_hash {
516            return Err(RewindError::LayoutMismatch);
517        }
518        if self.build_mode() != snap.build_mode {
519            return Err(RewindError::LayoutMismatch);
520        }
521        // The planes are about to be rewritten behind every huge-signal
522        // mirror (`state::huge_mirror`).
523        self.arena.huge.invalidate_all();
524        match (snap.build_mode, snap.range) {
525            (BuildMode::FourState, None) => {
526                if snap.inline.len() != self.arena.inline().len()
527                    || snap.wide_aval.len() != self.arena.wide_aval().len()
528                    || snap.wide_bval.len() != self.arena.wide_bval().len()
529                {
530                    return Err(RewindError::LayoutMismatch);
531                }
532                // SAFETY: `&mut self` guarantees no other live borrow of any
533                // Arena plane is held across this call (Arena's own
534                // single-logical-writer discipline, `state::arena`'s module
535                // doc comment); this `StateEngine` is the sole owner
536                // reaching this `Arena` at this moment, and the length
537                // checks above already proved every slice below is in
538                // bounds.
539                unsafe {
540                    self.arena.inline_mut()[..snap.inline.len()].copy_from_slice(&snap.inline);
541                    self.arena.wide_aval_mut()[..snap.wide_aval.len()]
542                        .copy_from_slice(&snap.wide_aval);
543                    self.arena.wide_bval_mut()[..snap.wide_bval.len()]
544                        .copy_from_slice(&snap.wide_bval);
545                }
546            }
547            (BuildMode::FourState, Some((SlotClass::Inline, start, count))) => {
548                let (s, e) = (start as usize, start as usize + count as usize);
549                if e > self.arena.inline().len() || snap.inline.len() != count as usize {
550                    return Err(RewindError::LayoutMismatch);
551                }
552                // SAFETY: same argument as the `None` arm above, restricted
553                // to the `[s, e)` sub-slice the bounds check just proved is
554                // in range.
555                unsafe {
556                    self.arena.inline_mut()[s..e].copy_from_slice(&snap.inline);
557                }
558            }
559            (BuildMode::FourState, Some((SlotClass::Wide | SlotClass::Memory, start, count))) => {
560                let (s, e) = (start as usize, start as usize + count as usize);
561                if e > self.arena.wide_aval().len()
562                    || e > self.arena.wide_bval().len()
563                    || snap.wide_aval.len() != count as usize
564                    || snap.wide_bval.len() != count as usize
565                {
566                    return Err(RewindError::LayoutMismatch);
567                }
568                // SAFETY: same argument as the `None` arm above, restricted
569                // to the `[s, e)` sub-slice the bounds check just proved is
570                // in range, applied to both wide planes.
571                unsafe {
572                    self.arena.wide_aval_mut()[s..e].copy_from_slice(&snap.wide_aval);
573                    self.arena.wide_bval_mut()[s..e].copy_from_slice(&snap.wide_bval);
574                }
575            }
576            (BuildMode::TwoState, None) => {
577                if snap.inline_2s.len() != self.arena.inline_2s().len()
578                    || snap.wide_2s.len() != self.arena.wide_2s().len()
579                {
580                    return Err(RewindError::LayoutMismatch);
581                }
582                // SAFETY: same argument as the `FourState`/`None` arm above,
583                // applied to the 2-state planes.
584                unsafe {
585                    self.arena.inline_2s_mut()[..snap.inline_2s.len()]
586                        .copy_from_slice(&snap.inline_2s);
587                    self.arena.wide_2s_mut()[..snap.wide_2s.len()].copy_from_slice(&snap.wide_2s);
588                }
589            }
590            (BuildMode::TwoState, Some((SlotClass::Inline, start, count))) => {
591                let (s, e) = (start as usize, start as usize + count as usize);
592                if e > self.arena.inline_2s().len() || snap.inline_2s.len() != count as usize {
593                    return Err(RewindError::LayoutMismatch);
594                }
595                // SAFETY: same argument as the `FourState`/`Inline` arm
596                // above, restricted to the `[s, e)` sub-slice the bounds
597                // check just proved is in range, applied to the 2-state
598                // inline plane.
599                unsafe {
600                    self.arena.inline_2s_mut()[s..e].copy_from_slice(&snap.inline_2s);
601                }
602            }
603            (BuildMode::TwoState, Some((SlotClass::Wide | SlotClass::Memory, start, count))) => {
604                let (s, e) = (start as usize, start as usize + count as usize);
605                if e > self.arena.wide_2s().len() || snap.wide_2s.len() != count as usize {
606                    return Err(RewindError::LayoutMismatch);
607                }
608                // SAFETY: same argument as the `FourState`/`Wide|Memory` arm
609                // above, restricted to the `[s, e)` sub-slice the bounds
610                // check just proved is in range, applied to the 2-state
611                // wide plane.
612                unsafe {
613                    self.arena.wide_2s_mut()[s..e].copy_from_slice(&snap.wide_2s);
614                }
615            }
616            (_, Some((SlotClass::ForceWide, _, _))) => unreachable!(
617                "StateEngine::restore: an ArenaSnapshot's own `range` is only ever set by \
618                 Self::snapshot, which already rejects SlotClass::ForceWide before constructing \
619                 one -- no ArenaSnapshot with a ForceWide range can exist to reach this arm"
620            ),
621        }
622        Ok(())
623    }
624
625    /// ST-098 (documented alongside `snapshot`/`restore` as this same
626    /// primitive family's third bulk operation, per the plan's own prose):
627    /// re-blits every cell's [`CellReset`] (`state::instance`'s
628    /// `cell_reset`, populated by `allocate_instance`) over the arena — a
629    /// declared initializer's exact value for a slot that has one, all-X (4-
630    /// state) or `0` (2-state) for an uninitialized non-net, non-force slot,
631    /// and nothing at all for a net-backed slot (its reset state is the
632    /// emergent `Z` `driver::resolve_net_step` folds to over zero active
633    /// drivers, never a stored arena default) or a `force_driver` contribution
634    /// cell (**review fix, Task 16 r1, blocking finding 2** — never a real
635    /// elaborated signal's storage; see [`CellReset::ForceContribution`]'s
636    /// own doc comment for why blitting over one would silently corrupt a
637    /// live `force`). Bypasses `commit_write`/`PreWriteHook`/`VcRecord`
638    /// entirely (a direct [`write_slot`]/[`write_inline_2s`]/[`write_wide_2s`]
639    /// poke, the same tier of primitive `Self::restore` uses) — this
640    /// establishes the simulation's actual starting state before any process
641    /// runs, not an ordinary mid-run write a scheduler/waveform/VPI listener
642    /// should observe as a "change." **Review fix (Task 16 r1, blocking
643    /// finding 1):** build-mode-aware — branches once on `self.build_mode()`
644    /// and re-blits whichever plane-pair is this engine's actual live data
645    /// (see this module's top doc comment, deviation 5).
646    pub fn reset_to_initial(&mut self) {
647        self.reset_to_initial_with(false);
648    }
649
650    /// [`Self::reset_to_initial`], optionally starting every uninitialized
651    /// 4-state variable and memory row at 0 instead of X (`x_as_zero`): the
652    /// non-LRM `--x-initial zero` run option, for benches written against a
653    /// 2-state simulator (Verilator's `--x-initial 0`, VCS's `+vcs+initreg+0`).
654    /// Nets keep their LRM starting values.
655    pub fn reset_to_initial_with(&mut self, x_as_zero: bool) {
656        let build_mode = self.build_mode();
657        for idx in 0..self.cell_reset.len() {
658            let loc = self.cells[idx];
659            match &self.cell_reset[idx] {
660                CellReset::Net => {}
661                CellReset::ForceContribution => {}
662                // A memory is ONE cell whose `loc` addresses row 0; every row
663                // must be reset (an unwritten `mem[k]` reads X, LRM §6.8 --
664                // iverilog-verified), not just the first.
665                CellReset::AllX => {
666                    let rows = if loc.class == SlotClass::Memory {
667                        loc.num_rows as u64
668                    } else {
669                        1
670                    };
671                    for row in 0..rows {
672                        let row_loc = if loc.class == SlotClass::Memory {
673                            super::write::resolve_mem_row(loc, row)
674                        } else {
675                            loc
676                        };
677                        match build_mode {
678                            BuildMode::FourState => {
679                                let value = if x_as_zero {
680                                    match row_loc.class {
681                                        SlotClass::Inline => FourStateValue::Narrow(0),
682                                        _ => FourStateValue::Wide {
683                                            aval: vec![0; row_loc.elem_word_count as usize],
684                                            bval: vec![0; row_loc.elem_word_count as usize],
685                                        },
686                                    }
687                                } else {
688                                    all_x_value_for(row_loc)
689                                };
690                                write_slot(&self.arena, row_loc, &value);
691                            }
692                            BuildMode::TwoState => reset_2s_cell_to_zero(&self.arena, row_loc),
693                        }
694                    }
695                }
696                // A memory's `initial` (layout): `Narrow(0)` for a 2-state
697                // memory with no initializer (`int m[4]`, `real r[2]`) --
698                // every row 0 -- or `Wide` holding every row's
699                // `elem_word_count`-word value in row order (a declared
700                // initializer, `int m[2] = '{1, 2}`).
701                CellReset::Initial(fv) if loc.class == SlotClass::Memory => {
702                    let words = loc.elem_word_count as usize;
703                    for row in 0..loc.num_rows as usize {
704                        let row_loc = super::write::resolve_mem_row(loc, row as u64);
705                        let (aval, bval) = match fv {
706                            FourStateValue::Wide { aval, bval } => (
707                                aval[row * words..(row + 1) * words].to_vec(),
708                                bval[row * words..(row + 1) * words].to_vec(),
709                            ),
710                            FourStateValue::Narrow(_) => (vec![0; words], vec![0; words]),
711                        };
712                        match build_mode {
713                            BuildMode::FourState => write_slot(
714                                &self.arena,
715                                row_loc,
716                                &FourStateValue::Wide { aval, bval },
717                            ),
718                            BuildMode::TwoState => {
719                                write_wide_2s(&self.arena, row_loc.word_offset, &aval);
720                            }
721                        }
722                    }
723                }
724                CellReset::Initial(fv) => match build_mode {
725                    BuildMode::FourState => {
726                        let value = fv.clone();
727                        write_slot(&self.arena, loc, &value);
728                    }
729                    BuildMode::TwoState => write_2s_cell_from_initial(&self.arena, loc, fv),
730                },
731            }
732        }
733    }
734}
735
736#[cfg(test)]
737mod tests {
738    use super::super::write::{read_inline_2s, read_wide_2s};
739    use super::*;
740    use crate::netlist::{SymbolKey, TemplateKey};
741    use crate::state::instance::TemplateRegistry;
742    use crate::state::write::SlotLocation;
743    use crate::state::{SignalAccess, SlotDescriptor, SlotOffset, StateLayout};
744
745    fn fresh_engine() -> StateEngine {
746        StateEngine::new(TemplateRegistry::new())
747    }
748
749    /// Allocates one inline cell (mirrors `write::tests::inline_cell`,
750    /// duplicated locally per this crate's existing per-task-module test
751    /// helper convention).
752    fn inline_cell(engine: &mut StateEngine) -> StateCellId {
753        // SAFETY: local, single-owned engine; no other live borrow of the
754        // inline plane, and every test below performs no concurrent
755        // allocation on the same engine.
756        let word_offset = unsafe { engine.arena.alloc_inline_word() };
757        let loc = SlotLocation {
758            class: SlotClass::Inline,
759            word_offset,
760            bit_width: 8,
761            elem_word_count: 1,
762            row_stride_words: 0,
763            num_rows: 0,
764        };
765        engine.alloc_cell(loc)
766    }
767
768    fn narrow_word(val: u64, unk: u64) -> u128 {
769        (val as u128) | ((unk as u128) << 64)
770    }
771
772    fn narrow_at(engine: &StateEngine, cell: StateCellId) -> u128 {
773        match engine.read(cell) {
774            FourStateValue::Narrow(w) => w,
775            FourStateValue::Wide { .. } => panic!("expected Narrow"),
776        }
777    }
778
779    fn t(units: i64) -> SimTime {
780        SimTime {
781            sim_units: units,
782            delta: 0,
783        }
784    }
785
786    /// ST-036: 3 sequential `commit_write`s to 3 *different* cells through an
787    /// `UndoLog`-backed `StateEngine` (`checkpoint_marker()` taken before all
788    /// 3); `rollback_to` the marker; all 3 cells must read back their
789    /// pre-write (uninitialized, all-zero-word) values — not just the last
790    /// one, proving multi-entry replay.
791    #[test]
792    fn rollback_to_a_marker_undoes_writes_in_reverse_order() {
793        let mut engine = fresh_engine();
794        let cell_a = inline_cell(&mut engine);
795        let cell_b = inline_cell(&mut engine);
796        let cell_c = inline_cell(&mut engine);
797        let mut log = UndoLog::new();
798
799        let mark = log.checkpoint_marker();
800        engine.commit_write(cell_a, FourStateValue::Narrow(narrow_word(1, 0)), &mut log);
801        engine.commit_write(cell_b, FourStateValue::Narrow(narrow_word(2, 0)), &mut log);
802        engine.commit_write(cell_c, FourStateValue::Narrow(narrow_word(3, 0)), &mut log);
803
804        log.rollback_to(&mut engine, mark)
805            .expect("mark is well within the retained log");
806
807        for cell in [cell_a, cell_b, cell_c] {
808            assert_eq!(
809                narrow_at(&engine, cell),
810                narrow_word(0, 0),
811                "every cell written since `mark` must be back to its pre-write value"
812            );
813        }
814    }
815
816    /// ST-036: write A, take `mark`, write B and C, `rollback_to(mark)`; A's
817    /// write must survive (still the post-write value) while B and C are
818    /// undone. Fails if `rollback_to` always rewinds the whole log
819    /// regardless of the marker.
820    #[test]
821    fn rollback_partial_marker_only_undoes_writes_after_it() {
822        let mut engine = fresh_engine();
823        let cell_a = inline_cell(&mut engine);
824        let cell_b = inline_cell(&mut engine);
825        let cell_c = inline_cell(&mut engine);
826        let mut log = UndoLog::new();
827
828        engine.commit_write(cell_a, FourStateValue::Narrow(narrow_word(1, 0)), &mut log);
829        let mark = log.checkpoint_marker();
830        engine.commit_write(cell_b, FourStateValue::Narrow(narrow_word(2, 0)), &mut log);
831        engine.commit_write(cell_c, FourStateValue::Narrow(narrow_word(3, 0)), &mut log);
832
833        log.rollback_to(&mut engine, mark)
834            .expect("mark is well within the retained log");
835
836        assert_eq!(
837            narrow_at(&engine, cell_a),
838            narrow_word(1, 0),
839            "A's write predates `mark` and must survive"
840        );
841        assert_eq!(
842            narrow_at(&engine, cell_b),
843            narrow_word(0, 0),
844            "B's write postdates `mark` and must be undone"
845        );
846        assert_eq!(
847            narrow_at(&engine, cell_c),
848            narrow_word(0, 0),
849            "C's write postdates `mark` and must be undone"
850        );
851    }
852
853    /// Append entries (via real `commit_write` calls, each stamped with a
854    /// distinct `SimTime` via `set_current_time`) at times 1, 2, 3;
855    /// `fossil_collect(SimTime{sim_units: 2,..})`; the log's remaining
856    /// length must reflect only the `t=3` entry — and, critically, a
857    /// subsequent `rollback_to` targeting a marker *before* the
858    /// fossil-collected entries must now return
859    /// `Err(RewindError::PastFossilHorizon)` rather than silently
860    /// under-rolling-back.
861    #[test]
862    fn fossil_collect_trims_entries_at_or_before_gvt_never_past_it() {
863        let mut engine = fresh_engine();
864        let cell_a = inline_cell(&mut engine);
865        let cell_b = inline_cell(&mut engine);
866        let cell_c = inline_cell(&mut engine);
867        let mut log = UndoLog::new();
868
869        let mark_before_any = log.checkpoint_marker();
870
871        engine.set_current_time(t(1));
872        engine.commit_write(cell_a, FourStateValue::Narrow(narrow_word(1, 0)), &mut log);
873        engine.set_current_time(t(2));
874        engine.commit_write(cell_b, FourStateValue::Narrow(narrow_word(2, 0)), &mut log);
875        engine.set_current_time(t(3));
876        engine.commit_write(cell_c, FourStateValue::Narrow(narrow_word(3, 0)), &mut log);
877
878        log.fossil_collect(t(2));
879        assert_eq!(
880            log.retained_len(),
881            1,
882            "only the t=3 entry (strictly after gvt=2) must remain"
883        );
884
885        let result = log.rollback_to(&mut engine, mark_before_any);
886        assert_eq!(
887            result,
888            Err(RewindError::PastFossilHorizon),
889            "a marker before the fossil-collected horizon must fail loudly, never silently under-roll-back"
890        );
891    }
892
893    /// Write distinct values to 4 inline words; `snapshot(Some((Inline, 1,
894    /// 2)))` (only words 1-2); mutate word 1 to something else; `restore`
895    /// the partial snapshot; word 1 must be back to its snapshotted value
896    /// *and* words 0/3 (outside the snapshotted range) must be unaffected —
897    /// proving it's genuinely a slice operation, not silently promoted to a
898    /// full-arena restore.
899    #[test]
900    fn snapshot_restore_round_trips_a_partial_range_not_just_the_whole_arena() {
901        let mut engine = fresh_engine();
902        let cells: Vec<StateCellId> = (0..4).map(|_| inline_cell(&mut engine)).collect();
903        for (i, &cell) in cells.iter().enumerate() {
904            engine.commit_write(
905                cell,
906                FourStateValue::Narrow(narrow_word(i as u64 + 1, 0)),
907                &mut (),
908            );
909        }
910
911        let snap = engine.snapshot(Some((SlotClass::Inline, 1, 2)));
912
913        engine.commit_write(
914            cells[1],
915            FourStateValue::Narrow(narrow_word(99, 0)),
916            &mut (),
917        );
918
919        engine
920            .restore(&snap)
921            .expect("same engine, same layout hash, in-bounds range");
922
923        assert_eq!(
924            narrow_at(&engine, cells[1]),
925            narrow_word(2, 0),
926            "word 1 must be back to its snapshotted value"
927        );
928        assert_eq!(
929            narrow_at(&engine, cells[0]),
930            narrow_word(1, 0),
931            "word 0 is outside the snapshotted range and must be unaffected"
932        );
933        assert_eq!(
934            narrow_at(&engine, cells[3]),
935            narrow_word(4, 0),
936            "word 3 is outside the snapshotted range and must be unaffected"
937        );
938    }
939
940    /// Snapshot a `StateEngine` built from template set A; construct a
941    /// *different* `StateEngine` from template set B (different
942    /// `layout_hash`); `restore` A's snapshot into B; must return
943    /// `Err(LayoutMismatch)`, and B's arena contents must be byte-identical
944    /// to before the attempted restore — no partial corruption on the
945    /// rejected path.
946    #[test]
947    fn restore_with_a_layout_hash_mismatch_is_a_typed_error_never_silent_corruption() {
948        let templates_a = TemplateRegistry::new();
949        let tmpl_a = TemplateKey {
950            definition: 100,
951            params_hash: 0,
952        };
953        // SAFETY: `templates_a` is a local, single-owned value with no other
954        // live borrow and no concurrent caller.
955        unsafe {
956            templates_a.register(
957                tmpl_a,
958                StateLayout {
959                    slots: vec![],
960                    inline_words: 1,
961                    wide_words: 0,
962                },
963            );
964        }
965        let mut engine_a = StateEngine::new(templates_a);
966        let handle_a = engine_a
967            .allocate_instance(tmpl_a)
968            .expect("tmpl_a is registered");
969        drop(handle_a);
970        let snap_a = engine_a.snapshot(None);
971
972        let templates_b = TemplateRegistry::new();
973        let tmpl_b = TemplateKey {
974            definition: 200,
975            params_hash: 0,
976        };
977        // SAFETY: same reasoning as `templates_a` above.
978        unsafe {
979            templates_b.register(
980                tmpl_b,
981                StateLayout {
982                    slots: vec![],
983                    inline_words: 1,
984                    wide_words: 0,
985                },
986            );
987        }
988        let mut engine_b = StateEngine::new(templates_b);
989        let handle_b = engine_b
990            .allocate_instance(tmpl_b)
991            .expect("tmpl_b is registered");
992        let cell_b = engine_b.alloc_cell(SlotLocation {
993            class: SlotClass::Inline,
994            word_offset: handle_b.base.inline_base,
995            bit_width: 8,
996            elem_word_count: 1,
997            row_stride_words: 0,
998            num_rows: 0,
999        });
1000        engine_b.commit_write(
1001            cell_b,
1002            FourStateValue::Narrow(narrow_word(0x2A, 0)),
1003            &mut (),
1004        );
1005        drop(handle_b);
1006
1007        let before: Vec<u128> = engine_b.arena.inline().to_vec();
1008
1009        let result = engine_b.restore(&snap_a);
1010        assert_eq!(
1011            result,
1012            Err(RewindError::LayoutMismatch),
1013            "restoring across two different template sets must be rejected"
1014        );
1015
1016        assert_eq!(
1017            engine_b.arena.inline().to_vec(),
1018            before,
1019            "a rejected restore must never partially corrupt the target arena"
1020        );
1021    }
1022
1023    /// A template with one slot initialized to `8'hAA` and one uninitialized
1024    /// `logic [7:0]`; mutate both away from their reset state;
1025    /// `reset_to_initial`; the first must read back `0xAA` known, the second
1026    /// must read back all-X (`val=0, mask=0xFF`).
1027    #[test]
1028    fn reset_to_initial_reblits_declared_initializers_and_x_fills_the_rest() {
1029        let templates = TemplateRegistry::new();
1030        let tmpl = TemplateKey {
1031            definition: 42,
1032            params_hash: 0,
1033        };
1034        let initial_value = FourStateValue::Narrow(0xAA);
1035        // SAFETY: `templates` is a local, single-owned value with no other
1036        // live borrow and no concurrent caller.
1037        unsafe {
1038            templates.register(
1039                tmpl,
1040                StateLayout {
1041                    slots: vec![
1042                        SlotDescriptor {
1043                            key: SymbolKey(0),
1044                            offset: SlotOffset {
1045                                class: SlotClass::Inline,
1046                                word_offset: 0,
1047                                bit_width: 8,
1048                                elem_word_count: 1,
1049                                row_stride_words: 0,
1050                                num_rows: 0,
1051                            },
1052                            initial: Some(initial_value),
1053                            net_info: None,
1054                        },
1055                        SlotDescriptor {
1056                            key: SymbolKey(1),
1057                            offset: SlotOffset {
1058                                class: SlotClass::Inline,
1059                                word_offset: 1,
1060                                bit_width: 8,
1061                                elem_word_count: 1,
1062                                row_stride_words: 0,
1063                                num_rows: 0,
1064                            },
1065                            initial: None,
1066                            net_info: None,
1067                        },
1068                    ],
1069                    inline_words: 2,
1070                    wide_words: 0,
1071                },
1072            );
1073        }
1074        let mut engine = StateEngine::new(templates);
1075        let handle = engine.allocate_instance(tmpl).expect("tmpl is registered");
1076        let cell_initialized = handle.cell_for_slot(0);
1077        let cell_uninitialized = handle.cell_for_slot(1);
1078        drop(handle);
1079
1080        // Mutate both away from their reset state.
1081        engine.commit_write(
1082            cell_initialized,
1083            FourStateValue::Narrow(narrow_word(0x11, 0)),
1084            &mut (),
1085        );
1086        engine.commit_write(
1087            cell_uninitialized,
1088            FourStateValue::Narrow(narrow_word(0x22, 0)),
1089            &mut (),
1090        );
1091
1092        engine.reset_to_initial();
1093
1094        let initialized = narrow_at(&engine, cell_initialized);
1095        assert_eq!(
1096            initialized as u64 & 0xFF,
1097            0xAA,
1098            "the declared initializer must be re-blitted exactly"
1099        );
1100        assert_eq!(
1101            (initialized >> 64) as u64 & 0xFF,
1102            0,
1103            "a fully-known initializer has no unknown bits"
1104        );
1105
1106        let uninitialized = narrow_at(&engine, cell_uninitialized);
1107        assert_eq!(
1108            uninitialized as u64 & 0xFF,
1109            0,
1110            "an uninitialized slot's value half must reset to 0"
1111        );
1112        assert_eq!(
1113            (uninitialized >> 64) as u64 & 0xFF,
1114            0xFF,
1115            "an uninitialized slot must reset to all-X (mask=all-1) within its width"
1116        );
1117    }
1118
1119    /// **Review fix (Task 16 r1, blocking finding 1).** A `BuildMode::TwoState`
1120    /// engine's declared-initializer and uninitialized inline slots, mutated
1121    /// through the *actual live* 2-state plane (`write_inline_2s`, Task 13 —
1122    /// `write_slot`/`commit_write` only ever touch the inert, always-zero
1123    /// 4-state planes in a `TwoState` build); `reset_to_initial()` must
1124    /// observe and affect that same live plane. Before this fix,
1125    /// `reset_to_initial` unconditionally called `write_slot` (4-state-only),
1126    /// so it silently did nothing to either cell's real data.
1127    #[test]
1128    fn reset_to_initial_is_build_mode_aware_for_two_state_engines() {
1129        let mut engine = StateEngine::new_with_mode(TemplateRegistry::new(), BuildMode::TwoState);
1130        assert_eq!(engine.build_mode(), BuildMode::TwoState);
1131
1132        let cell_initialized = inline_cell(&mut engine);
1133        let cell_uninitialized = inline_cell(&mut engine);
1134        // `cell_reset` is `pub(crate)`, directly pokeable from within this
1135        // crate — the same shortcut `write::StateEngine::alloc_cell` and
1136        // `instance::StateEngine::allocate_instance` themselves use, standing
1137        // in here for a full `allocate_instance`/`TemplateRegistry` round
1138        // trip this test doesn't otherwise need.
1139        engine.cell_reset[cell_initialized.0 as usize] =
1140            CellReset::Initial(FourStateValue::Narrow(0xAA));
1141        // `cell_uninitialized` keeps `alloc_cell`'s own conservative
1142        // `CellReset::AllX` default.
1143
1144        let loc_initialized = engine.cells[cell_initialized.0 as usize];
1145        let loc_uninitialized = engine.cells[cell_uninitialized.0 as usize];
1146
1147        write_inline_2s(&engine.arena, loc_initialized.word_offset, 0x11);
1148        write_inline_2s(&engine.arena, loc_uninitialized.word_offset, 0x22);
1149
1150        engine.reset_to_initial();
1151
1152        assert_eq!(
1153            read_inline_2s(&engine.arena, loc_initialized.word_offset),
1154            0xAA,
1155            "a declared initializer must be re-blitted into the live 2-state plane"
1156        );
1157        assert_eq!(
1158            read_inline_2s(&engine.arena, loc_uninitialized.word_offset),
1159            0,
1160            "an uninitialized 2-state slot must reset to 0, not all-X (there is no mask half in this plane)"
1161        );
1162    }
1163
1164    /// **Review fix (Task 16 r1, blocking finding 1), the `Wide`-class half:**
1165    /// same shape as
1166    /// `reset_to_initial_is_build_mode_aware_for_two_state_engines` above, but
1167    /// for a wide (`elem_word_count > 1`) 2-state slot — proving the fix
1168    /// covers the `wide_2s` plane, not just `inline_2s`.
1169    #[test]
1170    fn reset_to_initial_two_state_wide_all_x_resets_every_word_to_zero() {
1171        let mut engine = StateEngine::new_with_mode(TemplateRegistry::new(), BuildMode::TwoState);
1172        // SAFETY: local, single-owned engine; no other live borrow of the
1173        // wide plane, and this test performs no concurrent allocation.
1174        let word_offset = unsafe { engine.arena.alloc_wide_words(2) };
1175        let loc = SlotLocation {
1176            class: SlotClass::Wide,
1177            word_offset,
1178            bit_width: 128,
1179            elem_word_count: 2,
1180            row_stride_words: 0,
1181            num_rows: 0,
1182        };
1183        engine.alloc_cell(loc); // keeps `alloc_cell`'s own `CellReset::AllX` default
1184
1185        write_wide_2s(&engine.arena, word_offset, &[0x1111_1111, 0x2222_2222]);
1186        assert_eq!(
1187            read_wide_2s(&engine.arena, word_offset, 2),
1188            &[0x1111_1111, 0x2222_2222]
1189        );
1190
1191        engine.reset_to_initial();
1192
1193        assert_eq!(
1194            read_wide_2s(&engine.arena, word_offset, 2),
1195            &[0, 0],
1196            "an uninitialized wide 2-state slot must reset to all-zero words"
1197        );
1198    }
1199
1200    /// **Review fix (Task 16 r1, blocking finding 1).** `snapshot`/`restore`'s
1201    /// partial-range slice semantics (already proven for `FourState` by
1202    /// `snapshot_restore_round_trips_a_partial_range_not_just_the_whole_arena`
1203    /// above), re-proven against a `BuildMode::TwoState` engine's *live*
1204    /// `inline_2s` plane. Before this fix, `snapshot`/`restore` unconditionally
1205    /// captured/restored the inert 4-state planes, so this would have silently
1206    /// left `word 1`'s real (2-state) value at its mutated `99`, not its
1207    /// snapshotted `2`.
1208    #[test]
1209    fn snapshot_restore_two_state_round_trips_the_live_plane_not_the_inert_four_state_one() {
1210        let mut engine = StateEngine::new_with_mode(TemplateRegistry::new(), BuildMode::TwoState);
1211        let cells: Vec<StateCellId> = (0..4).map(|_| inline_cell(&mut engine)).collect();
1212        let offsets: Vec<u32> = cells
1213            .iter()
1214            .map(|&c| engine.cells[c.0 as usize].word_offset)
1215            .collect();
1216        for (i, &off) in offsets.iter().enumerate() {
1217            write_inline_2s(&engine.arena, off, i as u64 + 1);
1218        }
1219
1220        let snap = engine.snapshot(Some((SlotClass::Inline, offsets[1], 2)));
1221
1222        write_inline_2s(&engine.arena, offsets[1], 99);
1223
1224        engine
1225            .restore(&snap)
1226            .expect("same engine, same layout hash, same build mode, in-bounds range");
1227
1228        assert_eq!(
1229            read_inline_2s(&engine.arena, offsets[1]),
1230            2,
1231            "word 1 must be back to its snapshotted 2-state value"
1232        );
1233        assert_eq!(
1234            read_inline_2s(&engine.arena, offsets[0]),
1235            1,
1236            "word 0 is outside the snapshotted range and must be unaffected"
1237        );
1238        assert_eq!(
1239            read_inline_2s(&engine.arena, offsets[3]),
1240            4,
1241            "word 3 is outside the snapshotted range and must be unaffected"
1242        );
1243    }
1244
1245    /// **Review fix (Task 16 r1, blocking finding 1), the cross-mode-rejection
1246    /// half:** a snapshot taken from a `FourState` engine, `restore`d against a
1247    /// `TwoState` engine with the *same* (empty) template set — so
1248    /// `layout_hash` alone would not catch the mismatch — must be rejected
1249    /// with `Err(LayoutMismatch)` before touching any plane, never silently
1250    /// interpreted as 4-state data landing in the 2-state target's arena.
1251    #[test]
1252    fn restore_rejects_a_build_mode_mismatch_even_when_layout_hash_matches() {
1253        let mut four_state_engine =
1254            StateEngine::new_with_mode(TemplateRegistry::new(), BuildMode::FourState);
1255        let cell = inline_cell(&mut four_state_engine);
1256        four_state_engine.commit_write(cell, FourStateValue::Narrow(narrow_word(0x2A, 0)), &mut ());
1257        let snap = four_state_engine.snapshot(None);
1258
1259        let mut two_state_engine =
1260            StateEngine::new_with_mode(TemplateRegistry::new(), BuildMode::TwoState);
1261        let two_cell = inline_cell(&mut two_state_engine);
1262        let loc = two_state_engine.cells[two_cell.0 as usize];
1263        write_inline_2s(&two_state_engine.arena, loc.word_offset, 0x77);
1264        let before = two_state_engine.arena.inline_2s().to_vec();
1265
1266        let result = two_state_engine.restore(&snap);
1267        assert_eq!(
1268            result,
1269            Err(RewindError::LayoutMismatch),
1270            "restoring a FourState snapshot into a TwoState engine must be rejected"
1271        );
1272        assert_eq!(
1273            two_state_engine.arena.inline_2s().to_vec(),
1274            before,
1275            "a rejected cross-build-mode restore must never partially corrupt the target arena"
1276        );
1277    }
1278
1279    /// **Review fix (Task 16 r1, blocking finding 2).** Reproduces the exact
1280    /// scenario the finding reported: force a net (via `driver::force_driver`,
1281    /// a live SV `force` statement), confirm the returned contribution cell
1282    /// reads back the forced value, then `reset_to_initial()`. Before this
1283    /// fix, `reset_to_initial` iterated every `CellReset::AllX` entry
1284    /// unconditionally — including `force_driver`'s synthetic contribution
1285    /// cells, which share the ordinary inline plane and are indistinguishable
1286    /// from a real variable once `alloc_cell` has run — and blitted all-X
1287    /// over the live forced value with no error of any kind.
1288    #[test]
1289    fn reset_to_initial_never_corrupts_a_live_force_contribution_cell() {
1290        let mut engine = fresh_engine();
1291        let net_cell = inline_cell(&mut engine);
1292        let mut set = crate::state::driver::DriverSet {
1293            resolved: net_cell,
1294            drivers: Vec::new(),
1295            net_kind: crate::state::NetKind::Wire,
1296        };
1297
1298        let forced_cell = crate::state::driver::force_driver(
1299            &mut engine,
1300            &mut set,
1301            net_cell,
1302            FourStateValue::Narrow(narrow_word(0x2A, 0)),
1303        );
1304        assert_eq!(
1305            narrow_at(&engine, forced_cell),
1306            narrow_word(0x2A, 0),
1307            "force_driver must seed its own contribution cell with the forced value"
1308        );
1309
1310        engine.reset_to_initial();
1311
1312        assert_eq!(
1313            narrow_at(&engine, forced_cell),
1314            narrow_word(0x2A, 0),
1315            "reset_to_initial must never blit a reset value over a live force_driver contribution cell"
1316        );
1317    }
1318}