Skip to main content

vj_sim/determinism/
kind_guard.rs

1//! DETERMINISM::KIND_GUARD (Task 23, ledger DT-048) — the CI kind-count guard
2//! for the `sv-lang`/`sv-lang-kinds` variant enums that `vj-codegen`'s
3//! already-shipped exhaustive matches depend on staying in sync with.
4//!
5//! `#[non_exhaustive]` on `sv_lang::kinds::ExpressionKind`/`StatementKind`
6//! (`sv-lang-kinds` crate) means a version bump that adds a new variant
7//! compiles cleanly against any `match` with a wildcard fallthrough arm —
8//! silently routing the new variant through whatever the `other => ..` arm
9//! does, with zero compiler signal. Two already-merged, already-shipped
10//! exhaustive matches in the `codegen` track depend on a human re-reviewing
11//! them on every `sv-lang-kinds` version bump:
12//! - `crates/vj-codegen/src/expr.rs::lower_expr`'s `match e.kind()` over
13//!   `sv_lang::kinds::ExpressionKind` (shipped commit `9ebd739`).
14//! - `crates/vj-codegen/src/func.rs::lower_stmt`'s `match s.kind()` over
15//!   `sv_lang::kinds::StatementKind` (same commit).
16//!
17//! This module does not depend on `vj-codegen` (this crate must not —
18//! `docs/CONTRACTS.md` open question 6) and does not itself touch either
19//! match; it only pins the two kinds' `COUNT` constants to the values those
20//! matches were reviewed against, so a `sv-lang`/`sv-lang-kinds` version bump
21//! that changes either count fails this crate's test suite instead of
22//! silently under-approximating `lower_expr`/`lower_stmt`.
23//!
24//! **`SymbolKind` is guarded as of integration (2026-09-26)**: the netlist
25//! track's elaboration now matches on it (see
26//! `symbol_kind_count_matches_pinned_sv_lang_version` for the sites).
27//! **`TimingControlKind` is guarded as of A2-slice Task S3a**:
28//! `crates/vj-sim/src/process/cfg.rs::CfgBuilder::walk_timed` is its
29//! first dispatch site in this crate (`process::cfg::build_cfg`'s CPS CFG
30//! builder, PR-006/PR-007) -- see
31//! `timing_control_kind_count_matches_pinned_sv_lang_version` below. That
32//! same file's `CfgBuilder::walk_stmt` is also this crate's first
33//! `StatementKind` dispatch site (`vj-codegen`'s `func.rs::lower_stmt`
34//! remains the other), so `statement_kind_count_matches_pinned_sv_lang_
35//! version` below now protects both.
36//!
37//! **Maintenance instruction:** when `sv-lang`/`sv-lang-kinds` is bumped in
38//! the workspace root `Cargo.toml`, re-verify both `lower_expr`/`lower_stmt`
39//! fallthrough arms remain correct for any newly-added variant, then update
40//! this module's two hardcoded `expected` counts below in the same commit as
41//! the version bump.
42
43use std::fmt;
44
45/// A pinned kind-count no longer matches the pinned `sv-lang`/`sv-lang-kinds`
46/// version this module was last reviewed against.
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct KindCountDrift {
49    pub kind_name: &'static str,
50    pub expected: u16,
51    pub actual: u16,
52}
53
54impl fmt::Display for KindCountDrift {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        write!(
57            f,
58            "{}::COUNT drifted: expected {} (last reviewed value), found {} -- re-review \
59             every exhaustive match over {} in crates/vj-codegen (see this module's doc \
60             comment for the specific match sites) before updating the expected count",
61            self.kind_name, self.expected, self.actual, self.kind_name
62        )
63    }
64}
65
66impl std::error::Error for KindCountDrift {}
67
68/// The one comparison primitive both the real CI guard tests and the
69/// fixture-bump proof share: `Ok(())` iff `actual == expected`, else
70/// `Err(KindCountDrift { .. })`. Never a panic -- a CI job calling this wants
71/// a typed, reportable failure, not an abort.
72pub fn assert_kind_count(
73    kind_name: &'static str,
74    actual: u16,
75    expected: u16,
76) -> Result<(), KindCountDrift> {
77    if actual == expected {
78        Ok(())
79    } else {
80        Err(KindCountDrift {
81            kind_name,
82            expected,
83            actual,
84        })
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    /// DT-048: the real, live CI guard. Fails the build the moment a
93    /// `sv-lang`/`sv-lang-kinds` version bump changes `ExpressionKind::COUNT`
94    /// away from the value `crates/vj-codegen/src/expr.rs::lower_expr`'s
95    /// exhaustive match was last reviewed against.
96    #[test]
97    fn expression_kind_count_matches_pinned_sv_lang_version() {
98        assert_eq!(
99            assert_kind_count("ExpressionKind", sv_lang::kinds::ExpressionKind::COUNT, 41,),
100            Ok(()),
101            "sv_lang::kinds::ExpressionKind::COUNT no longer matches this module's pinned \
102             expected value -- re-review crates/vj-codegen/src/expr.rs::lower_expr's \
103             exhaustive match before updating the expected count"
104        );
105    }
106
107    /// DT-048: same shape for `StatementKind`, protecting
108    /// `crates/vj-codegen/src/func.rs::lower_stmt`'s exhaustive match.
109    #[test]
110    fn statement_kind_count_matches_pinned_sv_lang_version() {
111        assert_eq!(
112            assert_kind_count("StatementKind", sv_lang::kinds::StatementKind::COUNT, 32),
113            Ok(()),
114            "sv_lang::kinds::StatementKind::COUNT no longer matches this module's pinned \
115             expected value -- re-review crates/vj-codegen/src/func.rs::lower_stmt's \
116             exhaustive match before updating the expected count"
117        );
118    }
119
120    /// DT-048: `TimingControlKind`, protecting
121    /// `crates/vj-sim/src/process/cfg.rs::CfgBuilder::walk_timed`'s
122    /// `if`/`else-if` dispatch (Task S3a, ledger PR-006/PR-007). Added in the
123    /// same commit as that file's first `TimingControlKind` match site, per
124    /// this module's own maintenance instruction above.
125    #[test]
126    fn timing_control_kind_count_matches_pinned_sv_lang_version() {
127        assert_eq!(
128            assert_kind_count(
129                "TimingControlKind",
130                sv_lang::kinds::TimingControlKind::COUNT,
131                10,
132            ),
133            Ok(()),
134            "sv_lang::kinds::TimingControlKind::COUNT no longer matches this module's pinned \
135             expected value -- re-review crates/vj-sim/src/process/cfg.rs::CfgBuilder::\
136             walk_timed's dispatch before updating the expected count"
137        );
138    }
139
140    /// DT-048: `SymbolKind`, protecting netlist elaboration's whitelist
141    /// matches (`netlist/template.rs::build_members`, `netlist/elaborate.rs`'s
142    /// three member/element/top-symbol matches, `netlist/instance.rs::
143    /// array_path_of`). Each ends in a loud `UnsupportedConstruct` arm, so a new
144    /// variant cannot be silently mishandled, but a bump must still force a
145    /// review of which new kinds those whitelists should accept.
146    #[test]
147    fn symbol_kind_count_matches_pinned_sv_lang_version() {
148        assert_eq!(
149            assert_kind_count("SymbolKind", sv_lang::kinds::SymbolKind::COUNT, 101),
150            Ok(()),
151            "sv_lang::kinds::SymbolKind::COUNT no longer matches this module's pinned \
152             expected value -- re-review the netlist SymbolKind whitelist matches before \
153             updating the expected count"
154        );
155    }
156
157    /// DT-048's own acceptance text: "verified to actually fire on a
158    /// deliberately bumped test fixture, not merely present." The real
159    /// `sv-lang` crate is version-pinned and this track cannot and must not
160    /// edit it to literally bump a kind count, so this test exercises the
161    /// exact comparison function the two tests above call, with a
162    /// deliberately wrong `expected` standing in for "the version was bumped
163    /// and nobody updated this module's constant". This is the test that
164    /// catches a stubbed `assert_kind_count` that always returns `Ok(())`
165    /// regardless of input -- a stub the two tests above alone couldn't rule
166    /// out, since today's real counts happen to equal 41/32.
167    #[test]
168    fn kind_count_guard_fires_when_a_bump_is_simulated() {
169        assert_eq!(
170            assert_kind_count("ExpressionKind", 42, 41),
171            Err(KindCountDrift {
172                kind_name: "ExpressionKind",
173                expected: 41,
174                actual: 42,
175            })
176        );
177    }
178
179    /// The trivial complementary case, so the drift test above isn't the
180    /// only thing pinning `assert_kind_count`'s branch behavior.
181    #[test]
182    fn assert_kind_count_is_ok_when_counts_genuinely_match() {
183        assert_eq!(assert_kind_count("X", 7, 7), Ok(()));
184    }
185}