Skip to main content

voltaic_driver/
cache.rs

1use std::path::Path;
2
3use std::path::PathBuf;
4
5pub(crate) fn cache_key(source: &str, fingerprint: &Fingerprint) -> blake3::Hash {
6  let mut hasher = blake3::Hasher::new();
7  let length = u64::try_from(source.len()).unwrap_or(u64::MAX);
8  hasher.update(&length.to_le_bytes());
9  hasher.update(source.as_bytes());
10  hasher.update(fingerprint.as_bytes());
11  hasher.finalize()
12}
13
14#[cfg(test)]
15fn fp() -> Fingerprint {
16  Fingerprint::r#of(b"compiler")
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[repr(transparent)]
21pub struct Fingerprint(blake3::Hash);
22
23impl Fingerprint {
24  pub fn current() -> Self {
25    match std::env::current_exe().and_then(std::fs::read) {
26      Ok(bytes) => Fingerprint::r#of(&bytes),
27      Err(_) => Fingerprint::r#of(&Fingerprint::now().to_le_bytes()),
28    }
29  }
30
31  fn now() -> u128 {
32    std::time::SystemTime::now()
33      .duration_since(std::time::UNIX_EPOCH)
34      .map_or(0, |d| d.as_nanos())
35  }
36
37  pub fn r#of(bytes: &[u8]) -> Self {
38    Fingerprint(blake3::hash(bytes))
39  }
40
41  pub(crate) fn as_bytes(&self) -> &[u8; 32] {
42    self.0.as_bytes()
43  }
44}
45
46pub trait CacheReporter {
47  fn report(&self, __vlt_a1: &str, __vlt_a2: bool);
48}
49
50#[cfg(test)]
51struct Collect(std::cell::RefCell<Vec<String>>);
52
53#[cfg(test)]
54impl CacheReporter for Collect {
55  fn report(&self, label: &str, hit: bool) {
56    let outcome = if hit { "HIT" } else { "MISS" };
57    self.0.borrow_mut().push(format!("{} {}", label, outcome));
58    ();
59  }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
63pub struct Silent;
64
65impl CacheReporter for Silent {
66  fn report(&self, _label: &str, _hit: bool) {
67    ();
68  }
69}
70
71pub(crate) struct Store {
72  dir: PathBuf,
73}
74
75impl Store {
76  pub(crate) fn new(out: &Path) -> Self {
77    Store {
78      dir: out.join(".voltaic-cache"),
79    }
80  }
81
82  fn paths(&self, key: &blake3::Hash) -> (PathBuf, PathBuf) {
83    let rust_path = self.dir.join(format!("{}.rs", key));
84    let map_path = self.dir.join(format!("{}.rs.map", key));
85    (rust_path, map_path)
86  }
87
88  pub(crate) fn get(&self, key: &blake3::Hash) -> Option<Cached> {
89    let found = self.paths(key);
90    let text = std::fs::read_to_string(found.0).ok()?;
91    let map = std::fs::read_to_string(found.1).ok()?;
92    voltaic_diag::parse_map(&map).ok()?;
93    Some(Cached { r#rust: text, map })
94  }
95
96  pub(crate) fn put(&self, key: &blake3::Hash, cached: &Cached) {
97    if std::fs::create_dir_all(&self.dir).is_err() {
98      return ();
99    }
100    let found = self.paths(key);
101    if std::fs::write(&found.1, &cached.map).is_err() {
102      return ();
103    }
104    if std::fs::write(&found.0, &cached.r#rust).is_err() {
105      let _dropped = std::fs::remove_file(&found.1).ok();
106    }
107    ();
108  }
109}
110
111#[cfg(test)]
112static NEXT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
113
114#[cfg(test)]
115struct Dir(std::path::PathBuf);
116
117#[cfg(test)]
118impl Dir {
119  fn new(tag: &str) -> Self {
120    let n = NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
121    let pid = std::process::id();
122    let dir = std::env::temp_dir().join(format!("voltaic-store-{}-{}-{}", tag, pid, n));
123    std::fs::create_dir_all(&dir).expect("should create");
124    Dir(dir)
125  }
126}
127
128#[cfg(test)]
129impl Drop for Dir {
130  fn drop(&mut self) {
131    let _dropped = std::fs::remove_dir_all(&self.0).ok();
132    ();
133  }
134}
135
136#[cfg(test)]
137fn entry() -> Cached {
138  Cached {
139    r#rust: "fn main() {}\n".to_string(),
140    map: "voltaic-map 1 a.vlt\n1 1 1 1\n".to_string(),
141  }
142}
143
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub(crate) struct Cached {
146  pub(crate) r#rust: String,
147  pub(crate) map: String,
148}
149
150#[cfg(test)]
151mod __vlt_tests {
152  use super::*;
153
154  #[test]
155  fn the_same_source_and_compiler_give_the_same_key() {
156    assert_eq!(
157      cache_key("main : ()\nmain =\n", &fp()),
158      cache_key("main : ()\nmain =\n", &fp())
159    );
160  }
161
162  #[test]
163  fn one_changed_byte_changes_the_key() {
164    assert_ne!(cache_key("print 1\n", &fp()), cache_key("print 2\n", &fp()));
165  }
166
167  #[test]
168  fn a_different_compiler_changes_the_key() {
169    let a = cache_key("main : ()\nmain =\n", &Fingerprint::r#of(b"old"));
170    let b = cache_key("main : ()\nmain =\n", &Fingerprint::r#of(b"new"));
171    assert_ne!(a, b);
172  }
173
174  #[test]
175  fn the_boundary_between_fields_is_unambiguous() {
176    assert_ne!(
177      cache_key("ab", &Fingerprint::r#of(b"c")),
178      cache_key("a", &Fingerprint::r#of(b"bc"))
179    );
180  }
181
182  #[test]
183  fn renders_as_hex() {
184    let hex = cache_key("x", &fp()).to_string();
185    assert_eq!(hex.len(), 64);
186    assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
187  }
188
189  #[test]
190  fn differs_for_different_bytes() {
191    assert_ne!(
192      Fingerprint::r#of(b"compiler-a"),
193      Fingerprint::r#of(b"compiler-b")
194    );
195  }
196
197  #[test]
198  fn is_stable_for_the_same_bytes() {
199    assert_eq!(Fingerprint::r#of(b"same"), Fingerprint::r#of(b"same"));
200  }
201
202  #[test]
203  fn the_current_compiler_has_a_stable_fingerprint() {
204    assert_eq!(Fingerprint::current(), Fingerprint::current());
205  }
206
207  #[test]
208  fn a_reporter_sees_both_outcomes() {
209    let collect = Collect(std::cell::RefCell::new(Vec::new()));
210    collect.report("a.vlt", false);
211    collect.report("a.vlt", true);
212    assert_eq!(collect.0.borrow().as_slice(), ["a.vlt MISS", "a.vlt HIT"]);
213  }
214
215  #[test]
216  fn accepts_everything_and_says_nothing() {
217    Silent.report("a.vlt", true);
218    Silent.report("a.vlt", false);
219  }
220
221  #[test]
222  fn a_stored_entry_comes_back() {
223    let dir = Dir::new("roundtrip");
224    let store = Store::new(&dir.0);
225    let key = blake3::hash(b"k");
226    assert_eq!(store.get(&key), None, "nothing is cached yet");
227    store.put(&key, &entry());
228    assert_eq!(store.get(&key), Some(entry()));
229  }
230
231  #[test]
232  fn an_unknown_key_is_a_miss() {
233    let dir = Dir::new("unknown");
234    let store = Store::new(&dir.0);
235    store.put(&blake3::hash(b"a"), &entry());
236    assert_eq!(store.get(&blake3::hash(b"b")), None);
237  }
238
239  #[test]
240  fn a_corrupt_map_is_a_miss() {
241    let dir = Dir::new("corrupt");
242    let store = Store::new(&dir.0);
243    let key = blake3::hash(b"k");
244    store.put(&key, &entry());
245
246    let found = store.paths(&key);
247    std::fs::write(&found.1, "not a voltaic map at all\n").expect("should overwrite");
248    assert_eq!(store.get(&key), None);
249  }
250
251  #[test]
252  fn a_half_written_entry_is_a_miss() {
253    let dir = Dir::new("half");
254    let store = Store::new(&dir.0);
255    let key = blake3::hash(b"k");
256    store.put(&key, &entry());
257
258    let found = store.paths(&key);
259    std::fs::remove_file(&found.0).expect("should remove");
260    assert_eq!(store.get(&key), None);
261  }
262
263  #[test]
264  fn an_unwritable_store_is_simply_always_a_miss() {
265    let store = Store::new(std::path::Path::new("/proc/nonexistent-voltaic"));
266    let key = blake3::hash(b"k");
267    store.put(&key, &entry());
268    assert_eq!(store.get(&key), None);
269  }
270}