use crate::priority::call_graph::FunctionId;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use super::PurityResult;
const CACHE_VERSION: u32 = 1;
const CACHE_FILE: &str = ".debtmap/purity_cache.postcard";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PurityCache {
version: u32,
entries: HashMap<FunctionId, CachedPurity>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct CachedPurity {
result: PurityResult,
source_hash: u64,
deps_hash: u64,
file_mtime: u64,
}
impl PurityCache {
pub fn new() -> Self {
Self {
version: CACHE_VERSION,
entries: HashMap::new(),
}
}
pub fn load(project_root: &Path) -> Result<Self> {
let cache_path = project_root.join(CACHE_FILE);
if !cache_path.exists() {
return Ok(Self::new());
}
let bytes = std::fs::read(&cache_path)?;
let cache: PurityCache = postcard::from_bytes(&bytes)?;
if cache.version != CACHE_VERSION {
eprintln!("Cache version mismatch, rebuilding cache");
return Ok(Self::new());
}
Ok(cache)
}
pub fn save(&self, project_root: &Path) -> Result<()> {
let cache_path = project_root.join(CACHE_FILE);
std::fs::create_dir_all(cache_path.parent().unwrap())?;
let bytes = postcard::to_allocvec(self)?;
std::fs::write(&cache_path, bytes)?;
Ok(())
}
pub fn is_valid(
&self,
func_id: &FunctionId,
current_mtime: u64,
current_source_hash: u64,
current_deps_hash: u64,
) -> bool {
if let Some(cached) = self.entries.get(func_id) {
cached.file_mtime == current_mtime
&& cached.source_hash == current_source_hash
&& cached.deps_hash == current_deps_hash
} else {
false
}
}
pub fn insert(
&mut self,
func_id: FunctionId,
result: PurityResult,
source_hash: u64,
deps_hash: u64,
file_mtime: u64,
) {
self.entries.insert(
func_id,
CachedPurity {
result,
source_hash,
deps_hash,
file_mtime,
},
);
}
pub fn get(&self, func_id: &FunctionId) -> Option<&PurityResult> {
self.entries.get(func_id).map(|cached| &cached.result)
}
pub fn invalidate_file(&mut self, file_path: &Path) {
self.entries.retain(|id, _| id.file != file_path);
}
pub fn invalidate_dependents(&mut self, changed_func_ids: &[FunctionId]) {
if !changed_func_ids.is_empty() {
self.entries.clear();
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
impl Default for PurityCache {
fn default() -> Self {
Self::new()
}
}
#[allow(dead_code)]
pub fn hash_string(s: &str) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
s.hash(&mut hasher);
hasher.finish()
}
#[allow(dead_code)]
pub fn hash_deps(deps: &[FunctionId]) -> u64 {
let mut sorted_deps = deps.to_vec();
sorted_deps.sort_by(|a, b| {
a.file
.cmp(&b.file)
.then(a.name.cmp(&b.name))
.then(a.line.cmp(&b.line))
});
let deps_string = sorted_deps
.iter()
.map(|id| format!("{}:{}:{}", id.file.display(), id.name, id.line))
.collect::<Vec<_>>()
.join("|");
hash_string(&deps_string)
}
#[allow(dead_code)]
pub fn get_mtime(file_path: &Path) -> Result<u64> {
let metadata = std::fs::metadata(file_path)?;
let modified = metadata.modified()?;
let duration = modified.duration_since(std::time::UNIX_EPOCH)?;
Ok(duration.as_secs())
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn test_cache_new() {
let cache = PurityCache::new();
assert!(cache.is_empty());
assert_eq!(cache.version, CACHE_VERSION);
}
#[test]
fn test_hash_deps_deterministic() {
let func1 = FunctionId::new(PathBuf::from("test.rs"), "foo".to_string(), 100);
let func2 = FunctionId::new(PathBuf::from("test.rs"), "bar".to_string(), 200);
let deps1 = vec![func1.clone(), func2.clone()];
let deps2 = vec![func2.clone(), func1.clone()];
assert_eq!(hash_deps(&deps1), hash_deps(&deps2));
}
}