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 = ParsedFile::parse(source, language).map(Arc::new);
73 self.store(key, parsed.clone());
74 parsed
75 }
76
77 pub fn stats(&self) -> SemanticParseCacheStats {
79 lock_inner(&self.inner).stats
80 }
81
82 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}