vj 0.2.0

vj: a SystemVerilog (IEEE 1800-2023) simulator built on slang and LLVM.
//! Source-level declaration checks that slang does not make.
//!
//! * IEEE 1800-2023 29.3: a combinational or sequential UDP body is
//!   `table entry { entry } endtable`, at least one entry; slang parses an empty table.
//! * IEEE 1800-2023 23.2.2.1: "If a port declaration does not include a net or variable type,
//!   then the port can be again declared in a net or variable declaration. If the net or
//!   variable is declared as a vector, the range specification between the two declarations of
//!   a port shall be identical." slang merges the two declarations and never compares the
//!   ranges, so `output x; reg [7:0] x;` (a scalar port redeclared as a vector) would run with
//!   an 8-bit port. Only the unambiguous case is rejected: a port declared with no range at all
//!   that a later net or variable declaration gives one.

use std::collections::HashSet;

use sv_lang::kinds::SyntaxKind;
use sv_lang::nodes::{
    DPIExportSyntax, DPIImportSyntax, DataDeclarationSyntax, DataTypeSyntax, NetDeclarationSyntax,
    PortDeclarationSyntax, PortHeaderSyntax, UdpDeclarationSyntax,
};
use sv_lang::{AstNode, Node, SyntaxTree};

/// The 1-based line of byte `offset` in `tree`.
fn line_of(tree: &SyntaxTree, offset: usize) -> usize {
    let text = tree.text();
    1 + text.as_bytes()[..offset.min(text.len())]
        .iter()
        .filter(|&&b| b == b'\n')
        .count()
}

/// Rejects a user-defined primitive whose table has no entry (LRM 29.3).
pub(crate) fn check_udp_tables(trees: &[SyntaxTree]) -> Result<(), String> {
    for tree in trees {
        for udp in tree.root().find_all::<UdpDeclarationSyntax>() {
            if udp.body().entries().is_empty() {
                let name = udp.name().map(|t| t.text().to_owned()).unwrap_or_default();
                return Err(format!(
                    "line {}: error: the table of primitive `{name}` has no entry; a UDP \
                     table needs at least one (IEEE 1800-2023 29.3)\n",
                    line_of(tree, udp.syntax().byte_range().start)
                ));
            }
        }
    }
    Ok(())
}

/// True when `ty` is an integer-vector type or an implicit type with a packed range.
fn type_has_range(ty: &DataTypeSyntax<'_>) -> bool {
    match ty {
        DataTypeSyntax::IntegerType(t) => !t.dimensions().is_empty(),
        DataTypeSyntax::ImplicitType(t) => !t.dimensions().is_empty(),
        _ => false,
    }
}

/// Rejects a non-ANSI port with no net or variable type and no range that a sibling net or
/// variable declaration redeclares as a vector (LRM 23.2.2.1).
pub(crate) fn check_nonansi_port_ranges(trees: &[SyntaxTree]) -> Result<(), String> {
    for tree in trees {
        for node in tree.root().descendants() {
            let Some(port) = PortDeclarationSyntax::cast(node) else {
                continue;
            };
            let (keyword, ty) = match port.header() {
                PortHeaderSyntax::NetPortHeader(h) => (h.net_type(), h.data_type()),
                PortHeaderSyntax::VariablePortHeader(h) => (h.var_keyword(), h.data_type()),
                PortHeaderSyntax::InterfacePortHeader(_) => continue,
            };
            let untyped_scalar = keyword.is_none()
                && matches!(ty, DataTypeSyntax::ImplicitType(ref t) if t.dimensions().is_empty());
            if !untyped_scalar {
                continue;
            }
            let names: HashSet<String> = port
                .declarators()
                .iter()
                .filter_map(|d| d.name().map(|t| t.text().to_owned()))
                .collect();
            // Module-level ports only: a task or function's `input n; reg [7:0] n;` is the
            // legacy form every tool accepts and the LRM states no range rule for (13.3).
            let Some(parent) = node.parent().filter(|p| {
                matches!(
                    p.kind(),
                    SyntaxKind::ModuleDeclaration
                        | SyntaxKind::InterfaceDeclaration
                        | SyntaxKind::ProgramDeclaration
                )
            }) else {
                continue;
            };
            for sibling in parent.children() {
                if sibling.kind() == SyntaxKind::PortDeclaration {
                    continue;
                }
                let (vector, decls): (bool, Vec<String>) =
                    if let Some(d) = DataDeclarationSyntax::cast(sibling) {
                        (
                            type_has_range(&d.r#type()),
                            d.declarators()
                                .iter()
                                .filter_map(|x| x.name().map(|t| t.text().to_owned()))
                                .collect(),
                        )
                    } else if let Some(d) = NetDeclarationSyntax::cast(sibling) {
                        (
                            type_has_range(&d.r#type()),
                            d.declarators()
                                .iter()
                                .filter_map(|x| x.name().map(|t| t.text().to_owned()))
                                .collect(),
                        )
                    } else {
                        continue;
                    };
                if let Some(name) = decls.iter().find(|n| names.contains(*n))
                    && vector
                {
                    return Err(format!(
                        "line {}: error: port `{name}` is declared without a range and \
                         redeclared as a vector; the two ranges must be identical \
                         (IEEE 1800-2023 23.2.2.1)\n",
                        line_of(tree, Node::byte_range(&sibling).start)
                    ));
                }
            }
        }
    }
    Ok(())
}

/// IEEE 1800-2023 35.5.4 and 35.4: the `"DPI"` version string is deprecated ("Use of the string
/// "DPI" shall generate a compile-time warning or error" naming both facts below), and every
/// declaration of one `c_identifier` shall use the same version string. slang accepts both.
/// Returns the warnings to print; a mixed-version `c_identifier` is an error.
pub(crate) fn check_dpi_spec_strings(trees: &[SyntaxTree]) -> Result<Vec<String>, String> {
    let mut warnings = Vec::new();
    let mut seen: std::collections::HashMap<String, String> = std::collections::HashMap::new();
    for tree in trees {
        let text = tree.text();
        for node in tree.root().descendants() {
            let (spec, cid) = if let Some(i) = DPIImportSyntax::cast(node) {
                let cid = match i.c_identifier() {
                    Some(t) => t.text().to_owned(),
                    None => text[i.method().name().syntax().byte_range()]
                        .trim()
                        .to_owned(),
                };
                (i.spec_string(), cid)
            } else if let Some(e) = DPIExportSyntax::cast(node) {
                let cid = e
                    .c_identifier()
                    .or_else(|| e.name())
                    .map(|t| t.text().to_owned())
                    .unwrap_or_default();
                (e.spec_string(), cid)
            } else {
                continue;
            };
            let Some(spec) = spec else { continue };
            let version = spec.text().trim_matches('"').to_owned();
            let line = line_of(tree, node.byte_range().start);
            if version == "DPI" {
                warnings.push(format!(
                    "line {line}: warning: \"DPI\" is deprecated and should be replaced with \
                     \"DPI-C\"; use of the \"DPI-C\" string may require changes in the DPI \
                     application's C code (IEEE 1800-2023 35.5.4, H.14)\n"
                ));
            }
            if let Some(prev) = seen.insert(cid.clone(), version.clone())
                && prev != version
            {
                return Err(format!(
                    "line {line}: error: `{cid}` is declared with both \"{prev}\" and \
                     \"{version}\"; all declarations of one c_identifier shall use the same \
                     DPI version string (IEEE 1800-2023 35.4)\n"
                ));
            }
        }
    }
    Ok(warnings)
}