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}