vj 0.2.0

vj: a SystemVerilog (IEEE 1800-2023) simulator built on slang and LLVM.
//! Plan-time constant evaluation that is aware of a design's
//! [`TimingModel`] specparam table (`docs/superpowers/plans/2026-10-03-sdf.md`
//! Task 2): a drop-in replacement for `primitives.rs`'s private `constant_of`
//! that, for an expression referencing a `Specparam` symbol, reads the
//! table's CURRENT value instead of the symbol's own frozen
//! `constant_value()` -- so a later SDF `LABEL` annotation (Task 28), which
//! mutates the table in place, is picked up by any later re-evaluation
//! without this function itself changing. For every other expression shape
//! this delegates to the exact same general constant-folding
//! `primitives.rs::constant_of` uses (`e.constant_value().or_else(|| eval
//! .eval_constant(e))`) -- not re-implemented, duplicated here only because
//! `primitives.rs`'s copy is a private `fn` in a sibling module.

use sv_lang::kinds::{ExpressionKind, SymbolKind};
use sv_lang::{ConstantValue, EvalSession, Expression, SemNode};
use vj_sim::netlist::InstanceId;
use vj_sim::netlist::specify::TimingModel;
use vj_sim::symid::{SymbolUid, is_value_name};

/// Mirrors `primitives.rs::constant_of` (a constant expression's value,
/// folding what slang left unfolded) -- see this module's own doc comment
/// for why it is a second, small copy rather than a shared export.
fn constant_of(eval: &EvalSession<'_>, e: Expression<'_>) -> Option<ConstantValue> {
    e.constant_value().or_else(|| eval.eval_constant(e))
}

/// Evaluates `node` as a constant in `instance`'s scope, specparam-aware:
/// when `node` is an expression naming a `Specparam` symbol (unwrapping one
/// implicit `Conversion`, exactly as `primitives.rs::signal_bit` already
/// does for a terminal reference), the CURRENT value in `model` is read
/// instead of that symbol's own frozen `constant_value()`. Every other
/// expression shape is [`constant_of`] unchanged. `node` not reducing to an
/// expression at all, or reducing to one that is not a constant (and not a
/// found specparam reference), is a named `Err`, never a silent default.
///
/// `#[allow(dead_code)]`: this task (`docs/superpowers/plans/
/// 2026-10-03-sdf.md` Task 2) is the plan-time evaluator alone -- no caller
/// exists yet (Track A's `crate::specify::plan`, Task 4a onward, is the
/// first). Exercised directly by this module's own tests below, not dead
/// API.
#[allow(dead_code)]
pub(crate) fn eval_specparam_aware(
    eval: &EvalSession<'_>,
    model: &TimingModel,
    instance: InstanceId,
    node: SemNode<'_>,
) -> Result<ConstantValue, String> {
    let e = node.as_expression().ok_or_else(|| {
        "a specparam-aware evaluation of a node that is not an expression".to_string()
    })?;
    let e = if e.kind() == ExpressionKind::Conversion {
        e.operand().unwrap_or(e)
    } else {
        e
    };
    if is_value_name(e.kind())
        && let Some(sym) = e.referenced_symbol()
        && sym.kind() == SymbolKind::Specparam
    {
        return model
            .specparam(instance, &sym.uid())
            .cloned()
            .ok_or_else(|| {
                format!(
                    "specparam `{}` has no value in this design's TimingModel (not seeded by \
                     TimingModel::seed_specparams)",
                    sym.name()
                )
            });
    }
    constant_of(eval, e)
        .ok_or_else(|| "a specparam-aware expression that did not fold to a constant".to_string())
}

#[cfg(test)]
mod tests {
    use sv_lang::kinds::SymbolKind;
    use sv_lang::{Compilation, Design, Session, Symbol};
    use vj_sim::netlist::InstanceId;
    use vj_sim::netlist::specify::TimingModel;
    use vj_sim::symid::SymbolUid;

    use super::eval_specparam_aware;

    fn compile(src: &str) -> Design {
        let session = Session::new();
        let mut comp = Compilation::new(&session).expect("compilation created");
        comp.add_source(src).expect("source added");
        comp.compile().expect("design compiled")
    }

    fn top_body(design: &Design) -> Symbol<'_> {
        design
            .top_instances()
            .next()
            .expect("one top instance")
            .instance_body()
            .expect("top instance has a body")
    }

    /// The `specify` block's lone `TimingPath`'s delay-list expression
    /// (`tRise` in `(a=>q) = tRise;`) -- the shape this evaluator exists
    /// for.
    fn path_delay_expr<'d>(body: &Symbol<'d>) -> sv_lang::Expression<'d> {
        let specify = body
            .members()
            .find(|s| s.kind() == SymbolKind::SpecifyBlock)
            .expect("specify block member");
        let path = specify
            .members()
            .find(|s| s.kind() == SymbolKind::TimingPath)
            .expect("TimingPath member");
        path.timing_path_delays()
            .next()
            .expect("one delay expression")
    }

    #[test]
    fn matches_constant_of_before_any_mutation() {
        let mut design = compile(
            "module m(input a, output q);\n  specparam tRise = 10;\n  assign q = a;\n  \
             specify\n    (a => q) = tRise;\n  endspecify\nendmodule\n",
        );
        // `eval_session()` exclusively borrows `design` for the rest of this
        // scope (mirrors `run.rs`'s own `eval`/`design` ordering) -- every
        // further read of the design goes through `eval.design()`.
        let eval = design.eval_session();
        let body = top_body(eval.design());
        let mut model = TimingModel::new();
        model.seed_specparams(&[(InstanceId(0), body)]);
        let delay = path_delay_expr(&body);

        let via_model = eval_specparam_aware(&eval, &model, InstanceId(0), delay.as_sem_node())
            .unwrap_or_else(|e| panic!("eval_specparam_aware failed: {e}"));
        // "the SAME value as constant_of before any mutation" (Task 2's own
        // acceptance text) -- `delay.constant_value()` IS that baseline
        // (`tRise`'s frozen value, unfolded through the ordinary constant
        // path, no TimingModel involved at all).
        let via_constant_value = delay
            .constant_value()
            .expect("tRise reference folds to a constant");
        assert_eq!(via_model.as_i64(), via_constant_value.as_i64());
        assert_eq!(via_model.as_i64(), Some(10));
    }

    #[test]
    fn reflects_a_direct_mutation_of_the_table() {
        // "the UPDATED value after a direct test-only mutation of the
        // table (simulating what T28 will do for real)" -- Task 2's own
        // acceptance text.
        let mut design = compile(
            "module m(input a, output q);\n  specparam tRise = 10;\n  specparam tFall = 22;\n  \
             assign q = a;\n  specify\n    (a => q) = tRise;\n  endspecify\nendmodule\n",
        );
        let eval = design.eval_session();
        let body = top_body(eval.design());
        let mut model = TimingModel::new();
        model.seed_specparams(&[(InstanceId(0), body)]);
        let delay = path_delay_expr(&body);

        let rise_uid = body
            .members()
            .find(|s| s.name() == "tRise")
            .expect("tRise member")
            .uid();
        let new_value = body
            .members()
            .find(|s| s.name() == "tFall")
            .expect("tFall member")
            .constant_value()
            .expect("tFall has a constant value");
        model.set_specparam(InstanceId(0), rise_uid, new_value);

        let via_model = eval_specparam_aware(&eval, &model, InstanceId(0), delay.as_sem_node())
            .unwrap_or_else(|e| panic!("eval_specparam_aware failed: {e}"));
        assert_eq!(
            via_model.as_i64(),
            Some(22),
            "mutating the table must change what the evaluator reports, with no re-plan"
        );
    }

    #[test]
    fn non_specparam_constant_expressions_still_fold() {
        let mut design = compile(
            "module m(input a, output q);\n  assign q = a;\n  specify\n    (a => q) = 5;\n  endspecify\nendmodule\n",
        );
        let eval = design.eval_session();
        let body = top_body(eval.design());
        let model = TimingModel::new();
        let delay = path_delay_expr(&body);

        let via_model = eval_specparam_aware(&eval, &model, InstanceId(0), delay.as_sem_node())
            .unwrap_or_else(|e| panic!("eval_specparam_aware failed: {e}"));
        assert_eq!(via_model.as_i64(), Some(5));
    }

    #[test]
    fn resolves_a_specparam_declared_inside_the_specify_block() {
        // Review fix (r1): mirrors `matches_constant_of_before_any_mutation`
        // above, but `tRise` is declared INSIDE the `specify` block (LRM
        // 30.3's own `specify_item` production lists
        // `specparam_declaration`) rather than at module scope. The real
        // `Specparam` symbol this delay expression's `referenced_symbol()`
        // unwraps to is a child of the `SpecifyBlock` member, not a direct
        // module member -- `TimingModel::seed_specparams` must still find
        // it (see `specify.rs`'s own fix for the seeding half of this).
        let mut design = compile(
            "module m(input a, output q);\n  assign q = a;\n  specify\n    (a => q) = \
             tRise;\n    specparam tRise = 10;\n  endspecify\nendmodule\n",
        );
        let eval = design.eval_session();
        let body = top_body(eval.design());
        let mut model = TimingModel::new();
        model.seed_specparams(&[(InstanceId(0), body)]);
        let delay = path_delay_expr(&body);

        let via_model = eval_specparam_aware(&eval, &model, InstanceId(0), delay.as_sem_node())
            .unwrap_or_else(|e| panic!("eval_specparam_aware failed: {e}"));
        assert_eq!(via_model.as_i64(), Some(10));
    }

    #[test]
    fn an_unseeded_specparam_is_a_named_error_not_a_silent_default() {
        let mut design = compile(
            "module m(input a, output q);\n  specparam tRise = 10;\n  assign q = a;\n  \
             specify\n    (a => q) = tRise;\n  endspecify\nendmodule\n",
        );
        let eval = design.eval_session();
        let body = top_body(eval.design());
        // Deliberately never seeded.
        let model = TimingModel::new();
        let delay = path_delay_expr(&body);

        let err = eval_specparam_aware(&eval, &model, InstanceId(0), delay.as_sem_node())
            .expect_err("an unseeded specparam reference must be a named error");
        assert!(err.contains("tRise"), "{err}");
    }
}