vj-sim 0.1.0

Netlist, scheduler and process model of the vj SystemVerilog simulator.
Documentation
//! DETERMINISM::KIND_GUARD (Task 23, ledger DT-048) — the CI kind-count guard
//! for the `sv-lang`/`sv-lang-kinds` variant enums that `sv-sim-codegen`'s
//! already-shipped exhaustive matches depend on staying in sync with.
//!
//! `#[non_exhaustive]` on `sv_lang::kinds::ExpressionKind`/`StatementKind`
//! (`sv-lang-kinds` crate) means a version bump that adds a new variant
//! compiles cleanly against any `match` with a wildcard fallthrough arm —
//! silently routing the new variant through whatever the `other => ..` arm
//! does, with zero compiler signal. Two already-merged, already-shipped
//! exhaustive matches in the `codegen` track depend on a human re-reviewing
//! them on every `sv-lang-kinds` version bump:
//! - `crates/sv-sim-codegen/src/expr.rs::lower_expr`'s `match e.kind()` over
//!   `sv_lang::kinds::ExpressionKind` (shipped commit `9ebd739`).
//! - `crates/sv-sim-codegen/src/func.rs::lower_stmt`'s `match s.kind()` over
//!   `sv_lang::kinds::StatementKind` (same commit).
//!
//! This module does not depend on `sv-sim-codegen` (this crate must not —
//! `docs/CONTRACTS.md` open question 6) and does not itself touch either
//! match; it only pins the two kinds' `COUNT` constants to the values those
//! matches were reviewed against, so a `sv-lang`/`sv-lang-kinds` version bump
//! that changes either count fails this crate's test suite instead of
//! silently under-approximating `lower_expr`/`lower_stmt`.
//!
//! **`SymbolKind` is guarded as of integration (2026-09-26)**: the netlist
//! track's elaboration now matches on it (see
//! `symbol_kind_count_matches_pinned_sv_lang_version` for the sites).
//! **`TimingControlKind` is guarded as of A2-slice Task S3a**:
//! `crates/sv-sim-sim/src/process/cfg.rs::CfgBuilder::walk_timed` is its
//! first dispatch site in this crate (`process::cfg::build_cfg`'s CPS CFG
//! builder, PR-006/PR-007) -- see
//! `timing_control_kind_count_matches_pinned_sv_lang_version` below. That
//! same file's `CfgBuilder::walk_stmt` is also this crate's first
//! `StatementKind` dispatch site (`sv-sim-codegen`'s `func.rs::lower_stmt`
//! remains the other), so `statement_kind_count_matches_pinned_sv_lang_
//! version` below now protects both.
//!
//! **Maintenance instruction:** when `sv-lang`/`sv-lang-kinds` is bumped in
//! the workspace root `Cargo.toml`, re-verify both `lower_expr`/`lower_stmt`
//! fallthrough arms remain correct for any newly-added variant, then update
//! this module's two hardcoded `expected` counts below in the same commit as
//! the version bump.

use std::fmt;

/// A pinned kind-count no longer matches the pinned `sv-lang`/`sv-lang-kinds`
/// version this module was last reviewed against.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KindCountDrift {
    pub kind_name: &'static str,
    pub expected: u16,
    pub actual: u16,
}

impl fmt::Display for KindCountDrift {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{}::COUNT drifted: expected {} (last reviewed value), found {} -- re-review \
             every exhaustive match over {} in crates/sv-sim-codegen (see this module's doc \
             comment for the specific match sites) before updating the expected count",
            self.kind_name, self.expected, self.actual, self.kind_name
        )
    }
}

impl std::error::Error for KindCountDrift {}

/// The one comparison primitive both the real CI guard tests and the
/// fixture-bump proof share: `Ok(())` iff `actual == expected`, else
/// `Err(KindCountDrift { .. })`. Never a panic -- a CI job calling this wants
/// a typed, reportable failure, not an abort.
pub fn assert_kind_count(
    kind_name: &'static str,
    actual: u16,
    expected: u16,
) -> Result<(), KindCountDrift> {
    if actual == expected {
        Ok(())
    } else {
        Err(KindCountDrift {
            kind_name,
            expected,
            actual,
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// DT-048: the real, live CI guard. Fails the build the moment a
    /// `sv-lang`/`sv-lang-kinds` version bump changes `ExpressionKind::COUNT`
    /// away from the value `crates/sv-sim-codegen/src/expr.rs::lower_expr`'s
    /// exhaustive match was last reviewed against.
    #[test]
    fn expression_kind_count_matches_pinned_sv_lang_version() {
        assert_eq!(
            assert_kind_count("ExpressionKind", sv_lang::kinds::ExpressionKind::COUNT, 41,),
            Ok(()),
            "sv_lang::kinds::ExpressionKind::COUNT no longer matches this module's pinned \
             expected value -- re-review crates/sv-sim-codegen/src/expr.rs::lower_expr's \
             exhaustive match before updating the expected count"
        );
    }

    /// DT-048: same shape for `StatementKind`, protecting
    /// `crates/sv-sim-codegen/src/func.rs::lower_stmt`'s exhaustive match.
    #[test]
    fn statement_kind_count_matches_pinned_sv_lang_version() {
        assert_eq!(
            assert_kind_count("StatementKind", sv_lang::kinds::StatementKind::COUNT, 32),
            Ok(()),
            "sv_lang::kinds::StatementKind::COUNT no longer matches this module's pinned \
             expected value -- re-review crates/sv-sim-codegen/src/func.rs::lower_stmt's \
             exhaustive match before updating the expected count"
        );
    }

    /// DT-048: `TimingControlKind`, protecting
    /// `crates/sv-sim-sim/src/process/cfg.rs::CfgBuilder::walk_timed`'s
    /// `if`/`else-if` dispatch (Task S3a, ledger PR-006/PR-007). Added in the
    /// same commit as that file's first `TimingControlKind` match site, per
    /// this module's own maintenance instruction above.
    #[test]
    fn timing_control_kind_count_matches_pinned_sv_lang_version() {
        assert_eq!(
            assert_kind_count(
                "TimingControlKind",
                sv_lang::kinds::TimingControlKind::COUNT,
                10,
            ),
            Ok(()),
            "sv_lang::kinds::TimingControlKind::COUNT no longer matches this module's pinned \
             expected value -- re-review crates/sv-sim-sim/src/process/cfg.rs::CfgBuilder::\
             walk_timed's dispatch before updating the expected count"
        );
    }

    /// DT-048: `SymbolKind`, protecting netlist elaboration's whitelist
    /// matches (`netlist/template.rs::build_members`, `netlist/elaborate.rs`'s
    /// three member/element/top-symbol matches, `netlist/instance.rs::
    /// array_path_of`). Each ends in a loud `UnsupportedConstruct` arm, so a new
    /// variant cannot be silently mishandled, but a bump must still force a
    /// review of which new kinds those whitelists should accept.
    #[test]
    fn symbol_kind_count_matches_pinned_sv_lang_version() {
        assert_eq!(
            assert_kind_count("SymbolKind", sv_lang::kinds::SymbolKind::COUNT, 101),
            Ok(()),
            "sv_lang::kinds::SymbolKind::COUNT no longer matches this module's pinned \
             expected value -- re-review the netlist SymbolKind whitelist matches before \
             updating the expected count"
        );
    }

    /// DT-048's own acceptance text: "verified to actually fire on a
    /// deliberately bumped test fixture, not merely present." The real
    /// `sv-lang` crate is version-pinned and this track cannot and must not
    /// edit it to literally bump a kind count, so this test exercises the
    /// exact comparison function the two tests above call, with a
    /// deliberately wrong `expected` standing in for "the version was bumped
    /// and nobody updated this module's constant". This is the test that
    /// catches a stubbed `assert_kind_count` that always returns `Ok(())`
    /// regardless of input -- a stub the two tests above alone couldn't rule
    /// out, since today's real counts happen to equal 41/32.
    #[test]
    fn kind_count_guard_fires_when_a_bump_is_simulated() {
        assert_eq!(
            assert_kind_count("ExpressionKind", 42, 41),
            Err(KindCountDrift {
                kind_name: "ExpressionKind",
                expected: 41,
                actual: 42,
            })
        );
    }

    /// The trivial complementary case, so the drift test above isn't the
    /// only thing pinning `assert_kind_count`'s branch behavior.
    #[test]
    fn assert_kind_count_is_ok_when_counts_genuinely_match() {
        assert_eq!(assert_kind_count("X", 7, 7), Ok(()));
    }
}