Skip to main content

vyre_libs/parsing/
source_cache.rs

1//! ROADMAP L2 / E2  -  content-hash LRU cache for parsed source.
2//!
3//! Substrate that any language's parse pipeline can opt into without
4//! plumbing a cache through every layer of the parser. The cache is
5//! keyed by the BLAKE3 content hash of the source bytes (or any
6//! caller-chosen extra-key extension), so two callers with the same
7//! source share the parsed artifact even if they hold distinct
8//! string allocations.
9//!
10//! ## Why content hash, not string identity
11//!
12//! In the downstream analyzer scan loop the same `.h` header is included from
13//! many translation units. Identity-keyed memoisation misses every
14//! caller because each caller holds its own `String`. Content-hash
15//! lookup lets every translation unit share a single parse.
16//!
17//! ## Why LRU, not unbounded
18//!
19//! Workspace scans touch tens of thousands of distinct files. An
20//! unbounded cache grows without bound; an LRU bounded by entry
21//! count keeps the working set in memory and evicts cold entries
22//! deterministically. Hits refresh recency in O(1); eviction scans
23//! only the bounded live set on insert.
24//!
25//! ## Thread safety
26//!
27//! The cache is `Send + Sync`  -  backed by a `Mutex<...>` so the
28//! parse work proceeds outside the global cache lock and only the lookup /
29//! insert / eviction touches it. Concurrent callers asking for the same key
30//! coalesce through a per-key in-flight slot, so the expensive parse closure
31//! runs once and every waiter receives the same `Arc<T>`.
32
33use blake3::Hasher;
34use std::cmp::{Ordering, Reverse};
35use std::collections::{BinaryHeap, HashMap};
36use std::sync::{Arc, Condvar, Mutex, MutexGuard};
37
38/// 32-byte BLAKE3 content hash used as the cache key.
39#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
40pub struct SourceHash(pub [u8; 32]);
41
42impl SourceHash {
43    /// Hash `source` plus the optional `extra` discriminator. The
44    /// `extra` channel lets callers separate caches that share source
45    /// bytes but differ in build flags (e.g. preprocessor `-D` set).
46    #[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/// Convert source byte length to the non-zero `u32` extent used by generated
59/// parsing Programs.
60///
61/// Empty inputs still need one logical lane so generated Programs keep their
62/// buffer declarations valid. Inputs larger than `u32::MAX` saturate at the
63/// maximum Program-visible extent instead of panicking during cache population.
64#[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
69/// Bounded LRU cache mapping `SourceHash` to `Arc<T>`. Eviction is
70/// LRU by last-touched order. Hits are O(1); eviction scans the bounded live
71/// set only when an insert needs space.
72pub 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    /// Build an empty cache that holds at most `capacity` entries.
144    /// `capacity == 0` disables caching entirely (every lookup is a
145    /// miss and nothing is stored).
146    #[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    /// Look up `key`. Returns the cached `Arc<T>` on hit (and bumps
161    /// recency); returns `None` on miss.
162    #[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    /// Insert `value` for `key`, evicting the oldest entry if the
171    /// cache is at capacity. Returns the inserted `Arc<T>`.
172    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    /// Look up `key`; on miss, run `parse(source)` to produce the
190    /// value and insert it. Returns the cached or freshly inserted
191    /// `Arc<T>`.
192    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    /// Total number of entries currently held.
228    #[must_use]
229    pub fn len(&self) -> usize {
230        self.lock_inner().entries.len()
231    }
232
233    /// `true` iff the cache holds zero entries.
234    #[must_use]
235    pub fn is_empty(&self) -> bool {
236        self.len() == 0
237    }
238
239    /// Lock the LRU inner state, failing closed on a poisoned lock.
240    ///
241    /// # Panics
242    /// Panics when the lock is poisoned. A poisoned lock means a panic left the LRU links
243    /// inconsistent, and continuing would serve entries the cache no longer owns.
244    fn lock_inner(&self) -> MutexGuard<'_, LruInner<T>> {
245        // Fail closed on a poisoned lock instead of silently recovering with
246        // `into_inner()` (Law 10). A poison here means another thread panicked
247        // mid-mutation of the LRU maps (entries/recency/coldest can be left
248        // inconsistent), so handing that half-updated state back as if nothing
249        // happened would silently corrupt every subsequent get/insert. Propagate
250        // the panic loudly (same poison policy as the readiness mutex below).
251        // (The in-flight-parse state lock is a SEPARATE, deliberate protocol:
252        // a panicking parse is an expected, flagged condition there.)
253        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    /// Same content + same extra hash to the same key.
377    #[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    /// Distinct content hashes to distinct keys.
385    #[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    /// Distinct extras hash to distinct keys even with the same source.
393    #[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    /// `get_or_parse` is invoked once per content-hash, even when the
401    /// source bytes come from distinct caller `Vec` allocations.
402    #[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    /// LRU eviction kicks the least-recently-used entry when capacity
486    /// is reached.
487    #[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    /// Re-fetching an entry bumps it to most-recently-used so a
500    /// subsequent insertion evicts a different one.
501    #[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    /// Capacity 0 disables caching: the parse closure runs every call.
529    #[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    /// `is_empty` reflects emptiness vs. populated-state.
552    #[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    /// Updating an existing key keeps capacity stable (the `len` stays
561    /// at one, no eviction loop fires).
562    #[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}