voltaic-driver 0.1.1

Incremental compilation driver for the Voltaic compiler.
Documentation
use std::path::Path;

use std::path::PathBuf;

pub(crate) fn cache_key(source: &str, fingerprint: &Fingerprint) -> blake3::Hash {
  let mut hasher = blake3::Hasher::new();
  let length = u64::try_from(source.len()).unwrap_or(u64::MAX);
  hasher.update(&length.to_le_bytes());
  hasher.update(source.as_bytes());
  hasher.update(fingerprint.as_bytes());
  hasher.finalize()
}

#[cfg(test)]
fn fp() -> Fingerprint {
  Fingerprint::r#of(b"compiler")
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
pub struct Fingerprint(blake3::Hash);

impl Fingerprint {
  pub fn current() -> Self {
    match std::env::current_exe().and_then(std::fs::read) {
      Ok(bytes) => Fingerprint::r#of(&bytes),
      Err(_) => Fingerprint::r#of(&Fingerprint::now().to_le_bytes()),
    }
  }

  fn now() -> u128 {
    std::time::SystemTime::now()
      .duration_since(std::time::UNIX_EPOCH)
      .map_or(0, |d| d.as_nanos())
  }

  pub fn r#of(bytes: &[u8]) -> Self {
    Fingerprint(blake3::hash(bytes))
  }

  pub(crate) fn as_bytes(&self) -> &[u8; 32] {
    self.0.as_bytes()
  }
}

pub trait CacheReporter {
  fn report(&self, __rdx_a1: &str, __rdx_a2: bool);
}

#[cfg(test)]
struct Collect(std::cell::RefCell<Vec<String>>);

#[cfg(test)]
impl CacheReporter for Collect {
  fn report(&self, label: &str, hit: bool) {
    let outcome = if hit { "HIT" } else { "MISS" };
    self.0.borrow_mut().push(format!("{} {}", label, outcome));
    ();
  }
}

pub struct Silent;

impl CacheReporter for Silent {
  fn report(&self, _label: &str, _hit: bool) {
    ();
  }
}

pub(crate) struct Store {
  dir: PathBuf,
}

impl Store {
  pub(crate) fn new(out: &Path) -> Self {
    Store {
      dir: out.join(".voltaic-cache"),
    }
  }

  fn paths(&self, key: &blake3::Hash) -> (PathBuf, PathBuf) {
    let rust_path = self.dir.join(format!("{}.rs", key));
    let map_path = self.dir.join(format!("{}.rs.map", key));
    (rust_path, map_path)
  }

  pub(crate) fn get(&self, key: &blake3::Hash) -> Option<Cached> {
    let found = self.paths(key);
    let text = std::fs::read_to_string(found.0).ok()?;
    let map = std::fs::read_to_string(found.1).ok()?;
    voltaic_diag::parse_map(&map).ok()?;
    Some(Cached { r#rust: text, map })
  }

  pub(crate) fn put(&self, key: &blake3::Hash, cached: &Cached) {
    if std::fs::create_dir_all(&self.dir).is_err() {
      return ();
    }
    let found = self.paths(key);
    if std::fs::write(&found.1, &cached.map).is_err() {
      return ();
    }
    if std::fs::write(&found.0, &cached.r#rust).is_err() {
      let _dropped = std::fs::remove_file(&found.1).ok();
    }
    ();
  }
}

#[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 new(tag: &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-store-{}-{}-{}", tag, pid, n));
    std::fs::create_dir_all(&dir).expect("should create");
    Dir(dir)
  }
}

#[cfg(test)]
impl Drop for Dir {
  fn drop(&mut self) {
    let _dropped = std::fs::remove_dir_all(&self.0).ok();
    ();
  }
}

#[cfg(test)]
fn entry() -> Cached {
  Cached {
    r#rust: "fn main() {}\n".to_string(),
    map: "voltaic-map 1 a.sdx\n1 1 1 1\n".to_string(),
  }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Cached {
  pub(crate) r#rust: String,
  pub(crate) map: String,
}

#[cfg(test)]
mod __rdx_tests {
  use super::*;

  #[test]
  fn the_same_source_and_compiler_give_the_same_key() {
    assert_eq!(
      cache_key("main : ()\nmain =\n", &fp()),
      cache_key("main : ()\nmain =\n", &fp())
    );
  }

  #[test]
  fn one_changed_byte_changes_the_key() {
    assert_ne!(cache_key("print 1\n", &fp()), cache_key("print 2\n", &fp()));
  }

  #[test]
  fn a_different_compiler_changes_the_key() {
    let a = cache_key("main : ()\nmain =\n", &Fingerprint::r#of(b"old"));
    let b = cache_key("main : ()\nmain =\n", &Fingerprint::r#of(b"new"));
    assert_ne!(a, b);
  }

  #[test]
  fn the_boundary_between_fields_is_unambiguous() {
    assert_ne!(
      cache_key("ab", &Fingerprint::r#of(b"c")),
      cache_key("a", &Fingerprint::r#of(b"bc"))
    );
  }

  #[test]
  fn renders_as_hex() {
    let hex = cache_key("x", &fp()).to_string();
    assert_eq!(hex.len(), 64);
    assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
  }

  #[test]
  fn differs_for_different_bytes() {
    assert_ne!(
      Fingerprint::r#of(b"compiler-a"),
      Fingerprint::r#of(b"compiler-b")
    );
  }

  #[test]
  fn is_stable_for_the_same_bytes() {
    assert_eq!(Fingerprint::r#of(b"same"), Fingerprint::r#of(b"same"));
  }

  #[test]
  fn the_current_compiler_has_a_stable_fingerprint() {
    assert_eq!(Fingerprint::current(), Fingerprint::current());
  }

  #[test]
  fn a_reporter_sees_both_outcomes() {
    let collect = Collect(std::cell::RefCell::new(Vec::new()));
    collect.report("a.sdx", false);
    collect.report("a.sdx", true);
    assert_eq!(collect.0.borrow().as_slice(), ["a.sdx MISS", "a.sdx HIT"]);
  }

  #[test]
  fn accepts_everything_and_says_nothing() {
    Silent.report("a.sdx", true);
    Silent.report("a.sdx", false);
  }

  #[test]
  fn a_stored_entry_comes_back() {
    let dir = Dir::new("roundtrip");
    let store = Store::new(&dir.0);
    let key = blake3::hash(b"k");
    assert_eq!(store.get(&key), None, "nothing is cached yet");
    store.put(&key, &entry());
    assert_eq!(store.get(&key), Some(entry()));
  }

  #[test]
  fn an_unknown_key_is_a_miss() {
    let dir = Dir::new("unknown");
    let store = Store::new(&dir.0);
    store.put(&blake3::hash(b"a"), &entry());
    assert_eq!(store.get(&blake3::hash(b"b")), None);
  }

  #[test]
  fn a_corrupt_map_is_a_miss() {
    let dir = Dir::new("corrupt");
    let store = Store::new(&dir.0);
    let key = blake3::hash(b"k");
    store.put(&key, &entry());

    let found = store.paths(&key);
    std::fs::write(&found.1, "not a voltaic map at all\n").expect("should overwrite");
    assert_eq!(store.get(&key), None);
  }

  #[test]
  fn a_half_written_entry_is_a_miss() {
    let dir = Dir::new("half");
    let store = Store::new(&dir.0);
    let key = blake3::hash(b"k");
    store.put(&key, &entry());

    let found = store.paths(&key);
    std::fs::remove_file(&found.0).expect("should remove");
    assert_eq!(store.get(&key), None);
  }

  #[test]
  fn an_unwritable_store_is_simply_always_a_miss() {
    let store = Store::new(std::path::Path::new("/proc/nonexistent-voltaic"));
    let key = blake3::hash(b"k");
    store.put(&key, &entry());
    assert_eq!(store.get(&key), None);
  }
}