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 = ParsedFile::parse(source, language).map(Arc::new);
73        self.store(key, parsed.clone());
74        parsed
75    }
76
77    /// Returns current cache counters.
78    pub fn stats(&self) -> SemanticParseCacheStats {
79        lock_inner(&self.inner).stats
80    }
81
82    /// Clears cached entries and counters.
83    pub fn clear(&self) {
84        let mut inner = lock_inner(&self.inner);
85        inner.entries.clear();
86        inner.order.clear();
87        inner.stats = SemanticParseCacheStats::default();
88    }
89
90    fn lookup(&self, key: ParseCacheKey) -> Option<Option<Arc<ParsedFile>>> {
91        let mut inner = lock_inner(&self.inner);
92        let parsed = inner.entries.get(&key).cloned();
93        if parsed.is_some() {
94            promote_key(&mut inner.order, key);
95            inner.stats.hits += 1;
96        } else {
97            inner.stats.misses += 1;
98        }
99        parsed
100    }
101
102    fn store(&self, key: ParseCacheKey, parsed: Option<Arc<ParsedFile>>) {
103        let mut inner = lock_inner(&self.inner);
104        if self.max_entries == 0 {
105            inner.stats.stores += 1;
106            return;
107        }
108
109        if let Entry::Occupied(mut entry) = inner.entries.entry(key) {
110            entry.insert(parsed);
111            promote_key(&mut inner.order, key);
112            inner.stats.stores += 1;
113            return;
114        }
115
116        while inner.entries.len() >= self.max_entries {
117            let Some(evicted) = inner.order.pop_front() else {
118                break;
119            };
120            inner.entries.remove(&evicted);
121        }
122
123        inner.entries.insert(key, parsed);
124        inner.order.push_back(key);
125        inner.stats.stores += 1;
126    }
127}
128
129impl Default for SemanticParseCache {
130    fn default() -> Self {
131        Self::new(Self::DEFAULT_MAX_ENTRIES)
132    }
133}
134
135fn lock_inner(
136    mutex: &Mutex<SemanticParseCacheInner>,
137) -> std::sync::MutexGuard<'_, SemanticParseCacheInner> {
138    match mutex.lock() {
139        Ok(guard) => guard,
140        Err(poisoned) => poisoned.into_inner(),
141    }
142}
143
144fn promote_key(order: &mut VecDeque<ParseCacheKey>, key: ParseCacheKey) {
145    if let Some(position) = order.iter().position(|existing| *existing == key) {
146        order.remove(position);
147    }
148    order.push_back(key);
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn caches_successful_parse_results() {
157        let cache = SemanticParseCache::default();
158        let source = "fn hello() {}";
159
160        let first = cache.parse(source, Language::Rust);
161        let second = cache.parse(source, Language::Rust);
162
163        assert!(first.is_some());
164        assert!(second.is_some());
165        let stats = cache.stats();
166        assert_eq!(stats.hits, 1);
167        assert_eq!(stats.misses, 1);
168        assert_eq!(stats.stores, 1);
169    }
170
171    #[test]
172    fn caches_failed_parse_results() {
173        let cache = SemanticParseCache::default();
174        let source = "not valid";
175
176        assert!(cache.parse(source, Language::Unknown).is_none());
177        assert!(cache.parse(source, Language::Unknown).is_none());
178
179        let stats = cache.stats();
180        assert_eq!(stats.hits, 1);
181        assert_eq!(stats.misses, 1);
182        assert_eq!(stats.stores, 1);
183    }
184
185    #[test]
186    fn evicts_least_recently_used_entries_when_bound_is_reached() {
187        let cache = SemanticParseCache::new(2);
188
189        let first = "fn first() {}";
190        let second = "fn second() {}";
191        let third = "fn third() {}";
192
193        assert!(cache.parse(first, Language::Rust).is_some());
194        assert!(cache.parse(second, Language::Rust).is_some());
195        assert!(cache.parse(first, Language::Rust).is_some());
196        assert!(cache.parse(third, Language::Rust).is_some());
197
198        let stats_after_warm = cache.stats();
199        assert_eq!(stats_after_warm.hits, 1);
200
201        assert!(cache.parse(second, Language::Rust).is_some());
202        let stats = cache.stats();
203        assert_eq!(stats.hits, 1);
204        assert_eq!(stats.misses, 4);
205        assert_eq!(stats.stores, 4);
206    }
207}