vj-sim 0.3.0

Netlist, scheduler and process model of the vj SystemVerilog simulator.
Documentation
//! sva plan T1 (`docs/superpowers/plans/2026-10-03-sva.md` §1.3) — the
//! Preponed-region snapshot ring (LRM 1800-2023 §16.5.1, §16.5.2): a flat,
//! bounded ring of whole/partial-arena [`ArenaSnapshot`]s, built directly on
//! the existing coarse checkpoint primitive ([`StateEngine::snapshot`]/
//! [`StateEngine::restore`], Task 16, ST-098) rather than a second accessor
//! plane -- the plan's own §1.3 framing: "a `sim_save`-shaped snapshot ring,
//! not a second accessor plane".
//!
//! One ring entry is taken once per simulation time step, at the Preponed
//! region boundary (before `Region::Active` runs that step) --
//! [`Self::get`]`(0)` is always "the value as of the end of the *previous*
//! time step" (the literal 16.5.1 sampled value, and the same value a
//! clocking block's `#1step` input skew samples: §1.3's "both consumers
//! read ring entry 0"); entries `1..=depth` are progressively older ticks,
//! consumed by `$past(expr, n)` (16.9.3) once a later task lowers it.
//!
//! **Deviation (recorded per this task's own instructions).** The plan's
//! prose ("wire `depth` from a new `StaticAnalysisView` query... computed
//! by Task 1's own small elaboration walk") describes threading a live
//! `sv_lang::Design` walk through `analysis::StaticAnalysisView` and from
//! there into a running `Engine`. Neither `analysis/mod.rs` (that trait)
//! nor `vj`/`vj-runtime` (the one place that constructs a real,
//! `StateEngine`-backed `Engine`) is in this task's own `Files:` list, and
//! no consumer of a live depth value exists yet (`$past` itself is
//! T16-T18; the Observed-region automaton that would actually *read* a
//! ring entry is T2+). Wiring an elaboration-time query through two files
//! this task does not own, to feed a value nothing yet reads, would be
//! premature integration. This task instead makes `depth` a plain runtime
//! constructor parameter -- satisfying the acceptance text's actual
//! requirement ("ring depth is computed per-design, not fixed", i.e. not a
//! hardcoded constant baked into this type) -- and leaves computing that
//! parameter from a live `Design` to the first task with a real consumer
//! for the result.

use super::rollback::ArenaSnapshot;

/// A bounded ring of [`ArenaSnapshot`]s: "current" (the most recently
/// pushed entry, ring index 0) plus up to `depth` older entries. Never
/// retains more than `depth + 1` entries, no matter how many times
/// [`Self::push`] is called -- the "a design with no `$past` retention need
/// pays no retention cost beyond entry 0" guard (see
/// `snapshot_ring_depth_0_design_allocates_no_ring` below). Whether a ring
/// is allocated *at all* for a given instance is a decision this type does
/// not make -- `sched::control::Engine` only ever constructs one when the
/// design has at least one assertion/clocking-block/`$past` site to begin
/// with (§1.4's "not every instance, to avoid taxing designs with no SVA at
/// all"); a design with none never calls [`Self::new`] and pays nothing.
pub struct SnapshotRing {
    /// Front = oldest retained, back = most recent (`push` appends,
    /// dropping the front once more than `depth + 1` entries are held) --
    /// a `VecDeque` so both ends are O(1), matching `RegionQueues`' own
    /// established "`VecDeque` when both ends matter" convention
    /// (`sched::queues`).
    entries: std::collections::VecDeque<ArenaSnapshot>,
    depth: usize,
}

impl SnapshotRing {
    /// `depth` = N (16.9.3/16.9.4's largest literal `number_of_ticks`
    /// argument across the whole design that needs this ring; `0` if
    /// `$past`/`$past_gclk` is never called with a nonzero tick count for
    /// it). Allocates nothing up front (`VecDeque::new()`, never
    /// `with_capacity`) -- the first [`Self::push`] call is this ring's
    /// first real allocation.
    pub fn new(depth: usize) -> Self {
        SnapshotRing {
            entries: std::collections::VecDeque::new(),
            depth,
        }
    }

    /// `N` as passed to [`Self::new`] -- how many entries beyond "current"
    /// this ring retains.
    pub fn depth(&self) -> usize {
        self.depth
    }

    /// How many entries are currently retained (`<= depth + 1`) -- a test/
    /// diagnostic accessor; [`Self::push`]/[`Self::get`] already enforce the
    /// bound internally without consulting this.
    pub fn len(&self) -> usize {
        self.entries.len()
    }

    /// `true` before the first [`Self::push`] (clippy's `len_without_is_empty`).
    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }

    /// Appends `snap` as the new "current" entry, evicting the oldest entry
    /// once the ring already holds `depth + 1` entries -- never grows past
    /// that bound no matter how many times this is called.
    pub fn push(&mut self, snap: ArenaSnapshot) {
        self.entries.push_back(snap);
        while self.entries.len() > self.depth + 1 {
            self.entries.pop_front();
        }
    }

    /// The entry `ticks_back` pushes before the most recent one (`0` =
    /// "current", the value [`Self::push`] most recently stored). `None`
    /// when `ticks_back` exceeds how many entries are actually retained --
    /// either because the ring hasn't been pushed to that many times yet
    /// (simulation start; 16.9.3's own "before the Nth previous clock tick,
    /// treat the value as X" case, left to the `$past` lowering task to
    /// turn into the right X-valued result) or because `ticks_back >
    /// depth` (a caller bug this primitive reports rather than panics on --
    /// unlike `RegionQueues`'s "this region can never be serviced at all"
    /// structural impossibility, this is a plain out-of-range read).
    pub fn get(&self, ticks_back: usize) -> Option<&ArenaSnapshot> {
        if ticks_back > self.depth {
            return None;
        }
        let len = self.entries.len();
        let idx = len.checked_sub(1)?.checked_sub(ticks_back)?;
        self.entries.get(idx)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::state::{
        FourStateValue, SignalAccess, SlotClass, SlotLocation, StateCellId, StateEngine,
        TemplateRegistry,
    };

    /// Mirrors `rollback.rs`'s own `fresh_engine`/`inline_cell` test
    /// helpers (duplicated locally per this crate's existing
    /// per-task-module test helper convention, `rollback.rs`'s own doc
    /// comment on `inline_cell`).
    fn fresh_engine() -> StateEngine {
        StateEngine::new(TemplateRegistry::new())
    }

    fn inline_cell(engine: &mut StateEngine) -> StateCellId {
        // SAFETY: local, single-owned engine; no other live borrow of the
        // inline plane, and this helper performs no concurrent allocation
        // on the same engine.
        let word_offset = unsafe { engine.arena.alloc_inline_word() };
        let loc = SlotLocation {
            class: SlotClass::Inline,
            word_offset,
            bit_width: 8,
            elem_word_count: 1,
            row_stride_words: 0,
            num_rows: 0,
        };
        engine.alloc_cell(loc)
    }

    fn narrow(val: u64) -> FourStateValue {
        FourStateValue::Narrow(val as u128)
    }

    fn read_narrow(engine: &StateEngine, cell: StateCellId) -> u128 {
        match engine.read(cell) {
            FourStateValue::Narrow(w) => w,
            FourStateValue::Wide { .. } => panic!("expected Narrow"),
        }
    }

    /// 16.5.1: a depth-1 ring (one prior tick retained beyond "current")
    /// pushed once per simulated tick must give back exactly the value that
    /// was live at the moment of each push -- `get(1)` (one tick back) must
    /// read as the OLDER tick's value even after a newer one has been
    /// pushed, and `get(0)` (current) must read as the newest.
    #[test]
    fn snapshot_ring_depth_1_returns_previous_ticks_value() {
        let mut engine = fresh_engine();
        let cell = inline_cell(&mut engine);
        let mut ring = SnapshotRing::new(1);

        engine.commit_write(cell, narrow(10), &mut ());
        ring.push(engine.snapshot(None)); // tick 0: cell == 10

        engine.commit_write(cell, narrow(20), &mut ());
        ring.push(engine.snapshot(None)); // tick 1: cell == 20

        // Mutate further without pushing -- proves `get` reads the RING's
        // own retained copies, never the engine's live, since-mutated state.
        engine.commit_write(cell, narrow(99), &mut ());

        engine
            .restore(ring.get(1).expect("depth 1 retains one prior tick"))
            .expect("same engine, same layout");
        assert_eq!(
            read_narrow(&engine, cell),
            10,
            "ticks_back=1 must give back the OLDER (tick 0) value"
        );

        engine
            .restore(ring.get(0).expect("current is always retained"))
            .expect("same engine, same layout");
        assert_eq!(
            read_narrow(&engine, cell),
            20,
            "ticks_back=0 (current) must give back the newest (tick 1) value"
        );

        assert!(
            ring.get(2).is_none(),
            "depth 1 retains only ticks_back in {{0,1}} -- 2 is out of range"
        );
    }

    /// Perf/memory guard (§1.4's "not every instance, to avoid taxing
    /// designs with no SVA at all", applied at the ring's own smallest
    /// nonzero case): a depth-0 ring never retains more than its single
    /// "current" entry, no matter how many times it is pushed to -- proving
    /// the ring is genuinely bounded (never an ever-growing log) rather
    /// than merely documented to be.
    #[test]
    fn snapshot_ring_depth_0_design_allocates_no_ring() {
        let mut engine = fresh_engine();
        let cell = inline_cell(&mut engine);
        let mut ring = SnapshotRing::new(0);
        assert_eq!(ring.depth(), 0);

        for i in 0..5u64 {
            engine.commit_write(cell, narrow(i), &mut ());
            ring.push(engine.snapshot(None));
            assert_eq!(
                ring.len(),
                1,
                "depth 0 must never retain more than entry 0, even after {} pushes",
                i + 1
            );
        }

        engine
            .restore(ring.get(0).expect("current is always retained"))
            .expect("same engine, same layout");
        assert_eq!(
            read_narrow(&engine, cell),
            4,
            "the lone retained entry must be the MOST RECENT push"
        );
        assert!(
            ring.get(1).is_none(),
            "depth 0 retains no prior ticks at all"
        );
    }

    #[test]
    fn fresh_ring_has_no_entries_yet() {
        let ring = SnapshotRing::new(3);
        assert_eq!(ring.len(), 0);
        assert!(ring.get(0).is_none());
    }
}