use crate::cache::CacheReporter;
use crate::cache::Cached;
use crate::cache::Fingerprint;
use crate::cache::Silent;
use crate::cache::Store;
use crate::cache::cache_key;
use crate::check::diagnose;
use crate::check::io_diagnostic;
use voltaic_diag::Diagnostic;
use std::path::Path;
use std::path::PathBuf;
pub fn compile_dir(src: &Path, out: &Path) -> Result<Compiled, Vec<Diagnostic>> {
compile_dir_with(src, out, &Silent)
}
pub(crate) fn compile_file(
source: &Path,
out: &Path,
store: &Store,
fingerprint: &Fingerprint,
) -> Result<Outcome, Vec<Diagnostic>> {
let path = source.display().to_string();
let text = std::fs::read_to_string(source)
.map_err(|e| vec![io_diagnostic(&path, format!("cannot read {}: {}", path, e))])?;
let key = cache_key(&text, fingerprint);
if let Some(hit) = store.get(&key) {
let cached = write_outputs(source, out, &path, &hit)?;
return Ok(Outcome {
written: cached,
was_cached: true,
});
}
let mut program =
voltaic_parser::parse_source(&text).map_err(|failure| diagnose(source, &text, &failure))?;
let analysis = crate::annotate::analyse_at(&mut program, source);
if !analysis.open.is_empty() {
return Err(crate::annotate::refusals(
source,
&text,
analysis.open.as_slice(),
));
}
let scope = voltaic_emit::MethodScope::enter(voltaic_emit::MethodTable::from_spans(
analysis.methods.as_slice(),
));
let emitted = voltaic_emit::emit_with_map(&program, &text, &path);
drop(scope);
let fresh = Cached {
r#rust: emitted.0,
map: voltaic_diag::render_map(&emitted.1),
};
let written = write_outputs(source, out, &path, &fresh)?;
store.put(&key, &fresh);
Ok(Outcome {
written,
was_cached: false,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Outcome {
pub(crate) written: PathBuf,
pub(crate) was_cached: bool,
}
pub(crate) fn write_outputs(
source: &Path,
out: &Path,
path: &str,
content: &Cached,
) -> Result<PathBuf, Vec<Diagnostic>> {
let stem = source.file_stem().map_or_else(
|| "generated".to_string(),
|s| s.to_string_lossy().to_string(),
);
let target = out.join(format!("{}.rs", stem));
let out_shown = out.display().to_string();
std::fs::create_dir_all(out).map_err(|e| {
vec![io_diagnostic(
path,
format!("cannot create {}: {}", out_shown, e),
)]
})?;
let target_shown = target.display().to_string();
std::fs::write(&target, &content.r#rust).map_err(|e| {
vec![io_diagnostic(
path,
format!("cannot write {}: {}", target_shown, e),
)]
})?;
let mut map_name = target.file_name().expect("a name").to_os_string();
map_name.push(".map");
let sidecar = target.with_file_name(map_name);
let sidecar_shown = sidecar.display().to_string();
std::fs::write(&sidecar, &content.map).map_err(|e| {
vec![io_diagnostic(
path,
format!("cannot write {}: {}", sidecar_shown, e),
)]
})?;
Ok(target)
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Compiled {
pub inputs: Vec<PathBuf>,
pub outputs: Vec<PathBuf>,
}
impl Compiled {
pub fn rerun_directives(&self) -> Vec<String> {
self
.inputs
.iter()
.map(|p| {
let shown = p.display();
format!("cargo::rerun-if-changed={}", shown)
})
.collect()
}
pub fn is_empty(&self) -> bool {
self.inputs.is_empty()
}
}
pub(crate) fn vlt_in(dir: &Path) -> Vec<PathBuf> {
let Ok(entries) = std::fs::read_dir(dir) else {
return Vec::new();
};
let mut found: Vec<PathBuf> = entries
.filter_map(Result::ok)
.map(|e| e.path())
.filter(|p| p.is_file() && p.extension().is_some_and(|x| x == "vlt" || x == "sdx"))
.collect();
found.sort();
found
}
#[cfg(test)]
static NEXT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
#[cfg(test)]
struct Dir(std::path::PathBuf);
#[cfg(test)]
impl Dir {
fn r#with(tag: &str, names: &[&str]) -> Self {
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-vlt-in-{}-{}-{}", tag, pid, n));
std::fs::create_dir_all(&dir).expect("should create");
for name in names {
std::fs::write(
dir.join(name),
"main : ()\nmain =\n println! \\-> \"#{1}\"\n",
)
.expect("should write");
}
Dir(dir)
}
}
#[cfg(test)]
impl Drop for Dir {
fn drop(&mut self) {
let _dropped = std::fs::remove_dir_all(&self.0).ok();
();
}
}
pub fn compile_dir_with(
src: &Path,
out: &Path,
reporter: &dyn CacheReporter,
) -> Result<Compiled, Vec<Diagnostic>> {
let store = Store::new(out);
let fingerprint = Fingerprint::current();
let mut compiled = Compiled::default();
let mut diagnostics = Vec::new();
for source in vlt_in(src) {
let label = source
.file_name()
.map_or_else(String::new, |n| n.to_string_lossy().to_string());
match compile_file(&source, out, &store, &fingerprint) {
Ok(outcome) => {
reporter.report(&label, outcome.was_cached);
compiled.inputs.push(source);
compiled.outputs.push(outcome.written)
}
Err(reported) => {
reporter.report(&label, false);
diagnostics.extend(reported)
}
};
}
if diagnostics.is_empty() {
Ok(compiled)
} else {
Err(diagnostics)
}
}
#[cfg(test)]
mod __vlt_tests {
use super::*;
#[test]
fn names_every_input_for_cargo() {
let compiled = Compiled {
inputs: vec![PathBuf::from("src/a.vlt"), PathBuf::from("src/b.vlt")],
outputs: vec![PathBuf::from("out/a.rs"), PathBuf::from("out/b.rs")],
};
assert_eq!(
compiled.rerun_directives(),
vec!(
"cargo::rerun-if-changed=src/a.vlt",
"cargo::rerun-if-changed=src/b.vlt"
)
);
}
#[test]
fn an_empty_run_asks_for_no_reruns() {
let empty = Compiled::default();
let directives = empty.rerun_directives();
assert!(directives.is_empty());
assert!(empty.is_empty());
}
#[test]
fn finds_vlt_files_and_sorts_them() {
let dir = Dir::r#with("sorted", &["b.vlt", "a.vlt", "c.vlt"]);
let found: Vec<String> = vlt_in(&dir.0)
.iter()
.map(|p| p.file_name().expect("a name").to_string_lossy().to_string())
.collect();
assert_eq!(found, vec!("a.vlt", "b.vlt", "c.vlt"));
}
#[test]
fn ignores_other_extensions() {
let dir = Dir::r#with("mixed", &["keep.vlt", "skip.rs", "skip.vrs", "skip.md"]);
assert_eq!(vlt_in(&dir.0).len(), 1);
}
#[test]
fn yields_nothing_for_a_missing_directory() {
let found = vlt_in(std::path::Path::new("/nonexistent/voltaic"));
assert!(found.is_empty());
}
#[test]
fn yields_nothing_for_an_empty_directory() {
let dir = Dir::r#with("empty", &[]);
let found = vlt_in(&dir.0);
assert!(found.is_empty());
}
}