use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::module_interface::AXI_FORMAT_VERSION;
pub const CACHE_SCHEMA_VERSION: u32 = 1;
pub const CACHE_DIR_NAME: &str = ".axon_cache";
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ModuleEntry {
content_hash: String,
dep_interfaces: BTreeMap<String, String>,
interface_hash: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct CachedDiagnostic {
pub file: String,
pub line: u32,
pub column: u32,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ProjectEntry {
key: String,
merged_warnings: Vec<CachedDiagnostic>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct CacheManifest {
schema_version: u32,
axi_format: u32,
compiler_version: String,
modules: BTreeMap<String, ModuleEntry>,
#[serde(default)]
project: Option<ProjectEntry>,
}
impl CacheManifest {
fn fresh() -> Self {
CacheManifest {
schema_version: CACHE_SCHEMA_VERSION,
axi_format: AXI_FORMAT_VERSION,
compiler_version: env!("CARGO_PKG_VERSION").to_string(),
modules: BTreeMap::new(),
project: None,
}
}
fn is_current(&self) -> bool {
self.schema_version == CACHE_SCHEMA_VERSION
&& self.axi_format == AXI_FORMAT_VERSION
&& self.compiler_version == env!("CARGO_PKG_VERSION")
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct CacheStats {
pub validation_hits: usize,
pub validation_misses: usize,
pub early_cutoffs: usize,
}
pub struct CompilationCache {
root: PathBuf,
manifest: CacheManifest,
previous_content: BTreeMap<String, String>,
pub stats: CacheStats,
dirty: bool,
}
impl CompilationCache {
pub fn open(dir: &Path) -> CompilationCache {
let manifest_path = dir.join("manifest.json");
let manifest = std::fs::read_to_string(&manifest_path)
.ok()
.and_then(|s| serde_json::from_str::<CacheManifest>(&s).ok())
.filter(CacheManifest::is_current)
.unwrap_or_else(CacheManifest::fresh);
let previous_content = manifest
.modules
.iter()
.map(|(k, v)| (k.clone(), v.content_hash.clone()))
.collect();
CompilationCache {
root: dir.to_path_buf(),
manifest,
previous_content,
stats: CacheStats::default(),
dirty: false,
}
}
pub fn validation_hit(
&mut self,
module: &str,
content_hash: &str,
dep_interfaces: &BTreeMap<String, String>,
) -> bool {
let hit = self
.manifest
.modules
.get(module)
.map(|e| e.content_hash == content_hash && &e.dep_interfaces == dep_interfaces)
.unwrap_or(false);
if hit {
self.stats.validation_hits += 1;
let cutoff = dep_interfaces.keys().any(|dep| {
match (
self.previous_content.get(dep),
self.manifest.modules.get(dep),
) {
(Some(prev), Some(entry)) => &entry.content_hash != prev,
_ => false,
}
});
if cutoff {
self.stats.early_cutoffs += 1;
}
} else {
self.stats.validation_misses += 1;
}
hit
}
pub fn record_clean(
&mut self,
module: &str,
content_hash: &str,
dep_interfaces: BTreeMap<String, String>,
interface_hash: &str,
axi_json: &str,
) {
self.manifest.modules.insert(
module.to_string(),
ModuleEntry {
content_hash: content_hash.to_string(),
dep_interfaces,
interface_hash: interface_hash.to_string(),
},
);
self.dirty = true;
let axi_dir = self.root.join("interfaces");
let _ = std::fs::create_dir_all(&axi_dir);
let _ = atomic_write(&axi_dir.join(format!("{module}.axi")), axi_json.as_bytes());
}
pub fn project_warnings(&self, key: &str) -> Option<Vec<CachedDiagnostic>> {
self.manifest
.project
.as_ref()
.filter(|p| p.key == key)
.map(|p| p.merged_warnings.clone())
}
pub fn record_project(&mut self, key: &str, merged_warnings: Vec<CachedDiagnostic>) {
self.manifest.project = Some(ProjectEntry {
key: key.to_string(),
merged_warnings,
});
self.dirty = true;
}
pub fn clear_project(&mut self) {
if self.manifest.project.is_some() {
self.manifest.project = None;
self.dirty = true;
}
}
pub fn flush(&mut self) {
if !self.dirty {
return;
}
let _ = std::fs::create_dir_all(&self.root);
if let Ok(json) = serde_json::to_string_pretty(&self.manifest) {
let _ = atomic_write(&self.root.join("manifest.json"), json.as_bytes());
}
self.dirty = false;
}
}
fn atomic_write(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
let tmp = path.with_extension("tmp");
std::fs::write(&tmp, bytes)?;
let _ = std::fs::remove_file(path);
std::fs::rename(&tmp, path)
}
#[cfg(test)]
mod tests {
use super::*;
fn deps(pairs: &[(&str, &str)]) -> BTreeMap<String, String> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[test]
fn miss_then_hit_then_source_invalidation() {
let dir = std::env::temp_dir().join(format!(
"axon_cache_test_{}_{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&dir);
let mut c = CompilationCache::open(&dir);
assert!(!c.validation_hit("m", "h1", &deps(&[])));
c.record_clean("m", "h1", deps(&[]), "i1", "{}");
c.flush();
let mut c2 = CompilationCache::open(&dir);
assert!(c2.validation_hit("m", "h1", &deps(&[])), "law 3");
assert!(!c2.validation_hit("m", "h2", &deps(&[])), "law 1");
assert_eq!(c2.stats.validation_hits, 1);
assert_eq!(c2.stats.validation_misses, 1);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn dependency_interface_invalidates() {
let dir = std::env::temp_dir().join(format!(
"axon_cache_dep_{}_{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&dir);
let mut c = CompilationCache::open(&dir);
c.record_clean("main", "h1", deps(&[("lib", "i1")]), "im", "{}");
c.flush();
let mut c2 = CompilationCache::open(&dir);
assert!(c2.validation_hit("main", "h1", &deps(&[("lib", "i1")])));
assert!(!c2.validation_hit("main", "h1", &deps(&[("lib", "i2")])), "law 2");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn corrupt_manifest_self_heals() {
let dir = std::env::temp_dir().join(format!(
"axon_cache_heal_{}_{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("manifest.json"), b"{ not json").unwrap();
let mut c = CompilationCache::open(&dir); assert!(!c.validation_hit("m", "h1", &deps(&[])));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn schema_version_busts_wholesale() {
let dir = std::env::temp_dir().join(format!(
"axon_cache_ver_{}_{:?}",
std::process::id(),
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let stale = serde_json::json!({
"schema_version": 0,
"axi_format": 0,
"compiler_version": "0.0.0",
"modules": { "m": { "content_hash": "h1", "dep_interfaces": {}, "interface_hash": "i1" } }
});
std::fs::write(dir.join("manifest.json"), stale.to_string()).unwrap();
let mut c = CompilationCache::open(&dir);
assert!(!c.validation_hit("m", "h1", &deps(&[])), "law 6");
let _ = std::fs::remove_dir_all(&dir);
}
}