Skip to main content

voltaic_driver/
check.rs

1use crate::remap;
2
3use crate::remap::RustcMessage;
4
5use voltaic_diag::Diagnostic;
6
7use voltaic_diag::DiagnosticCode;
8
9use voltaic_parser::ParseFailure;
10
11use std::path::Path;
12
13use std::path::PathBuf;
14
15use std::process::Command;
16
17pub fn check(path: &Path, source: &str) -> Result<CheckOutcome, String> {
18  let vlt_path = path.display().to_string();
19
20  let mut program = match voltaic_parser::parse_source(source) {
21    Ok(parsed) => parsed,
22    Err(failure) => {
23      return Ok(CheckOutcome {
24        diagnostics: diagnose(path, source, &failure),
25        failed: true,
26      });
27    }
28  };
29
30  let analysis = crate::annotate::analyse_at(&mut program, path);
31  if !analysis.open.is_empty() {
32    return Ok(CheckOutcome {
33      diagnostics: crate::annotate::refusals(path, source, analysis.open.as_slice()),
34      failed: true,
35    });
36  }
37
38  let mut diagnostics = crate::lint(path, source, Some(&program));
39
40  let scope = voltaic_emit::MethodScope::enter(voltaic_emit::MethodTable::from_spans(
41    analysis.methods.as_slice(),
42  ));
43  let emitted = voltaic_emit::emit_with_map(&program, source, &vlt_path);
44  drop(scope);
45
46  let rustc = rustc_path().ok_or_else(|| "error: no rustc found. `voltaic check` compiles the generated Rust, so it needs the Rust toolchain on PATH.".to_string())?;
47
48  let dir = ScratchDir::new("check")?;
49  let vrs = dir.path().join("check.vrs");
50  let vrs_path = vrs.display().to_string();
51  std::fs::write(&vrs, &emitted.0)
52    .map_err(|e| format!("error: cannot write {}: {}", vrs_path, e))?;
53
54  let rustc_shown = rustc.display().to_string();
55  let invoked = Command::new(&rustc)
56    .args([
57      "--edition",
58      "2024",
59      "--error-format=short",
60      "--emit=metadata",
61    ])
62    .arg("-o")
63    .arg(dir.path().join("check.rmeta"))
64    .arg(&vrs)
65    .output();
66  let out = invoked.map_err(|e| format!("error: cannot run {}: {}", rustc_shown, e))?;
67
68  let stderr = String::from_utf8_lossy(&out.stderr);
69  let messages: Vec<RustcMessage> = stderr.lines().filter_map(RustcMessage::parse).collect();
70  let failed = messages.iter().any(RustcMessage::is_error);
71  let remapped = messages
72    .iter()
73    .map(|m| remap(m, &emitted.1, &vlt_path, source, &vrs_path, &emitted.0));
74  diagnostics.extend(remapped);
75
76  if messages.is_empty() && !out.status.success() {
77    let trimmed = stderr.trim_end();
78    return Err(format!("error: rustc failed:\n{}", trimmed));
79  }
80
81  Ok(CheckOutcome {
82    diagnostics,
83    failed,
84  })
85}
86
87#[derive(Debug, Clone, Default)]
88pub struct CheckOutcome {
89  pub diagnostics: Vec<Diagnostic>,
90  pub failed: bool,
91}
92
93pub(crate) struct ScratchDir(PathBuf);
94
95impl ScratchDir {
96  pub(crate) fn new(tag: &str) -> Result<Self, String> {
97    let n = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
98    let pid = std::process::id();
99    let dir = std::env::temp_dir().join(format!("voltaic-{}-{}-{}", tag, pid, n));
100    let shown = dir.display().to_string();
101    std::fs::create_dir_all(&dir).map_err(|e| format!("error: cannot create {}: {}", shown, e))?;
102    Ok(ScratchDir(dir))
103  }
104
105  pub(crate) fn path(&self) -> &Path {
106    &self.0
107  }
108}
109
110impl Drop for ScratchDir {
111  fn drop(&mut self) {
112    let _dropped = std::fs::remove_dir_all(&self.0).ok();
113    ();
114  }
115}
116
117pub(crate) static NEXT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
118
119pub fn diagnose(path: &Path, source: &str, failure: &ParseFailure) -> Vec<Diagnostic> {
120  let shown = path.display().to_string();
121  match failure {
122    ParseFailure::Lex(e) => vec![Diagnostic {
123      code: e.code.into(),
124      severity: voltaic_diag::Severity::Error,
125      message: e.message.clone(),
126      span: e.span,
127      related: Vec::new(),
128      help: e.help.clone(),
129      path: shown,
130      source: source.to_string(),
131    }],
132    ParseFailure::Parse(problems) => problems
133      .iter()
134      .map(|e| Diagnostic {
135        code: e.code.into(),
136        severity: voltaic_diag::Severity::Error,
137        message: e.message.clone(),
138        span: e.span,
139        related: Vec::new(),
140        help: e.help.clone(),
141        path: shown.clone(),
142        source: source.to_string(),
143      })
144      .collect(),
145  }
146}
147
148pub(crate) fn io_diagnostic(path: &str, message: String) -> Diagnostic {
149  Diagnostic {
150    code: DiagnosticCode::Foreign("io".to_string()),
151    severity: voltaic_diag::Severity::Error,
152    message,
153    span: (0, 0),
154    related: Vec::new(),
155    help: None,
156    path: path.to_string(),
157    source: String::new(),
158  }
159}
160
161pub fn rustc_path() -> Option<PathBuf> {
162  let from_path = std::env::var_os("PATH").into_iter().flat_map(|paths| {
163    let dirs: Vec<PathBuf> = std::env::split_paths(&paths)
164      .map(|dir| dir.join("rustc"))
165      .collect();
166    dirs
167  });
168  let baked = std::iter::once(PathBuf::from(env!("VOLTAIC_RUSTC")));
169  let from_env = std::env::var_os("RUSTC").map(PathBuf::from).into_iter();
170
171  from_path
172    .chain(baked)
173    .chain(from_env)
174    .find(|candidate| is_real_rustc(candidate))
175}
176
177pub(crate) fn is_real_rustc(candidate: &Path) -> bool {
178  if !candidate.is_file() {
179    return false;
180  }
181  let probed = Command::new(candidate).arg("--version").output();
182  probed
183    .is_ok_and(|o| o.status.success() && String::from_utf8_lossy(&o.stdout).starts_with("rustc "))
184}
185
186#[cfg(test)]
187mod __vlt_tests {
188  use super::*;
189
190  #[test]
191  fn creates_and_removes_itself() {
192    let gone = {
193      let dir = ScratchDir::new("test").expect("should create");
194      let live = dir.path().to_path_buf();
195      assert!(live.is_dir());
196      live
197    };
198    assert!(
199      !gone.exists(),
200      "the directory should be gone after the drop"
201    );
202  }
203
204  #[test]
205  fn two_directories_do_not_collide() {
206    let a = ScratchDir::new("test").expect("should create");
207    let b = ScratchDir::new("test").expect("should create");
208    assert_ne!(a.path(), b.path());
209  }
210
211  #[test]
212  fn carries_a_lex_errors_code_and_help_across() {
213    let source = "main : ()\nmain =\n\tprint 1\n";
214    let failure = voltaic_parser::parse_source(source).expect_err("a tab is an error");
215    let diagnostics = diagnose(Path::new("a.vlt"), source, &failure);
216    assert_eq!(diagnostics.len(), 1);
217    assert_eq!(diagnostics[0].code.text(), "VLT0001");
218    assert_eq!(diagnostics[0].path, "a.vlt");
219    assert!(diagnostics[0].help.is_some(), "the lexer's help survives");
220  }
221
222  #[test]
223  fn reports_every_recovered_parse_error() {
224    let source = "a : ()\na =\n  + 1\n\nb : ()\nb =\n  * \n";
225    let failure = voltaic_parser::parse_source(source).expect_err("should fail");
226    let diagnostics = diagnose(Path::new("a.vlt"), source, &failure);
227    let count = diagnostics.len();
228    assert!(count >= 2, "expected a diagnostic per error, got {}", count);
229    assert!(diagnostics.iter().all(|d| d.source == source));
230  }
231
232  #[test]
233  fn renders_without_a_snippet() {
234    let d = io_diagnostic("a.vlt", "cannot read a.vlt: missing".to_string());
235    assert_eq!(d.code.text(), "io");
236    assert_eq!(d.path, "a.vlt");
237    let out = voltaic_diag::render(std::slice::from_ref(&d), true);
238    assert!(out.contains("cannot read"), "{}", out);
239    assert!(!out.contains("  |"), "no snippet should be drawn:\n{}", out);
240  }
241
242  #[test]
243  fn finds_a_rustc() {
244    let found = rustc_path().expect("the toolchain should provide a rustc");
245    let shown = found.display();
246    assert!(is_real_rustc(&found), "{} is not rustc", shown);
247  }
248
249  #[test]
250  fn rejects_a_program_that_is_not_rustc() {
251    assert!(!is_real_rustc(Path::new("/bin/sh")));
252  }
253
254  #[test]
255  fn rejects_a_path_that_does_not_exist() {
256    assert!(!is_real_rustc(Path::new("/nonexistent/rustc")));
257  }
258
259  #[test]
260  fn rejects_a_directory() {
261    assert!(!is_real_rustc(Path::new("/tmp")));
262  }
263}