vj-codegen 0.1.0

LLVM code generator of the vj SystemVerilog simulator.
//! Task 29 (CG-069): CI-pinned `Kind::COUNT` constants for the three
//! `sv-lang-kinds` AST-node discriminant enums this crate's own exhaustive
//! matches (`expr.rs::lower_expr`'s `ExpressionKind` match, `func.rs::
//! lower_stmt`'s `StatementKind` match, and this task's new `session::
//! symbol_role`'s `SymbolKind` match) fall back to a hard [`crate::error::
//! LoweringError`] for once every explicitly-handled variant is exhausted.
//!
//! Each constant is a plain hand-pinned `usize`, not re-derived from the
//! live `sv_lang::kinds::*Kind::COUNT` associated constant at build time --
//! doing that would make this file's own tests
//! (`expression_kind_count_matches_pinned_constant` and its two siblings,
//! `tests/exhaustiveness.rs`) compare a constant against itself, which can
//! never disagree and is exactly the "gate that cannot fail" this project's
//! correctness posture forbids. Pinned against the vendored
//! `sv-lang-kinds` version this workspace's `Cargo.toml` pins with `=`
//! (`sv-lang-kinds = "=0.1.1"`, resolving to `0.1.1+slang.11.0` per
//! `Cargo.lock`), read directly off that crate's own generated
//! `src/generated/ast_kind.rs` (`ExpressionKind::COUNT = 41`,
//! `StatementKind::COUNT = 32`, `SymbolKind::COUNT = 101`) -- a future
//! `sv-lang-kinds` version bump that adds, removes, or renumbers a variant
//! changes that generated file's own `COUNT`, which shows up here as this
//! file's tests failing loudly at CI time, never as a silent stale pin.
//!
//! **`TimingControlKind` is deliberately NOT pinned here** (see Task 29's
//! own plan note, `docs/superpowers/plans/2026-09-23-wave-A-codegen.md`):
//! no code anywhere in `crates/sv-sim-codegen`'s `expr.rs`/`func.rs`/
//! `session.rs` exhaustively dispatches over it. `module.rs::
//! require_single_edge_head`'s `if tc.kind() != TimingControlKind::
//! SignalEvent` is a single-variant equality guard, not an exhaustive
//! dispatch, and that file already carries its own, narrower-scoped pin
//! (`module.rs::assert_kind_counts_match_pinned_slang`) for the matches it
//! itself owns. Wave A2's CPS process-lowering machinery is the only
//! subsystem that will ever have a genuine exhaustive `match` over
//! `TimingControlKind` to protect -- pinning `TIMING_CONTROL_KIND_COUNT`
//! here with nothing behind it would itself be a gate that cannot fail.

/// Pinned `sv_lang::kinds::ExpressionKind::COUNT` (41 variants as of
/// `sv-lang-kinds` 0.1.1+slang.11.0) -- guards `expr.rs::lower_expr`'s
/// exhaustive dispatch.
pub const EXPRESSION_KIND_COUNT: usize = 41;

/// Pinned `sv_lang::kinds::StatementKind::COUNT` (32 variants as of
/// `sv-lang-kinds` 0.1.1+slang.11.0) -- guards `func.rs::lower_stmt`'s
/// exhaustive dispatch.
pub const STATEMENT_KIND_COUNT: usize = 32;

/// Pinned `sv_lang::kinds::SymbolKind::COUNT` (101 variants as of
/// `sv-lang-kinds` 0.1.1+slang.11.0) -- guards `session::symbol_role`'s
/// exhaustive dispatch.
pub const SYMBOL_KIND_COUNT: usize = 101;

/// R-run Task 19 (XC-053): the codegen-side half of the cross-track
/// `SupportedConstructs` interface ledger row XC-053 asks for -- "CODEGEN
/// and RUNTIME each expose an enumerable SupportedConstructs set ... that
/// sv-sim-harness's classify_via_frontend/CorpusManifest consumes to
/// pre-classify every corpus file as expected-pass, known-gap, or
/// unclassified, before any run."
///
/// **Deviation from the plan's literal derivation** (`docs/superpowers/
/// plans/2026-09-27-run-wiring.md` Task 19, "Public names/types"): the plan
/// describes deriving this set by walking `module.rs::
/// reject_unsupported_in_body`'s own "allow-list". Reading that function
/// (`module.rs:998`, its own doc comment) shows it is not an allow-list at
/// all -- it is a narrow DENY-list (SVA assertions, `randomize()` calls;
/// it also held `$sdf_annotate` until SD-B T25 made that a real call)
/// that lets every OTHER `StatementKind`/
/// `ExpressionKind` straight through to `process::cfg`'s own CFG-shape
/// enforcement (that function's doc comment says so explicitly: "loops,
/// `case`, `wait`, fork/join, ... deliberately NOT rejected here"). It
/// carries no information about which constructs actually lower to a real
/// value/effect, so walking it would make `SUPPORTED_CONSTRUCTS` wildly
/// over-report support.
///
/// The real registry with exactly the property XC-053 needs already exists:
/// [`crate::coverage::supported_constructs`] (CG-158/CG-159, Task 28 of a
/// different, already-merged track) -- a hand-maintained `&'static
/// [&'static str]` of every `ExpressionKind`/`StatementKind` variant name
/// whose match arm in `expr.rs::lower_expr`/`func.rs::lower_stmt` genuinely
/// produces a lowering (not merely a better error message for an
/// otherwise-rejected variant), kept from silently drifting by its own
/// `tests/coverage_sink.rs::supported_constructs_lists_every_implemented_
/// kind` regression guard. This function reuses that list -- never a
/// second, independently hand-typed enum-variant list, which is exactly the
/// "two implementations, one diverges" bug this workspace's own memory of
/// prior incidents warns about -- and turns it into the `HashSet<
/// ConstructId>` shape XC-053's acceptance text asks for, by matching each
/// string name against the real, pinned `sv_lang_kinds::{ExpressionKind,
/// StatementKind}::ALL` variant list via `Debug` formatting (both enums are
/// plain, data-less C-style variants, so `{:?}` is exactly the bare variant
/// name, e.g. `"HierarchicalValue"`, with no risk of a stringly-typed typo
/// silently matching nothing -- `supported_construct_ids_is_nonempty_and_
/// only_contains_kinds_lower_actually_accepts` below is the regression
/// proof that every name in `coverage::supported_constructs()` really does
/// resolve to a real `ConstructId`, not silently drop unmatched entries).
///
/// **Naming deviation**: the plan's own text spells this
/// `SUPPORTED_CONSTRUCTS()` (screaming-case). A screaming-case function name
/// fails this workspace's `cargo clippy --workspace --all-targets -- -D
/// warnings` gate (`clippy::non_snake_case` is warn-by-default and this
/// project denies all warnings) -- and `crate::coverage` already exports a
/// *different*-shaped `supported_constructs() -> &'static [&'static str]`,
/// so reusing that exact name here (even in `snake_case`) would shadow-name
/// two functions with different return types under the same identifier
/// across sibling modules of the same crate. Named `supported_construct_ids`
/// instead: still a plain, discoverable `pub fn`, unambiguous from
/// `coverage::supported_constructs` at every call site.
pub fn supported_construct_ids() -> std::collections::HashSet<crate::coverage::ConstructId> {
    let names: std::collections::HashSet<&str> = crate::coverage::supported_constructs()
        .iter()
        .copied()
        .collect();
    let mut out = std::collections::HashSet::new();
    for &kind in sv_lang_kinds::ExpressionKind::ALL {
        if names.contains(format!("{kind:?}").as_str()) {
            out.insert(
                sv_sim_harness::histogram::construct_id_for_expression_kind(kind).unwrap_or_else(
                    |e| {
                        unreachable!(
                            "coverage::supported_constructs() names a real ExpressionKind \
                             variant ({kind:?}) that histogram.rs's own pinned mapping (exhaustive \
                             over sv-lang-kinds=0.1.1) has no ConstructId for: {e:?}"
                        )
                    },
                ),
            );
        }
    }
    for &kind in sv_lang_kinds::StatementKind::ALL {
        if names.contains(format!("{kind:?}").as_str()) {
            out.insert(
                sv_sim_harness::histogram::construct_id_for_statement_kind(kind).unwrap_or_else(
                    |e| {
                        unreachable!(
                            "coverage::supported_constructs() names a real StatementKind \
                             variant ({kind:?}) that histogram.rs's own pinned mapping (exhaustive \
                             over sv-lang-kinds=0.1.1) has no ConstructId for: {e:?}"
                        )
                    },
                ),
            );
        }
    }
    out
}

#[cfg(test)]
mod supported_construct_ids_tests {
    use super::supported_construct_ids;

    /// XC-053's acceptance text: "`SUPPORTED_CONSTRUCTS()`'s size is
    /// asserted `> 0`". Also cross-checks the *other* pinned kind-count
    /// guard this crate already carries (`module::
    /// assert_kind_counts_match_pinned_slang`, which pins `TimingControlKind
    /// ::COUNT == 10` alongside `StatementKind`/`ExpressionKind`) so a
    /// future `sv-lang-kinds` version bump that adds an 11th
    /// `TimingControlKind` variant (or changes `StatementKind`/
    /// `ExpressionKind`'s counts) fails this test suite loudly through that
    /// existing guard, rather than this function silently under- or
    /// over-reporting support against a version it was never re-reviewed
    /// against.
    #[test]
    fn supported_construct_ids_is_nonempty_and_kind_counts_are_pinned() {
        let ids = supported_construct_ids();
        assert!(
            !ids.is_empty(),
            "SUPPORTED_CONSTRUCTS must report at least one supported construct"
        );
        crate::module::assert_kind_counts_match_pinned_slang()
            .expect("pinned StatementKind/ExpressionKind/TimingControlKind counts must match");
    }

    /// Every name in `coverage::supported_constructs()` must resolve to a
    /// distinct `ConstructId` -- proves the `HashSet` really has one entry
    /// per supported kind, not fewer (e.g. from a string that matched no
    /// real variant and was silently skipped).
    #[test]
    fn supported_construct_ids_has_one_entry_per_supported_construct_name() {
        let ids = supported_construct_ids();
        assert_eq!(
            ids.len(),
            crate::coverage::supported_constructs().len(),
            "every string name in coverage::supported_constructs() must resolve to exactly \
             one distinct ConstructId -- a mismatch means a name failed to match any real \
             ExpressionKind/StatementKind variant (silently dropped) or two names collided"
        );
    }
}