1use blake3::Hasher;
34use std::cmp::{Ordering, Reverse};
35use std::collections::{BinaryHeap, HashMap};
36use std::sync::{Arc, Condvar, Mutex, MutexGuard};
37
38#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
40pub struct SourceHash(pub [u8; 32]);
41
42impl SourceHash {
43 #[must_use]
47 pub fn of(source: &[u8], extra: &[u8]) -> Self {
48 let mut hasher = Hasher::new();
49 hasher.update(source);
50 hasher.update(&[0u8; 1]);
51 hasher.update(extra);
52 let mut out = [0u8; 32];
53 out.copy_from_slice(hasher.finalize().as_bytes());
54 Self(out)
55 }
56}
57
58#[must_use]
65pub fn source_len_u32_nonzero(source: &[u8]) -> u32 {
66 u32::try_from(source.len()).unwrap_or(u32::MAX).max(1)
67}
68
69pub struct ParsedSourceLru<T> {
73 inner: Mutex<LruInner<T>>,
74}
75
76struct LruInner<T> {
77 capacity: usize,
78 entries: HashMap<SourceHash, Arc<T>>,
79 recency: HashMap<SourceHash, u64>,
80 coldest: BinaryHeap<Reverse<RecencyEntry>>,
81 in_flight: HashMap<SourceHash, Arc<InFlight<T>>>,
82 clock: u64,
83}
84
85struct InFlight<T> {
86 state: Mutex<InFlightState<T>>,
87 ready: Condvar,
88}
89
90struct InFlightState<T> {
91 result: Option<Arc<T>>,
92 panicked: bool,
93}
94
95enum CacheMissAction<T> {
96 Hit(Arc<T>),
97 Parse(Arc<InFlight<T>>),
98 Wait(Arc<InFlight<T>>),
99}
100
101#[cfg(test)]
102mod generated_source_extent_tests {
103 use super::source_len_u32_nonzero;
104
105 #[test]
106 fn source_len_u32_nonzero_pins_empty_small_and_boundary_inputs() {
107 assert_eq!(source_len_u32_nonzero(b""), 1);
108 assert_eq!(source_len_u32_nonzero(b"x"), 1);
109 assert_eq!(source_len_u32_nonzero(b"abcdef"), 6);
110
111 for len in 0usize..4096 {
112 let bytes = vec![0u8; len];
113 assert_eq!(
114 source_len_u32_nonzero(&bytes),
115 u32::try_from(len).unwrap_or(u32::MAX).max(1),
116 "generated source length case {len} must match the shared parser extent contract"
117 );
118 }
119 }
120}
121
122#[derive(Clone, Copy, Debug, Eq, PartialEq)]
123struct RecencyEntry {
124 tick: u64,
125 key: SourceHash,
126}
127
128impl Ord for RecencyEntry {
129 fn cmp(&self, other: &Self) -> Ordering {
130 self.tick
131 .cmp(&other.tick)
132 .then_with(|| self.key.cmp(&other.key))
133 }
134}
135
136impl PartialOrd for RecencyEntry {
137 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
138 Some(self.cmp(other))
139 }
140}
141
142impl<T> ParsedSourceLru<T> {
143 #[must_use]
147 pub fn with_capacity(capacity: usize) -> Self {
148 Self {
149 inner: Mutex::new(LruInner {
150 capacity,
151 entries: HashMap::with_capacity(capacity),
152 recency: HashMap::with_capacity(capacity),
153 coldest: BinaryHeap::with_capacity(capacity),
154 in_flight: HashMap::new(),
155 clock: 0,
156 }),
157 }
158 }
159
160 #[must_use]
163 pub fn get(&self, key: SourceHash) -> Option<Arc<T>> {
164 let mut inner = self.lock_inner();
165 let value = inner.entries.get(&key)?.clone();
166 bump_recency(&mut inner, key);
167 Some(value)
168 }
169
170 pub fn insert(&self, key: SourceHash, value: T) -> Arc<T> {
173 let arc = Arc::new(value);
174 let mut inner = self.lock_inner();
175 if inner.capacity == 0 {
176 return arc;
177 }
178 if !inner.entries.contains_key(&key) && inner.entries.len() >= inner.capacity {
179 if let Some(evicted) = pop_coldest_key(&mut inner) {
180 inner.entries.remove(&evicted);
181 inner.recency.remove(&evicted);
182 }
183 }
184 inner.entries.insert(key, arc.clone());
185 bump_recency(&mut inner, key);
186 arc
187 }
188
189 pub fn get_or_parse<F>(&self, source: &[u8], extra: &[u8], parse: F) -> Arc<T>
193 where
194 F: FnOnce(&[u8]) -> T,
195 {
196 let key = SourceHash::of(source, extra);
197 let mut parse = Some(parse);
198 loop {
199 match self.miss_action(key) {
200 CacheMissAction::Hit(hit) => return hit,
201 CacheMissAction::Wait(in_flight) => {
202 if let Some(result) = wait_for_in_flight(&in_flight) {
203 return result;
204 }
205 }
206 CacheMissAction::Parse(in_flight) => {
207 let Some(parse) = parse.take() else {
208 if let Some(result) = wait_for_in_flight(&in_flight) {
209 return result;
210 }
211 continue;
212 };
213 let parsed =
214 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| parse(source)));
215 match parsed {
216 Ok(value) => return self.finish_parse(key, in_flight, value),
217 Err(payload) => {
218 self.finish_panicked_parse(key, &in_flight);
219 std::panic::resume_unwind(payload);
220 }
221 }
222 }
223 }
224 }
225 }
226
227 #[must_use]
229 pub fn len(&self) -> usize {
230 self.lock_inner().entries.len()
231 }
232
233 #[must_use]
235 pub fn is_empty(&self) -> bool {
236 self.len() == 0
237 }
238
239 fn lock_inner(&self) -> MutexGuard<'_, LruInner<T>> {
245 self.inner
254 .lock()
255 .expect("parsed-source LRU cache lock was poisoned")
256 }
257
258 #[cfg(test)]
259 fn coldest_heap_len_for_diagnostics(&self) -> usize {
260 self.lock_inner().coldest.len()
261 }
262
263 fn miss_action(&self, key: SourceHash) -> CacheMissAction<T> {
264 let mut inner = self.lock_inner();
265 if let Some(value) = inner.entries.get(&key).cloned() {
266 bump_recency(&mut inner, key);
267 return CacheMissAction::Hit(value);
268 }
269 if let Some(in_flight) = inner.in_flight.get(&key) {
270 return CacheMissAction::Wait(Arc::clone(in_flight));
271 }
272 let in_flight = Arc::new(InFlight {
273 state: Mutex::new(InFlightState {
274 result: None,
275 panicked: false,
276 }),
277 ready: Condvar::new(),
278 });
279 inner.in_flight.insert(key, Arc::clone(&in_flight));
280 CacheMissAction::Parse(in_flight)
281 }
282
283 fn finish_parse(&self, key: SourceHash, in_flight: Arc<InFlight<T>>, value: T) -> Arc<T> {
284 let arc = Arc::new(value);
285 {
286 let mut inner = self.lock_inner();
287 if inner.capacity != 0 {
288 if !inner.entries.contains_key(&key) && inner.entries.len() >= inner.capacity {
289 if let Some(evicted) = pop_coldest_key(&mut inner) {
290 inner.entries.remove(&evicted);
291 inner.recency.remove(&evicted);
292 }
293 }
294 inner.entries.insert(key, Arc::clone(&arc));
295 bump_recency(&mut inner, key);
296 }
297 inner.in_flight.remove(&key);
298 }
299 let mut state = lock_in_flight_state(&in_flight);
300 state.result = Some(Arc::clone(&arc));
301 in_flight.ready.notify_all();
302 arc
303 }
304
305 fn finish_panicked_parse(&self, key: SourceHash, in_flight: &InFlight<T>) {
306 self.lock_inner().in_flight.remove(&key);
307 let mut state = lock_in_flight_state(in_flight);
308 state.panicked = true;
309 in_flight.ready.notify_all();
310 }
311}
312
313fn wait_for_in_flight<T>(in_flight: &InFlight<T>) -> Option<Arc<T>> {
314 let mut state = lock_in_flight_state(in_flight);
315 loop {
316 if let Some(result) = &state.result {
317 return Some(Arc::clone(result));
318 }
319 if state.panicked {
320 return None;
321 }
322 state = in_flight
323 .ready
324 .wait(state)
325 .unwrap_or_else(|error| error.into_inner());
326 }
327}
328
329fn lock_in_flight_state<T>(in_flight: &InFlight<T>) -> MutexGuard<'_, InFlightState<T>> {
330 in_flight
331 .state
332 .lock()
333 .unwrap_or_else(|error| error.into_inner())
334}
335
336fn bump_recency<T>(inner: &mut LruInner<T>, key: SourceHash) {
337 inner.clock = inner.clock.saturating_add(1);
338 let tick = inner.clock;
339 inner.recency.insert(key, tick);
340 inner.coldest.push(Reverse(RecencyEntry { tick, key }));
341 compact_coldest_heap_if_needed(inner);
342}
343
344fn pop_coldest_key<T>(inner: &mut LruInner<T>) -> Option<SourceHash> {
345 while let Some(Reverse(entry)) = inner.coldest.pop() {
346 if inner.entries.contains_key(&entry.key)
347 && inner.recency.get(&entry.key).copied() == Some(entry.tick)
348 {
349 return Some(entry.key);
350 }
351 }
352 None
353}
354
355fn compact_coldest_heap_if_needed<T>(inner: &mut LruInner<T>) {
356 let live = inner.entries.len();
357 if inner.coldest.len() <= live.saturating_mul(4).max(8) {
358 return;
359 }
360 inner.coldest.clear();
361 inner.coldest.reserve(live);
362 inner.coldest.extend(
363 inner
364 .recency
365 .iter()
366 .map(|(&key, &tick)| Reverse(RecencyEntry { tick, key })),
367 );
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373 use std::sync::atomic::{AtomicUsize, Ordering};
374 use std::sync::Barrier;
375
376 #[test]
378 fn source_hash_equal_for_equal_inputs() {
379 let a = SourceHash::of(b"int x = 1;", b"");
380 let b = SourceHash::of(b"int x = 1;", b"");
381 assert_eq!(a, b);
382 }
383
384 #[test]
386 fn source_hash_differs_for_different_source() {
387 let a = SourceHash::of(b"int x = 1;", b"");
388 let b = SourceHash::of(b"int x = 2;", b"");
389 assert_ne!(a, b);
390 }
391
392 #[test]
394 fn source_hash_differs_for_different_extra() {
395 let a = SourceHash::of(b"int x = 1;", b"-DA");
396 let b = SourceHash::of(b"int x = 1;", b"-DB");
397 assert_ne!(a, b);
398 }
399
400 #[test]
403 fn get_or_parse_dedups_across_callers() {
404 let cache: ParsedSourceLru<usize> = ParsedSourceLru::with_capacity(4);
405 let parse_calls = AtomicUsize::new(0);
406 let parse = || {
407 parse_calls.fetch_add(1, Ordering::SeqCst);
408 42usize
409 };
410 let src_a = b"hello world".to_vec();
411 let src_b = b"hello world".to_vec();
412 let a = cache.get_or_parse(&src_a, b"", |_s| parse());
413 let b = cache.get_or_parse(&src_b, b"", |_s| parse());
414 assert_eq!(*a, 42);
415 assert_eq!(*b, 42);
416 assert_eq!(parse_calls.load(Ordering::SeqCst), 1);
417 assert!(Arc::ptr_eq(&a, &b));
418 }
419
420 #[test]
421 fn concurrent_get_or_parse_coalesces_in_flight_parse() {
422 let cache = Arc::new(ParsedSourceLru::<usize>::with_capacity(4));
423 let source = Arc::new(b"same translation unit".to_vec());
424 let workers = 8usize;
425 let barrier = Arc::new(Barrier::new(workers));
426 let parse_calls = Arc::new(AtomicUsize::new(0));
427 let ready_to_parse = Arc::new((Mutex::new(0usize), std::sync::Condvar::new()));
428 let mut handles = Vec::with_capacity(workers);
429
430 for _ in 0..workers {
431 let cache = Arc::clone(&cache);
432 let source = Arc::clone(&source);
433 let barrier = Arc::clone(&barrier);
434 let parse_calls = Arc::clone(&parse_calls);
435 let ready_to_parse = Arc::clone(&ready_to_parse);
436 handles.push(std::thread::spawn(move || {
437 barrier.wait();
438 {
439 let (lock, wake) = ready_to_parse.as_ref();
440 let mut ready = lock
441 .lock()
442 .expect("Fix: source-cache readiness mutex must not be poisoned");
443 *ready += 1;
444 wake.notify_all();
445 }
446 cache.get_or_parse(source.as_slice(), b"", |_| {
447 parse_calls.fetch_add(1, Ordering::SeqCst);
448 let (lock, wake) = ready_to_parse.as_ref();
449 let mut ready = lock
450 .lock()
451 .expect("Fix: source-cache readiness mutex must not be poisoned");
452 while *ready < workers {
453 ready = wake
454 .wait(ready)
455 .expect("Fix: source-cache readiness condvar must not be poisoned");
456 }
457 99usize
458 })
459 }));
460 }
461
462 let results = handles
463 .into_iter()
464 .map(|handle| {
465 handle
466 .join()
467 .expect("Fix: source-cache worker must not panic")
468 })
469 .collect::<Vec<_>>();
470
471 assert_eq!(
472 parse_calls.load(Ordering::SeqCst),
473 1,
474 "Fix: concurrent same-key parse requests must share one in-flight parse"
475 );
476 for result in &results {
477 assert_eq!(**result, 99);
478 assert!(
479 Arc::ptr_eq(result, &results[0]),
480 "Fix: all waiters must receive the same cached Arc"
481 );
482 }
483 }
484
485 #[test]
488 fn lru_evicts_oldest_when_capacity_reached() {
489 let cache: ParsedSourceLru<u32> = ParsedSourceLru::with_capacity(2);
490 let _a = cache.get_or_parse(b"a", b"", |_| 1u32);
491 let _b = cache.get_or_parse(b"b", b"", |_| 2u32);
492 let _c = cache.get_or_parse(b"c", b"", |_| 3u32);
493 assert_eq!(cache.len(), 2);
494 assert!(cache.get(SourceHash::of(b"a", b"")).is_none());
495 assert!(cache.get(SourceHash::of(b"b", b"")).is_some());
496 assert!(cache.get(SourceHash::of(b"c", b"")).is_some());
497 }
498
499 #[test]
502 fn lru_recency_promotes_on_get() {
503 let cache: ParsedSourceLru<u32> = ParsedSourceLru::with_capacity(2);
504 let _a = cache.get_or_parse(b"a", b"", |_| 1u32);
505 let _b = cache.get_or_parse(b"b", b"", |_| 2u32);
506 assert!(cache.get(SourceHash::of(b"a", b"")).is_some());
507 let _c = cache.get_or_parse(b"c", b"", |_| 3u32);
508 assert!(cache.get(SourceHash::of(b"a", b"")).is_some());
509 assert!(cache.get(SourceHash::of(b"b", b"")).is_none());
510 assert!(cache.get(SourceHash::of(b"c", b"")).is_some());
511 }
512
513 #[test]
514 fn lru_eviction_does_not_scan_or_retain_stream_length_stale_recency() {
515 let cache: ParsedSourceLru<u32> = ParsedSourceLru::with_capacity(8);
516 for i in 0..128u32 {
517 let source = i.to_le_bytes();
518 let _ = cache.get_or_parse(&source, b"", |_| i);
519 }
520
521 assert_eq!(cache.len(), 8);
522 assert!(
523 cache.coldest_heap_len_for_diagnostics() <= 32,
524 "Fix: parsed-source LRU stale recency heap must stay cache-capacity scale, not corpus-size scale"
525 );
526 }
527
528 #[test]
530 fn capacity_zero_disables_caching() {
531 let cache: ParsedSourceLru<u32> = ParsedSourceLru::with_capacity(0);
532 let calls = AtomicUsize::new(0);
533 assert_eq!(
534 *cache.get_or_parse(b"a", b"", |_| {
535 calls.fetch_add(1, Ordering::SeqCst);
536 7u32
537 }),
538 7
539 );
540 assert_eq!(
541 *cache.get_or_parse(b"a", b"", |_| {
542 calls.fetch_add(1, Ordering::SeqCst);
543 7u32
544 }),
545 7
546 );
547 assert_eq!(calls.load(Ordering::SeqCst), 2);
548 assert_eq!(cache.len(), 0);
549 }
550
551 #[test]
553 fn is_empty_tracks_population() {
554 let cache: ParsedSourceLru<u32> = ParsedSourceLru::with_capacity(2);
555 assert!(cache.is_empty());
556 assert_eq!(*cache.get_or_parse(b"a", b"", |_| 1u32), 1);
557 assert!(!cache.is_empty());
558 }
559
560 #[test]
563 fn insert_existing_key_does_not_evict() {
564 let cache: ParsedSourceLru<u32> = ParsedSourceLru::with_capacity(2);
565 let key = SourceHash::of(b"a", b"");
566 assert!(cache.get(key).is_none());
567 let _first = cache.insert(key, 1);
568 assert_eq!(*cache.get(key).expect("Fix: after first insert"), 1);
569 let _second = cache.insert(key, 2);
570 assert_eq!(*cache.get(key).expect("Fix: after second insert"), 2);
571 assert_eq!(cache.len(), 1);
572 }
573
574 #[test]
575 fn poisoned_source_cache_lock_is_not_silently_recovered() {
576 let cache = Arc::new(ParsedSourceLru::<u32>::with_capacity(2));
577 let poisoned = Arc::clone(&cache);
578 let _ = std::thread::spawn(move || {
579 let _guard = poisoned.lock_inner();
580 panic!("poison parsed-source cache");
581 })
582 .join();
583
584 let panic = std::panic::catch_unwind(|| {
585 let _ = cache.len();
586 })
587 .expect_err("poisoned parsed-source cache must panic instead of recovering");
588 let message = panic
589 .downcast_ref::<String>()
590 .map(String::as_str)
591 .or_else(|| panic.downcast_ref::<&'static str>().copied())
592 .unwrap_or("<non-string panic>");
593 assert!(
594 message.contains("parsed-source LRU cache lock was poisoned"),
595 "{message}"
596 );
597 }
598}