Skip to main content

semantic/
cache.rs

1// SPDX-License-Identifier: Apache-2.0
2//! In-memory semantic parse cache keyed by stable content identity.
3
4use std::{
5    collections::{HashMap, VecDeque, hash_map::Entry},
6    sync::{Arc, Mutex, OnceLock},
7};
8
9use objects::object::ContentHash;
10
11use crate::parser::{Language, ParsedFile};
12
13#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
14struct ParseCacheKey {
15    content_hash: ContentHash,
16    language: Language,
17}
18
19/// Parse cache counters for warm/cold benchmarking.
20#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
21pub struct SemanticParseCacheStats {
22    /// Number of successful cache hits.
23    pub hits: usize,
24    /// Number of cache misses.
25    pub misses: usize,
26    /// Number of entries inserted into the cache.
27    pub stores: usize,
28}
29
30#[derive(Debug, Default)]
31struct SemanticParseCacheInner {
32    entries: HashMap<ParseCacheKey, Option<Arc<ParsedFile>>>,
33    order: VecDeque<ParseCacheKey>,
34    stats: SemanticParseCacheStats,
35}
36
37/// Shared cache for parsed semantic artifacts.
38#[derive(Debug)]
39pub struct SemanticParseCache {
40    inner: Mutex<SemanticParseCacheInner>,
41    max_entries: usize,
42}
43
44impl SemanticParseCache {
45    const DEFAULT_MAX_ENTRIES: usize = 256;
46
47    /// Create a bounded parse cache.
48    pub fn new(max_entries: usize) -> Self {
49        Self {
50            inner: Mutex::new(SemanticParseCacheInner::default()),
51            max_entries,
52        }
53    }
54
55    /// Returns the process-wide cache instance.
56    pub fn shared() -> &'static Self {
57        static CACHE: OnceLock<SemanticParseCache> = OnceLock::new();
58        CACHE.get_or_init(Self::default)
59    }
60
61    /// Parse a source file, reusing a cached AST when available.
62    pub fn parse(&self, source: &str, language: Language) -> Option<Arc<ParsedFile>> {
63        let key = ParseCacheKey {
64            content_hash: ContentHash::compute(source.as_bytes()),
65            language,
66        };
67
68        if let Some(parsed) = self.lookup(key) {
69            return parsed;
70        }
71
72        let parsed =
73            ParsedFile::parse_with_hash(Arc::<str>::from(source), language, key.content_hash)
74                .map(Arc::new);
75        self.store(key, parsed.clone());
76        parsed
77    }
78
79    /// Returns current cache counters.
80    pub fn stats(&self) -> SemanticParseCacheStats {
81        lock_inner(&self.inner).stats
82    }
83
84    /// Clears cached entries and counters.
85    pub fn clear(&self) {
86        let mut inner = lock_inner(&self.inner);
87        inner.entries.clear();
88        inner.order.clear();
89        inner.stats = SemanticParseCacheStats::default();
90    }
91
92    fn lookup(&self, key: ParseCacheKey) -> Option<Option<Arc<ParsedFile>>> {
93        let mut inner = lock_inner(&self.inner);
94        let parsed = inner.entries.get(&key).cloned();
95        if parsed.is_some() {
96            promote_key(&mut inner.order, key);
97            inner.stats.hits += 1;
98        } else {
99            inner.stats.misses += 1;
100        }
101        parsed
102    }
103
104    fn store(&self, key: ParseCacheKey, parsed: Option<Arc<ParsedFile>>) {
105        let mut inner = lock_inner(&self.inner);
106        if self.max_entries == 0 {
107            inner.stats.stores += 1;
108            return;
109        }
110
111        if let Entry::Occupied(mut entry) = inner.entries.entry(key) {
112            entry.insert(parsed);
113            promote_key(&mut inner.order, key);
114            inner.stats.stores += 1;
115            return;
116        }
117
118        while inner.entries.len() >= self.max_entries {
119            let Some(evicted) = inner.order.pop_front() else {
120                break;
121            };
122            inner.entries.remove(&evicted);
123        }
124
125        inner.entries.insert(key, parsed);
126        inner.order.push_back(key);
127        inner.stats.stores += 1;
128    }
129}
130
131impl Default for SemanticParseCache {
132    fn default() -> Self {
133        Self::new(Self::DEFAULT_MAX_ENTRIES)
134    }
135}
136
137fn lock_inner(
138    mutex: &Mutex<SemanticParseCacheInner>,
139) -> std::sync::MutexGuard<'_, SemanticParseCacheInner> {
140    match mutex.lock() {
141        Ok(guard) => guard,
142        Err(poisoned) => poisoned.into_inner(),
143    }
144}
145
146fn promote_key(order: &mut VecDeque<ParseCacheKey>, key: ParseCacheKey) {
147    if let Some(position) = order.iter().position(|existing| *existing == key) {
148        order.remove(position);
149    }
150    order.push_back(key);
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn caches_successful_parse_results() {
159        let cache = SemanticParseCache::default();
160        let source = "fn hello() {}";
161
162        let first = cache.parse(source, Language::Rust);
163        let second = cache.parse(source, Language::Rust);
164
165        assert!(first.is_some());
166        assert!(second.is_some());
167        let stats = cache.stats();
168        assert_eq!(stats.hits, 1);
169        assert_eq!(stats.misses, 1);
170        assert_eq!(stats.stores, 1);
171    }
172
173    #[test]
174    fn caches_failed_parse_results() {
175        let cache = SemanticParseCache::default();
176        let source = "not valid";
177
178        assert!(cache.parse(source, Language::Unknown).is_none());
179        assert!(cache.parse(source, Language::Unknown).is_none());
180
181        let stats = cache.stats();
182        assert_eq!(stats.hits, 1);
183        assert_eq!(stats.misses, 1);
184        assert_eq!(stats.stores, 1);
185    }
186
187    #[test]
188    fn evicts_least_recently_used_entries_when_bound_is_reached() {
189        let cache = SemanticParseCache::new(2);
190
191        let first = "fn first() {}";
192        let second = "fn second() {}";
193        let third = "fn third() {}";
194
195        assert!(cache.parse(first, Language::Rust).is_some());
196        assert!(cache.parse(second, Language::Rust).is_some());
197        assert!(cache.parse(first, Language::Rust).is_some());
198        assert!(cache.parse(third, Language::Rust).is_some());
199
200        let stats_after_warm = cache.stats();
201        assert_eq!(stats_after_warm.hits, 1);
202
203        assert!(cache.parse(second, Language::Rust).is_some());
204        let stats = cache.stats();
205        assert_eq!(stats.hits, 1);
206        assert_eq!(stats.misses, 4);
207        assert_eq!(stats.stores, 4);
208    }
209}