Skip to main content

voltaic_driver/
compile_dir.rs

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}