1use std::collections::BTreeMap;
45use std::path::{Path, PathBuf};
46
47use serde::{Deserialize, Serialize};
48
49use crate::module_interface::AXI_FORMAT_VERSION;
50
51pub const CACHE_SCHEMA_VERSION: u32 = 1;
53
54pub const CACHE_DIR_NAME: &str = ".axon_cache";
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58struct ModuleEntry {
59 content_hash: String,
60 dep_interfaces: BTreeMap<String, String>,
62 interface_hash: String,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
68pub struct CachedDiagnostic {
69 pub file: String,
70 pub line: u32,
71 pub column: u32,
72 pub message: String,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
81struct ProjectEntry {
82 key: String,
84 merged_warnings: Vec<CachedDiagnostic>,
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize)]
88struct CacheManifest {
89 schema_version: u32,
90 axi_format: u32,
91 compiler_version: String,
92 modules: BTreeMap<String, ModuleEntry>,
94 #[serde(default)]
96 project: Option<ProjectEntry>,
97}
98
99impl CacheManifest {
100 fn fresh() -> Self {
101 CacheManifest {
102 schema_version: CACHE_SCHEMA_VERSION,
103 axi_format: AXI_FORMAT_VERSION,
104 compiler_version: env!("CARGO_PKG_VERSION").to_string(),
105 modules: BTreeMap::new(),
106 project: None,
107 }
108 }
109
110 fn is_current(&self) -> bool {
111 self.schema_version == CACHE_SCHEMA_VERSION
112 && self.axi_format == AXI_FORMAT_VERSION
113 && self.compiler_version == env!("CARGO_PKG_VERSION")
114 }
115}
116
117#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
119pub struct CacheStats {
120 pub validation_hits: usize,
122 pub validation_misses: usize,
124 pub early_cutoffs: usize,
127}
128
129pub struct CompilationCache {
132 root: PathBuf,
133 manifest: CacheManifest,
134 previous_content: BTreeMap<String, String>,
137 pub stats: CacheStats,
138 dirty: bool,
139}
140
141impl CompilationCache {
142 pub fn open(dir: &Path) -> CompilationCache {
146 let manifest_path = dir.join("manifest.json");
147 let manifest = std::fs::read_to_string(&manifest_path)
148 .ok()
149 .and_then(|s| serde_json::from_str::<CacheManifest>(&s).ok())
150 .filter(CacheManifest::is_current)
151 .unwrap_or_else(CacheManifest::fresh);
152 let previous_content = manifest
153 .modules
154 .iter()
155 .map(|(k, v)| (k.clone(), v.content_hash.clone()))
156 .collect();
157 CompilationCache {
158 root: dir.to_path_buf(),
159 manifest,
160 previous_content,
161 stats: CacheStats::default(),
162 dirty: false,
163 }
164 }
165
166 pub fn validation_hit(
169 &mut self,
170 module: &str,
171 content_hash: &str,
172 dep_interfaces: &BTreeMap<String, String>,
173 ) -> bool {
174 let hit = self
175 .manifest
176 .modules
177 .get(module)
178 .map(|e| e.content_hash == content_hash && &e.dep_interfaces == dep_interfaces)
179 .unwrap_or(false);
180 if hit {
181 self.stats.validation_hits += 1;
182 let cutoff = dep_interfaces.keys().any(|dep| {
185 match (
186 self.previous_content.get(dep),
187 self.manifest.modules.get(dep),
188 ) {
189 (Some(prev), Some(entry)) => &entry.content_hash != prev,
193 _ => false,
194 }
195 });
196 if cutoff {
197 self.stats.early_cutoffs += 1;
198 }
199 } else {
200 self.stats.validation_misses += 1;
201 }
202 hit
203 }
204
205 pub fn record_clean(
208 &mut self,
209 module: &str,
210 content_hash: &str,
211 dep_interfaces: BTreeMap<String, String>,
212 interface_hash: &str,
213 axi_json: &str,
214 ) {
215 self.manifest.modules.insert(
216 module.to_string(),
217 ModuleEntry {
218 content_hash: content_hash.to_string(),
219 dep_interfaces,
220 interface_hash: interface_hash.to_string(),
221 },
222 );
223 self.dirty = true;
224 let axi_dir = self.root.join("interfaces");
225 let _ = std::fs::create_dir_all(&axi_dir);
226 let _ = atomic_write(&axi_dir.join(format!("{module}.axi")), axi_json.as_bytes());
227 }
228
229 pub fn project_warnings(&self, key: &str) -> Option<Vec<CachedDiagnostic>> {
234 self.manifest
235 .project
236 .as_ref()
237 .filter(|p| p.key == key)
238 .map(|p| p.merged_warnings.clone())
239 }
240
241 pub fn record_project(&mut self, key: &str, merged_warnings: Vec<CachedDiagnostic>) {
244 self.manifest.project = Some(ProjectEntry {
245 key: key.to_string(),
246 merged_warnings,
247 });
248 self.dirty = true;
249 }
250
251 pub fn clear_project(&mut self) {
255 if self.manifest.project.is_some() {
256 self.manifest.project = None;
257 self.dirty = true;
258 }
259 }
260
261 pub fn flush(&mut self) {
263 if !self.dirty {
264 return;
265 }
266 let _ = std::fs::create_dir_all(&self.root);
267 if let Ok(json) = serde_json::to_string_pretty(&self.manifest) {
268 let _ = atomic_write(&self.root.join("manifest.json"), json.as_bytes());
269 }
270 self.dirty = false;
271 }
272}
273
274fn atomic_write(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
278 let tmp = path.with_extension("tmp");
279 std::fs::write(&tmp, bytes)?;
280 let _ = std::fs::remove_file(path);
281 std::fs::rename(&tmp, path)
282}
283
284#[cfg(test)]
289mod tests {
290 use super::*;
291
292 fn deps(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
293 pairs
294 .iter()
295 .map(|(k, v)| (k.to_string(), v.to_string()))
296 .collect()
297 }
298
299 #[test]
300 fn miss_then_hit_then_source_invalidation() {
301 let dir = std::env::temp_dir().join(format!(
302 "axon_cache_test_{}_{:?}",
303 std::process::id(),
304 std::thread::current().id()
305 ));
306 let _ = std::fs::remove_dir_all(&dir);
307
308 let mut c = CompilationCache::open(&dir);
309 assert!(!c.validation_hit("m", "h1", &deps(&[])));
310 c.record_clean("m", "h1", deps(&[]), "i1", "{}");
311 c.flush();
312
313 let mut c2 = CompilationCache::open(&dir);
314 assert!(c2.validation_hit("m", "h1", &deps(&[])), "law 3");
315 assert!(!c2.validation_hit("m", "h2", &deps(&[])), "law 1");
316 assert_eq!(c2.stats.validation_hits, 1);
317 assert_eq!(c2.stats.validation_misses, 1);
318
319 let _ = std::fs::remove_dir_all(&dir);
320 }
321
322 #[test]
323 fn dependency_interface_invalidates() {
324 let dir = std::env::temp_dir().join(format!(
325 "axon_cache_dep_{}_{:?}",
326 std::process::id(),
327 std::thread::current().id()
328 ));
329 let _ = std::fs::remove_dir_all(&dir);
330
331 let mut c = CompilationCache::open(&dir);
332 c.record_clean("main", "h1", deps(&[("lib", "i1")]), "im", "{}");
333 c.flush();
334
335 let mut c2 = CompilationCache::open(&dir);
336 assert!(c2.validation_hit("main", "h1", &deps(&[("lib", "i1")])));
337 assert!(!c2.validation_hit("main", "h1", &deps(&[("lib", "i2")])), "law 2");
338
339 let _ = std::fs::remove_dir_all(&dir);
340 }
341
342 #[test]
343 fn corrupt_manifest_self_heals() {
344 let dir = std::env::temp_dir().join(format!(
345 "axon_cache_heal_{}_{:?}",
346 std::process::id(),
347 std::thread::current().id()
348 ));
349 let _ = std::fs::remove_dir_all(&dir);
350 std::fs::create_dir_all(&dir).unwrap();
351 std::fs::write(dir.join("manifest.json"), b"{ not json").unwrap();
352
353 let mut c = CompilationCache::open(&dir); assert!(!c.validation_hit("m", "h1", &deps(&[])));
355
356 let _ = std::fs::remove_dir_all(&dir);
357 }
358
359 #[test]
360 fn schema_version_busts_wholesale() {
361 let dir = std::env::temp_dir().join(format!(
362 "axon_cache_ver_{}_{:?}",
363 std::process::id(),
364 std::thread::current().id()
365 ));
366 let _ = std::fs::remove_dir_all(&dir);
367 std::fs::create_dir_all(&dir).unwrap();
368 let stale = serde_json::json!({
369 "schema_version": 0,
370 "axi_format": 0,
371 "compiler_version": "0.0.0",
372 "modules": { "m": { "content_hash": "h1", "dep_interfaces": {}, "interface_hash": "i1" } }
373 });
374 std::fs::write(dir.join("manifest.json"), stale.to_string()).unwrap();
375
376 let mut c = CompilationCache::open(&dir);
377 assert!(!c.validation_hit("m", "h1", &deps(&[])), "law 6");
378
379 let _ = std::fs::remove_dir_all(&dir);
380 }
381}