vj-sim 0.2.0

Netlist, scheduler and process model of the vj SystemVerilog simulator.
Documentation
//! STATE::EVENT — `event` variable ("trigger pulse") cells (R-run Task 16,
//! moved from `2026-09-27-wave-B-state.md` Task 5; ledger ST-111).
//!
//! **Deviation, documented (ST-111's own acceptance text explicitly leaves
//! the exact mechanism an "open joint decision with STATE/NETS" — this is
//! STATE's chosen resolution, not a bilaterally closed one):** rather than
//! adding a new [`super::SlotClass::Event`] variant (which would force a
//! mechanical ripple through every exhaustive `SlotClass` match in
//! `write.rs`/`rollback.rs`/`accessor_impl.rs`/`dedup.rs`/`instance.rs`, the
//! same shape Task 1 already does for row-addressing but for no actual
//! storage-shape benefit — an event has no persistent value at all), an
//! event variable is an ordinary 1-bit `SlotClass::Inline` cell whose
//! [`StateCellId`] is additionally tracked in a dedicated
//! `StateEngine::event_cells` set. This satisfies "a `StateCellId`-keyed
//! mechanism" literally, keeps every existing exhaustive match unchanged,
//! and is exactly as testable.
//!
//! Names no `sv_lang`/`sv_lang_kinds` item — reachable with or without this
//! crate's `frontend` feature (CONTRACTS D4), matching `sched::EventTerm`/
//! `sched::SensitivityTable`, the scheduler-side consumers of this cell.

use super::write::{ChangeResult, PreWriteHook, SlotLocation};
use super::{FourStateValue, SignalAccess, SlotClass, StateCellId};
use crate::state::instance::StateEngine;

/// Width of a design-declared `event` variable's cell (`state::layout`,
/// `vj::run::signal_slots_of`) -- a 2-state `SlotClass::Wide` cell
/// that compiled code (`vj-codegen::expr::lower_event_trigger`) treats
/// as two 64-bit halves: the low half COUNTS triggers, the high half holds
/// `tick + 1` of the most recent trigger (0 = never), which is what
/// `e.triggered` compares against the current time. Counting rather than
/// toggling matters because blocking writes are flushed once at segment end:
/// two toggles in one segment would cancel and report no change.
///
/// This is separate from the 1-bit [`StateEngine::alloc_event_cell`] model
/// below, which the compiled pipeline does not use.
pub const EVENT_CELL_WIDTH: u32 = 128;

impl StateEngine {
    /// ST-111: allocates a fresh event-variable cell — a real, ordinary
    /// 1-bit `SlotClass::Inline` cell (via [`super::arena::Arena::alloc_inline_word`],
    /// which zero-initializes its backing word, so the cell decodes to
    /// logic `0`/"not triggered" immediately after allocation with no
    /// further write needed) — and records its [`StateCellId`] in
    /// `self.event_cells`, the one thing that distinguishes it from an
    /// ordinary 1-bit variable cell (see this module's own doc comment for
    /// why no new `SlotClass` variant exists for this).
    pub fn alloc_event_cell(&mut self) -> StateCellId {
        // SAFETY: `&mut self` guarantees no other call into this
        // `StateEngine` (and therefore this `Arena`) races this allocation,
        // and nothing in this crate holds a live `&[u128]`/`&mut [u128]`
        // borrow of the inline plane across this call — the same
        // single-logical-writer discipline every other `alloc_inline_word`
        // call site in this crate (e.g. `allocate_instance`) already relies
        // on.
        let word_offset = unsafe { self.arena.alloc_inline_word() };
        let loc = SlotLocation {
            class: SlotClass::Inline,
            word_offset,
            bit_width: 1,
            elem_word_count: 1,
            row_stride_words: 0,
            num_rows: 0,
        };
        let cell = self.alloc_cell(loc);
        self.event_cells.insert(cell);
        cell
    }

    /// ST-111: the trigger pulse — an ordinary [`StateEngine::commit_write`]
    /// (so the real VC/`notify_change`/`Dispatch::notify_scheduler` path
    /// fires, which is exactly what wakes a process sensitive to
    /// `sched::EventTerm::NamedEventTrigger(cell)` via
    /// `sched::SensitivityTable`'s already-landed "any real change wakes
    /// it" handling — see `sched::sensitivity::SensitivityTable::install_term`'s
    /// `NamedEventTrigger` arm). Writes the logical complement of the cell's
    /// current value (an event cell's own "value" carries no LRM meaning —
    /// only the transition matters — so flipping it, rather than always
    /// writing the same constant, guarantees `commit_write`'s own
    /// old-vs-new diff always reports a real change, even across repeated
    /// triggers with no intervening clear).
    ///
    /// # Panics
    /// In debug builds, if `cell` was never allocated via
    /// [`Self::alloc_event_cell`] — a caller triggering an ordinary
    /// (non-event) cell through this path is a programming error, not a
    /// silently-tolerated case, matching this crate's "loud, never silent"
    /// discipline (ARCHITECTURE §7 item 9).
    pub fn trigger_event(
        &mut self,
        cell: StateCellId,
        hook: &mut dyn PreWriteHook,
    ) -> ChangeResult {
        debug_assert!(
            self.event_cells.contains(&cell),
            "trigger_event: {cell:?} was never allocated via StateEngine::alloc_event_cell -- \
             an ordinary variable/net cell has no LRM 'trigger' semantics to fire"
        );
        let current = match self.read(cell) {
            FourStateValue::Narrow(word) => (word as u64) & 1 != 0,
            FourStateValue::Wide { .. } => unreachable!(
                "event cell {cell:?} decoded as FourStateValue::Wide -- \
                 alloc_event_cell only ever allocates SlotClass::Inline cells"
            ),
        };
        let next = FourStateValue::Narrow(if current { 0u128 } else { 1u128 });
        self.commit_write(cell, next, hook)
    }

    /// ST-111: LRM `.triggered`'s "valid only through the current time
    /// step" — clears every tracked event cell back to `0`, via
    /// [`StateEngine::commit_write_no_hook`] (no `VcRecord` append, no
    /// scheduler notification on the clearing edge: clearing is not itself
    /// an LRM-observable event — see [`Self::commit_write_no_hook`]'s own
    /// doc comment for why that primitive is the one that skips both).
    pub fn clear_event_pulses(&mut self) {
        // Collect first: `commit_write_no_hook` takes `&mut self`, so this
        // avoids borrowing `self.event_cells` and `self` mutably at once.
        let cells: Vec<StateCellId> = self.event_cells.iter().copied().collect();
        for cell in cells {
            self.commit_write_no_hook(cell, FourStateValue::Narrow(0u128));
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::state::instance::TemplateRegistry;

    fn fresh_engine() -> StateEngine {
        StateEngine::new(TemplateRegistry::new())
    }

    #[test]
    fn alloc_event_cell_starts_at_zero_and_is_tracked() {
        let mut engine = fresh_engine();
        let ev = engine.alloc_event_cell();
        assert!(engine.event_cells.contains(&ev));
        match engine.read(ev) {
            FourStateValue::Narrow(w) => assert_eq!(w as u64 & 1, 0),
            FourStateValue::Wide { .. } => panic!("expected Narrow"),
        }
    }

    #[test]
    fn trigger_event_flips_the_cell_and_reports_a_change() {
        let mut engine = fresh_engine();
        let ev = engine.alloc_event_cell();
        let result = engine.trigger_event(ev, &mut ());
        assert!(result.changed);
        match engine.read(ev) {
            FourStateValue::Narrow(w) => assert_eq!(w as u64 & 1, 1),
            FourStateValue::Wide { .. } => panic!("expected Narrow"),
        }
    }

    /// Mutation-testing regression (2026-09-28): `trigger_event` must write
    /// the logical COMPLEMENT of the cell's current value, never a constant
    /// -- a mutant that always writes `1` still passes the single-trigger
    /// test above (a fresh cell starts at `0`, so writing a constant `1` is
    /// indistinguishable from flipping it), but silently drops the wake on
    /// every SECOND consecutive trigger with no intervening
    /// `clear_event_pulses` in between: `commit_write`'s own before/after
    /// diff sees `1 -> 1` (no change), so `sched::sensitivity`'s "any real
    /// change wakes it" mechanism this function's own doc comment describes
    /// never fires on that second trigger. A real `-> ev;` statement inside
    /// a tight loop with no intervening `@(ev)`/`.triggered` clear is exactly
    /// this shape (LRM 1800-2023 SS15.5.1 places no such restriction on how
    /// often `->` may fire).
    #[test]
    fn trigger_event_reports_a_change_on_every_consecutive_trigger_with_no_intervening_clear() {
        let mut engine = fresh_engine();
        let ev = engine.alloc_event_cell();

        let first = engine.trigger_event(ev, &mut ());
        assert!(first.changed, "first trigger (0 -> 1) must report a change");

        let second = engine.trigger_event(ev, &mut ());
        assert!(
            second.changed,
            "second consecutive trigger with no intervening clear must ALSO report a change \
             (it must flip 1 -> 0, never re-write the same value 1 -> 1)"
        );
        match engine.read(ev) {
            FourStateValue::Narrow(w) => assert_eq!(
                w as u64 & 1,
                0,
                "the second trigger must flip the cell back to 0"
            ),
            FourStateValue::Wide { .. } => panic!("expected Narrow"),
        }

        let third = engine.trigger_event(ev, &mut ());
        assert!(
            third.changed,
            "a third consecutive trigger must flip 0 -> 1 and report a change again"
        );
    }

    #[test]
    fn clear_event_pulses_resets_every_tracked_cell_to_zero() {
        let mut engine = fresh_engine();
        let a = engine.alloc_event_cell();
        let b = engine.alloc_event_cell();
        engine.trigger_event(a, &mut ());
        engine.trigger_event(b, &mut ());
        engine.clear_event_pulses();
        for cell in [a, b] {
            match engine.read(cell) {
                FourStateValue::Narrow(w) => assert_eq!(w as u64 & 1, 0),
                FourStateValue::Wide { .. } => panic!("expected Narrow"),
            }
        }
    }
}