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}