vj 0.2.0

vj: a SystemVerilog (IEEE 1800-2023) simulator built on slang and LLVM.
//! The command line of `vj run`:
//!
//! ```text
//! vj run FILE... [-I DIR | +incdir+DIR...] [-D NAME[=VAL] | +define+NAME[=VAL]...]
//!            [--top MODULE]... [-f LISTFILE | -F LISTFILE]... [+PLUSARG]...
//!            [dpi.c | lib.so | --sv-lib LIB]... [--vpi LIB | +vpi=LIB[,LIB]]...
//!            [-GNAME=VALUE]... [compat options] [timing options]
//! ```
//!
//! Any argument may also come from a list file (`-f`: paths relative to the
//! working directory; `-F`: paths relative to the list file's directory).
//! A list file holds the same arguments, separated by white space, with
//! `//` and `#` comments to the end of the line, `/* */` comments, and
//! `"double quoted"` arguments; list files nest.

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

use crate::dpi;
use crate::sources::SourceSet;
use crate::timing_options::TimingOptions;

/// A list file that includes itself (directly or not) is cut off here.
const MAX_LIST_DEPTH: usize = 32;

/// Everything `vj run` takes from its arguments.
#[derive(Debug, Default)]
pub struct RunArgs {
    pub sources: SourceSet,
    /// Raw `+`-prefixed argv tokens that are not `+incdir+` / `+define+`.
    pub plusargs: Vec<String>,
    /// DPI shared libraries (`--sv-lib`, `.so` paths).
    pub libs: Vec<PathBuf>,
    /// C/C++ sources to compile into a DPI library.
    pub c_sources: Vec<PathBuf>,
    pub timing: TimingOptions,
    /// VPI libraries (`--vpi LIB`, `+vpi=LIB[,LIB]`), in command-line order,
    /// as given (a C/C++ source is compiled by the caller).
    pub vpi_libs: Vec<String>,
    /// Extra slang driver options: `-G NAME=VALUE` overrides and the
    /// [`COMPAT_FLAGS`] / `--compat` relaxations
    /// (`vj_codegen::frontend::set_driver_options`).
    pub driver_options: Vec<String>,
    /// `--x-initial zero` (`crate::run::set_x_initial_zero`).
    pub x_initial_zero: bool,
    /// `--coverage`: collect code coverage (LRM 40; toggle so far) for the
    /// `$coverage_*` system functions (`vj_runtime::codecov`).
    pub coverage: bool,
    /// `--seed N` (`crate::run::set_seed`): the base state of every
    /// `$random`/`$urandom` generator of the run; `None` keeps the default.
    pub seed: Option<u64>,
}

/// slang's non-LRM compatibility relaxations `vj run` accepts (each is
/// an error under IEEE 1800 that other simulators tolerate):
/// `--allow-use-before-declare` resolves a name used before its declaration
/// to that later declaration; `--relax-enum-conversions` allows an implicit
/// integral-to-enum conversion (LRM 6.19.3 requires a cast);
/// `--allow-hierarchical-const` allows a hierarchical reference in a
/// constant expression. `--compat vcs|all` enables slang's bundles of these.
pub const COMPAT_FLAGS: &[&str] = &[
    "--allow-use-before-declare",
    "--relax-enum-conversions",
    "--allow-hierarchical-const",
];

/// One argument, and the directory a relative path in it is relative to
/// (`Some` for an argument of a `-F` list file).
struct Arg {
    text: String,
    base: Option<PathBuf>,
}

impl Arg {
    /// `p` as a path of this argument's origin.
    fn path(&self, p: &str) -> PathBuf {
        match &self.base {
            Some(b) if Path::new(p).is_relative() => b.join(p),
            _ => PathBuf::from(p),
        }
    }

    fn path_string(&self, p: &str) -> String {
        self.path(p).to_string_lossy().into_owned()
    }
}

/// The arguments of `args` with every `-f`/`-F` replaced by its list file's
/// arguments.
fn flatten(args: &[Arg], depth: usize, out: &mut Vec<Arg>) -> Result<(), String> {
    let mut it = args.iter();
    while let Some(a) = it.next() {
        let relative = match a.text.as_str() {
            "-f" => false,
            "-F" => true,
            _ => {
                out.push(Arg {
                    text: a.text.clone(),
                    base: a.base.clone(),
                });
                continue;
            }
        };
        let Some(file) = it.next() else {
            return Err(format!("{} needs a list file argument", a.text));
        };
        if depth >= MAX_LIST_DEPTH {
            return Err(format!(
                "list files nest more than {MAX_LIST_DEPTH} deep (`{}` includes itself?)",
                file.text
            ));
        }
        let path = file.path(&file.text);
        let text = std::fs::read_to_string(&path)
            .map_err(|e| format!("cannot read the list file {}: {e}", path.display()))?;
        let base = relative.then(|| path.parent().unwrap_or(Path::new("")).to_path_buf());
        let inner: Vec<Arg> = tokenize(&text)
            .map_err(|e| format!("{}: {e}", path.display()))?
            .into_iter()
            .map(|text| Arg {
                text,
                base: base.clone(),
            })
            .collect();
        flatten(&inner, depth + 1, out)?;
    }
    Ok(())
}

/// A list file's text as its arguments.
fn tokenize(text: &str) -> Result<Vec<String>, String> {
    let mut out = Vec::new();
    let mut cur = String::new();
    let mut quoted = false;
    let mut started = false;
    let mut chars = text.chars().peekable();
    while let Some(c) = chars.next() {
        if quoted {
            if c == '"' {
                quoted = false;
            } else {
                cur.push(c);
            }
            continue;
        }
        match c {
            '"' => {
                quoted = true;
                started = true;
            }
            '#' => {
                for c in chars.by_ref() {
                    if c == '\n' {
                        break;
                    }
                }
                if started {
                    out.push(std::mem::take(&mut cur));
                    started = false;
                }
            }
            '/' if chars.peek() == Some(&'/') => {
                for c in chars.by_ref() {
                    if c == '\n' {
                        break;
                    }
                }
                if started {
                    out.push(std::mem::take(&mut cur));
                    started = false;
                }
            }
            '/' if chars.peek() == Some(&'*') => {
                chars.next();
                let mut prev = ' ';
                let mut closed = false;
                for c in chars.by_ref() {
                    if prev == '*' && c == '/' {
                        closed = true;
                        break;
                    }
                    prev = c;
                }
                if !closed {
                    return Err("unterminated /* comment".to_string());
                }
                if started {
                    out.push(std::mem::take(&mut cur));
                    started = false;
                }
            }
            c if c.is_whitespace() => {
                if started {
                    out.push(std::mem::take(&mut cur));
                    started = false;
                }
            }
            c => {
                cur.push(c);
                started = true;
            }
        }
    }
    if quoted {
        return Err("unterminated \" quote".to_string());
    }
    if started {
        out.push(cur);
    }
    Ok(out)
}

/// `value` of an option that takes one, either attached (`-Ifoo`) or as the
/// next argument (`-I foo`).
fn option_value<'a>(
    a: &'a str,
    short: &str,
    it: &mut std::slice::Iter<'a, Arg>,
) -> Result<Option<&'a str>, String> {
    if a == short {
        return match it.next() {
            Some(v) => Ok(Some(v.text.as_str())),
            None => Err(format!("{short} needs an argument")),
        };
    }
    Ok(a.strip_prefix(short).filter(|v| !v.is_empty()))
}

/// Everything `vj build-aot` takes: the arguments of `run` (the design and
/// how it is read) plus where the library goes.
#[derive(Debug)]
pub struct BuildAotArgs {
    pub run: RunArgs,
    /// `-o OUT.so` (or, with `exe`, `OUT`, a standalone executable).
    pub out: PathBuf,
    /// `--runtime-lib LIB`: the runner archive (`libvj_aot.a`); `None`
    /// looks for it (`crate::aot::find_runtime_lib`).
    pub runtime_lib: Option<PathBuf>,
    /// `--exe`: link `out` as a standalone executable (`crate::aot::
    /// build_aot_exe`) instead of a shared library -- no `vj run-aot`,
    /// no host process, needed to run it.
    pub exe: bool,
}

/// The `build-aot` arguments `args` (everything after `build-aot`):
/// `-o OUT.so`, `--runtime-lib LIB` and `--exe`, the rest as for `run`.
pub fn parse_build_aot_args(args: &[String]) -> Result<BuildAotArgs, String> {
    let mut out = None;
    let mut runtime_lib = None;
    let mut exe = false;
    let mut rest = Vec::new();
    let mut it = args.iter();
    while let Some(a) = it.next() {
        match a.as_str() {
            "-o" | "--out" => match it.next() {
                Some(v) => out = Some(PathBuf::from(v)),
                None => return Err(format!("{a} needs an output path")),
            },
            "--runtime-lib" => match it.next() {
                Some(v) => runtime_lib = Some(PathBuf::from(v)),
                None => return Err("--runtime-lib needs a library path".to_string()),
            },
            "--exe" => exe = true,
            _ => rest.push(a.clone()),
        }
    }
    let out = out.ok_or("build-aot needs `-o OUT.so`")?;
    Ok(BuildAotArgs {
        run: parse_run_args(&rest)?,
        out,
        runtime_lib,
        exe,
    })
}

/// The `run` arguments `args` (everything after `run`).
pub fn parse_run_args(args: &[String]) -> Result<RunArgs, String> {
    let given: Vec<Arg> = args
        .iter()
        .map(|a| Arg {
            text: a.clone(),
            base: None,
        })
        .collect();
    let mut flat = Vec::new();
    flatten(&given, 0, &mut flat)?;
    let mut run = RunArgs::default();
    let mut it = flat.iter();
    while let Some(arg) = it.next() {
        let a = arg.text.as_str();
        if let Some(dirs) = a.strip_prefix("+incdir+") {
            for d in dirs.split('+').filter(|d| !d.is_empty()) {
                run.sources.include_dirs.push(arg.path_string(d));
            }
        } else if let Some(defs) = a.strip_prefix("+define+") {
            for d in defs.split('+').filter(|d| !d.is_empty()) {
                crate::sources::define_line(d)?;
                run.sources.defines.push(d.to_string());
            }
        } else if let Some(list) = a.strip_prefix("+vpi=") {
            // Still a plusarg: the design may test for it.
            run.plusargs.push(a.to_string());
            for lib in list.split(',').filter(|l| !l.is_empty()) {
                run.vpi_libs.push(arg.path_string(lib));
            }
        } else if a.starts_with('+') {
            run.plusargs.push(a.to_string());
        } else if a == "--vpi" || a == "-vpi" {
            match it.next() {
                Some(lib) => run.vpi_libs.push(lib.path_string(&lib.text)),
                None => return Err(format!("{a} needs a library argument")),
            }
        } else if let Some(over) = a.strip_prefix("-G").filter(|o| o.contains('=')) {
            // `-GNAME=VALUE`: override a top-level parameter.
            run.driver_options
                .extend(["-G".to_string(), over.to_string()]);
        } else if a == "-G" {
            match it.next() {
                Some(over) if over.text.contains('=') => run
                    .driver_options
                    .extend(["-G".to_string(), over.text.clone()]),
                _ => return Err("-G needs NAME=VALUE".to_string()),
            }
        } else if COMPAT_FLAGS.contains(&a) {
            run.driver_options.push(a.to_string());
        } else if let Some(ts) =
            option_value(a, "--timescale", &mut it)?.map(|v| v.trim_start_matches('='))
        {
            // The time unit/precision of design elements that declare none
            // (LRM 3.14.2.3 leaves that default to the tool), e.g. `1ns/1ps`.
            run.driver_options
                .extend(["--timescale".to_string(), ts.to_string()]);
        } else if a == "--compat" {
            match it.next().map(|m| m.text.as_str()) {
                Some(mode @ ("vcs" | "all")) => {
                    run.driver_options.extend([a.to_string(), mode.to_string()])
                }
                _ => return Err("--compat needs `vcs` or `all`".to_string()),
            }
        } else if a == "--coverage" {
            run.coverage = true;
        } else if a == "--x-initial" {
            match it.next().map(|m| m.text.as_str()) {
                Some("zero" | "0") => run.x_initial_zero = true,
                Some("x") => run.x_initial_zero = false,
                _ => return Err("--x-initial needs `x` (the LRM default) or `zero`".to_string()),
            }
        } else if a == "--seed" || a.starts_with("--seed=") {
            let text = match a.strip_prefix("--seed=") {
                Some(v) => Some(v),
                None => it.next().map(|m| m.text.as_str()),
            };
            match text.and_then(|t| t.parse::<u64>().ok()) {
                Some(n) => run.seed = Some(n),
                None => return Err("--seed needs a non-negative integer".to_string()),
            }
        } else if a == "--sv-lib" || a == "-sv_lib" {
            match it.next() {
                Some(lib) => run.libs.push(dpi::sv_lib_path(&lib.text)),
                None => return Err(format!("{a} needs a library argument")),
            }
        } else if a == "--top" || a == "-top" {
            match it.next() {
                Some(t) => run.sources.top_modules.push(t.text.clone()),
                None => return Err(format!("{a} needs a module name")),
            }
        } else if let Some(t) = a.strip_prefix("--top=") {
            run.sources.top_modules.push(t.to_string());
        } else if a == "--libmap" || a == "-libmap" {
            // A library map file (LRM 1800-2023 33.3.1); read in order.
            match it.next() {
                Some(m) => run.sources.library_maps.push(arg.path_string(&m.text)),
                None => return Err(format!("{a} needs a library map file")),
            }
        } else if let Some(m) = a.strip_prefix("--libmap=") {
            run.sources.library_maps.push(arg.path_string(m));
        } else if let Some(lib) = option_value(a, "-L", &mut it)? {
            // The library search order (LRM 33.8.1).
            run.sources.liblist.push(lib.to_string());
        } else if let Some(dir) = option_value(a, "-I", &mut it)? {
            run.sources.include_dirs.push(arg.path_string(dir));
        } else if let Some(def) = option_value(a, "-D", &mut it)? {
            crate::sources::define_line(def)?;
            run.sources.defines.push(def.to_string());
        } else if run.timing.parse_arg(a)? {
            // The timing options (`vj::timing_options`): LRM 31.8 per-bit
            // checks, 31.9.4 negative checks, 30.7.2 / 32.7 pulse limits, 30.7.4 pulse
            // style and showcancelled, Table 32-5's `TOOL_CONTROL`.
        } else if a.starts_with('-') {
            return Err(format!("unrecognized argument `{a}`"));
        } else if dpi::is_c_source(Path::new(a)) {
            run.c_sources.push(arg.path(a));
        } else if a.ends_with(".so") {
            run.libs.push(arg.path(a));
        } else {
            run.sources.files.push(arg.path(a));
        }
    }
    if run.sources.files.is_empty() {
        return Err("no source file given".to_string());
    }
    Ok(run)
}