blues-lsp 0.1.0

LSP language server for the Bluespec SystemVerilog language
Documentation
use std::{env, path::PathBuf};

use tracing::{error, warn};

use crate::util::path::maybe_canonicalize;

/// Split command line into individual arguments, respecting quatation and escapes
pub fn tokenize(cli: &str) -> Vec<String> {
    let mut out = Vec::new();

    let mut chars = cli.chars().peekable();

    loop {
        // Skip leading whitespace
        while let Some(' ') | Some('\t') = chars.peek() {
            chars.next();
        }

        if chars.peek().is_none() {
            break;
        }

        let mut arg = String::new();
        let mut in_quotes = false;

        loop {
            match chars.peek() {
                Some('\\') => {
                    chars.next();
                }
                Some('"') => {
                    chars.next();
                    in_quotes = !in_quotes;
                }
                Some(c) if c.is_ascii_whitespace() && !in_quotes => break,
                None => break,
                _ => (),
            }

            if let Some(c) = chars.next() {
                arg.push(c);
            }
        }

        if arg.is_empty() {
            continue;
        }

        out.push(arg);
    }

    out
}

#[derive(Debug)]
pub enum BscBackend {
    Bluesim,
    Verilog,
}

#[derive(Debug, Default)]
pub struct BscArgs {
    pub compiler: Option<PathBuf>,
    pub bluespec_dir: Option<PathBuf>,
    pub include_dirs: Vec<PathBuf>,
    pub bdir: Option<PathBuf>,
    pub defines: Vec<(String, String)>,
    pub cpp: bool,
    pub vpp: bool,
    pub top: Option<String>,
    pub backend: Option<BscBackend>,
    pub file: Option<PathBuf>,
    pub recursive: bool,
    pub use_prelude: bool,
}

impl BscArgs {
    pub fn include_dirs_all(&self) -> impl Iterator<Item = &PathBuf> {
        self.bluespec_dir.iter().chain(self.include_dirs.iter())
    }
}

pub fn parse_args(
    mut args: impl Iterator<Item = String>,
    bluespec_dir: Option<PathBuf>,
) -> Option<BscArgs> {
    let mut out = BscArgs {
        compiler: None,
        bluespec_dir,
        include_dirs: Vec::new(),
        bdir: None,
        defines: Vec::new(),
        cpp: false,
        vpp: true,
        top: None,
        backend: None,
        file: None,
        recursive: false,
        use_prelude: true,
    };

    if let Some(bluespec_dir) = &out.bluespec_dir {
        let bsc_libraries_path = bluespec_dir.join("Libraries/");
        if bsc_libraries_path.is_dir() {
            out.include_dirs.push(bluespec_dir.join("Libraries/"));
        } else {
            warn!(
                "computed Bluespec library dir '{}' does not exist",
                bsc_libraries_path.display()
            );
        }
    }

    loop {
        let Some(arg) = args.next() else {
            break;
        };

        // Skip Haskell Runtime system options
        if arg == "+RTS" {
            loop {
                match args.next().as_deref() {
                    Some("-RTS") | None => break,
                    _ => (),
                }
            }
            continue;
        }

        // Source file argument (no leading '-')
        if !arg.starts_with('-') {
            if out.file.is_some() {
                error!("multiple source file paths");
            }

            out.file = Some(arg.into());
            continue;
        }

        // Compiler flags

        let arg = if let Some(arg) = arg.strip_prefix("--") {
            arg
        } else {
            &arg[1..]
        };

        let (arg, inverted) = if let Some(arg) = arg.strip_prefix("no-") {
            (&arg[3..], true)
        } else {
            (arg, false)
        };

        match arg {
            "bdir" => {
                let Some(arg) = args.next() else {
                    error!("'-bdir' is missing an argument");
                    continue;
                };
                out.bdir = Some(PathBuf::from(arg));
            }
            "cpp" => {
                out.cpp = !inverted;
            }
            "vpp" => {
                out.vpp = !inverted;
            }
            "D" => {
                let Some(arg) = args.next() else {
                    error!("'-D' is missing an argument");
                    continue;
                };

                let (name, value) = match arg.split_once('=') {
                    Some((name, value)) => (name.into(), value.into()),
                    None => (arg, String::new()),
                };

                out.defines.push((name, value));
            }
            "e" => {
                // Linker command, ignore everything
                return None;
            }
            "g" => {
                let Some(arg) = args.next() else {
                    error!("'-g' is missing an argument");
                    continue;
                };
                out.top = Some(arg);
            }
            "i" => {
                let Some(arg) = args.next() else {
                    error!("'-i' is missing an argument");
                    continue;
                };
                out.bluespec_dir = Some(arg.into());
            }
            "p" => {
                let Some(arg) = args.next() else {
                    error!("'-p' is missing an argument");
                    continue;
                };

                let old = std::mem::take(&mut out.include_dirs);

                for path in arg.split(':') {
                    match path {
                        "%" => {
                            if let Some(dir) = &out.bluespec_dir {
                                out.include_dirs.push(dir.clone());
                            }
                        }
                        "+" => {
                            out.include_dirs.extend(old.iter().cloned());
                        }
                        "" => (),
                        path => out.include_dirs.push(path.into()),
                    }
                }
            }
            "sim" | "systemc" => {
                out.backend = Some(BscBackend::Bluesim);
            }
            "vsim" => {
                let Some(_) = args.next() else {
                    error!("'-vsim' is missing an argument");
                    continue;
                };
                out.backend = Some(BscBackend::Verilog);
            }
            "u" => {
                out.recursive = !inverted;
            }
            "use-prelude" => {
                out.use_prelude = !inverted;
            }
            // All single-argument cases we don't care about
            "demote-errors"
            | "expand-ATS-limit"
            | "fdir"
            | "final-cleanup"
            | "I"
            | "info-dir"
            | "L"
            | "l"
            | "max-tcheck-stack-depth"
            | "o"
            | "parallel-sim-link"
            | "promote-warnings"
            | "reset-prefix"
            | "simdir"
            | "steps"
            | "steps-warn-interval"
            | "steps-max-intervals"
            | "suppress-warnings"
            | "unspecified-to"
            | "vdir"
            | "verilog-filter"
            | "vsearch"
            | "Xc"
            | "Xc++"
            | "Xcpp"
            | "Xl"
            | "Xv" => {
                args.next();
            }
            // Two-arg flags we don't care about
            "show-rule-rel" => {
                args.next();
                args.next();
            }
            // Assume the rest have no extra arguments
            _ => (),
        }
    }

    Some(out)
}

pub fn parse_compdb(mut args: impl Iterator<Item = String>, cwd: PathBuf) -> Option<BscArgs> {
    let Some(compiler) = args.next() else {
        error!("entry has no args");
        return None;
    };
    let compiler = PathBuf::from(compiler);

    let compiler = match which::which_in(compiler, env::var_os("PATH"), &cwd) {
        Ok(compiler) => Some(maybe_canonicalize(&compiler)),
        Err(e) => {
            error!("could not resolve compiler path: {e}");
            None
        }
    };

    let bluespec_dir = match &compiler {
        Some(bsc) => {
            if !bsc.ends_with("bin/bsc") {
                warn!("bsc path has unusual structure (does not end with 'bin/bsc')");
                return None;
            }

            let bsc_install_path = bsc.parent()?.parent()?;
            let bsc_lib_path = bsc_install_path.join("lib/");

            if !bsc_lib_path.is_dir() {
                warn!(
                    "computed BLUESPECDIR '{}' does not exist",
                    bsc_lib_path.display()
                );
                return None;
            }

            Some(maybe_canonicalize(&bsc_lib_path))
        }
        None => None,
    };

    let mut args = parse_args(args, bluespec_dir)?;
    args.compiler = compiler;

    for path in &mut args.include_dirs {
        *path = maybe_canonicalize(&cwd.join(&path));
    }

    if let Some(bdir) = &mut args.bdir {
        *bdir = maybe_canonicalize(&cwd.join(&bdir));
    }

    if let Some(file) = &mut args.file {
        *file = maybe_canonicalize(&cwd.join(&file));
    }

    Some(args)
}

#[cfg(test)]
mod test {
    #[test]
    fn cli_tokenize() {
        let cli = r#"foo bar ba\ z "b a a \" a a r""#;
        let args = super::tokenize(cli);
        assert_eq!(args, ["foo", "bar", "ba z", r#"b a a " a a r"#]);
    }
}