agcodex_ast/
parser_cache.rs

1//! LRU cache for parsed ASTs with size-based eviction
2
3// use crate::error::{AstError, AstResult}; // unused
4use crate::types::ParsedAst;
5use lru::LruCache;
6use std::num::NonZeroUsize;
7use std::path::Path;
8use std::path::PathBuf;
9use std::sync::Arc;
10
11/// Parser cache for AST reuse
12#[derive(Debug)]
13pub struct ParserCache {
14    cache: LruCache<PathBuf, Arc<ParsedAst>>,
15    max_size_bytes: usize,
16    current_size_bytes: usize,
17}
18
19impl ParserCache {
20    /// Create a new parser cache with maximum size in bytes
21    pub fn new(max_size_bytes: usize) -> Self {
22        // Default to 100 entries, will evict based on size
23        // Use a sensible default capacity; NonZeroUsize::new only fails for 0
24        let cap =
25            NonZeroUsize::new(100).unwrap_or_else(|| unsafe { NonZeroUsize::new_unchecked(1) });
26        Self {
27            cache: LruCache::new(cap),
28            max_size_bytes,
29            current_size_bytes: 0,
30        }
31    }
32
33    /// Get a parsed AST from cache
34    pub fn get(&mut self, path: &Path) -> Option<Arc<ParsedAst>> {
35        self.cache.get(&path.to_path_buf()).cloned()
36    }
37
38    /// Insert a parsed AST into cache
39    pub fn insert(&mut self, path: PathBuf, ast: ParsedAst) {
40        let size = Self::estimate_size(&ast);
41
42        // Evict entries if needed to make space
43        while self.current_size_bytes + size > self.max_size_bytes && !self.cache.is_empty() {
44            if let Some((_, evicted)) = self.cache.pop_lru() {
45                self.current_size_bytes -= Self::estimate_size(&evicted);
46            }
47        }
48
49        // Insert new entry
50        let arc_ast = Arc::new(ast);
51        if let Some((_, old)) = self.cache.push(path, arc_ast) {
52            self.current_size_bytes -= Self::estimate_size(&old);
53        }
54        self.current_size_bytes += size;
55    }
56
57    /// Clear the cache
58    pub fn clear(&mut self) {
59        self.cache.clear();
60        self.current_size_bytes = 0;
61    }
62
63    /// Get cache statistics
64    pub fn stats(&self) -> CacheStats {
65        CacheStats {
66            entries: self.cache.len(),
67            size_bytes: self.current_size_bytes,
68            max_size_bytes: self.max_size_bytes,
69            hit_rate: 0.0, // Would need to track hits/misses for this
70        }
71    }
72
73    /// Estimate size of a parsed AST in bytes
74    const fn estimate_size(ast: &ParsedAst) -> usize {
75        // Source text size + estimated tree overhead
76        ast.source.len() + (ast.root_node.children_count * 64)
77    }
78
79    /// Invalidate cache entry for a path
80    pub fn invalidate(&mut self, path: &Path) {
81        if let Some(removed) = self.cache.pop(&path.to_path_buf()) {
82            self.current_size_bytes -= Self::estimate_size(&removed);
83        }
84    }
85
86    /// Check if a path is cached
87    pub fn contains(&self, path: &Path) -> bool {
88        self.cache.contains(&path.to_path_buf())
89    }
90}
91
92impl Clone for ParserCache {
93    fn clone(&self) -> Self {
94        // Create a new cache with same capacity
95        let cap =
96            NonZeroUsize::new(100).unwrap_or_else(|| unsafe { NonZeroUsize::new_unchecked(1) });
97        let mut new_cache = LruCache::new(cap);
98
99        // Clone all entries (they're Arc'd so cheap)
100        for (k, v) in self.cache.iter() {
101            new_cache.push(k.clone(), v.clone());
102        }
103
104        Self {
105            cache: new_cache,
106            max_size_bytes: self.max_size_bytes,
107            current_size_bytes: self.current_size_bytes,
108        }
109    }
110}
111
112/// Cache statistics
113#[derive(Debug, Clone)]
114pub struct CacheStats {
115    pub entries: usize,
116    pub size_bytes: usize,
117    pub max_size_bytes: usize,
118    pub hit_rate: f64,
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::language_registry::Language;
125    use crate::language_registry::LanguageRegistry;
126
127    #[test]
128    fn test_cache_basic() {
129        let mut cache = ParserCache::new(1024 * 1024); // 1MB
130        let registry = LanguageRegistry::new();
131
132        let code = "fn main() { println!(\"Hello\"); }";
133        let ast = registry.parse(&Language::Rust, code).unwrap();
134        let path = PathBuf::from("test.rs");
135
136        // Insert and retrieve
137        cache.insert(path.clone(), ast.clone());
138        assert!(cache.contains(&path));
139
140        let cached = cache.get(&path).unwrap();
141        assert_eq!(cached.source, code);
142
143        // Invalidate
144        cache.invalidate(&path);
145        assert!(!cache.contains(&path));
146    }
147
148    #[test]
149    fn test_cache_eviction() {
150        let mut cache = ParserCache::new(100); // Very small cache
151        let registry = LanguageRegistry::new();
152
153        // Insert multiple items that exceed cache size
154        for i in 0..10 {
155            let code = format!("fn func{}() {{ /* some code */ }}", i);
156            let ast = registry.parse(&Language::Rust, &code).unwrap();
157            let path = PathBuf::from(format!("test{}.rs", i));
158            cache.insert(path, ast);
159        }
160
161        // Cache should have evicted some entries
162        assert!(cache.cache.len() < 10);
163        assert!(cache.current_size_bytes <= cache.max_size_bytes);
164    }
165}