use log::info; use serde::{Deserialize, Serialize}; use regex::Regex; use ropey::Rope; use std::path::PathBuf; use std::process::{Command, Stdio}; use tower_lsp::lsp_types::*; use crate::server::LspServer; use crate::utils::command::is_command_valid; use super::{AbstractLinterConfiguration, LinterStatus}; #[derive(Debug, Serialize, Deserialize)] pub struct VerilatorConfiguration { pub language_id: String, pub linter: VerilatorLinter, } #[derive(Debug, Serialize, Deserialize)] pub struct VerilatorLinter { pub name: String, pub exe_name: String, /// 目前是否启动 pub enabled: bool, pub path: String, pub args: Vec, } impl VerilatorLinter { fn new(invoker: &str, args: Vec) -> Self { Self { name: "verilator".to_string(), exe_name: invoker.to_string(), enabled: false, path: invoker.to_string(), args, } } } /// convert captured severity string to DiagnosticSeverity fn verilator_severity(severity: &str) -> Option { match severity { "Error" => Some(DiagnosticSeverity::ERROR), s if s.starts_with("Warning") => Some(DiagnosticSeverity::WARNING), // NOTE: afaik, verilator doesn't have an info or hint severity _ => Some(DiagnosticSeverity::INFORMATION), } } /// syntax checking using verilator --lint-only fn verilator_syntax( rope: &Rope, file_path: PathBuf, verilator_syntax_path: &str, verilator_syntax_args: &[String], ) -> Option> { let mut child = Command::new(verilator_syntax_path) .stdin(Stdio::piped()) .stderr(Stdio::piped()) .stdout(Stdio::piped()) .args(verilator_syntax_args) .arg(file_path.to_str()?) .spawn() .ok()?; static RE: std::sync::OnceLock = std::sync::OnceLock::new(); let re = RE.get_or_init(|| { Regex::new( r"%(?PError|Warning)(-(?P[A-Z0-9_]+))?: (?P[^:]+):(?P\d+):((?P\d+):)? ?(?P.*)", ) .unwrap() }); // write file to stdin, read output from stdout rope.write_to(child.stdin.as_mut()?).ok()?; let output = child.wait_with_output().ok()?; if !output.status.success() { let mut diags: Vec = Vec::new(); let raw_output = String::from_utf8(output.stderr).ok()?; let filtered_output = raw_output .lines() .filter(|line| line.starts_with('%')) .collect::>(); for error in filtered_output { let caps = match re.captures(error) { Some(caps) => caps, None => continue, }; // check if diagnostic is for this file, since verilator can provide diagnostics for // included files if caps.name("filepath")?.as_str() != file_path.to_str().unwrap_or("") { continue; } let severity = verilator_severity(caps.name("severity")?.as_str()); let line: u32 = caps.name("line")?.as_str().to_string().parse().ok()?; let col: u32 = caps.name("col").map_or("1", |m| m.as_str()).parse().ok()?; let pos = Position::new(line - 1, col - 1); let msg = match severity { Some(DiagnosticSeverity::ERROR) => caps.name("message")?.as_str().to_string(), Some(DiagnosticSeverity::WARNING) => format!( "{}: {}", caps.name("warning_type")?.as_str(), caps.name("message")?.as_str() ), _ => "".to_string(), }; diags.push(Diagnostic::new( Range::new(pos, pos), severity, None, Some("verilator".to_string()), msg, None, None, )); } Some(diags) } else { None } } impl AbstractLinterConfiguration for VerilatorConfiguration { fn new(language_id: &str, invoker: &str, args: Vec) -> Self { VerilatorConfiguration { language_id: language_id.to_string(), linter: VerilatorLinter::new(invoker, args) } } fn provide_diagnostics( &self, uri: &Url, #[allow(unused)] rope: &Rope, server: &LspServer ) -> Option> { let mut diagnostics = Vec::::new(); let invoke_name = self.get_invoke_name(); let pathbuf = uri.to_file_path().unwrap(); let path_string = pathbuf.to_str().unwrap(); let child = Command::new(&invoke_name) .stdin(Stdio::piped()) .stderr(Stdio::piped()) .stdout(Stdio::piped()) .args(&self.linter.args) .arg(path_string) .spawn() .ok()?; let output = child.wait_with_output().ok()?; let output_string = String::from_utf8(output.stderr).ok()?; info!("verilator linter: {:?}, output:\n{}", path_string, output_string); static REGEX_STORE: std::sync::OnceLock = std::sync::OnceLock::new(); let regex = REGEX_STORE.get_or_init(|| { Regex::new(r"%(?PError|Warning)(-(?P[A-Z0-9_]+))?: (?P[^:]+):(?P\d+):((?P\d+):)? ?(?P.*)").unwrap() }); // verilator 错误输出样例 // %Warning-IMPLICIT: library/Apply/Comm/FDE/AGC/AGC.v:23:12: Signal definition not found, creating implicitly: 'r_rms' // : ... Suggested alternative: 'ref_rms' // 23 | assign r_rms = (reference * reference); // | ^~~~~ // ... Use "/* verilator lint_off IMPLICIT */" and lint_on around source to disable this message. // %Error: Exiting due to 5 warning(s) // 先将上面的输出处理为一整块一整块的数组,每块分解为两个部分:第一行(用于解析行号和列号)和后面几行(具体错误地址) let mut error_tuples = Vec::<(String, String)>::new(); let mut current_error_tuple: Option<(&str, Vec<&str>)> = None; for error_line in output_string.lines() { if error_line.starts_with("%") { if let Some((first, ref description)) = current_error_tuple { error_tuples.push((first.to_string(), description.join("\n"))); } current_error_tuple = Some((error_line, Vec::<&str>::new())); } else { if let Some((_, ref mut description)) = current_error_tuple { description.push(error_line); } else { continue; } } } for error_tuple in error_tuples { let captures = match regex.captures(error_tuple.0.as_str()) { Some(caps) => caps, None => continue }; // 因为 verilator 会递归检查,因此报错信息中可能会出现非本文件内的信息 if captures.name("filepath")?.as_str().replace("\\", "/") != path_string { continue; } let line: u32 = captures.name("line")?.as_str().to_string().parse().ok()?; let col: u32 = captures.name("col").map_or("1", |m| m.as_str()).parse().ok()?; // verilator 的诊断索引都是 one index 的 let pos = Position::new(line - 1, col - 1); let severity = verilator_severity(captures.name("severity")?.as_str()); let message = match severity { Some(DiagnosticSeverity::ERROR) => captures.name("message")?.as_str().to_string(), Some(DiagnosticSeverity::WARNING) => format!( "{}: {}", captures.name("warning_type")?.as_str(), captures.name("message")?.as_str() ), _ => "".to_string(), }; let message = format!("{}\n\n---\n\n{}", message, error_tuple.1); let diagnostic = Diagnostic { range: Range::new(pos, pos), code: None, severity, source: Some("Digital IDE: verilator".to_string()), message, related_information: None, tags: None, code_description: None, data: None }; diagnostics.push(diagnostic); } Some(diagnostics) } fn get_linter_path(&self) -> &str { &self.linter.path } fn get_exe_name(&self) -> String { if std::env::consts::OS == "windows" { format!("{}.exe", self.linter.exe_name) } else { self.linter.exe_name.to_string() } } }