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 const DEFAULT_MAX_ENTRIES: usize = 256;
46
47 pub fn new(max_entries: usize) -> Self {
49 Self {
50 inner: Mutex::new(SemanticParseCacheInner::default()),
51 max_entries,
52 }
53 }
54
55 pub fn shared() -> &'static Self {
57 static CACHE: OnceLock<SemanticParseCache> = OnceLock::new();
58 CACHE.get_or_init(Self::default)
59 }
60
61 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 pub fn stats(&self) -> SemanticParseCacheStats {
81 lock_inner(&self.inner).stats
82 }
83
84 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}