voltaic-driver 0.1.0

Incremental compilation driver for the Voltaic compiler.
Documentation
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 program =
    voltaic_parser::parse_source(&text).map_err(|failure| diagnose(source, &text, &failure))?;
  let emitted = voltaic_emit::emit_with_map(&program, &text, &path);
  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 sdx_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 == "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-sdx-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 sdx_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 __rdx_tests {
  use super::*;

  #[test]
  fn names_every_input_for_cargo() {
    let compiled = Compiled {
      inputs: vec![PathBuf::from("src/a.sdx"), PathBuf::from("src/b.sdx")],
      outputs: vec![PathBuf::from("out/a.rs"), PathBuf::from("out/b.rs")],
    };
    assert_eq!(
      compiled.rerun_directives(),
      vec!(
        "cargo::rerun-if-changed=src/a.sdx",
        "cargo::rerun-if-changed=src/b.sdx"
      )
    );
  }

  #[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_sdx_files_and_sorts_them() {
    let dir = Dir::r#with("sorted", &["b.sdx", "a.sdx", "c.sdx"]);
    let found: Vec<String> = sdx_in(&dir.0)
      .iter()
      .map(|p| p.file_name().expect("a name").to_string_lossy().to_string())
      .collect();
    assert_eq!(found, vec!("a.sdx", "b.sdx", "c.sdx"));
  }

  #[test]
  fn ignores_other_extensions() {
    let dir = Dir::r#with("mixed", &["keep.sdx", "skip.rs", "skip.xrs", "skip.md"]);
    assert_eq!(sdx_in(&dir.0).len(), 1);
  }

  #[test]
  fn yields_nothing_for_a_missing_directory() {
    let found = sdx_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 = sdx_in(&dir.0);
    assert!(found.is_empty());
  }
}