vj-codegen 0.1.0

LLVM code generator of the vj SystemVerilog simulator.
//! SVA (concurrent assertions, Clause 16/17) lowering support.
//! `docs/superpowers/plans/2026-10-03-sva.md`.
//!
//! Task 3: [`ranges`] recovers a sequence `##`-delay or `[...]`-repetition
//! operator's folded `(min, max)` bounds from syntax, a gap `sv-lang` 0.1.9
//! leaves open (see that module's own doc comment). Task 4: [`op`] mirrors
//! `UnaryAssertionOperator`/`BinaryAssertionOperator` (another gap `sv-lang`
//! 0.1.9 leaves open -- `AssertionExpr::op()` hands back only the raw C++
//! discriminant) as typed Rust enums, and recovers the positional
//! `Abort`/`Clocking`/`Case` children `AssertionExpr::children()`'s generic
//! `SemNode` walk doesn't label. Task 5: [`leaf`] compiles a Boolean leaf
//! (reusing the existing LLVM pipeline, no new codegen) and validates 16.6's
//! restrictions on it. Task 9: [`build`] is the automaton builder itself --
//! `fn build_automaton`, consuming [`ranges`]' recovered bounds and
//! producing a `sv_sim_sim::sva::ir::Automaton` (the automaton IR skeleton
//! T4 defines). See `build`'s own module doc comment for why it lives here
//! rather than at the plan's literal `sv-sim-sim/src/sva/build.rs` path.
//! Task 13: [`flow`] is the Annex F.5.4 local-variable flow algebra the
//! builder uses to decide whether an assigned local variable may be read
//! downstream (16.10). Task 14: [`actions`] classifies and validates the
//! subroutine-call match items of 16.11. Task 18: [`gclk`] checks where the
//! global clocking future sampled value functions of 16.9.4 may appear.
//! Task 27: [`disable`] resolves an assertion's disable condition (16.15).
pub mod actions;
pub mod build;
pub mod disable;
pub mod emit;
pub mod flow;
pub mod gclk;
pub mod leaf;
pub mod named;
pub mod op;
pub mod proc;
pub mod ranges;
pub mod trees;

use sv_lang::kinds::{AssertionExprKind, ExpressionKind};
use sv_lang::{AssertionExpr, Expression, SemNode};

/// Every Boolean leaf reachable anywhere in `expr`'s tree, in source order
/// -- "the boolean expressions at the leaves" `AssertionExpr::children`'s
/// own doc comment names, recovered generically for ANY
/// `AssertionExprKind` shape by exploiting the fact that a leaf child casts
/// to [`Expression`] while a composite sub-sequence/sub-property child
/// casts to [`AssertionExpr`] (so no per-kind positional decoding, unlike
/// [`op::abort_children`]/[`op::clocking_children`]/[`op::case_children`],
/// is needed just to *find* the leaves -- those decoders exist for a
/// different job, labelling which child is "the condition" vs "the body"
/// for the automaton builder, T9 onward). A child that is neither (e.g. a
/// `Clocking` node's own clock-event `TimingControl`) is correctly skipped:
/// 16.6 itself exempts "expressions in a clocking event" from leaf
/// validation. [`assertion_expr_leaf_children`] is where the two cast-based
/// dispatch's own exceptions live (a named sequence/property reference, and
/// a sequence match item) -- see its doc comment.
pub fn collect_boolean_leaves<'d>(expr: AssertionExpr<'d>, out: &mut Vec<Expression<'d>>) {
    for child in assertion_expr_leaf_children(expr) {
        if let Some(e) = child.as_expression() {
            out.push(e);
        } else if let Some(a) = child.as_assertion_expr() {
            collect_boolean_leaves(a, out);
        }
    }
}

/// `expr`'s own semantic children, with two corrections both
/// [`collect_boolean_leaves`] and [`leaf::validate_boolean_leaf`]'s walk
/// need identically (`pub(crate)` so `leaf.rs` shares this one definition
/// rather than risking the two walks diverging):
///
/// - **A [`SequenceWithMatch`](AssertionExprKind::SequenceWithMatch) or
///   [`FirstMatch`](AssertionExprKind::FirstMatch) node's match items are
///   not Boolean leaves.** Both `SequenceWithMatchExpr::visitExprs` and
///   `FirstMatchAssertionExpr::visitExprs` (vendored C++) visit the
///   sub-sequence AND every match item as direct, sibling semantic
///   children -- identical shape (`const AssertionExpr&` sub-sequence plus
///   a `std::span<const Expression* const>` of match items), confirmed
///   against `include/slang/ast/expressions/AssertionExpr.h` -- and a
///   match item (e.g. `cnt++`, legal per 16.11) happens to be a plain
///   `Expression` node, indistinguishable from a genuine Boolean leaf by a
///   bare `as_expression()` cast. The builder validates match items
///   through their own entry point (T14, `build::attach_match_items` and
///   [`actions`]); this function excludes them from the leaf set entirely
///   (rather than letting the generic dispatch fold one in and get it
///   wrongly rejected by [`leaf::validate_boolean_leaf`]) by recursing only
///   into the first child that casts to [`AssertionExpr`] (the
///   sub-sequence) and dropping the rest.
/// - **An [`AssertionInstance`](ExpressionKind::AssertionInstance)
///   `Expression` child (a named sequence/property reference, e.g. `seq1`
///   in `assert property (@(posedge clk) seq1)`) is not itself a Boolean
///   leaf.** It casts to [`Expression`] (so the plain `as_expression()`/
///   `as_assertion_expr()` dispatch would otherwise swallow it whole as an
///   opaque leaf), but its own semantic children -- `.body` (an
///   `AssertionExpr`, confirmed against the vendored C++:
///   `AssertionInstanceExpression::visitExprs` visits `body` plus every
///   local assertion variable's default-value initializer `Expression` --
///   verified live: a parameterless `seq1`'s `AssertionInstanceExpression`
///   node generically expands to exactly those) -- are where the sequence/
///   property's *real* Boolean leaves (and any static-lifetime-function
///   call inside a local variable's initializer) live. A bound argument's
///   *actual* value needs no separate handling here through
///   `Expression::assertion_arguments()`: slang already substitutes it
///   directly into `body`'s own tree (verified live: `seq1(2)`'s `x == f()`
///   body shows the literal `2` in place of `x`, not a dangling port
///   reference), so walking `body` generically already reaches it.
///   Recursing into this wrapper's own children (instead of returning it
///   as an opaque leaf) surfaces all of that.
pub(crate) fn assertion_expr_leaf_children<'d>(expr: AssertionExpr<'d>) -> Vec<SemNode<'d>> {
    let children = expr.children();
    let children: Vec<SemNode<'d>> = if matches!(
        expr.kind(),
        AssertionExprKind::SequenceWithMatch | AssertionExprKind::FirstMatch
    ) {
        children
            .into_iter()
            .filter(|c| c.as_assertion_expr().is_some())
            .collect()
    } else {
        children
    };
    children
        .into_iter()
        .flat_map(expand_assertion_instance)
        .collect()
}

/// If `node` is an [`AssertionInstance`](ExpressionKind::AssertionInstance)
/// `Expression` (a named sequence/property reference), its own semantic
/// children (expanded recursively, in case one of those is itself another
/// such reference) in place of the opaque wrapper node; otherwise `node`
/// itself, unchanged. See [`assertion_expr_leaf_children`]'s doc comment.
fn expand_assertion_instance<'d>(node: SemNode<'d>) -> Vec<SemNode<'d>> {
    match node.as_expression() {
        Some(e) if e.kind() == ExpressionKind::AssertionInstance => e
            .as_sem_node()
            .children()
            .into_iter()
            .flat_map(expand_assertion_instance)
            .collect(),
        _ => vec![node],
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use sv_lang::{AnalysisFlags, Compilation, Session, Symbol};

    /// Compiles `property_body` as a clocked `assert property` inside an
    /// `always_ff` (same shape as `op::tests::compiled`/`with_clocking_root`,
    /// this file's sibling test scaffolding), and hands the `Clocking`
    /// root `AssertionExpr` and the containing scope `Symbol` to `f`.
    fn with_clocking_root<R>(
        extra_decls: &str,
        property_body: &str,
        f: impl FnOnce(AssertionExpr<'_>, Symbol<'_>) -> R,
    ) -> R {
        let src = format!(
            "module m;\n\
             {extra_decls}\n\
             logic clk, a, b;\n\
             int cnt;\n\
             always_ff @(posedge clk) begin\n\
               assert property (@(posedge clk) {property_body});\n\
             end\n\
             endmodule\n"
        );
        let session = Session::new();
        let mut comp = Compilation::new(&session).unwrap();
        comp.add_source(&src).unwrap();
        let design = comp.compile().unwrap();
        assert!(
            !design.has_fatal_errors(),
            "fixture failed to compile: {src}"
        );
        let body = design
            .top_instances()
            .next()
            .unwrap()
            .instance_body()
            .unwrap();
        let analysis = design.analyze(AnalysisFlags::NONE, 1).unwrap();
        let proc = analysis.procedures(body).next().unwrap();
        let assertion = analysis.assertions(proc.symbol()).next().unwrap();
        let root = assertion.root().as_assertion_expr().unwrap();
        assert_eq!(root.kind(), AssertionExprKind::Clocking);
        f(root, body)
    }

    /// Review-fix regression: `collect_boolean_leaves` must recurse through
    /// a named sequence reference (`AssertionInstanceExpression`) and
    /// collect the Boolean leaf (`f() == 1`) living inside the referenced
    /// sequence's body, not the opaque reference itself.
    #[test]
    fn collects_leaf_through_named_sequence_reference() {
        with_clocking_root(
            "function static int f(); return 1; endfunction\n\
             sequence seq1; f() == 1; endsequence",
            "seq1",
            |clocking, _scope| {
                let mut leaves = Vec::new();
                collect_boolean_leaves(clocking, &mut leaves);
                assert_eq!(
                    leaves.len(),
                    1,
                    "expected exactly one real Boolean leaf (f() == 1), got {leaves:?}"
                );
                assert_eq!(
                    leaves[0].kind(),
                    ExpressionKind::BinaryOp,
                    "the collected leaf must be the sequence's own `f() == 1` expression, not \
                     the AssertionInstanceExpression wrapper for `seq1` itself"
                );
            },
        );
    }

    /// Review-fix regression: `collect_boolean_leaves` must never fold an
    /// inline sequence match item (`cnt++`, legal per 16.11) into the
    /// Boolean-leaf set -- only the sub-sequence's own leaves (`a`, `b`).
    #[test]
    fn excludes_inline_sequence_match_item_from_collected_leaves() {
        with_clocking_root("", "(a ##1 b, cnt++)", |clocking, _scope| {
            let mut leaves = Vec::new();
            collect_boolean_leaves(clocking, &mut leaves);
            assert_eq!(
                leaves.len(),
                2,
                "expected exactly the two sub-sequence leaves (a, b), got {leaves:?}"
            );
            for leaf in &leaves {
                assert_eq!(
                    leaf.kind(),
                    ExpressionKind::NamedValue,
                    "a sequence match item must never appear in the leaf set: {leaves:?}"
                );
            }
        });
    }

    /// Review-fix regression (r2): `first_match`'s match items
    /// (`AssertionExprKind::FirstMatch`, LRM 16.9.3/A.2.10) are the
    /// identical AST shape to `SequenceWithMatch`'s (confirmed against the
    /// vendored `FirstMatchAssertionExpr`: a sub-`AssertionExpr` plus a
    /// `matchItems` span, visited as siblings) and must be excluded from
    /// the collected leaf set the same way -- only the sub-sequence's own
    /// leaves (`a`, `b`), never the match item `cnt++` itself.
    #[test]
    fn excludes_first_match_match_item_from_collected_leaves() {
        with_clocking_root("", "first_match(a ##1 b, cnt++)", |clocking, _scope| {
            let mut leaves = Vec::new();
            collect_boolean_leaves(clocking, &mut leaves);
            assert_eq!(
                leaves.len(),
                2,
                "expected exactly the two sub-sequence leaves (a, b), got {leaves:?}"
            );
            for leaf in &leaves {
                assert_eq!(
                    leaf.kind(),
                    ExpressionKind::NamedValue,
                    "a first_match match item must never appear in the leaf set: {leaves:?}"
                );
            }
        });
    }
}