vj-codegen 0.1.0

LLVM code generator of the vj SystemVerilog simulator.
//! T14 (`docs/superpowers/plans/2026-10-03-sva.md`): sequence match-item
//! subroutine calls (LRM 16.11).
//!
//! `(seq, tk(x, data))` attaches a call of a task or a `void` function to the
//! end points of `seq`. This module is the classifier and validator for such a
//! call: [`lower_call`] turns the `Call` expression into a
//! [`Transition::SubroutineCall`] edge and a [`CallSite`] in the leaf table's
//! call table, and rejects every call 16.11 does not allow, loudly.
//!
//! ## What 16.11 says, and where it is enforced
//!
//! - **Task or void function.** A match item may call "tasks, task methods,
//!   void functions, void function methods, and system tasks and functions";
//!   a function that returns a value is not a match item. slang rejects a
//!   non-void function in this position itself, so the check here is the
//!   builder's own, independent copy (tested with lenient compilation).
//! - **Input and ref arguments only.** `output` and `inout` formals are
//!   rejected (the call returns nothing to the sequence). Each formal comes
//!   from the callee's [`Symbol::subroutine_arguments`]; a system call has no
//!   declared formals and every argument is by value.
//! - **By value or by reference.** An `input` argument is passed BY VALUE: the
//!   value of its expression SAMPLED at the end point, consistent with the
//!   match that produced it (the same value a leaf in the sequence would
//!   see), recorded as a [`CallArg::Value`] leaf. A `ref`/`const ref` argument
//!   is bound to the actual itself, a [`CallArg::Ref`] whose actual lives in
//!   the [`CallSite`]. "If a local variable appears in an actual argument
//!   expression, then that argument shall be passed by value": a local
//!   variable under a `ref` formal is an error.
//! - **Reactive region, fire and forget.** The call is scheduled in the
//!   Reactive region at every end point of the sequence it is attached to,
//!   neither waited on nor returning anything, and it runs even when a longer
//!   sequence it is part of later fails. That is the semantics of the IR edge
//!   ([`Transition::SubroutineCall`]); the interpreter
//!   (`sv_sim_sim::sva::trace::attempt_with_calls`) is its executable
//!   specification.
//!
//! - **Constant arguments.** A by-value actual with a constant value (a format
//!   string, a parameter) is a [`CallArg::Const`]: it is not sampled, and the
//!   dispatch lowers the expression itself.
//!
//! ## Where the call runs
//!
//! This module classifies and validates the call and records everything the
//! dispatch needs in the [`CallSite`] table (the callee, which arguments are
//! values, references or constants). The dispatch is the hosted assertion
//! kernel's (`process_lower::lower_match_item_calls`): the engine reports a
//! call as fired term `2 + CallId` with the sampled by-value arguments, and the
//! kernel runs the call through the same lowering a statement-position call of
//! a process gets (`lower_system_call_statement`, `func::lower_task_call`).
//! A task that can suspend (a delay or an event control in its body) is
//! [`CallSite::spawns`]: it cannot run inside that callback, so 16.11's
//! "thread of its own" is a real process, compiled from the call
//! (`sv_sim_sim::process::cfg::build_match_item_task_cfg`) and spawned by the
//! assertion runner once per end point (`sv_sim_exec::verif`), its formals bound
//! from the same sampled values.
#![allow(dead_code)]

use sv_lang::kinds::ExpressionKind;
use sv_lang::{ArgumentDirection, Expression, SubroutineKind, Symbol};
use sv_sim_sim::sva::ir::{self, CallArg, CallId, Transition};

use super::build::{LeafTable, collect_local_reads};
use super::leaf::validate_boolean_leaf;
use crate::error::LoweringError;
use crate::types::TypeWidth;

/// One subroutine call attached as a match item (16.11), as the engine (T29)
/// needs it to dispatch the call.
#[derive(Clone, Debug)]
pub struct CallSite<'d> {
    /// The `Call` expression itself.
    pub call: Expression<'d>,
    /// Each actual argument, in the order the callee's formals (or, for a
    /// system task, the call's written arguments) take them: how it is passed,
    /// and its expression. A [`CallArg::Value`] leaf is the compiled form of
    /// the same expression.
    pub args: Vec<(CallArg, Expression<'d>)>,
    /// The callee is a task that can suspend: each end point spawns it as a process of its
    /// own (`sv_sim_sim::process::cfg::build_match_item_task_cfg`) instead of the assertion
    /// kernel running it inline.
    pub spawns: bool,
}

/// What kind of subroutine a call expression names.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Callee {
    /// A user task or `void` function.
    User,
    /// A system task or `void` system function.
    System,
}

/// True when `e` is a subroutine call (the shape [`lower_call`] handles).
pub(super) fn is_call(e: Expression<'_>) -> bool {
    e.kind() == ExpressionKind::Call
}

/// Classifies and validates the call `e` (a match item), allocates its
/// [`CallSite`] in `leaves` and returns the edge label that fires it. The
/// by-value arguments are registered as leaves under the CURRENT flow set of
/// `leaves`, so a local variable one of them reads must already be assigned
/// (16.10).
pub(super) fn lower_call<'d>(
    a: &mut ir::Automaton,
    leaves: &mut LeafTable<'d>,
    e: Expression<'d>,
    scope: &Symbol<'d>,
) -> Result<Transition, LoweringError> {
    let name = e
        .call_subroutine_name()
        .unwrap_or_else(|| "<unnamed>".to_string());
    let callee = classify(e, &name, scope)?;
    // A task that can suspend (16.11: "scheduled in the Reactive region ... as a thread of its
    // own") cannot run inside the assertion kernel's callback: it is compiled as a process of
    // its own that the runner spawns, once per end point, with the sampled arguments
    // ([`CallSite::spawns`]).
    let spawns = callee == Callee::User
        && e.call_subroutine()
            .is_some_and(|sub| sv_sim_sim::process::cfg::subroutine_can_suspend(&sub));
    let formals: Vec<Symbol<'d>> = match callee {
        Callee::User => e
            .call_subroutine()
            .ok_or_else(|| {
                LoweringError::unsupported_at(
                    &format!(
                        "sva automaton builder: the user call `{name}` has no subroutine symbol"
                    ),
                    scope,
                )
            })?
            .subroutine_arguments()
            .collect(),
        Callee::System => Vec::new(),
    };
    let mut actuals = e.operands();
    // A method call carries its receiver as the leading operand: it is the
    // `this` of the Reactive-region call, read when the call runs (see
    // `classify`), not an argument.
    if callee == Callee::User && e.call_this_class().is_some() && !actuals.is_empty() {
        actuals.remove(0);
    }
    // A built-in method of a queue, dynamic array or associative array (`q.push_back(x)`,
    // `q.delete()`: a `void` function method, 16.11) likewise carries the collection as
    // its leading operand. It is the object the Reactive-region call works on, read when
    // the call runs, never an argument: sampling its handle as one is meaningless (a
    // collection is not a leaf value).
    if callee == Callee::System
        && !actuals.is_empty()
        && crate::collections::coll_of_expr(&actuals[0]).is_some()
    {
        actuals.remove(0);
    }
    if callee == Callee::User && actuals.len() != formals.len() {
        return Err(LoweringError::unsupported_at(
            &format!(
                "sva automaton builder: the call `{name}` has {} actual arguments for {} \
                 formals (slang binds a defaulted or named argument to its own slot, so they \
                 must agree)",
                actuals.len(),
                formals.len()
            ),
            scope,
        ));
    }

    let mut args = Vec::with_capacity(actuals.len());
    let mut sites = Vec::with_capacity(actuals.len());
    for (i, actual) in actuals.iter().enumerate() {
        let by_ref = match formals.get(i) {
            None => false,
            Some(formal) => match formal.formal_argument_direction() {
                ArgumentDirection::In => false,
                ArgumentDirection::Ref => true,
                dir @ (ArgumentDirection::Out | ArgumentDirection::InOut) => {
                    return Err(LoweringError::unsupported_at(
                        &format!(
                            "the call `{name}` is a sequence match item, but its formal \
                             argument `{}` is {} (LRM 16.11: a subroutine called from a match \
                             item takes input and ref arguments only)",
                            formal.name(),
                            if dir == ArgumentDirection::Out {
                                "output"
                            } else {
                                "inout"
                            }
                        ),
                        scope,
                    ));
                }
            },
        };
        validate_boolean_leaf(*actual, scope)?;
        if !by_ref && is_constant_actual(*actual) {
            // A constant (a string literal, a parameter, `8'd3`) has the same value
            // at every end point: nothing to sample, and a format string is far
            // wider than a leaf's 64 bits.
            args.push(CallArg::Const);
        } else if by_ref {
            let mut syms = Vec::new();
            let mut reads_lvalue = false;
            collect_local_reads(actual.as_sem_node(), &mut syms, &mut reads_lvalue);
            if let Some(local) = syms.first() {
                return Err(LoweringError::unsupported_at(
                    &format!(
                        "the call `{name}` passes local variable `{}` to a `ref` formal (LRM \
                         16.11: if a local variable appears in an actual argument expression, \
                         that argument shall be passed by value)",
                        local.name()
                    ),
                    scope,
                ));
            }
            args.push(CallArg::Ref);
        } else {
            // An argument wider than 64 bits travels as one `Value` entry per word of
            // its four-state value (value limbs, then unknown limbs); `sites` keeps
            // the first as the argument's entry and the dispatch re-assembles the
            // words by the actual's width.
            let width = actual.expr_type().map_or(1, |t| t.value_width());
            let words = leaves.push_arg_leaves(a, *actual, width, scope)?;
            args.extend(words.iter().map(|l| CallArg::Value(*l)));
            sites.push((CallArg::Value(words[0]), *actual));
            continue;
        }
        sites.push((*args.last().expect("pushed above"), *actual));
    }
    let id = CallId(leaves.calls.len() as u32);
    leaves.calls.push(CallSite {
        call: e,
        args: sites,
        spawns,
    });
    Ok(Transition::SubroutineCall { call: id, args })
}

/// Whether the by-value actual `e` needs no sampling: it has a constant value
/// (a string literal's is not always reported as one, so it is named too).
fn is_constant_actual(e: Expression<'_>) -> bool {
    e.kind() == ExpressionKind::StringLiteral || e.constant_value().is_some()
}

/// 16.11's "task or void function" rule. A class method call (16.11 allows
/// "task methods" and "void function methods") is dispatched like any other call
/// of a task: the receiver handle is read when the Reactive-region call runs,
/// not sampled (it is not an argument), and the method inlines with its bound
/// `this` (`func::lower_task_call`).
fn classify(e: Expression<'_>, name: &str, scope: &Symbol<'_>) -> Result<Callee, LoweringError> {
    let system = e.is_system_call().unwrap_or(false);
    let is_task = e.subroutine_kind() == Some(SubroutineKind::Task);
    let is_void = e.expr_type().is_some_and(|t| t.is_void());
    if !(is_task || is_void) {
        return Err(LoweringError::unsupported_at(
            &format!(
                "the call `{name}` is a sequence match item, but it is a function that returns a \
                 value (LRM 16.11: only a task or a void function may be called from a match \
                 item)"
            ),
            scope,
        ));
    }
    Ok(if system { Callee::System } else { Callee::User })
}