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    /// Shared parse-cache page. Capture-time corpus file budget uses this
46    /// same cap so a real repo is not fail-closed by a smaller universe.
47    pub const DEFAULT_MAX_ENTRIES: usize = 256;
48
49    /// Create a bounded parse cache.
50    pub fn new(max_entries: usize) -> Self {
51        Self {
52            inner: Mutex::new(SemanticParseCacheInner::default()),
53            max_entries,
54        }
55    }
56
57    /// Returns the process-wide cache instance.
58    pub fn shared() -> &'static Self {
59        static CACHE: OnceLock<SemanticParseCache> = OnceLock::new();
60        CACHE.get_or_init(Self::default)
61    }
62
63    /// Parse a source file, reusing a cached AST when available.
64    pub fn parse(&self, source: &str, language: Language) -> Option<Arc<ParsedFile>> {
65        let key = ParseCacheKey {
66            content_hash: ContentHash::compute(source.as_bytes()),
67            language,
68        };
69
70        if let Some(parsed) = self.lookup(key) {
71            return parsed;
72        }
73
74        let parsed =
75            ParsedFile::parse_with_hash(Arc::<str>::from(source), language, key.content_hash)
76                .map(Arc::new);
77        self.store(key, parsed.clone());
78        parsed
79    }
80
81    /// Returns current cache counters.
82    pub fn stats(&self) -> SemanticParseCacheStats {
83        lock_inner(&self.inner).stats
84    }
85
86    /// Clears cached entries and counters.
87    pub fn clear(&self) {
88        let mut inner = lock_inner(&self.inner);
89        inner.entries.clear();
90        inner.order.clear();
91        inner.stats = SemanticParseCacheStats::default();
92    }
93
94    fn lookup(&self, key: ParseCacheKey) -> Option<Option<Arc<ParsedFile>>> {
95        let mut inner = lock_inner(&self.inner);
96        let parsed = inner.entries.get(&key).cloned();
97        if parsed.is_some() {
98            promote_key(&mut inner.order, key);
99            inner.stats.hits += 1;
100        } else {
101            inner.stats.misses += 1;
102        }
103        parsed
104    }
105
106    fn store(&self, key: ParseCacheKey, parsed: Option<Arc<ParsedFile>>) {
107        let mut inner = lock_inner(&self.inner);
108        if self.max_entries == 0 {
109            inner.stats.stores += 1;
110            return;
111        }
112
113        if let Entry::Occupied(mut entry) = inner.entries.entry(key) {
114            entry.insert(parsed);
115            promote_key(&mut inner.order, key);
116            inner.stats.stores += 1;
117            return;
118        }
119
120        while inner.entries.len() >= self.max_entries {
121            let Some(evicted) = inner.order.pop_front() else {
122                break;
123            };
124            inner.entries.remove(&evicted);
125        }
126
127        inner.entries.insert(key, parsed);
128        inner.order.push_back(key);
129        inner.stats.stores += 1;
130    }
131}
132
133impl Default for SemanticParseCache {
134    fn default() -> Self {
135        Self::new(Self::DEFAULT_MAX_ENTRIES)
136    }
137}
138
139fn lock_inner(
140    mutex: &Mutex<SemanticParseCacheInner>,
141) -> std::sync::MutexGuard<'_, SemanticParseCacheInner> {
142    match mutex.lock() {
143        Ok(guard) => guard,
144        Err(poisoned) => poisoned.into_inner(),
145    }
146}
147
148fn promote_key(order: &mut VecDeque<ParseCacheKey>, key: ParseCacheKey) {
149    if let Some(position) = order.iter().position(|existing| *existing == key) {
150        order.remove(position);
151    }
152    order.push_back(key);
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn caches_successful_parse_results() {
161        let cache = SemanticParseCache::default();
162        let source = "fn hello() {}";
163
164        let first = cache.parse(source, Language::Rust);
165        let second = cache.parse(source, Language::Rust);
166
167        assert!(first.is_some());
168        assert!(second.is_some());
169        let stats = cache.stats();
170        assert_eq!(stats.hits, 1);
171        assert_eq!(stats.misses, 1);
172        assert_eq!(stats.stores, 1);
173    }
174
175    #[test]
176    fn caches_failed_parse_results() {
177        let cache = SemanticParseCache::default();
178        let source = "not valid";
179
180        assert!(cache.parse(source, Language::Unknown).is_none());
181        assert!(cache.parse(source, Language::Unknown).is_none());
182
183        let stats = cache.stats();
184        assert_eq!(stats.hits, 1);
185        assert_eq!(stats.misses, 1);
186        assert_eq!(stats.stores, 1);
187    }
188
189    #[test]
190    fn evicts_least_recently_used_entries_when_bound_is_reached() {
191        let cache = SemanticParseCache::new(2);
192
193        let first = "fn first() {}";
194        let second = "fn second() {}";
195        let third = "fn third() {}";
196
197        assert!(cache.parse(first, Language::Rust).is_some());
198        assert!(cache.parse(second, Language::Rust).is_some());
199        assert!(cache.parse(first, Language::Rust).is_some());
200        assert!(cache.parse(third, Language::Rust).is_some());
201
202        let stats_after_warm = cache.stats();
203        assert_eq!(stats_after_warm.hits, 1);
204
205        assert!(cache.parse(second, Language::Rust).is_some());
206        let stats = cache.stats();
207        assert_eq!(stats.hits, 1);
208        assert_eq!(stats.misses, 4);
209        assert_eq!(stats.stores, 4);
210    }
211}