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}