Coverage Report

Created: 2025-09-08 21:26

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/home/noah/src/ruchy/src/runtime/cache.rs
Line
Count
Source
1
//! Bytecode and compilation caching for improved REPL performance
2
//!
3
//! This module provides caching mechanisms to avoid re-parsing and
4
//! re-compiling expressions that have been seen before.
5
6
use std::cell::RefCell;
7
use std::collections::HashMap;
8
use std::hash::{Hash, Hasher};
9
use std::rc::Rc;
10
11
use crate::frontend::ast::Expr;
12
13
/// A cache key that represents source code
14
#[derive(Clone, Debug, Eq)]
15
pub struct CacheKey {
16
    /// The source code
17
    source: String,
18
    /// Hash of the source for fast comparison
19
    hash: u64,
20
}
21
22
impl CacheKey {
23
    /// Create a new cache key from source code
24
49
    pub fn new(source: String) -> Self {
25
49
        let hash = {
26
49
            let mut hasher = std::collections::hash_map::DefaultHasher::new();
27
49
            source.hash(&mut hasher);
28
49
            hasher.finish()
29
        };
30
49
        CacheKey { source, hash }
31
49
    }
32
}
33
34
impl PartialEq for CacheKey {
35
62
    fn eq(&self, other: &Self) -> bool {
36
62
        self.hash == other.hash && 
self.source == other.source44
37
62
    }
38
}
39
40
impl Hash for CacheKey {
41
39
    fn hash<H: Hasher>(&self, state: &mut H) {
42
39
        self.hash.hash(state);
43
39
    }
44
}
45
46
/// Cached compilation result
47
#[derive(Clone)]
48
pub struct CachedResult {
49
    /// The parsed AST
50
    pub ast: Rc<Expr>,
51
    /// The transpiled Rust code (if applicable)
52
    pub rust_code: Option<String>,
53
    /// Timestamp when cached
54
    pub timestamp: std::time::Instant,
55
}
56
57
/// Bytecode cache for REPL expressions
58
pub struct BytecodeCache {
59
    /// Cache storage
60
    cache: RefCell<HashMap<CacheKey, CachedResult>>,
61
    /// Maximum cache size (number of entries)
62
    max_size: usize,
63
    /// Track access order for LRU eviction
64
    access_order: RefCell<Vec<CacheKey>>,
65
    /// Statistics
66
    hits: RefCell<usize>,
67
    misses: RefCell<usize>,
68
}
69
70
impl BytecodeCache {
71
    /// Create a new bytecode cache with specified max size
72
17
    pub fn with_capacity(max_size: usize) -> Self {
73
17
        BytecodeCache {
74
17
            cache: RefCell::new(HashMap::with_capacity(max_size)),
75
17
            max_size,
76
17
            access_order: RefCell::new(Vec::with_capacity(max_size)),
77
17
            hits: RefCell::new(0),
78
17
            misses: RefCell::new(0),
79
17
        }
80
17
    }
81
82
    /// Create a default cache with 1000 entry capacity
83
15
    pub fn new() -> Self {
84
15
        Self::with_capacity(1000)
85
15
    }
86
87
    /// Get a cached result if available
88
26
    pub fn get(&self, source: &str) -> Option<CachedResult> {
89
26
        let key = CacheKey::new(source.to_string());
90
91
26
        if let Some(
result19
) = self.cache.borrow().get(&key) {
92
19
            *self.hits.borrow_mut() += 1;
93
94
            // Update access order for LRU
95
19
            let mut access = self.access_order.borrow_mut();
96
35
            if let Some(
pos19
) =
access.iter()19
.
position19
(|k| k == &key) {
97
19
                access.remove(pos);
98
19
            
}0
99
19
            access.push(key.clone());
100
101
19
            Some(result.clone())
102
        } else {
103
7
            *self.misses.borrow_mut() += 1;
104
7
            None
105
        }
106
26
    }
107
108
    /// Store a compilation result in the cache
109
18
    pub fn insert(&self, source: String, ast: Rc<Expr>, rust_code: Option<String>) {
110
18
        let key = CacheKey::new(source);
111
112
        // Check if we need to evict
113
18
        if self.cache.borrow().len() >= self.max_size {
114
1
            self.evict_lru();
115
17
        }
116
117
18
        let result = CachedResult {
118
18
            ast,
119
18
            rust_code,
120
18
            timestamp: std::time::Instant::now(),
121
18
        };
122
123
18
        self.cache.borrow_mut().insert(key.clone(), result);
124
18
        self.access_order.borrow_mut().push(key);
125
18
    }
126
127
    /// Evict least recently used entry
128
1
    fn evict_lru(&self) {
129
1
        let mut access = self.access_order.borrow_mut();
130
1
        if !access.is_empty() {
131
1
            let lru_key = access.remove(0);
132
1
            self.cache.borrow_mut().remove(&lru_key);
133
1
        
}0
134
1
    }
135
136
    /// Clear the entire cache
137
2
    pub fn clear(&self) {
138
2
        self.cache.borrow_mut().clear();
139
2
        self.access_order.borrow_mut().clear();
140
2
        *self.hits.borrow_mut() = 0;
141
2
        *self.misses.borrow_mut() = 0;
142
2
    }
143
144
    /// Get cache statistics
145
6
    pub fn stats(&self) -> CacheStats {
146
6
        CacheStats {
147
6
            size: self.cache.borrow().len(),
148
6
            capacity: self.max_size,
149
6
            hits: *self.hits.borrow(),
150
6
            misses: *self.misses.borrow(),
151
6
            hit_rate: self.calculate_hit_rate(),
152
6
        }
153
6
    }
154
155
    /// Calculate hit rate as a percentage
156
    #[allow(clippy::cast_precision_loss)]
157
6
    fn calculate_hit_rate(&self) -> f64 {
158
6
        let hits = *self.hits.borrow() as f64;
159
6
        let total = hits + *self.misses.borrow() as f64;
160
6
        if total > 0.0 {
161
4
            (hits / total) * 100.0
162
        } else {
163
2
            0.0
164
        }
165
6
    }
166
167
    /// Remove entries older than specified duration
168
1
    pub fn evict_older_than(&self, age: std::time::Duration) {
169
1
        let now = std::time::Instant::now();
170
1
        let mut cache = self.cache.borrow_mut();
171
1
        let mut access = self.access_order.borrow_mut();
172
173
        // Find keys to remove
174
1
        let keys_to_remove: Vec<CacheKey> = cache
175
1
            .iter()
176
1
            .filter(|(_, result)| now.duration_since(result.timestamp) > age)
177
1
            .map(|(key, _)| key.clone())
178
1
            .collect();
179
180
        // Remove from cache and access order
181
2
        for 
key1
in keys_to_remove {
182
1
            cache.remove(&key);
183
1
            if let Some(pos) = access.iter().position(|k| k == &key) {
184
1
                access.remove(pos);
185
1
            
}0
186
        }
187
1
    }
188
}
189
190
impl Default for BytecodeCache {
191
0
    fn default() -> Self {
192
0
        Self::new()
193
0
    }
194
}
195
196
/// Cache statistics
197
#[derive(Debug, Clone)]
198
pub struct CacheStats {
199
    /// Current number of cached entries
200
    pub size: usize,
201
    /// Maximum capacity
202
    pub capacity: usize,
203
    /// Number of cache hits
204
    pub hits: usize,
205
    /// Number of cache misses
206
    pub misses: usize,
207
    /// Hit rate percentage
208
    pub hit_rate: f64,
209
}
210
211
impl std::fmt::Display for CacheStats {
212
0
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
213
0
        write!(
214
0
            f,
215
0
            "Cache: {}/{} entries, {} hits, {} misses ({:.1}% hit rate)",
216
            self.size, self.capacity, self.hits, self.misses, self.hit_rate
217
        )
218
0
    }
219
}
220
221
/// Expression cache for parsed ASTs
222
pub struct ExpressionCache {
223
    inner: BytecodeCache,
224
}
225
226
impl ExpressionCache {
227
    /// Create a new expression cache
228
8
    pub fn new() -> Self {
229
8
        ExpressionCache {
230
8
            inner: BytecodeCache::new(),
231
8
        }
232
8
    }
233
234
    /// Try to get a parsed expression from cache
235
7
    pub fn get_parsed(&self, source: &str) -> Option<Rc<Expr>> {
236
7
        self.inner.get(source).map(|result| result.ast)
237
7
    }
238
239
    /// Cache a parsed expression
240
4
    pub fn cache_parsed(&self, source: String, ast: Rc<Expr>) {
241
4
        self.inner.insert(source, ast, None);
242
4
    }
243
244
    /// Try to get transpiled code from cache
245
2
    pub fn get_transpiled(&self, source: &str) -> Option<String> {
246
2
        self.inner.get(source).and_then(|result| result.rust_code)
247
2
    }
248
249
    /// Cache transpiled code
250
1
    pub fn cache_transpiled(&self, source: String, ast: Rc<Expr>, rust_code: String) {
251
1
        self.inner.insert(source, ast, Some(rust_code));
252
1
    }
253
254
    /// Get cache statistics
255
1
    pub fn stats(&self) -> CacheStats {
256
1
        self.inner.stats()
257
1
    }
258
259
    /// Clear the cache
260
1
    pub fn clear(&self) {
261
1
        self.inner.clear();
262
1
    }
263
}
264
265
impl Default for ExpressionCache {
266
0
    fn default() -> Self {
267
0
        Self::new()
268
0
    }
269
}
270
271
#[cfg(test)]
272
#[allow(clippy::unwrap_used)]
273
mod tests {
274
    use super::*;
275
    use crate::frontend::ast::{ExprKind, Literal, Span};
276
277
    fn make_test_expr(value: i64) -> Rc<Expr> {
278
        Rc::new(Expr {
279
            kind: ExprKind::Literal(Literal::Integer(value)),
280
            span: Span { start: 0, end: 0 },
281
            attributes: Vec::new(),
282
        })
283
    }
284
285
    #[test]
286
    fn test_cache_key() {
287
        let key1 = CacheKey::new("let x = 42".to_string());
288
        let key2 = CacheKey::new("let x = 42".to_string());
289
        let key3 = CacheKey::new("let y = 42".to_string());
290
291
        assert_eq!(key1, key2);
292
        assert_ne!(key1, key3);
293
    }
294
295
    #[test]
296
    fn test_bytecode_cache_basic() {
297
        let cache = BytecodeCache::with_capacity(3);
298
299
        // Cache miss
300
        assert!(cache.get("let x = 1").is_none());
301
        assert_eq!(cache.stats().misses, 1);
302
303
        // Insert and hit
304
        cache.insert("let x = 1".to_string(), make_test_expr(1), None);
305
        assert!(cache.get("let x = 1").is_some());
306
        assert_eq!(cache.stats().hits, 1);
307
    }
308
309
    #[test]
310
    fn test_cache_lru_eviction() {
311
        let cache = BytecodeCache::with_capacity(2);
312
313
        cache.insert("expr1".to_string(), make_test_expr(1), None);
314
        cache.insert("expr2".to_string(), make_test_expr(2), None);
315
316
        // Access expr1 to make it more recent
317
        let _ = cache.get("expr1");
318
319
        // This should evict expr2 (least recently used)
320
        cache.insert("expr3".to_string(), make_test_expr(3), None);
321
322
        assert!(cache.get("expr1").is_some());
323
        assert!(cache.get("expr2").is_none()); // Evicted
324
        assert!(cache.get("expr3").is_some());
325
    }
326
327
    #[test]
328
    fn test_expression_cache() {
329
        let cache = ExpressionCache::new();
330
331
        let expr = make_test_expr(42);
332
        cache.cache_parsed("let x = 42".to_string(), Rc::clone(&expr));
333
334
        let cached = cache.get_parsed("let x = 42").unwrap();
335
        assert!(Rc::ptr_eq(&expr, &cached));
336
337
        // Test transpiled code caching
338
        cache.cache_transpiled(
339
            "let y = 10".to_string(),
340
            make_test_expr(10),
341
            "let y = 10;".to_string(),
342
        );
343
344
        assert_eq!(
345
            cache.get_transpiled("let y = 10"),
346
            Some("let y = 10;".to_string())
347
        );
348
    }
349
350
    #[test]
351
    fn test_cache_stats() {
352
        let cache = BytecodeCache::with_capacity(10);
353
354
        for i in 0..5 {
355
            cache.insert(format!("expr{i}"), make_test_expr(i), None);
356
        }
357
358
        // Generate some hits and misses
359
        let _ = cache.get("expr1");
360
        let _ = cache.get("expr2");
361
        let _ = cache.get("expr_missing");
362
363
        let stats = cache.stats();
364
        assert_eq!(stats.size, 5);
365
        assert_eq!(stats.capacity, 10);
366
        assert_eq!(stats.hits, 2);
367
        assert_eq!(stats.misses, 1);
368
        assert!(stats.hit_rate > 60.0 && stats.hit_rate < 70.0);
369
    }
370
}