vj 0.2.0

vj: a SystemVerilog (IEEE 1800-2023) simulator built on slang and LLVM.
//! A source file's syntax tree with every token's original location: the
//! file, line and column its text came from (the macro definition or
//! argument for a token a macro expansion produced, the included file for
//! one from an `` `include ``), as slang's source manager reports them.
//!
//! sv-lang's [`sv_lang::Token`] carries no location, so the tree is built
//! from slang's own syntax walk (`sv_lang_sys::slang_syntax_tree_walk`),
//! which reports one: a node's members arrive in its syntax struct's member
//! order, a list as one member and an absent optional member as
//! [`Elem::Absent`], so `members[i]` is the generated accessor's member
//! `i`.

use std::ffi::c_void;
use std::os::raw::c_char;
use std::rc::Rc;

use sv_lang::kinds::{SyntaxKind, TokenKind, TriviaKind};
use sv_lang_sys as sys;

/// A token: its text and the place it came from.
#[derive(Clone, Debug)]
pub struct Tok {
    pub kind: TokenKind,
    pub text: String,
    pub file: Rc<str>,
    pub line: usize,
    pub col: usize,
    /// Synthesized by error recovery (empty text).
    pub missing: bool,
    /// The trivia before it: (kind, text).
    pub trivia: Vec<(TriviaKind, String)>,
}

/// A member of a node.
#[derive(Clone, Debug)]
pub enum Elem {
    Node(Node),
    Tok(Tok),
    List(Vec<Elem>),
    Absent,
}

/// A syntax node: its kind and its members, in member order.
#[derive(Clone, Debug)]
pub struct Node {
    pub kind: SyntaxKind,
    pub members: Vec<Elem>,
}

impl Node {
    /// Member `i` as a node.
    pub fn node(&self, i: usize) -> Option<&Node> {
        match self.members.get(i)? {
            Elem::Node(n) => Some(n),
            _ => None,
        }
    }

    /// Member `i` as a token (`None` if absent or missing).
    pub fn tok(&self, i: usize) -> Option<&Tok> {
        match self.members.get(i)? {
            Elem::Tok(t) if !t.missing => Some(t),
            _ => None,
        }
    }

    /// Member `i` as a list's elements (empty if not a list).
    pub fn list(&self, i: usize) -> &[Elem] {
        match self.members.get(i) {
            Some(Elem::List(v)) => v,
            _ => &[],
        }
    }

    /// The nodes of list member `i` (separators skipped).
    pub fn list_nodes(&self, i: usize) -> Vec<&Node> {
        self.list(i)
            .iter()
            .filter_map(|e| match e {
                Elem::Node(n) => Some(n),
                _ => None,
            })
            .collect()
    }

    /// Every token of the node, in order.
    pub fn tokens(&self) -> Vec<&Tok> {
        let mut out = Vec::new();
        collect_tokens(&self.members, &mut out);
        out
    }
}

fn collect_tokens<'a>(elems: &'a [Elem], out: &mut Vec<&'a Tok>) {
    for e in elems {
        match e {
            Elem::Node(n) => collect_tokens(&n.members, out),
            Elem::Tok(t) => out.push(t),
            Elem::List(v) => collect_tokens(v, out),
            Elem::Absent => {}
        }
    }
}

/// A container being filled by the walk.
enum Open {
    Node(Node),
    List(Vec<Elem>),
}

struct Sink {
    sm: sys::slang_source_manager,
    stack: Vec<Open>,
    root: Option<Node>,
    trivia: Vec<(TriviaKind, String)>,
    files: Vec<Rc<str>>,
}

impl Sink {
    fn push(&mut self, e: Elem) {
        match self.stack.last_mut() {
            Some(Open::Node(n)) => n.members.push(e),
            Some(Open::List(v)) => v.push(e),
            None => {
                if let Elem::Node(n) = e {
                    self.root = Some(n);
                }
            }
        }
    }

    fn file(&mut self, name: String) -> Rc<str> {
        if let Some(f) = self.files.iter().find(|f| ***f == *name) {
            return f.clone();
        }
        let f: Rc<str> = Rc::from(name);
        self.files.push(f.clone());
        f
    }
}

/// # Safety
/// `user` is the `&mut Sink` passed to `slang_syntax_tree_walk`.
unsafe fn sink<'a>(user: *mut c_void) -> &'a mut Sink {
    // SAFETY: see the function's own contract.
    unsafe { &mut *user.cast::<Sink>() }
}

/// # Safety
/// `ptr`/`len` describe bytes valid for the duration of the callback.
unsafe fn text(ptr: *const c_char, len: usize) -> String {
    if ptr.is_null() || len == 0 {
        return String::new();
    }
    // SAFETY: see the function's own contract.
    let bytes = unsafe { std::slice::from_raw_parts(ptr.cast::<u8>(), len) };
    String::from_utf8_lossy(bytes).into_owned()
}

unsafe extern "C" fn on_start_node(user: *mut c_void, kind: u32, _s: u32) {
    // SAFETY: slang passes back the `user` given to the walk.
    let s = unsafe { sink(user) };
    s.stack.push(Open::Node(Node {
        kind: SyntaxKind::from_raw(kind as u16).unwrap_or(SyntaxKind::Unknown),
        members: Vec::new(),
    }));
}

unsafe extern "C" fn on_finish_node(user: *mut c_void) {
    // SAFETY: as above.
    let s = unsafe { sink(user) };
    if let Some(Open::Node(n)) = s.stack.pop() {
        s.push(Elem::Node(n));
    }
}

unsafe extern "C" fn on_start_list(user: *mut c_void, _form: sys::slang_member_form) {
    // SAFETY: as above.
    let s = unsafe { sink(user) };
    s.stack.push(Open::List(Vec::new()));
}

unsafe extern "C" fn on_finish_list(user: *mut c_void) {
    // SAFETY: as above.
    let s = unsafe { sink(user) };
    if let Some(Open::List(v)) = s.stack.pop() {
        s.push(Elem::List(v));
    }
}

unsafe extern "C" fn on_absent(user: *mut c_void) {
    // SAFETY: as above.
    let s = unsafe { sink(user) };
    s.push(Elem::Absent);
}

unsafe extern "C" fn on_trivia(user: *mut c_void, kind: u32, t: *const c_char, len: usize) {
    // SAFETY: as above; `t`/`len` are valid for the callback.
    let (s, txt) = unsafe { (sink(user), text(t, len)) };
    s.trivia.push((
        TriviaKind::from_raw(kind as u8).unwrap_or(TriviaKind::Unknown),
        txt,
    ));
}

unsafe extern "C" fn on_token(
    user: *mut c_void,
    kind: u32,
    t: *const c_char,
    len: usize,
    loc: sys::slang_loc,
    flags: u16,
) {
    // SAFETY: as above; `t`/`len` are valid for the callback.
    let (s, txt) = unsafe { (sink(user), text(t, len)) };
    // A macro expansion's token: where its text was written.
    // SAFETY: `s.sm` is the live source manager the tree was parsed with.
    let at = unsafe {
        if loc.buffer != 0 && sys::slang_source_manager_is_macro_loc(s.sm, loc) {
            sys::slang_source_manager_original_loc(s.sm, loc)
        } else {
            loc
        }
    };
    let (file, line, col) = if at.buffer == 0 {
        (String::new(), 0, 0)
    } else {
        // SAFETY: as above; the file name is borrowed from the manager.
        unsafe {
            let f = sys::slang_source_manager_file_name(s.sm, at);
            (
                text(f.data, f.len),
                sys::slang_source_manager_line(s.sm, at),
                sys::slang_source_manager_column(s.sm, at),
            )
        }
    };
    let file = s.file(file);
    let trivia = std::mem::take(&mut s.trivia);
    s.push(Elem::Tok(Tok {
        kind: TokenKind::from_raw(kind as u16).unwrap_or(TokenKind::Unknown),
        text: txt,
        file,
        line,
        col,
        missing: flags & sys::SLANG_TOKEN_MISSING as u16 != 0,
        trivia,
    }));
}

/// `src` (the file `path`) parsed as slang parses it for elaboration: the
/// compilation unit's tree, or `None` if parsing failed or reported an
/// error (a design with syntax errors is left to elaboration to report).
pub fn parse(src: &str, path: &str, include_dirs: &[String]) -> Option<Node> {
    let mut err = sys::slang_error::INIT;
    // SAFETY: each call gets valid pointers; every handle created here is
    // released before returning.
    unsafe {
        let sm = sys::slang_source_manager_create(&mut err);
        if sm.is_null() || err.status != sys::SLANG_OK {
            return None;
        }
        // `include` resolves as it does for the compilation (`-I`, `+incdir+`).
        for dir in include_dirs {
            sys::slang_source_manager_add_include_dir(
                sm,
                dir.as_ptr().cast(),
                dir.len(),
                false,
                &mut err,
            );
            if err.status != sys::SLANG_OK {
                sys::slang_source_manager_destroy(sm);
                return None;
            }
        }
        let tree = sys::slang_syntax_tree_from_text(
            sm,
            src.as_ptr().cast(),
            src.len(),
            path.as_ptr().cast(),
            path.len(),
            path.as_ptr().cast(),
            path.len(),
            std::ptr::null_mut(),
            &mut err,
        );
        let mut out = None;
        if !tree.is_null() && err.status == sys::SLANG_OK {
            let diags = sys::slang_syntax_tree_diagnostics(tree, &mut err);
            let mut has_error = false;
            if !diags.is_null() {
                for i in 0..sys::slang_diagnostics_count(diags) {
                    let mut d: sys::slang_diag = std::mem::zeroed();
                    if sys::slang_diagnostics_at(diags, i, &mut d)
                        && d.severity >= sys::SLANG_SEVERITY_ERROR
                    {
                        has_error = true;
                    }
                }
                sys::slang_diagnostics_destroy(diags);
            }
            if !has_error {
                let mut s = Sink {
                    sm,
                    stack: Vec::new(),
                    root: None,
                    trivia: Vec::new(),
                    files: Vec::new(),
                };
                let cbs = sys::slang_syntax_sink {
                    start_node: Some(on_start_node),
                    trivia: Some(on_trivia),
                    token: Some(on_token),
                    absent: Some(on_absent),
                    start_list: Some(on_start_list),
                    finish_list: Some(on_finish_list),
                    finish_node: Some(on_finish_node),
                };
                sys::slang_syntax_tree_walk(tree, &cbs, (&mut s as *mut Sink).cast(), &mut err);
                if err.status == sys::SLANG_OK {
                    out = s.root;
                }
            }
        }
        if !tree.is_null() {
            sys::slang_syntax_tree_release(tree);
        }
        sys::slang_source_manager_destroy(sm);
        out
    }
}