1use 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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
21pub struct SemanticParseCacheStats {
22 pub hits: usize,
24 pub misses: usize,
26 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#[derive(Debug)]
39pub struct SemanticParseCache {
40 inner: Mutex<SemanticParseCacheInner>,
41 max_entries: usize,
42}
43
44impl SemanticParseCache {
45 pub const DEFAULT_MAX_ENTRIES: usize = 256;
48
49 pub fn new(max_entries: usize) -> Self {
51 Self {
52 inner: Mutex::new(SemanticParseCacheInner::default()),
53 max_entries,
54 }
55 }
56
57 pub fn shared() -> &'static Self {
59 static CACHE: OnceLock<SemanticParseCache> = OnceLock::new();
60 CACHE.get_or_init(Self::default)
61 }
62
63 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 pub fn stats(&self) -> SemanticParseCacheStats {
83 lock_inner(&self.inner).stats
84 }
85
86 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}