vj 0.2.0

vj: a SystemVerilog (IEEE 1800-2023) simulator built on slang and LLVM.
//! The source files of a run: several files elaborated together, with the
//! `-I`/`+incdir+`, `-D`/`+define+` and `--top` options of the command line.
//!
//! A single file with no options is parsed exactly as it always was
//! (`compile_source_named`). Anything else goes through
//! [`vj_codegen::frontend::compile_units`]: every file is its own
//! compilation unit (slang's default, so a macro defined in one file is not
//! visible in the next -- `include` the shared file, or pass `-D`), the
//! include directories live in the session's source manager, and `--top`
//! reaches the compilation options.
//!
//! Each file is rewritten for `randsequence` on its own (`crate::randseq`),
//! after the command line's macros are defined in front of it: a tree parsed
//! from text takes no predefines, so they are `` `define `` lines on a line
//! of their own, followed by a `` `line `` that puts the file's own text back
//! at line 1.

use std::path::{Path, PathBuf};

use vj_codegen::frontend::{UnitOptions, compile_units};

use crate::run::RunError;

/// The sources and the source-level options of one run.
#[derive(Debug, Clone, Default)]
pub struct SourceSet {
    /// The files, in command-line order (a file named twice is read once).
    pub files: Vec<PathBuf>,
    /// `-I` / `+incdir+`.
    pub include_dirs: Vec<String>,
    /// `-D` / `+define+`: `NAME` (defined as `1`) or `NAME=VALUE`.
    pub defines: Vec<String>,
    /// `--top` / `-top`: the modules to elaborate as tops (empty: every
    /// module no other instantiates, LRM 1800-2023 23.3.1).
    pub top_modules: Vec<String>,
    /// `--libmap FILE`: library map files (LRM 1800-2023 33.3.1), in order.
    pub library_maps: Vec<String>,
    /// `-L LIB`: the library search order that replaces the map files'
    /// declaration order (LRM 33.8.1).
    pub liblist: Vec<String>,
}

impl SourceSet {
    /// One file, no options.
    pub fn single(path: &Path) -> Self {
        SourceSet {
            files: vec![path.to_path_buf()],
            ..SourceSet::default()
        }
    }

    /// The file errors are labeled with: the first one named.
    pub(crate) fn label(&self) -> &Path {
        self.files.first().map_or(Path::new("<no source>"), |p| p)
    }

    /// One file and nothing else: the original single-file path.
    fn is_plain(&self) -> bool {
        self.files.len() == 1
            && self.include_dirs.is_empty()
            && self.defines.is_empty()
            && self.top_modules.is_empty()
            && self.library_maps.is_empty()
            && self.liblist.is_empty()
    }
}

/// `-D` text (`NAME` or `NAME=VALUE`) as the `` `define `` line that makes
/// it, or why it is not one.
pub fn define_line(define: &str) -> Result<String, String> {
    let (name, value) = define.split_once('=').unwrap_or((define, "1"));
    let ident = !name.is_empty()
        && name.chars().enumerate().all(|(i, c)| {
            c == '_' || c.is_ascii_alphabetic() || (i > 0 && (c.is_ascii_digit() || c == '$'))
        });
    if !ident {
        return Err(format!(
            "`{define}` does not define a macro: `{name}` is not an identifier"
        ));
    }
    if value.contains(['\n', '\r']) {
        return Err(format!("the value of macro `{name}` contains a line break"));
    }
    Ok(format!("`define {name} {value}\n"))
}

/// `src`, the text of the file `name`, behind the `defines`.
fn with_defines(src: &str, name: &str, defines: &[String]) -> Result<String, String> {
    if defines.is_empty() {
        return Ok(src.to_string());
    }
    let mut text = String::new();
    for d in defines {
        text.push_str(&define_line(d)?);
    }
    let quoted = name.replace('\\', "\\\\").replace('"', "\\\"");
    text.push_str(&format!("`line 1 \"{quoted}\" 0\n"));
    text.push_str(src);
    Ok(text)
}

/// The design of `set`, with the source texts the run-time messages and
/// `disable label;` read registered (`vj_sim::process::cfg`), and the
/// system tasks and functions VPI libraries registered as the design calls
/// them (`crate::vpi::systf::prepare`: each file's text is rewritten for the
/// argument counts it uses, and every name is declared to slang).
pub(crate) fn compile(
    set: &SourceSet,
) -> Result<(sv_lang::Design, Vec<vj_codegen::vpi_systf::SysTfDecl>), RunError> {
    use vj_sim::process::cfg;
    if set.files.is_empty() {
        return Err(RunError::Diagnostics("no source files".to_string()));
    }
    if set.is_plain() {
        let path = &set.files[0];
        let src = std::fs::read_to_string(path).map_err(RunError::Io)?;
        let src =
            crate::randseq::expand(&src, &path.to_string_lossy()).map_err(RunError::Diagnostics)?;
        let (src, systf_decls, systf_codegen) = crate::vpi::prepare_systf(&src);
        // `disable label;` reads its target from the source text.
        cfg::set_source_text(Some(&src));
        cfg::set_source_name(Some(&path.display().to_string()));
        let design = vj_codegen::frontend::compile_source_named_with_systfs(
            &src,
            &path.to_string_lossy(),
            &systf_decls,
        )
        .map_err(RunError::Compile)?;
        return Ok((design, systf_codegen));
    }
    if !set.library_maps.is_empty() || !set.liblist.is_empty() {
        return compile_with_libraries(set);
    }
    let mut units: Vec<(String, String, String)> = Vec::new();
    let mut seen: Vec<PathBuf> = Vec::new();
    let mut decls: Vec<vj_codegen::frontend::SystfDeclaration> = Vec::new();
    // The declared names some file calls: a declaration from a file that
    // only registers the name (no calls: no arity, and no `sizetf` width)
    // gives way to one from a file that calls it.
    let mut called: Vec<String> = Vec::new();
    let mut codegen = Vec::new();
    for path in &set.files {
        let key = std::fs::canonicalize(path).unwrap_or_else(|_| path.clone());
        if seen.contains(&key) {
            continue;
        }
        seen.push(key);
        let name = path.to_string_lossy().into_owned();
        let src = std::fs::read_to_string(path)
            .map_err(|e| RunError::Io(std::io::Error::new(e.kind(), format!("{name}: {e}"))))?;
        let text = with_defines(&src, &name, &set.defines).map_err(RunError::Diagnostics)?;
        let text = crate::randseq::expand_with_includes(&text, &name, &set.include_dirs)
            .map_err(RunError::Diagnostics)?;
        // A name one file calls with several argument counts is renamed per
        // count in that file; the registrations are the same for every file.
        let (text, file_decls, file_codegen) = crate::vpi::prepare_systf(&text);
        for d in file_decls {
            let calls = crate::vpi::systf::is_called(&text, &d.0);
            match decls.iter().position(|e| e.0 == d.0) {
                None => decls.push(d.clone()),
                Some(at) if calls && !called.contains(&d.0) => decls[at] = d.clone(),
                Some(_) => {}
            }
            if calls && !called.contains(&d.0) {
                called.push(d.0);
            }
        }
        codegen = file_codegen;
        units.push((name.clone(), name, text));
    }
    cfg::set_source_texts(
        units
            .iter()
            .map(|(name, _, text)| (name.clone(), text.clone()))
            .collect(),
    );
    cfg::set_source_name(Some(&units[0].0));
    let options = UnitOptions {
        include_dirs: set.include_dirs.clone(),
        top_modules: set.top_modules.clone(),
        ..UnitOptions::default()
    };
    vj_codegen::frontend::set_system_functions(decls);
    let design = compile_units(&units, &options).map_err(RunError::Compile)?;
    Ok((design, codegen))
}

/// The design of `set` under library map files and/or a `-L` library order
/// (LRM 1800-2023 33.3, 33.8.1): slang's source loader maps each file to its
/// library and binds the instances ([`vj_codegen::frontend::
/// compile_library_units`]), so it reads the files itself. The text rewrites
/// of the plain path (`-D` defines, `randsequence`, VPI system tasks) cannot
/// apply to text slang reads; a file one of them would change is refused
/// by name rather than elaborated differently.
fn compile_with_libraries(
    set: &SourceSet,
) -> Result<(sv_lang::Design, Vec<vj_codegen::vpi_systf::SysTfDecl>), RunError> {
    use vj_sim::process::cfg;
    if !set.defines.is_empty() {
        return Err(RunError::Diagnostics(
            "-D/+define+ cannot be combined with --libmap/-L (the library loader reads the files itself)"
                .to_string(),
        ));
    }
    let mut files: Vec<String> = Vec::new();
    let mut texts: Vec<(String, String)> = Vec::new();
    for path in &set.files {
        let name = path.to_string_lossy().into_owned();
        let src = std::fs::read_to_string(path)
            .map_err(|e| RunError::Io(std::io::Error::new(e.kind(), format!("{name}: {e}"))))?;
        let rewritten = crate::randseq::expand_with_includes(&src, &name, &set.include_dirs)
            .map_err(RunError::Diagnostics)?;
        let (rewritten, decls, _) = crate::vpi::prepare_systf(&rewritten);
        if rewritten != src || !decls.is_empty() {
            return Err(RunError::Diagnostics(format!(
                "{name}: randsequence / VPI system-task sources cannot be combined with --libmap/-L"
            )));
        }
        texts.push((name.clone(), src));
        files.push(name);
    }
    cfg::set_source_texts(texts);
    cfg::set_source_name(files.first().map(String::as_str));
    let options = UnitOptions {
        include_dirs: set.include_dirs.clone(),
        top_modules: set.top_modules.clone(),
        library_maps: set.library_maps.clone(),
        liblist: set.liblist.clone(),
    };
    let design =
        vj_codegen::frontend::compile_library_units(&files, &options).map_err(RunError::Compile)?;
    Ok((design, Vec::new()))
}

/// Parses the files of `set` (the command line's macros and include
/// directories applied) and stops before elaboration: `vj run
/// --parse-only`. The preprocessor's and the grammar's diagnostics are the
/// error; a design that parses but would not elaborate, or would end in an
/// error when run, passes.
pub fn parse_only(set: &SourceSet) -> Result<(), RunError> {
    if set.files.is_empty() {
        return Err(RunError::Diagnostics("no source files".to_string()));
    }
    let mut units: Vec<(String, String, String)> = Vec::new();
    let mut seen: Vec<PathBuf> = Vec::new();
    for path in &set.files {
        let key = std::fs::canonicalize(path).unwrap_or_else(|_| path.clone());
        if seen.contains(&key) {
            continue;
        }
        seen.push(key);
        let name = path.to_string_lossy().into_owned();
        let src = std::fs::read_to_string(path)
            .map_err(|e| RunError::Io(std::io::Error::new(e.kind(), format!("{name}: {e}"))))?;
        let text = with_defines(&src, &name, &set.defines).map_err(RunError::Diagnostics)?;
        units.push((name.clone(), name, text));
    }
    // Parsing reads the files only; library maps and the library order bind
    // instances at elaboration (LRM 33.3, 33.8.1), so they do not apply here.
    let options = UnitOptions {
        include_dirs: set.include_dirs.clone(),
        top_modules: set.top_modules.clone(),
        ..UnitOptions::default()
    };
    vj_codegen::frontend::parse_units(&units, &options).map_err(RunError::Compile)
}