Skip to main content

reovim_kernel/mm/
cache.rs

1//! Generic line-based cache with content hash validation.
2//!
3//! This module provides `LineCache<T>`, a lock-free cache that stores
4//! values indexed by line number with hash-based invalidation. It uses
5//! `ArcSwap` for the RCU (Read-Copy-Update) pattern, enabling lock-free
6//! reads on the hot path.
7//!
8//! # Design Philosophy
9//!
10//! Following the kernel "mechanism, not policy" principle:
11//! - Generic type parameter `T` allows any cache value type
12//! - Hash validation is provided but interpretation is up to the driver
13//! - No syntax-specific knowledge in the kernel
14//!
15//! # Performance
16//!
17//! - Read operations: O(1) hash lookup, lock-free via `ArcSwap::load()`
18//! - Write operations: O(n) RCU clone, acceptable for background updates
19//! - Memory: One `Arc<HashMap>` plus one per concurrent reader
20//!
21//! # Example
22//!
23//! ```
24//! use reovim_kernel::api::v1::*;
25//! use std::collections::HashMap;
26//!
27//! // Create a cache for syntax highlighting spans
28//! let cache: LineCache<Vec<(usize, usize)>> = LineCache::new();
29//!
30//! // Store some entries (line_idx -> (hash, value))
31//! let mut entries = HashMap::new();
32//! entries.insert(0, (12345u64, vec![(0, 5), (6, 10)]));
33//! entries.insert(1, (67890u64, vec![(0, 3)]));
34//! cache.store(entries);
35//!
36//! // Check if a line is cached with matching hash
37//! assert!(cache.has(0, 12345));
38//! assert!(!cache.has(0, 99999)); // Wrong hash
39//!
40//! // Get cached value if hash matches
41//! let spans = cache.get_if_valid(0, 12345);
42//! assert!(spans.is_some());
43//! ```
44
45use std::collections::HashMap;
46
47use reovim_arch::sync::ArcSwap;
48
49/// A generic line-based cache with hash-based validation.
50///
51/// `LineCache<T>` stores values indexed by line number, where each entry
52/// includes a content hash for invalidation checking. This enables efficient
53/// cache reuse when line content hasn't changed.
54///
55/// # Thread Safety
56///
57/// `LineCache` is `Send + Sync` when `T` is. The underlying `ArcSwap` provides
58/// lock-free reads and atomic updates via the RCU pattern.
59///
60/// # Type Parameters
61///
62/// - `T`: The cached value type. Must be `Clone + Send + Sync + 'static`.
63#[derive(Debug)]
64pub struct LineCache<T> {
65    /// Inner storage: `line_idx` -> (`content_hash`, `cached_value`)
66    inner: ArcSwap<HashMap<usize, (u64, T)>>,
67}
68
69impl<T: Clone + Send + Sync + 'static> LineCache<T> {
70    /// Create a new empty cache.
71    #[must_use]
72    pub fn new() -> Self {
73        Self {
74            inner: ArcSwap::from_pointee(HashMap::new()),
75        }
76    }
77
78    /// Check if a line is cached with a matching hash.
79    ///
80    /// Returns `true` if the line exists in the cache AND the stored hash
81    /// matches the provided hash. This is a fast validity check.
82    ///
83    /// # Arguments
84    ///
85    /// * `line_idx` - The line index to check
86    /// * `hash` - The expected content hash
87    #[must_use]
88    pub fn has(&self, line_idx: usize, hash: u64) -> bool {
89        self.inner
90            .load()
91            .get(&line_idx)
92            .is_some_and(|(h, _)| *h == hash)
93    }
94
95    /// Get cached value only if the hash matches.
96    ///
97    /// Returns `Some(value)` if the line is cached AND the stored hash
98    /// matches. Returns `None` if not cached or hash mismatch.
99    ///
100    /// # Arguments
101    ///
102    /// * `line_idx` - The line index to retrieve
103    /// * `hash` - The expected content hash
104    #[must_use]
105    pub fn get_if_valid(&self, line_idx: usize, hash: u64) -> Option<T> {
106        self.inner
107            .load()
108            .get(&line_idx)
109            .filter(|(h, _)| *h == hash)
110            .map(|(_, v)| v.clone())
111    }
112
113    /// Get cached value without hash validation.
114    ///
115    /// Returns `Some((hash, value))` if the line is cached, regardless of
116    /// whether the hash is current. Use this when you need to inspect
117    /// the cached hash.
118    #[must_use]
119    pub fn get(&self, line_idx: usize) -> Option<(u64, T)> {
120        self.inner.load().get(&line_idx).cloned()
121    }
122
123    /// Store entries in the cache, replacing all existing content.
124    ///
125    /// This uses the RCU pattern: atomically swaps the entire cache contents.
126    /// Concurrent readers see either the old or new state, never a partial update.
127    ///
128    /// # Arguments
129    ///
130    /// * `entries` - Map of `line_idx` -> (hash, value) to store
131    pub fn store(&self, entries: HashMap<usize, (u64, T)>) {
132        // RCU update: follows event_bus.rs:173-180 pattern
133        // The closure must return the new value
134        self.inner.rcu(move |_current| entries.clone());
135    }
136
137    /// Update specific entries without replacing the entire cache.
138    ///
139    /// Merges the provided entries into the existing cache. Existing entries
140    /// not in `updates` are preserved.
141    ///
142    /// # Arguments
143    ///
144    /// * `updates` - Map of `line_idx` -> (hash, value) to merge
145    pub fn update(&self, updates: &HashMap<usize, (u64, T)>) {
146        let updates = updates.clone();
147        self.inner.rcu(move |current| {
148            let mut new_map = (**current).clone();
149            new_map.extend(updates.clone());
150            new_map
151        });
152    }
153
154    /// Clone current entries for external modification.
155    ///
156    /// Returns a clone of all cached entries. Use this when you need to
157    /// inspect or transform the cache contents.
158    #[must_use]
159    pub fn clone_entries(&self) -> HashMap<usize, (u64, T)> {
160        (**self.inner.load()).clone()
161    }
162
163    /// Invalidate (remove) entries from a specific line onward.
164    ///
165    /// Removes all entries where `line_idx >= from_line`. This is useful
166    /// when text is inserted or deleted, invalidating subsequent lines.
167    ///
168    /// # Arguments
169    ///
170    /// * `from_line` - First line index to invalidate (inclusive)
171    pub fn invalidate_from(&self, from_line: usize) {
172        self.inner.rcu(|current| {
173            let filtered: HashMap<usize, (u64, T)> = current
174                .iter()
175                .filter(|(idx, _)| **idx < from_line)
176                .map(|(k, v)| (*k, v.clone()))
177                .collect();
178            filtered
179        });
180    }
181
182    /// Invalidate a specific line.
183    ///
184    /// Removes the entry for a single line if it exists.
185    ///
186    /// # Arguments
187    ///
188    /// * `line_idx` - The line index to invalidate
189    pub fn invalidate_line(&self, line_idx: usize) {
190        self.inner.rcu(|current| {
191            let mut new_map = (**current).clone();
192            new_map.remove(&line_idx);
193            new_map
194        });
195    }
196
197    /// Clear all cached entries.
198    pub fn clear(&self) {
199        self.inner.rcu(|_| HashMap::new());
200    }
201
202    /// Get the number of cached lines.
203    #[must_use]
204    pub fn len(&self) -> usize {
205        self.inner.load().len()
206    }
207
208    /// Check if the cache is empty.
209    #[must_use]
210    pub fn is_empty(&self) -> bool {
211        self.inner.load().is_empty()
212    }
213
214    /// Get all cached line indices.
215    #[must_use]
216    pub fn cached_lines(&self) -> Vec<usize> {
217        self.inner.load().keys().copied().collect()
218    }
219}
220
221impl<T: Clone + Send + Sync + 'static> Default for LineCache<T> {
222    fn default() -> Self {
223        Self::new()
224    }
225}
226
227// LineCache is automatically Send + Sync when T is, because:
228// - ArcSwap<HashMap<K, V>> is Send + Sync when HashMap<K, V> is
229// - HashMap<usize, (u64, T)> is Send + Sync when T is