vj-sim 0.3.0

Netlist, scheduler and process model of the vj SystemVerilog simulator.
Documentation
//! Task S5 (`docs/superpowers/plans/2026-09-26-wave-A2-slice.md`): the
//! process TEMPLATE dedup key (PR-028, PR-029, PR-030) and the
//! `ProcessInstanceId` registration-order tie-break (PR-075, PR-076,
//! PR-077).
//!
//! **PR-028's core claim** ("process template dedup must key off slang's own
//! `InstanceBodySymbol` canonicalization ... compiling one kernel per
//! canonical process shape and sharing it across every instance") is
//! implemented here as a direct `(definition, resolved params)` equality key
//! ([`ProcessTemplateKey`]), not a from-scratch structural-equality pass over
//! the AST: `definition` is `sv_lang`'s own stable [`crate::symid::SymbolId`] for
//! the shared `Definition` symbol (the same symbol every structurally-
//! identical instance's body resolves back to via
//! `instance_body_definition()`), and `params` is the instance's own
//! elaborated, already-folded [`sv_lang::ConstantValue`] list. Two instances
//! with an identical `(definition, params)` pair are, by construction,
//! instances of the same module with the same resolved parameter values --
//! exactly `PR-028`'s dedup criterion -- with no separate call to
//! `Symbol::instance_body_has_same_type` needed at this layer (unlike
//! `netlist::dedup`'s own `TemplateKey`, which buckets on a lossy `u64` hash
//! and so *must* re-verify with the real oracle before merging; this key
//! carries the real, structured parameter values, so key equality already
//! *is* the ground truth).
//!
//! **Deviation from the plan's illustrative signature** (`fn
//! template_key_of(instance_body: &Symbol<'_>) -> ProcessTemplateKey`, taking
//! only the instance `body`): verified against pinned `sv-lang = "=0.1.9"`
//! that `Symbol::parameters()`'s own doc/SAFETY comment ("count is 0 for
//! non-instances") means it is populated only when called on an
//! `Instance`-kind symbol, never on that instance's `InstanceBody` -- the
//! exact same constraint `netlist::dedup::template_key_of` already
//! documents and works around. [`template_key_of`] therefore takes both the
//! owning `Instance` symbol (for `parameters()`) and its `InstanceBody`
//! (for `instance_body_definition()`), by value (`Symbol` is `Copy` and
//! pointer-sized), mirroring that precedent exactly.
//!
//! **Deviation: `ProcessTemplateKey`'s `Hash`/`Eq` are hand-written, not
//! derived.** `sv_lang::ConstantValue` derives only `Clone, Debug, PartialEq`
//! (it is `#[non_exhaustive]` and its `Real` variant is a bare `f64`, which
//! has no `Eq`; its `Integer` variant wraps `SVInt`, which derives `Eq` but
//! not `Hash`), so `#[derive(Eq, Hash)]` on a struct embedding
//! `Vec<ConstantValue>` does not compile. [`hash_constant_value`] below
//! provides a `Hash` implementation kept manually consistent with
//! `ConstantValue`'s derived `PartialEq` (structurally recursing the same
//! way, normalizing `-0.0`/`0.0` to the same hash since `PartialEq` already
//! treats them as equal), and `Eq` is asserted as a marker trait on top of
//! that same `PartialEq`. The one honest gap this leaves (documented, not
//! silently swept under a `#[derive]`): a `real`-valued module parameter
//! equal to `NaN` is `PartialEq`-unequal to itself, so `Eq`'s reflexivity
//! technically does not hold for that one value class. This can only ever
//! cost a missed dedup opportunity (two `NaN`-parameterized instances
//! compiling their own, functionally-identical kernels instead of sharing
//! one) -- never a wrong simulation result, since every instance still gets
//! its own correct `ActivationRecord`/state-arena storage regardless of
//! kernel sharing (PR-030) -- and the A2 slice's construct set has no `real`
//! parameters at all, so no slice fixture can even reach this path.
use crate::symid::SymbolUid;
use std::hash::{Hash, Hasher};

use crate::symid::SymbolId;
use sv_lang::{ConstantValue, Symbol};

use crate::netlist::InstanceId;
use crate::process::ProcessInstanceId;

/// PR-029: `struct ProcessTemplateKey{definition: SymbolId, params:
/// Vec<ConstantValue>}` -- a hashable dedup key built from
/// `Symbol::definition()`+`parameters()` (see this module's doc comment for
/// the `instance_body_definition()` deviation), used for O(1) amortized
/// template lookup (an ordinary `HashMap<ProcessTemplateKey, _>` in the
/// caller, e.g. codegen's future per-template kernel cache).
#[derive(Clone, Debug)]
pub struct ProcessTemplateKey {
    pub definition: SymbolId,
    pub params: Vec<ConstantValue>,
}

impl PartialEq for ProcessTemplateKey {
    fn eq(&self, other: &Self) -> bool {
        self.definition == other.definition && self.params == other.params
    }
}

/// See this module's doc comment ("`Hash`/`Eq` are hand-written, not
/// derived") for exactly why this is a plain marker `impl` over the
/// `PartialEq` above rather than `#[derive(Eq)]`.
impl Eq for ProcessTemplateKey {}

impl Hash for ProcessTemplateKey {
    fn hash<H: Hasher>(&self, state: &mut H) {
        self.definition.hash(state);
        self.params.len().hash(state);
        for p in &self.params {
            hash_constant_value(p, state);
        }
    }
}

/// A hand-written `Hash` for `sv_lang::ConstantValue`, kept structurally in
/// step with its own derived `PartialEq` (see this module's doc comment).
/// Each variant is tagged with a distinct discriminant byte before its
/// payload is hashed, so e.g. `Integer(0)` and `String("0")` never collide
/// on payload alone. `Integer`/`Real` are hashed via a stable textual/bit
/// form rather than field-by-field (`SVInt` has no public field accessors
/// besides its already-`Display`-backed `to_string()`, and does not itself
/// implement `Hash`).
fn hash_constant_value<H: Hasher>(cv: &ConstantValue, state: &mut H) {
    match cv {
        ConstantValue::Integer(i) => {
            0u8.hash(state);
            i.to_string().hash(state);
        }
        ConstantValue::Real(r) => {
            1u8.hash(state);
            // Normalize -0.0 to 0.0's bit pattern: `PartialEq` (`==`) already
            // treats them as equal, and `Hash` must agree with `Eq` for
            // every pair its own `PartialEq` calls equal.
            let bits = if *r == 0.0 {
                0.0f64.to_bits()
            } else {
                r.to_bits()
            };
            bits.hash(state);
        }
        ConstantValue::String(s) => {
            2u8.hash(state);
            s.hash(state);
        }
        ConstantValue::Null => 3u8.hash(state),
        ConstantValue::Unbounded => 4u8.hash(state),
        ConstantValue::Array(elems) => {
            5u8.hash(state);
            elems.len().hash(state);
            for e in elems {
                hash_constant_value(e, state);
            }
        }
        ConstantValue::Map(s, is_empty) => {
            6u8.hash(state);
            s.hash(state);
            is_empty.hash(state);
        }
        ConstantValue::Union(s) => {
            7u8.hash(state);
            s.hash(state);
        }
        ConstantValue::TaggedUnion {
            member,
            value,
            text,
        } => {
            9u8.hash(state);
            member.hash(state);
            text.hash(state);
            hash_constant_value(value, state);
        }
        ConstantValue::Bad => 8u8.hash(state),
        // `ConstantValue` is `#[non_exhaustive]` upstream -- this is not one
        // of this workspace's own pinned "kind" enums the ARCHITECTURE ยง7
        // item 9 exhaustive-match discipline targets (those are
        // `StatementKind`/`ExpressionKind`/`TimingControlKind`/the
        // procedure-kind enum, each guarded by its own `::COUNT` pin in
        // `determinism::kind_guard`); a plain value type has no such count
        // to pin against. A future `sv-lang` variant still gets a stable
        // (if coarser) hash via its `Debug` text rather than silently
        // colliding with an existing tag or panicking.
        other => {
            9u8.hash(state);
            format!("{other:?}").hash(state);
        }
    }
}

/// PR-029: builds a [`ProcessTemplateKey`] from an instance's owning
/// `Instance` symbol (for `parameters()`) and its `InstanceBody` (for
/// `instance_body_definition()`) -- see this module's doc comment for why
/// both are needed. `params` uses each parameter's elaborated
/// [`ConstantValue`] (`Symbol::constant_value()`, forced during elaboration,
/// full-width, `defparam`-aware) rather than the printed-text
/// `parameter_value()` netlist's own `TemplateKey` uses, per this task's
/// literal `Vec<ConstantValue>` field type. A parameter whose constant value
/// failed to fold (`None`) is recorded as `ConstantValue::Bad` -- a real,
/// distinguishable member of the key rather than being silently dropped
/// (which would shift every later parameter's position and could spuriously
/// equate two instances with a different number of foldable parameters).
pub fn template_key_of<'d>(instance: Symbol<'d>, body: Symbol<'d>) -> ProcessTemplateKey {
    let definition = body
        .instance_body_definition()
        .map(|def| def.uid())
        // Defensive fallback, not a silent success path: a well-formed
        // elaborated `InstanceBody` always has a `Definition`
        // (`instance_body_definition()` only returns `None` for a
        // malformed/uninstantiated body, which the A2-slice's `discover_
        // processes`/module-discovery layer never hands this function in
        // the first place). Falling back to the body's OWN hierarchical
        // path keeps the key well-defined and deterministic rather than
        // panicking, while guaranteeing such a body never spuriously
        // dedups with any other instance's key (every body's own
        // hierarchical path is unique).
        .unwrap_or_else(|| body.uid());

    let params = instance
        .parameters()
        .map(|p| p.constant_value().unwrap_or(ConstantValue::Bad))
        .collect();

    ProcessTemplateKey { definition, params }
}

/// PR-075/076/077: assigns `ProcessInstanceId(0), ProcessInstanceId(1), ...`
/// in exactly `order`'s sequence. `order` is meant to be
/// `netlist::NetlistView::registration_order()`'s own DFS-order output --
/// this function performs no traversal of its own (see
/// `no_second_preorder_walk_of_design_exists_in_process_dedup_rs` in
/// `tests/process_dedup.rs`): PR-076's reconciliation note is explicit that
/// `registration_order()` (elaboration's own deterministic pre-order DFS
/// over the design's top-level instance list) directly **is** the tie-break
/// order, so a
/// second, independently-derived pre-order walk here would just be a second
/// place the "canonical instance order" could silently disagree with the
/// first -- exactly the bug class PR-076's `superseded_by` note names and
/// this function is deliberately shaped to make impossible.
pub fn assign_process_instance_ids(
    order: &[InstanceId],
) -> std::collections::HashMap<InstanceId, ProcessInstanceId> {
    order
        .iter()
        .enumerate()
        .map(|(i, id)| (*id, ProcessInstanceId(i as u64)))
        .collect()
}

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

    /// PR-075/077, at the pure-function level (no `sv-lang` parse needed):
    /// ids follow `order`'s own sequence, not `InstanceId`'s numeric value.
    /// The full `netlist::elaborate`-backed determinism proof lives in
    /// `tests/process_dedup.rs`.
    #[test]
    fn assign_process_instance_ids_follows_input_order_not_instance_id_value() {
        let order = vec![InstanceId(7), InstanceId(3), InstanceId(9)];
        let ids = assign_process_instance_ids(&order);

        assert_eq!(ids.len(), 3);
        assert_eq!(ids[&InstanceId(7)], ProcessInstanceId(0));
        assert_eq!(ids[&InstanceId(3)], ProcessInstanceId(1));
        assert_eq!(ids[&InstanceId(9)], ProcessInstanceId(2));
    }
}