vj-codegen 0.2.0

LLVM code generator of the vj SystemVerilog simulator.
//! `let` define-before-use (IEEE 1800-2023 11.12).
//!
//! "In the scope of declaration, a let body shall be defined before it is
//! used." slang substitutes a `let` at its use site and accepts a
//! call-form forward use (`early(a)` before `let early(x) = ...;`); only the
//! no-argument form, which is an ordinary identifier lookup, is rejected
//! ("use of undeclared identifier"). This pass restores the rule for the call
//! form, on the syntax trees: every invocation of a bare identifier is
//! resolved the way a lexical lookup would be -- innermost enclosing scope
//! first -- against the `let` declarations of that name directly in each
//! scope, and one that is declared later in source order than the use is an
//! error.
//!
//! Source order is the pre-order position in the tree (not a buffer offset:
//! an `include`d file lives in another buffer, whose offsets are
//! incomparable with the includer's).

use sv_lang::kinds::SyntaxKind;
use sv_lang::nodes::{InvocationExpressionSyntax, LetDeclarationSyntax};
use sv_lang::{AstNode, Node, SyntaxTree};

/// One `let` declaration: its name, the scope node it is declared directly in
/// and its pre-order position.
struct LetDecl<'t> {
    name: String,
    scope: Node<'t>,
    at: usize,
}

/// `Ok(())` if no call of a `let` precedes that `let`'s declaration in its
/// scope; otherwise the error text naming the `let` and the use's line.
pub fn check_let_definition_order(trees: &[SyntaxTree]) -> Result<(), String> {
    for tree in trees {
        check_tree(tree)?;
    }
    Ok(())
}

fn check_tree(tree: &SyntaxTree) -> Result<(), String> {
    let mut lets: Vec<LetDecl<'_>> = Vec::new();
    // `(callee name, the invocation, pre-order position)`.
    let mut calls: Vec<(String, Node<'_>, usize)> = Vec::new();
    for (at, node) in tree.root().descendants().enumerate() {
        // Only these two kinds matter; every other node is irrelevant to the
        // rule (not an unhandled case), hence `if` rather than a `match`.
        let kind = node.kind();
        if kind == SyntaxKind::LetDeclaration {
            let Some(decl) = <LetDeclarationSyntax<'_> as AstNode>::cast(node) else {
                continue;
            };
            let (Some(id), Some(scope)) = (decl.identifier(), node.parent()) else {
                continue;
            };
            lets.push(LetDecl {
                name: id.text().to_string(),
                scope,
                at,
            });
        } else if kind == SyntaxKind::InvocationExpression {
            let Some(inv) = <InvocationExpressionSyntax<'_> as AstNode>::cast(node) else {
                continue;
            };
            let callee = inv.left().syntax();
            if callee.kind() != SyntaxKind::IdentifierName {
                continue;
            }
            let Some(tok) = callee.first_token() else {
                continue;
            };
            calls.push((tok.text().to_string(), node, at));
        }
    }
    if lets.is_empty() {
        return Ok(());
    }
    for (name, call, at) in &calls {
        let mut scope = call.parent();
        while let Some(s) = scope {
            let mut here = lets.iter().filter(|l| l.name == *name && l.scope == s);
            if let Some(first) = here.next() {
                // Declared in this scope: the lookup stops here, whichever
                // way it goes.
                let earliest = here.fold(first.at, |m, l| m.min(l.at));
                if earliest > *at {
                    return Err(format!(
                        "let `{name}` is used{} before its declaration: a let body shall be \
                         defined before it is used (IEEE 1800-2023 11.12)",
                        line_of(tree, call, name)
                    ));
                }
                break;
            }
            scope = s.parent();
        }
    }
    Ok(())
}

/// ` at line N` for the invocation `call` of `name`, when its buffer offset
/// is in the tree's own text; empty otherwise (an included buffer).
fn line_of(tree: &SyntaxTree, call: &Node<'_>, name: &str) -> String {
    let text = tree.text();
    let start = call.byte_range().start;
    match text.get(start..) {
        Some(rest) if rest.trim_start().starts_with(name) => {
            let line = text[..start].bytes().filter(|&b| b == b'\n').count() + 1;
            format!(" at line {line}")
        }
        _ => String::new(),
    }
}

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

    fn check(src: &str) -> Result<(), String> {
        let session = sv_lang::Session::new();
        let tree = session.parse(src).expect("parse");
        check_let_definition_order(&[tree])
    }

    #[test]
    fn call_before_declaration_is_rejected() {
        let err = check(
            "module m;\n  logic a;\n  initial if (early(a)) $display(1);\n  let early(x) = !x;\nendmodule\n",
        )
        .unwrap_err();
        assert!(
            err.contains("let `early` is used at line 3 before its declaration"),
            "{err}"
        );
    }

    #[test]
    fn call_after_declaration_is_accepted() {
        check("module m;\n  logic a;\n  let early(x) = !x;\n  initial if (early(a)) $display(1);\nendmodule\n")
            .unwrap();
    }

    #[test]
    fn an_inner_let_shadows_a_later_outer_one() {
        // The use finds the inner declaration (earlier than the use); the
        // outer let of the same name is declared after, but never reached.
        check(
            "module m;\n  logic a;\n  initial begin\n    let f(x) = x;\n    if (f(a)) $display(1);\n  end\n  let f(x) = !x;\nendmodule\n",
        )
        .unwrap();
    }

    #[test]
    fn a_call_of_a_name_that_is_not_a_let_is_ignored() {
        check(
            "module m;\n  logic a;\n  initial if (f(a)) $display(1);\n  function logic f(logic x); return x; endfunction\n  let g(x) = x;\nendmodule\n",
        )
        .unwrap();
    }

    #[test]
    fn a_later_let_in_a_sibling_scope_is_not_a_forward_use() {
        check(
            "module m;\n  logic a;\n  initial begin : b1\n    if (f(a)) $display(1);\n  end\n  initial begin : b2\n    let f(x) = x;\n  end\nendmodule\n",
        )
        .unwrap();
    }
}