## 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
    }