use crate::remap;
use crate::remap::RustcMessage;
use voltaic_diag::Diagnostic;
use voltaic_diag::DiagnosticCode;
use voltaic_parser::ParseFailure;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
pub fn check(path: &Path, source: &str) -> Result<CheckOutcome, String> {
let vlt_path = path.display().to_string();
let mut program = match voltaic_parser::parse_source(source) {
Ok(parsed) => parsed,
Err(failure) => {
return Ok(CheckOutcome {
diagnostics: diagnose(path, source, &failure),
failed: true,
});
}
};
let analysis = crate::annotate::analyse_at(&mut program, path);
if !analysis.open.is_empty() {
return Ok(CheckOutcome {
diagnostics: crate::annotate::refusals(path, source, analysis.open.as_slice()),
failed: true,
});
}
let mut diagnostics = crate::lint(path, source, Some(&program));
let scope = voltaic_emit::MethodScope::enter(voltaic_emit::MethodTable::from_spans(
analysis.methods.as_slice(),
));
let emitted = voltaic_emit::emit_with_map(&program, source, &vlt_path);
drop(scope);
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())?;
let dir = ScratchDir::new("check")?;
let vrs = dir.path().join("check.vrs");
let vrs_path = vrs.display().to_string();
std::fs::write(&vrs, &emitted.0)
.map_err(|e| format!("error: cannot write {}: {}", vrs_path, e))?;
let rustc_shown = rustc.display().to_string();
let invoked = Command::new(&rustc)
.args([
"--edition",
"2024",
"--error-format=short",
"--emit=metadata",
])
.arg("-o")
.arg(dir.path().join("check.rmeta"))
.arg(&vrs)
.output();
let out = invoked.map_err(|e| format!("error: cannot run {}: {}", rustc_shown, e))?;
let stderr = String::from_utf8_lossy(&out.stderr);
let messages: Vec<RustcMessage> = stderr.lines().filter_map(RustcMessage::parse).collect();
let failed = messages.iter().any(RustcMessage::is_error);
let remapped = messages
.iter()
.map(|m| remap(m, &emitted.1, &vlt_path, source, &vrs_path, &emitted.0));
diagnostics.extend(remapped);
if messages.is_empty() && !out.status.success() {
let trimmed = stderr.trim_end();
return Err(format!("error: rustc failed:\n{}", trimmed));
}
Ok(CheckOutcome {
diagnostics,
failed,
})
}
#[derive(Debug, Clone, Default)]
pub struct CheckOutcome {
pub diagnostics: Vec<Diagnostic>,
pub failed: bool,
}
pub(crate) struct ScratchDir(PathBuf);
impl ScratchDir {
pub(crate) fn new(tag: &str) -> Result<Self, String> {
let n = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let pid = std::process::id();
let dir = std::env::temp_dir().join(format!("voltaic-{}-{}-{}", tag, pid, n));
let shown = dir.display().to_string();
std::fs::create_dir_all(&dir).map_err(|e| format!("error: cannot create {}: {}", shown, e))?;
Ok(ScratchDir(dir))
}
pub(crate) fn path(&self) -> &Path {
&self.0
}
}
impl Drop for ScratchDir {
fn drop(&mut self) {
let _dropped = std::fs::remove_dir_all(&self.0).ok();
();
}
}
pub(crate) static NEXT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
pub fn diagnose(path: &Path, source: &str, failure: &ParseFailure) -> Vec<Diagnostic> {
let shown = path.display().to_string();
match failure {
ParseFailure::Lex(e) => vec![Diagnostic {
code: e.code.into(),
severity: voltaic_diag::Severity::Error,
message: e.message.clone(),
span: e.span,
related: Vec::new(),
help: e.help.clone(),
path: shown,
source: source.to_string(),
}],
ParseFailure::Parse(problems) => problems
.iter()
.map(|e| Diagnostic {
code: e.code.into(),
severity: voltaic_diag::Severity::Error,
message: e.message.clone(),
span: e.span,
related: Vec::new(),
help: e.help.clone(),
path: shown.clone(),
source: source.to_string(),
})
.collect(),
}
}
pub(crate) fn io_diagnostic(path: &str, message: String) -> Diagnostic {
Diagnostic {
code: DiagnosticCode::Foreign("io".to_string()),
severity: voltaic_diag::Severity::Error,
message,
span: (0, 0),
related: Vec::new(),
help: None,
path: path.to_string(),
source: String::new(),
}
}
pub fn rustc_path() -> Option<PathBuf> {
let from_path = std::env::var_os("PATH").into_iter().flat_map(|paths| {
let dirs: Vec<PathBuf> = std::env::split_paths(&paths)
.map(|dir| dir.join("rustc"))
.collect();
dirs
});
let baked = std::iter::once(PathBuf::from(env!("VOLTAIC_RUSTC")));
let from_env = std::env::var_os("RUSTC").map(PathBuf::from).into_iter();
from_path
.chain(baked)
.chain(from_env)
.find(|candidate| is_real_rustc(candidate))
}
pub(crate) fn is_real_rustc(candidate: &Path) -> bool {
if !candidate.is_file() {
return false;
}
let probed = Command::new(candidate).arg("--version").output();
probed
.is_ok_and(|o| o.status.success() && String::from_utf8_lossy(&o.stdout).starts_with("rustc "))
}
#[cfg(test)]
mod __vlt_tests {
use super::*;
#[test]
fn creates_and_removes_itself() {
let gone = {
let dir = ScratchDir::new("test").expect("should create");
let live = dir.path().to_path_buf();
assert!(live.is_dir());
live
};
assert!(
!gone.exists(),
"the directory should be gone after the drop"
);
}
#[test]
fn two_directories_do_not_collide() {
let a = ScratchDir::new("test").expect("should create");
let b = ScratchDir::new("test").expect("should create");
assert_ne!(a.path(), b.path());
}
#[test]
fn carries_a_lex_errors_code_and_help_across() {
let source = "main : ()\nmain =\n\tprint 1\n";
let failure = voltaic_parser::parse_source(source).expect_err("a tab is an error");
let diagnostics = diagnose(Path::new("a.vlt"), source, &failure);
assert_eq!(diagnostics.len(), 1);
assert_eq!(diagnostics[0].code.text(), "VLT0001");
assert_eq!(diagnostics[0].path, "a.vlt");
assert!(diagnostics[0].help.is_some(), "the lexer's help survives");
}
#[test]
fn reports_every_recovered_parse_error() {
let source = "a : ()\na =\n + 1\n\nb : ()\nb =\n * \n";
let failure = voltaic_parser::parse_source(source).expect_err("should fail");
let diagnostics = diagnose(Path::new("a.vlt"), source, &failure);
let count = diagnostics.len();
assert!(count >= 2, "expected a diagnostic per error, got {}", count);
assert!(diagnostics.iter().all(|d| d.source == source));
}
#[test]
fn renders_without_a_snippet() {
let d = io_diagnostic("a.vlt", "cannot read a.vlt: missing".to_string());
assert_eq!(d.code.text(), "io");
assert_eq!(d.path, "a.vlt");
let out = voltaic_diag::render(std::slice::from_ref(&d), true);
assert!(out.contains("cannot read"), "{}", out);
assert!(!out.contains(" |"), "no snippet should be drawn:\n{}", out);
}
#[test]
fn finds_a_rustc() {
let found = rustc_path().expect("the toolchain should provide a rustc");
let shown = found.display();
assert!(is_real_rustc(&found), "{} is not rustc", shown);
}
#[test]
fn rejects_a_program_that_is_not_rustc() {
assert!(!is_real_rustc(Path::new("/bin/sh")));
}
#[test]
fn rejects_a_path_that_does_not_exist() {
assert!(!is_real_rustc(Path::new("/nonexistent/rustc")));
}
#[test]
fn rejects_a_directory() {
assert!(!is_real_rustc(Path::new("/tmp")));
}
}