Coverage Report

Created: 2025-09-05 15: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
25
    pub fn new(source: String) -> Self {
25
25
        let hash = {
26
25
            let mut hasher = std::collections::hash_map::DefaultHasher::new();
27
25
            source.hash(&mut hasher);
28
25
            hasher.finish()
29
        };
30
25
        CacheKey { source, hash }
31
25
    }
32
}
33
34
impl PartialEq for CacheKey {
35
23
    fn eq(&self, other: &Self) -> bool {
36
23
        self.hash == other.hash && 
self.source == other.source18
37
23
    }
38
}
39
40
impl Hash for CacheKey {
41
22
    fn hash<H: Hasher>(&self, state: &mut H) {
42
22
        self.hash.hash(state);
43
22
    }
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
4
    pub fn with_capacity(max_size: usize) -> Self {
73
4
        BytecodeCache {
74
4
            cache: RefCell::new(HashMap::with_capacity(max_size)),
75
4
            max_size,
76
4
            access_order: RefCell::new(Vec::with_capacity(max_size)),
77
4
            hits: RefCell::new(0),
78
4
            misses: RefCell::new(0),
79
4
        }
80
4
    }
81
82
    /// Create a default cache with 1000 entry capacity
83
1
    pub fn new() -> Self {
84
1
        Self::with_capacity(1000)
85
1
    }
86
87
    /// Get a cached result if available
88
11
    pub fn get(&self, source: &str) -> Option<CachedResult> {
89
11
        let key = CacheKey::new(source.to_string());
90
91
11
        if let Some(
result8
) = self.cache.borrow().get(&key) {
92
8
            *self.hits.borrow_mut() += 1;
93
94
            // Update access order for LRU
95
8
            let mut access = self.access_order.borrow_mut();
96
11
            if let Some(
pos8
) =
access.iter()8
.
position8
(|k| k == &key) {
97
8
                access.remove(pos);
98
8
            
}0
99
8
            access.push(key.clone());
100
101
8
            Some(result.clone())
102
        } else {
103
3
            *self.misses.borrow_mut() += 1;
104
3
            None
105
        }
106
11
    }
107
108
    /// Store a compilation result in the cache
109
11
    pub fn insert(&self, source: String, ast: Rc<Expr>, rust_code: Option<String>) {
110
11
        let key = CacheKey::new(source);
111
112
        // Check if we need to evict
113
11
        if self.cache.borrow().len() >= self.max_size {
114
1
            self.evict_lru();
115
10
        }
116
117
11
        let result = CachedResult {
118
11
            ast,
119
11
            rust_code,
120
11
            timestamp: std::time::Instant::now(),
121
11
        };
122
123
11
        self.cache.borrow_mut().insert(key.clone(), result);
124
11
        self.access_order.borrow_mut().push(key);
125
11
    }
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
0
    pub fn clear(&self) {
138
0
        self.cache.borrow_mut().clear();
139
0
        self.access_order.borrow_mut().clear();
140
0
        *self.hits.borrow_mut() = 0;
141
0
        *self.misses.borrow_mut() = 0;
142
0
    }
143
144
    /// Get cache statistics
145
3
    pub fn stats(&self) -> CacheStats {
146
3
        CacheStats {
147
3
            size: self.cache.borrow().len(),
148
3
            capacity: self.max_size,
149
3
            hits: *self.hits.borrow(),
150
3
            misses: *self.misses.borrow(),
151
3
            hit_rate: self.calculate_hit_rate(),
152
3
        }
153
3
    }
154
155
    /// Calculate hit rate as a percentage
156
    #[allow(clippy::cast_precision_loss)]
157
3
    fn calculate_hit_rate(&self) -> f64 {
158
3
        let hits = *self.hits.borrow() as f64;
159
3
        let total = hits + *self.misses.borrow() as f64;
160
3
        if total > 0.0 {
161
3
            (hits / total) * 100.0
162
        } else {
163
0
            0.0
164
        }
165
3
    }
166
167
    /// Remove entries older than specified duration
168
0
    pub fn evict_older_than(&self, age: std::time::Duration) {
169
0
        let now = std::time::Instant::now();
170
0
        let mut cache = self.cache.borrow_mut();
171
0
        let mut access = self.access_order.borrow_mut();
172
173
        // Find keys to remove
174
0
        let keys_to_remove: Vec<CacheKey> = cache
175
0
            .iter()
176
0
            .filter(|(_, result)| now.duration_since(result.timestamp) > age)
177
0
            .map(|(key, _)| key.clone())
178
0
            .collect();
179
180
        // Remove from cache and access order
181
0
        for key in keys_to_remove {
182
0
            cache.remove(&key);
183
0
            if let Some(pos) = access.iter().position(|k| k == &key) {
184
0
                access.remove(pos);
185
0
            }
186
        }
187
0
    }
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
1
    pub fn new() -> Self {
229
1
        ExpressionCache {
230
1
            inner: BytecodeCache::new(),
231
1
        }
232
1
    }
233
234
    /// Try to get a parsed expression from cache
235
1
    pub fn get_parsed(&self, source: &str) -> Option<Rc<Expr>> {
236
1
        self.inner.get(source).map(|result| result.ast)
237
1
    }
238
239
    /// Cache a parsed expression
240
1
    pub fn cache_parsed(&self, source: String, ast: Rc<Expr>) {
241
1
        self.inner.insert(source, ast, None);
242
1
    }
243
244
    /// Try to get transpiled code from cache
245
1
    pub fn get_transpiled(&self, source: &str) -> Option<String> {
246
1
        self.inner.get(source).and_then(|result| result.rust_code)
247
1
    }
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
0
    pub fn stats(&self) -> CacheStats {
256
0
        self.inner.stats()
257
0
    }
258
259
    /// Clear the cache
260
0
    pub fn clear(&self) {
261
0
        self.inner.clear();
262
0
    }
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
11
    fn make_test_expr(value: i64) -> Rc<Expr> {
278
11
        Rc::new(Expr {
279
11
            kind: ExprKind::Literal(Literal::Integer(value)),
280
11
            span: Span { start: 0, end: 0 },
281
11
            attributes: Vec::new(),
282
11
        })
283
11
    }
284
285
    #[test]
286
1
    fn test_cache_key() {
287
1
        let key1 = CacheKey::new("let x = 42".to_string());
288
1
        let key2 = CacheKey::new("let x = 42".to_string());
289
1
        let key3 = CacheKey::new("let y = 42".to_string());
290
291
1
        assert_eq!(key1, key2);
292
1
        assert_ne!(key1, key3);
293
1
    }
294
295
    #[test]
296
1
    fn test_bytecode_cache_basic() {
297
1
        let cache = BytecodeCache::with_capacity(3);
298
299
        // Cache miss
300
1
        assert!(cache.get("let x = 1").is_none());
301
1
        assert_eq!(cache.stats().misses, 1);
302
303
        // Insert and hit
304
1
        cache.insert("let x = 1".to_string(), make_test_expr(1), None);
305
1
        assert!(cache.get("let x = 1").is_some());
306
1
        assert_eq!(cache.stats().hits, 1);
307
1
    }
308
309
    #[test]
310
1
    fn test_cache_lru_eviction() {
311
1
        let cache = BytecodeCache::with_capacity(2);
312
313
1
        cache.insert("expr1".to_string(), make_test_expr(1), None);
314
1
        cache.insert("expr2".to_string(), make_test_expr(2), None);
315
316
        // Access expr1 to make it more recent
317
1
        let _ = cache.get("expr1");
318
319
        // This should evict expr2 (least recently used)
320
1
        cache.insert("expr3".to_string(), make_test_expr(3), None);
321
322
1
        assert!(cache.get("expr1").is_some());
323
1
        assert!(cache.get("expr2").is_none()); // Evicted
324
1
        assert!(cache.get("expr3").is_some());
325
1
    }
326
327
    #[test]
328
1
    fn test_expression_cache() {
329
1
        let cache = ExpressionCache::new();
330
331
1
        let expr = make_test_expr(42);
332
1
        cache.cache_parsed("let x = 42".to_string(), Rc::clone(&expr));
333
334
1
        let cached = cache.get_parsed("let x = 42").unwrap();
335
1
        assert!(Rc::ptr_eq(&expr, &cached));
336
337
        // Test transpiled code caching
338
1
        cache.cache_transpiled(
339
1
            "let y = 10".to_string(),
340
1
            make_test_expr(10),
341
1
            "let y = 10;".to_string(),
342
        );
343
344
1
        assert_eq!(
345
1
            cache.get_transpiled("let y = 10"),
346
1
            Some("let y = 10;".to_string())
347
        );
348
1
    }
349
350
    #[test]
351
1
    fn test_cache_stats() {
352
1
        let cache = BytecodeCache::with_capacity(10);
353
354
6
        for 
i5
in 0..5 {
355
5
            cache.insert(format!("expr{i}"), make_test_expr(i), None);
356
5
        }
357
358
        // Generate some hits and misses
359
1
        let _ = cache.get("expr1");
360
1
        let _ = cache.get("expr2");
361
1
        let _ = cache.get("expr_missing");
362
363
1
        let stats = cache.stats();
364
1
        assert_eq!(stats.size, 5);
365
1
        assert_eq!(stats.capacity, 10);
366
1
        assert_eq!(stats.hits, 2);
367
1
        assert_eq!(stats.misses, 1);
368
1
        assert!(stats.hit_rate > 60.0 && stats.hit_rate < 70.0);
369
1
    }
370
}