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, __vlt_a1: &str, __vlt_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));
();
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
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.vlt\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 __vlt_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.vlt", false);
collect.report("a.vlt", true);
assert_eq!(collect.0.borrow().as_slice(), ["a.vlt MISS", "a.vlt HIT"]);
}
#[test]
fn accepts_everything_and_says_nothing() {
Silent.report("a.vlt", true);
Silent.report("a.vlt", 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);
}
}