vj 0.3.0

vj: a SystemVerilog (IEEE 1800-2023) simulator built on slang and LLVM.
//! FSM recognition pragmas (IEEE 1800-2023 40.4): `/* tool state_vector ... */`
//! and `/* tool enum NAME */` comments (block or one-line comment, 40.4.7),
//! read from the token trivia of each module declaration.
//!
//! vj recognises no FSM by itself (40.4 asks for none): only an FSM the
//! pragmas name is collected. An FSM is its state vector (a signal, a
//! part-select of one, or a concatenation of whole signals, 40.4.1-40.4.3)
//! and its legal states (the parameters carrying the same enumeration name,
//! 40.4.6). The next-state signal (40.4.4/40.4.5) is optional and not needed
//! for state coverage, so it is parsed and ignored.

use std::collections::HashMap;

use sv_lang::kinds::{SyntaxKind, TokenKind, TriviaKind};

use crate::preproc::{self, Node, Tok};

/// One state-vector piece: a signal and an optional `[msb:lsb]` part-select.
pub type Part = (String, Option<(u32, u32)>);

/// An FSM named by pragmas inside one module definition.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FsmDef {
    /// The signal's name, or the pragma's FSM name for a part-select or
    /// concatenation (40.4.2).
    pub name: String,
    /// The pieces, most significant first.
    pub parts: Vec<Part>,
    /// The parameters carrying the FSM's enumeration name, in declaration
    /// order.
    pub states: Vec<String>,
}

/// A parsed `tool ...` pragma comment.
enum Pragma {
    StateVector {
        parts: Vec<Part>,
        name: Option<String>,
        en: Option<String>,
    },
    Enum(String),
}

/// The comment body without its `/* */` or `//` markers.
fn comment_body(text: &str) -> &str {
    if let Some(b) = text.strip_prefix("/*") {
        b.strip_suffix("*/").unwrap_or(b)
    } else {
        text.strip_prefix("//").unwrap_or(text)
    }
}

fn parse_part(s: &str) -> Option<Part> {
    let s = s.trim();
    let Some((n, rest)) = s.split_once('[') else {
        return (!s.is_empty()).then(|| (s.to_string(), None));
    };
    let (a, b) = rest.strip_suffix(']')?.split_once(':')?;
    Some((
        n.trim().to_string(),
        Some((a.trim().parse().ok()?, b.trim().parse().ok()?)),
    ))
}

fn parse_pragma(text: &str) -> Option<Pragma> {
    let body = comment_body(text).trim();
    let rest = body.strip_prefix("tool")?;
    if !rest.starts_with(char::is_whitespace) {
        return None;
    }
    let rest = rest.trim_start();
    if let Some(r) = rest.strip_prefix("enum") {
        let name = r.split_whitespace().next()?;
        return Some(Pragma::Enum(name.to_string()));
    }
    let r = rest.strip_prefix("state_vector")?;
    if !r.starts_with(char::is_whitespace) {
        return None;
    }
    let r = r.trim_start();
    let (parts, tail) = if let Some(c) = r.strip_prefix('{') {
        let (list, tail) = c.split_once('}')?;
        let parts = list
            .split(',')
            .map(parse_part)
            .collect::<Option<Vec<_>>>()?;
        (parts, tail)
    } else {
        // `sig`, `sig[3:0]`, or `sig [3:0]`.
        let end = r.find(char::is_whitespace).unwrap_or(r.len());
        let (mut head, mut tail) = r.split_at(end);
        let t = tail.trim_start();
        let owned;
        if t.starts_with('[') && !head.contains('[') {
            let close = t.find(']')? + 1;
            owned = format!("{head}{}", &t[..close]);
            head = &owned;
            tail = &t[close..];
        }
        (vec![parse_part(head)?], tail)
    };
    let mut words = tail.split_whitespace();
    let mut name = None;
    let mut en = None;
    while let Some(w) = words.next() {
        if w == "enum" {
            en = words.next().map(str::to_string);
        } else if name.is_none() {
            name = Some(w.to_string());
        }
    }
    Some(Pragma::StateVector { parts, name, en })
}

/// Every FSM the pragmas of `src` (the file `path`) describe, by module
/// definition name.
pub fn extract(src: &str, path: &str, include_dirs: &[String]) -> HashMap<String, Vec<FsmDef>> {
    let mut out = HashMap::new();
    if !src.contains("state_vector") {
        return out;
    }
    if let Some(root) = preproc::parse(src, path, include_dirs) {
        walk(&root, &mut out);
    }
    out
}

fn walk(n: &Node, out: &mut HashMap<String, Vec<FsmDef>>) {
    if n.kind == SyntaxKind::ModuleDeclaration {
        let toks = n.tokens();
        let name = toks
            .iter()
            .skip_while(|t| t.kind != TokenKind::ModuleKeyword)
            .find(|t| t.kind == TokenKind::Identifier)
            .map(|t| t.text.clone());
        if let Some(name) = name {
            let defs = module_fsms(&toks);
            if !defs.is_empty() {
                out.entry(name).or_default().extend(defs);
            }
        }
        return;
    }
    for m in &n.members {
        walk_elem(m, out);
    }
}

fn walk_elem(e: &preproc::Elem, out: &mut HashMap<String, Vec<FsmDef>>) {
    match e {
        preproc::Elem::Node(n) => walk(n, out),
        preproc::Elem::List(v) => v.iter().for_each(|e| walk_elem(e, out)),
        _ => {}
    }
}

/// The names a declaration lists starting at token `i` (a variable or
/// parameter declaration: the identifiers at depth 0 after the start or a
/// comma).
fn declared_names(toks: &[&Tok], i: usize) -> Vec<String> {
    let mut names = Vec::new();
    let mut depth = 0i32;
    let mut want = true;
    for t in &toks[i..] {
        match t.kind {
            TokenKind::Semicolon if depth == 0 => break,
            TokenKind::OpenParenthesis | TokenKind::OpenBracket | TokenKind::OpenBrace => {
                depth += 1
            }
            TokenKind::CloseParenthesis | TokenKind::CloseBracket | TokenKind::CloseBrace => {
                depth -= 1;
                if depth < 0 {
                    break;
                }
            }
            TokenKind::Comma if depth == 0 => want = true,
            TokenKind::Identifier if depth == 0 && want => {
                names.push(t.text.clone());
                want = false;
            }
            _ => {}
        }
    }
    names
}

fn module_fsms(toks: &[&Tok]) -> Vec<FsmDef> {
    // (parts, name, enum) of each state_vector pragma; signal -> enum from
    // declaration pragmas; enum -> parameter names.
    let mut vectors: Vec<(Vec<Part>, Option<String>, Option<String>)> = Vec::new();
    let mut sig_enum: HashMap<String, String> = HashMap::new();
    let mut enum_states: HashMap<String, Vec<String>> = HashMap::new();
    let mut in_param = false;
    for (i, t) in toks.iter().enumerate() {
        if matches!(
            t.kind,
            TokenKind::ParameterKeyword | TokenKind::LocalParamKeyword
        ) {
            in_param = true;
        }
        for (k, text) in &t.trivia {
            if !matches!(k, TriviaKind::BlockComment | TriviaKind::LineComment) {
                continue;
            }
            match parse_pragma(text) {
                Some(Pragma::StateVector { parts, name, en }) => vectors.push((parts, name, en)),
                Some(Pragma::Enum(e)) if in_param => {
                    enum_states
                        .entry(e)
                        .or_default()
                        .extend(declared_names(toks, i));
                }
                Some(Pragma::Enum(e)) => {
                    // 40.4.5: the first declared signal is the current state,
                    // the next one the next state; both join the FSM.
                    for n in declared_names(toks, i) {
                        sig_enum.entry(n).or_insert_with(|| e.clone());
                    }
                }
                None => {}
            }
        }
        if t.kind == TokenKind::Semicolon {
            in_param = false;
        }
    }
    vectors
        .into_iter()
        .map(|(parts, name, en)| {
            let en = en.or_else(|| sig_enum.get(&parts[0].0).cloned());
            let name = name.unwrap_or_else(|| parts[0].0.clone());
            let states = en
                .and_then(|e| enum_states.get(&e).cloned())
                .unwrap_or_default();
            FsmDef {
                name,
                parts,
                states,
            }
        })
        .collect()
}

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

    #[test]
    fn pragma_forms() {
        let Some(Pragma::StateVector { parts, name, en }) =
            parse_pragma("/* tool state_vector cs[3:0] F enum E2 */")
        else {
            panic!()
        };
        assert_eq!(parts, vec![("cs".to_string(), Some((3, 0)))]);
        assert_eq!((name.as_deref(), en.as_deref()), (Some("F"), Some("E2")));
        let Some(Pragma::StateVector { parts, .. }) =
            parse_pragma("// tool state_vector {a, b} CAT enum E")
        else {
            panic!()
        };
        assert_eq!(parts.len(), 2);
        assert!(parse_pragma("// tool state_vectors x").is_none());
        assert!(matches!(parse_pragma("// tool enum E"), Some(Pragma::Enum(e)) if e == "E"));
    }
}