Skip to main content

sv_sim_sim/state/
snapshot.rs

1//! sva plan T1 (`docs/superpowers/plans/2026-10-03-sva.md` §1.3) — the
2//! Preponed-region snapshot ring (LRM 1800-2023 §16.5.1, §16.5.2): a flat,
3//! bounded ring of whole/partial-arena [`ArenaSnapshot`]s, built directly on
4//! the existing coarse checkpoint primitive ([`StateEngine::snapshot`]/
5//! [`StateEngine::restore`], Task 16, ST-098) rather than a second accessor
6//! plane -- the plan's own §1.3 framing: "a `sim_save`-shaped snapshot ring,
7//! not a second accessor plane".
8//!
9//! One ring entry is taken once per simulation time step, at the Preponed
10//! region boundary (before `Region::Active` runs that step) --
11//! [`Self::get`]`(0)` is always "the value as of the end of the *previous*
12//! time step" (the literal 16.5.1 sampled value, and the same value a
13//! clocking block's `#1step` input skew samples: §1.3's "both consumers
14//! read ring entry 0"); entries `1..=depth` are progressively older ticks,
15//! consumed by `$past(expr, n)` (16.9.3) once a later task lowers it.
16//!
17//! **Deviation (recorded per this task's own instructions).** The plan's
18//! prose ("wire `depth` from a new `StaticAnalysisView` query... computed
19//! by Task 1's own small elaboration walk") describes threading a live
20//! `sv_lang::Design` walk through `analysis::StaticAnalysisView` and from
21//! there into a running `Engine`. Neither `analysis/mod.rs` (that trait)
22//! nor `sv-sim-bin`/`sv-sim-runtime` (the one place that constructs a real,
23//! `StateEngine`-backed `Engine`) is in this task's own `Files:` list, and
24//! no consumer of a live depth value exists yet (`$past` itself is
25//! T16-T18; the Observed-region automaton that would actually *read* a
26//! ring entry is T2+). Wiring an elaboration-time query through two files
27//! this task does not own, to feed a value nothing yet reads, would be
28//! premature integration. This task instead makes `depth` a plain runtime
29//! constructor parameter -- satisfying the acceptance text's actual
30//! requirement ("ring depth is computed per-design, not fixed", i.e. not a
31//! hardcoded constant baked into this type) -- and leaves computing that
32//! parameter from a live `Design` to the first task with a real consumer
33//! for the result.
34
35use super::rollback::ArenaSnapshot;
36
37/// A bounded ring of [`ArenaSnapshot`]s: "current" (the most recently
38/// pushed entry, ring index 0) plus up to `depth` older entries. Never
39/// retains more than `depth + 1` entries, no matter how many times
40/// [`Self::push`] is called -- the "a design with no `$past` retention need
41/// pays no retention cost beyond entry 0" guard (see
42/// `snapshot_ring_depth_0_design_allocates_no_ring` below). Whether a ring
43/// is allocated *at all* for a given instance is a decision this type does
44/// not make -- `sched::control::Engine` only ever constructs one when the
45/// design has at least one assertion/clocking-block/`$past` site to begin
46/// with (§1.4's "not every instance, to avoid taxing designs with no SVA at
47/// all"); a design with none never calls [`Self::new`] and pays nothing.
48pub struct SnapshotRing {
49    /// Front = oldest retained, back = most recent (`push` appends,
50    /// dropping the front once more than `depth + 1` entries are held) --
51    /// a `VecDeque` so both ends are O(1), matching `RegionQueues`' own
52    /// established "`VecDeque` when both ends matter" convention
53    /// (`sched::queues`).
54    entries: std::collections::VecDeque<ArenaSnapshot>,
55    depth: usize,
56}
57
58impl SnapshotRing {
59    /// `depth` = N (16.9.3/16.9.4's largest literal `number_of_ticks`
60    /// argument across the whole design that needs this ring; `0` if
61    /// `$past`/`$past_gclk` is never called with a nonzero tick count for
62    /// it). Allocates nothing up front (`VecDeque::new()`, never
63    /// `with_capacity`) -- the first [`Self::push`] call is this ring's
64    /// first real allocation.
65    pub fn new(depth: usize) -> Self {
66        SnapshotRing {
67            entries: std::collections::VecDeque::new(),
68            depth,
69        }
70    }
71
72    /// `N` as passed to [`Self::new`] -- how many entries beyond "current"
73    /// this ring retains.
74    pub fn depth(&self) -> usize {
75        self.depth
76    }
77
78    /// How many entries are currently retained (`<= depth + 1`) -- a test/
79    /// diagnostic accessor; [`Self::push`]/[`Self::get`] already enforce the
80    /// bound internally without consulting this.
81    pub fn len(&self) -> usize {
82        self.entries.len()
83    }
84
85    /// `true` before the first [`Self::push`] (clippy's `len_without_is_empty`).
86    pub fn is_empty(&self) -> bool {
87        self.entries.is_empty()
88    }
89
90    /// Appends `snap` as the new "current" entry, evicting the oldest entry
91    /// once the ring already holds `depth + 1` entries -- never grows past
92    /// that bound no matter how many times this is called.
93    pub fn push(&mut self, snap: ArenaSnapshot) {
94        self.entries.push_back(snap);
95        while self.entries.len() > self.depth + 1 {
96            self.entries.pop_front();
97        }
98    }
99
100    /// The entry `ticks_back` pushes before the most recent one (`0` =
101    /// "current", the value [`Self::push`] most recently stored). `None`
102    /// when `ticks_back` exceeds how many entries are actually retained --
103    /// either because the ring hasn't been pushed to that many times yet
104    /// (simulation start; 16.9.3's own "before the Nth previous clock tick,
105    /// treat the value as X" case, left to the `$past` lowering task to
106    /// turn into the right X-valued result) or because `ticks_back >
107    /// depth` (a caller bug this primitive reports rather than panics on --
108    /// unlike `RegionQueues`'s "this region can never be serviced at all"
109    /// structural impossibility, this is a plain out-of-range read).
110    pub fn get(&self, ticks_back: usize) -> Option<&ArenaSnapshot> {
111        if ticks_back > self.depth {
112            return None;
113        }
114        let len = self.entries.len();
115        let idx = len.checked_sub(1)?.checked_sub(ticks_back)?;
116        self.entries.get(idx)
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use crate::state::{
124        FourStateValue, SignalAccess, SlotClass, SlotLocation, StateCellId, StateEngine,
125        TemplateRegistry,
126    };
127
128    /// Mirrors `rollback.rs`'s own `fresh_engine`/`inline_cell` test
129    /// helpers (duplicated locally per this crate's existing
130    /// per-task-module test helper convention, `rollback.rs`'s own doc
131    /// comment on `inline_cell`).
132    fn fresh_engine() -> StateEngine {
133        StateEngine::new(TemplateRegistry::new())
134    }
135
136    fn inline_cell(engine: &mut StateEngine) -> StateCellId {
137        // SAFETY: local, single-owned engine; no other live borrow of the
138        // inline plane, and this helper performs no concurrent allocation
139        // on the same engine.
140        let word_offset = unsafe { engine.arena.alloc_inline_word() };
141        let loc = SlotLocation {
142            class: SlotClass::Inline,
143            word_offset,
144            bit_width: 8,
145            elem_word_count: 1,
146            row_stride_words: 0,
147            num_rows: 0,
148        };
149        engine.alloc_cell(loc)
150    }
151
152    fn narrow(val: u64) -> FourStateValue {
153        FourStateValue::Narrow(val as u128)
154    }
155
156    fn read_narrow(engine: &StateEngine, cell: StateCellId) -> u128 {
157        match engine.read(cell) {
158            FourStateValue::Narrow(w) => w,
159            FourStateValue::Wide { .. } => panic!("expected Narrow"),
160        }
161    }
162
163    /// 16.5.1: a depth-1 ring (one prior tick retained beyond "current")
164    /// pushed once per simulated tick must give back exactly the value that
165    /// was live at the moment of each push -- `get(1)` (one tick back) must
166    /// read as the OLDER tick's value even after a newer one has been
167    /// pushed, and `get(0)` (current) must read as the newest.
168    #[test]
169    fn snapshot_ring_depth_1_returns_previous_ticks_value() {
170        let mut engine = fresh_engine();
171        let cell = inline_cell(&mut engine);
172        let mut ring = SnapshotRing::new(1);
173
174        engine.commit_write(cell, narrow(10), &mut ());
175        ring.push(engine.snapshot(None)); // tick 0: cell == 10
176
177        engine.commit_write(cell, narrow(20), &mut ());
178        ring.push(engine.snapshot(None)); // tick 1: cell == 20
179
180        // Mutate further without pushing -- proves `get` reads the RING's
181        // own retained copies, never the engine's live, since-mutated state.
182        engine.commit_write(cell, narrow(99), &mut ());
183
184        engine
185            .restore(ring.get(1).expect("depth 1 retains one prior tick"))
186            .expect("same engine, same layout");
187        assert_eq!(
188            read_narrow(&engine, cell),
189            10,
190            "ticks_back=1 must give back the OLDER (tick 0) value"
191        );
192
193        engine
194            .restore(ring.get(0).expect("current is always retained"))
195            .expect("same engine, same layout");
196        assert_eq!(
197            read_narrow(&engine, cell),
198            20,
199            "ticks_back=0 (current) must give back the newest (tick 1) value"
200        );
201
202        assert!(
203            ring.get(2).is_none(),
204            "depth 1 retains only ticks_back in {{0,1}} -- 2 is out of range"
205        );
206    }
207
208    /// Perf/memory guard (§1.4's "not every instance, to avoid taxing
209    /// designs with no SVA at all", applied at the ring's own smallest
210    /// nonzero case): a depth-0 ring never retains more than its single
211    /// "current" entry, no matter how many times it is pushed to -- proving
212    /// the ring is genuinely bounded (never an ever-growing log) rather
213    /// than merely documented to be.
214    #[test]
215    fn snapshot_ring_depth_0_design_allocates_no_ring() {
216        let mut engine = fresh_engine();
217        let cell = inline_cell(&mut engine);
218        let mut ring = SnapshotRing::new(0);
219        assert_eq!(ring.depth(), 0);
220
221        for i in 0..5u64 {
222            engine.commit_write(cell, narrow(i), &mut ());
223            ring.push(engine.snapshot(None));
224            assert_eq!(
225                ring.len(),
226                1,
227                "depth 0 must never retain more than entry 0, even after {} pushes",
228                i + 1
229            );
230        }
231
232        engine
233            .restore(ring.get(0).expect("current is always retained"))
234            .expect("same engine, same layout");
235        assert_eq!(
236            read_narrow(&engine, cell),
237            4,
238            "the lone retained entry must be the MOST RECENT push"
239        );
240        assert!(
241            ring.get(1).is_none(),
242            "depth 0 retains no prior ticks at all"
243        );
244    }
245
246    #[test]
247    fn fresh_ring_has_no_entries_yet() {
248        let ring = SnapshotRing::new(3);
249        assert_eq!(ring.len(), 0);
250        assert!(ring.get(0).is_none());
251    }
252}