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