1use crate::cache::CacheReporter;
2
3use crate::cache::Cached;
4
5use crate::cache::Fingerprint;
6
7use crate::cache::Silent;
8
9use crate::cache::Store;
10
11use crate::cache::cache_key;
12
13use crate::check::diagnose;
14
15use crate::check::io_diagnostic;
16
17use voltaic_diag::Diagnostic;
18
19use std::path::Path;
20
21use std::path::PathBuf;
22
23pub fn compile_dir(src: &Path, out: &Path) -> Result<Compiled, Vec<Diagnostic>> {
24 compile_dir_with(src, out, &Silent)
25}
26
27pub(crate) fn compile_file(
28 source: &Path,
29 out: &Path,
30 store: &Store,
31 fingerprint: &Fingerprint,
32) -> Result<Outcome, Vec<Diagnostic>> {
33 let path = source.display().to_string();
34 let text = std::fs::read_to_string(source)
35 .map_err(|e| vec![io_diagnostic(&path, format!("cannot read {}: {}", path, e))])?;
36
37 let key = cache_key(&text, fingerprint);
38 if let Some(hit) = store.get(&key) {
39 let cached = write_outputs(source, out, &path, &hit)?;
40 return Ok(Outcome {
41 written: cached,
42 was_cached: true,
43 });
44 }
45
46 let mut program =
47 voltaic_parser::parse_source(&text).map_err(|failure| diagnose(source, &text, &failure))?;
48 let analysis = crate::annotate::analyse_at(&mut program, source);
49 if !analysis.open.is_empty() {
50 return Err(crate::annotate::refusals(
51 source,
52 &text,
53 analysis.open.as_slice(),
54 ));
55 }
56 let scope = voltaic_emit::MethodScope::enter(voltaic_emit::MethodTable::from_spans(
57 analysis.methods.as_slice(),
58 ));
59 let emitted = voltaic_emit::emit_with_map(&program, &text, &path);
60 drop(scope);
61 let fresh = Cached {
62 r#rust: emitted.0,
63 map: voltaic_diag::render_map(&emitted.1),
64 };
65
66 let written = write_outputs(source, out, &path, &fresh)?;
67 store.put(&key, &fresh);
68 Ok(Outcome {
69 written,
70 was_cached: false,
71 })
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub(crate) struct Outcome {
76 pub(crate) written: PathBuf,
77 pub(crate) was_cached: bool,
78}
79
80pub(crate) fn write_outputs(
81 source: &Path,
82 out: &Path,
83 path: &str,
84 content: &Cached,
85) -> Result<PathBuf, Vec<Diagnostic>> {
86 let stem = source.file_stem().map_or_else(
87 || "generated".to_string(),
88 |s| s.to_string_lossy().to_string(),
89 );
90 let target = out.join(format!("{}.rs", stem));
91
92 let out_shown = out.display().to_string();
93 std::fs::create_dir_all(out).map_err(|e| {
94 vec![io_diagnostic(
95 path,
96 format!("cannot create {}: {}", out_shown, e),
97 )]
98 })?;
99 let target_shown = target.display().to_string();
100 std::fs::write(&target, &content.r#rust).map_err(|e| {
101 vec![io_diagnostic(
102 path,
103 format!("cannot write {}: {}", target_shown, e),
104 )]
105 })?;
106
107 let mut map_name = target.file_name().expect("a name").to_os_string();
108 map_name.push(".map");
109 let sidecar = target.with_file_name(map_name);
110 let sidecar_shown = sidecar.display().to_string();
111 std::fs::write(&sidecar, &content.map).map_err(|e| {
112 vec![io_diagnostic(
113 path,
114 format!("cannot write {}: {}", sidecar_shown, e),
115 )]
116 })?;
117
118 Ok(target)
119}
120
121#[derive(Debug, Clone, Default, PartialEq, Eq)]
122pub struct Compiled {
123 pub inputs: Vec<PathBuf>,
124 pub outputs: Vec<PathBuf>,
125}
126
127impl Compiled {
128 pub fn rerun_directives(&self) -> Vec<String> {
129 self
130 .inputs
131 .iter()
132 .map(|p| {
133 let shown = p.display();
134 format!("cargo::rerun-if-changed={}", shown)
135 })
136 .collect()
137 }
138
139 pub fn is_empty(&self) -> bool {
140 self.inputs.is_empty()
141 }
142}
143
144pub(crate) fn vlt_in(dir: &Path) -> Vec<PathBuf> {
145 let Ok(entries) = std::fs::read_dir(dir) else {
146 return Vec::new();
147 };
148 let mut found: Vec<PathBuf> = entries
149 .filter_map(Result::ok)
150 .map(|e| e.path())
151 .filter(|p| p.is_file() && p.extension().is_some_and(|x| x == "vlt" || x == "sdx"))
152 .collect();
153 found.sort();
154 found
155}
156
157#[cfg(test)]
158static NEXT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
159
160#[cfg(test)]
161struct Dir(std::path::PathBuf);
162
163#[cfg(test)]
164impl Dir {
165 fn r#with(tag: &str, names: &[&str]) -> Self {
166 let n = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
167 let pid = std::process::id();
168 let dir = std::env::temp_dir().join(format!("voltaic-vlt-in-{}-{}-{}", tag, pid, n));
169 std::fs::create_dir_all(&dir).expect("should create");
170 for name in names {
171 std::fs::write(
172 dir.join(name),
173 "main : ()\nmain =\n println! \\-> \"#{1}\"\n",
174 )
175 .expect("should write");
176 }
177 Dir(dir)
178 }
179}
180
181#[cfg(test)]
182impl Drop for Dir {
183 fn drop(&mut self) {
184 let _dropped = std::fs::remove_dir_all(&self.0).ok();
185 ();
186 }
187}
188
189pub fn compile_dir_with(
190 src: &Path,
191 out: &Path,
192 reporter: &dyn CacheReporter,
193) -> Result<Compiled, Vec<Diagnostic>> {
194 let store = Store::new(out);
195 let fingerprint = Fingerprint::current();
196
197 let mut compiled = Compiled::default();
198 let mut diagnostics = Vec::new();
199
200 for source in vlt_in(src) {
201 let label = source
202 .file_name()
203 .map_or_else(String::new, |n| n.to_string_lossy().to_string());
204
205 match compile_file(&source, out, &store, &fingerprint) {
206 Ok(outcome) => {
207 reporter.report(&label, outcome.was_cached);
208 compiled.inputs.push(source);
209 compiled.outputs.push(outcome.written)
210 }
211 Err(reported) => {
212 reporter.report(&label, false);
213 diagnostics.extend(reported)
214 }
215 };
216 }
217
218 if diagnostics.is_empty() {
219 Ok(compiled)
220 } else {
221 Err(diagnostics)
222 }
223}
224
225#[cfg(test)]
226mod __vlt_tests {
227 use super::*;
228
229 #[test]
230 fn names_every_input_for_cargo() {
231 let compiled = Compiled {
232 inputs: vec![PathBuf::from("src/a.vlt"), PathBuf::from("src/b.vlt")],
233 outputs: vec![PathBuf::from("out/a.rs"), PathBuf::from("out/b.rs")],
234 };
235 assert_eq!(
236 compiled.rerun_directives(),
237 vec!(
238 "cargo::rerun-if-changed=src/a.vlt",
239 "cargo::rerun-if-changed=src/b.vlt"
240 )
241 );
242 }
243
244 #[test]
245 fn an_empty_run_asks_for_no_reruns() {
246 let empty = Compiled::default();
247 let directives = empty.rerun_directives();
248 assert!(directives.is_empty());
249 assert!(empty.is_empty());
250 }
251
252 #[test]
253 fn finds_vlt_files_and_sorts_them() {
254 let dir = Dir::r#with("sorted", &["b.vlt", "a.vlt", "c.vlt"]);
255 let found: Vec<String> = vlt_in(&dir.0)
256 .iter()
257 .map(|p| p.file_name().expect("a name").to_string_lossy().to_string())
258 .collect();
259 assert_eq!(found, vec!("a.vlt", "b.vlt", "c.vlt"));
260 }
261
262 #[test]
263 fn ignores_other_extensions() {
264 let dir = Dir::r#with("mixed", &["keep.vlt", "skip.rs", "skip.vrs", "skip.md"]);
265 assert_eq!(vlt_in(&dir.0).len(), 1);
266 }
267
268 #[test]
269 fn yields_nothing_for_a_missing_directory() {
270 let found = vlt_in(std::path::Path::new("/nonexistent/voltaic"));
271 assert!(found.is_empty());
272 }
273
274 #[test]
275 fn yields_nothing_for_an_empty_directory() {
276 let dir = Dir::r#with("empty", &[]);
277 let found = vlt_in(&dir.0);
278 assert!(found.is_empty());
279 }
280}