use crate::frontend::ast::Expr;
use std::cell::RefCell;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::rc::Rc;
#[derive(Clone, Debug, Eq)]
pub struct CacheKey {
source: String,
hash: u64,
}
impl CacheKey {
pub fn new(source: String) -> Self {
let hash = {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
source.hash(&mut hasher);
hasher.finish()
};
CacheKey { source, hash }
}
}
impl PartialEq for CacheKey {
fn eq(&self, other: &Self) -> bool {
self.hash == other.hash && self.source == other.source
}
}
impl Hash for CacheKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.hash.hash(state);
}
}
#[derive(Clone)]
pub struct CachedResult {
pub ast: Rc<Expr>,
pub rust_code: Option<String>,
pub timestamp: std::time::Instant,
}
pub struct BytecodeCache {
cache: RefCell<HashMap<CacheKey, CachedResult>>,
max_size: usize,
access_order: RefCell<Vec<CacheKey>>,
hits: RefCell<usize>,
misses: RefCell<usize>,
}
impl BytecodeCache {
pub fn with_capacity(max_size: usize) -> Self {
BytecodeCache {
cache: RefCell::new(HashMap::with_capacity(max_size)),
max_size,
access_order: RefCell::new(Vec::with_capacity(max_size)),
hits: RefCell::new(0),
misses: RefCell::new(0),
}
}
pub fn new() -> Self {
Self::with_capacity(1000)
}
pub fn get(&self, source: &str) -> Option<CachedResult> {
let key = CacheKey::new(source.to_string());
if let Some(result) = self.cache.borrow().get(&key) {
*self.hits.borrow_mut() += 1;
let mut access = self.access_order.borrow_mut();
if let Some(pos) = access.iter().position(|k| k == &key) {
access.remove(pos);
}
access.push(key.clone());
Some(result.clone())
} else {
*self.misses.borrow_mut() += 1;
None
}
}
pub fn insert(&self, source: String, ast: Rc<Expr>, rust_code: Option<String>) {
let key = CacheKey::new(source);
if self.cache.borrow().len() >= self.max_size {
self.evict_lru();
}
let result = CachedResult {
ast,
rust_code,
timestamp: std::time::Instant::now(),
};
self.cache.borrow_mut().insert(key.clone(), result);
self.access_order.borrow_mut().push(key);
}
fn evict_lru(&self) {
let mut access = self.access_order.borrow_mut();
if !access.is_empty() {
let lru_key = access.remove(0);
self.cache.borrow_mut().remove(&lru_key);
}
}
pub fn clear(&self) {
self.cache.borrow_mut().clear();
self.access_order.borrow_mut().clear();
*self.hits.borrow_mut() = 0;
*self.misses.borrow_mut() = 0;
}
pub fn stats(&self) -> CacheStats {
CacheStats {
size: self.cache.borrow().len(),
capacity: self.max_size,
hits: *self.hits.borrow(),
misses: *self.misses.borrow(),
hit_rate: self.calculate_hit_rate(),
}
}
#[allow(clippy::cast_precision_loss)]
fn calculate_hit_rate(&self) -> f64 {
let hits = *self.hits.borrow() as f64;
let total = hits + *self.misses.borrow() as f64;
if total > 0.0 {
(hits / total) * 100.0
} else {
0.0
}
}
pub fn evict_older_than(&self, age: std::time::Duration) {
let now = std::time::Instant::now();
let mut cache = self.cache.borrow_mut();
let mut access = self.access_order.borrow_mut();
let keys_to_remove: Vec<CacheKey> = cache
.iter()
.filter(|(_, result)| now.duration_since(result.timestamp) > age)
.map(|(key, _)| key.clone())
.collect();
for key in keys_to_remove {
cache.remove(&key);
if let Some(pos) = access.iter().position(|k| k == &key) {
access.remove(pos);
}
}
}
}
impl Default for BytecodeCache {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct CacheStats {
pub size: usize,
pub capacity: usize,
pub hits: usize,
pub misses: usize,
pub hit_rate: f64,
}
impl std::fmt::Display for CacheStats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Cache: {}/{} entries, {} hits, {} misses ({:.1}% hit rate)",
self.size, self.capacity, self.hits, self.misses, self.hit_rate
)
}
}
pub struct ExpressionCache {
inner: BytecodeCache,
}
impl ExpressionCache {
pub fn new() -> Self {
ExpressionCache {
inner: BytecodeCache::new(),
}
}
pub fn get_parsed(&self, source: &str) -> Option<Rc<Expr>> {
self.inner.get(source).map(|result| result.ast)
}
pub fn cache_parsed(&self, source: String, ast: Rc<Expr>) {
self.inner.insert(source, ast, None);
}
pub fn get_transpiled(&self, source: &str) -> Option<String> {
self.inner.get(source).and_then(|result| result.rust_code)
}
pub fn cache_transpiled(&self, source: String, ast: Rc<Expr>, rust_code: String) {
self.inner.insert(source, ast, Some(rust_code));
}
pub fn stats(&self) -> CacheStats {
self.inner.stats()
}
pub fn clear(&self) {
self.inner.clear();
}
}
impl Default for ExpressionCache {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::frontend::ast::{ExprKind, Literal, Span};
fn make_test_expr(value: i64) -> Rc<Expr> {
Rc::new(Expr {
kind: ExprKind::Literal(Literal::Integer(value, None)),
span: Span { start: 0, end: 0 },
attributes: Vec::new(),
leading_comments: vec![],
trailing_comment: None,
})
}
#[test]
fn test_cache_key() {
let key1 = CacheKey::new("let x = 42".to_string());
let key2 = CacheKey::new("let x = 42".to_string());
let key3 = CacheKey::new("let y = 42".to_string());
assert_eq!(key1, key2);
assert_ne!(key1, key3);
}
#[test]
fn test_bytecode_cache_basic() {
let cache = BytecodeCache::with_capacity(3);
assert!(cache.get("let x = 1").is_none());
assert_eq!(cache.stats().misses, 1);
cache.insert("let x = 1".to_string(), make_test_expr(1), None);
assert!(cache.get("let x = 1").is_some());
assert_eq!(cache.stats().hits, 1);
}
#[test]
fn test_cache_lru_eviction() {
let cache = BytecodeCache::with_capacity(2);
cache.insert("expr1".to_string(), make_test_expr(1), None);
cache.insert("expr2".to_string(), make_test_expr(2), None);
let _ = cache.get("expr1");
cache.insert("expr3".to_string(), make_test_expr(3), None);
assert!(cache.get("expr1").is_some());
assert!(cache.get("expr2").is_none()); assert!(cache.get("expr3").is_some());
}
#[test]
fn test_expression_cache() {
let cache = ExpressionCache::new();
let expr = make_test_expr(42);
cache.cache_parsed("let x = 42".to_string(), Rc::clone(&expr));
let cached = cache.get_parsed("let x = 42").unwrap();
assert!(Rc::ptr_eq(&expr, &cached));
cache.cache_transpiled(
"let y = 10".to_string(),
make_test_expr(10),
"let y = 10;".to_string(),
);
assert_eq!(
cache.get_transpiled("let y = 10"),
Some("let y = 10;".to_string())
);
}
#[test]
fn test_cache_stats() {
let cache = BytecodeCache::with_capacity(10);
for i in 0..5 {
cache.insert(format!("expr{i}"), make_test_expr(i), None);
}
let _ = cache.get("expr1");
let _ = cache.get("expr2");
let _ = cache.get("expr_missing");
let stats = cache.stats();
assert_eq!(stats.size, 5);
assert_eq!(stats.capacity, 10);
assert_eq!(stats.hits, 2);
assert_eq!(stats.misses, 1);
assert!(stats.hit_rate > 60.0 && stats.hit_rate < 70.0);
}
#[test]
fn test_cache_key_hash() {
use std::collections::HashSet;
let key1 = CacheKey::new("let x = 1".to_string());
let key2 = CacheKey::new("let x = 1".to_string());
let key3 = CacheKey::new("let x = 2".to_string());
let mut set = HashSet::new();
set.insert(key1.clone());
assert!(set.contains(&key2));
assert!(!set.contains(&key3));
}
#[test]
fn test_bytecode_cache_default() {
let cache = BytecodeCache::default();
assert_eq!(cache.stats().capacity, 1000);
assert_eq!(cache.stats().size, 0);
}
#[test]
fn test_expression_cache_default() {
let cache = ExpressionCache::default();
assert_eq!(cache.stats().size, 0);
}
#[test]
fn test_cache_stats_empty() {
let cache = BytecodeCache::new();
let stats = cache.stats();
assert_eq!(stats.size, 0);
assert_eq!(stats.hits, 0);
assert_eq!(stats.misses, 0);
assert_eq!(stats.hit_rate, 0.0);
}
#[test]
fn test_cache_stats_clone() {
let cache = BytecodeCache::new();
let _ = cache.get("missing");
let stats = cache.stats();
let cloned = stats.clone();
assert_eq!(stats.misses, cloned.misses);
assert_eq!(stats.capacity, cloned.capacity);
}
#[test]
fn test_cached_result_clone() {
let result = CachedResult {
ast: make_test_expr(42),
rust_code: Some("let x = 42;".to_string()),
timestamp: std::time::Instant::now(),
};
let cloned = result.clone();
assert!(Rc::ptr_eq(&result.ast, &cloned.ast));
assert_eq!(result.rust_code, cloned.rust_code);
}
#[test]
fn test_bytecode_cache_insert_duplicate() {
let cache = BytecodeCache::with_capacity(10);
cache.insert("expr1".to_string(), make_test_expr(1), None);
cache.insert("expr1".to_string(), make_test_expr(2), None);
assert_eq!(cache.stats().size, 1);
}
#[test]
fn test_expression_cache_get_transpiled_none() {
let cache = ExpressionCache::new();
cache.cache_parsed("test".to_string(), make_test_expr(1));
assert!(cache.get_transpiled("test").is_none());
}
#[test]
fn test_cache_key_debug() {
let key = CacheKey::new("test".to_string());
let debug = format!("{:?}", key);
assert!(debug.contains("CacheKey"));
assert!(debug.contains("test"));
}
#[test]
fn test_cache_stats_debug() {
let cache = BytecodeCache::new();
let stats = cache.stats();
let debug = format!("{:?}", stats);
assert!(debug.contains("CacheStats"));
}
}
#[cfg(test)]
mod property_tests_cache {
use proptest::proptest;
proptest! {
#[test]
fn test_new_never_panics(input: String) {
let _input = if input.len() > 100 { &input[..100] } else { &input[..] };
let _ = std::panic::catch_unwind(|| {
});
}
}
}
#[cfg(test)]
mod mutation_tests {
use super::*;
#[test]
fn test_bytecode_cache_clear_not_stub() {
use crate::frontend::ast::{ExprKind, Literal, Span};
let cache = BytecodeCache::new();
let expr = Rc::new(Expr {
kind: ExprKind::Literal(Literal::Integer(42, None)),
span: Span { start: 0, end: 0 },
attributes: Vec::new(),
leading_comments: vec![],
trailing_comment: None,
});
cache.insert("test".to_string(), expr, None);
assert_eq!(cache.stats().size, 1);
cache.clear();
assert_eq!(cache.stats().size, 0, "Clear should empty cache");
assert_eq!(cache.stats().hits, 0, "Clear should reset hits");
assert_eq!(cache.stats().misses, 0, "Clear should reset misses");
}
#[test]
fn test_cache_stats_display_not_stub() {
let cache = BytecodeCache::new();
let stats = cache.stats();
let display = format!("{stats}");
assert!(
display.contains("Cache:"),
"Display should contain 'Cache:'"
);
assert!(
display.contains("entries"),
"Display should contain 'entries'"
);
assert!(display.contains("hits"), "Display should contain 'hits'");
assert!(
display.contains("misses"),
"Display should contain 'misses'"
);
}
#[test]
fn test_evict_older_than_comparison_operator() {
use crate::frontend::ast::{ExprKind, Literal, Span};
let cache = BytecodeCache::new();
let expr = Rc::new(Expr {
kind: ExprKind::Literal(Literal::Integer(42, None)),
span: Span { start: 0, end: 0 },
attributes: Vec::new(),
leading_comments: vec![],
trailing_comment: None,
});
cache.insert("test".to_string(), Rc::clone(&expr), None);
assert_eq!(cache.stats().size, 1);
std::thread::sleep(std::time::Duration::from_millis(1100));
cache.evict_older_than(std::time::Duration::from_secs(1));
assert_eq!(
cache.stats().size,
0,
"Items older than 1s should be evicted (>)"
);
}
#[test]
fn test_expression_cache_clear_not_stub() {
use crate::frontend::ast::{ExprKind, Literal, Span};
let cache = ExpressionCache::new();
let expr = Rc::new(Expr {
kind: ExprKind::Literal(Literal::Integer(42, None)),
span: Span { start: 0, end: 0 },
attributes: Vec::new(),
leading_comments: vec![],
trailing_comment: None,
});
cache.cache_parsed("expr".to_string(), expr);
assert_eq!(cache.stats().size, 1);
cache.clear();
assert_eq!(cache.stats().size, 0, "Clear should empty expression cache");
}
}
#[cfg(test)]
mod coverage_push_round29 {
use super::*;
use crate::frontend::ast::{ExprKind, Literal, Span};
fn make_expr(value: i64) -> Rc<Expr> {
Rc::new(Expr {
kind: ExprKind::Literal(Literal::Integer(value, None)),
span: Span { start: 0, end: 0 },
attributes: Vec::new(),
leading_comments: vec![],
trailing_comment: None,
})
}
#[test]
fn test_cache_key_clone() {
let key1 = CacheKey::new("test".to_string());
let key2 = key1.clone();
assert_eq!(key1, key2);
assert_eq!(key1.hash, key2.hash);
}
#[test]
fn test_cache_key_different_content_different_hash() {
let key1 = CacheKey::new("abc".to_string());
let key2 = CacheKey::new("xyz".to_string());
assert_ne!(key1.hash, key2.hash);
}
#[test]
fn test_cache_key_empty_string() {
let key = CacheKey::new("".to_string());
assert_eq!(key.source, "");
}
#[test]
fn test_cache_key_unicode() {
let key = CacheKey::new("日本語🎉".to_string());
assert_eq!(key.source, "日本語🎉");
}
#[test]
fn test_bytecode_cache_capacity_zero() {
let cache = BytecodeCache::with_capacity(0);
cache.insert("test".to_string(), make_expr(1), None);
assert_eq!(cache.stats().capacity, 0);
}
#[test]
fn test_bytecode_cache_capacity_one() {
let cache = BytecodeCache::with_capacity(1);
cache.insert("expr1".to_string(), make_expr(1), None);
assert_eq!(cache.stats().size, 1);
cache.insert("expr2".to_string(), make_expr(2), None);
assert!(cache.get("expr1").is_none());
assert!(cache.get("expr2").is_some());
}
#[test]
fn test_bytecode_cache_get_updates_lru() {
let cache = BytecodeCache::with_capacity(2);
cache.insert("a".to_string(), make_expr(1), None);
cache.insert("b".to_string(), make_expr(2), None);
let _ = cache.get("a");
cache.insert("c".to_string(), make_expr(3), None);
assert!(cache.get("a").is_some());
assert!(cache.get("b").is_none());
assert!(cache.get("c").is_some());
}
#[test]
fn test_bytecode_cache_multiple_misses() {
let cache = BytecodeCache::new();
for i in 0..10 {
assert!(cache.get(&format!("missing{i}")).is_none());
}
assert_eq!(cache.stats().misses, 10);
assert_eq!(cache.stats().hits, 0);
assert_eq!(cache.stats().hit_rate, 0.0);
}
#[test]
fn test_bytecode_cache_hit_rate_calculation() {
let cache = BytecodeCache::new();
cache.insert("test".to_string(), make_expr(1), None);
let _ = cache.get("test");
let _ = cache.get("test");
let _ = cache.get("test");
let _ = cache.get("missing");
let stats = cache.stats();
assert!((stats.hit_rate - 75.0).abs() < 0.1);
}
#[test]
fn test_bytecode_cache_with_rust_code() {
let cache = BytecodeCache::new();
cache.insert(
"test".to_string(),
make_expr(1),
Some("let x = 1;".to_string()),
);
let result = cache.get("test").unwrap();
assert_eq!(result.rust_code, Some("let x = 1;".to_string()));
}
#[test]
fn test_bytecode_cache_clear_after_operations() {
let cache = BytecodeCache::new();
for i in 0..5 {
cache.insert(format!("expr{i}"), make_expr(i), None);
}
let _ = cache.get("expr0");
let _ = cache.get("expr1");
let _ = cache.get("missing");
cache.clear();
assert_eq!(cache.stats().size, 0);
assert_eq!(cache.stats().hits, 0);
assert_eq!(cache.stats().misses, 0);
assert!(cache.get("expr0").is_none());
}
#[test]
fn test_expression_cache_stats() {
let cache = ExpressionCache::new();
cache.cache_parsed("a".to_string(), make_expr(1));
cache.cache_parsed("b".to_string(), make_expr(2));
let stats = cache.stats();
assert_eq!(stats.size, 2);
}
#[test]
fn test_expression_cache_get_parsed_miss() {
let cache = ExpressionCache::new();
assert!(cache.get_parsed("nonexistent").is_none());
}
#[test]
fn test_expression_cache_get_transpiled_miss() {
let cache = ExpressionCache::new();
assert!(cache.get_transpiled("nonexistent").is_none());
}
#[test]
fn test_expression_cache_overwrite() {
let cache = ExpressionCache::new();
cache.cache_parsed("test".to_string(), make_expr(1));
cache.cache_parsed("test".to_string(), make_expr(2));
assert_eq!(cache.stats().size, 1);
}
#[test]
fn test_cache_stats_display_format() {
let cache = BytecodeCache::with_capacity(100);
cache.insert("test".to_string(), make_expr(1), None);
let _ = cache.get("test");
let _ = cache.get("missing");
let stats = cache.stats();
let display = format!("{stats}");
assert!(display.contains("1/100"));
assert!(display.contains("1 hits"));
assert!(display.contains("1 misses"));
assert!(display.contains("hit rate"));
}
#[test]
fn test_cached_result_timestamp() {
let result = CachedResult {
ast: make_expr(1),
rust_code: None,
timestamp: std::time::Instant::now(),
};
let elapsed = result.timestamp.elapsed();
assert!(elapsed.as_secs() < 1);
}
#[test]
fn test_evict_older_than_keeps_recent() {
let cache = BytecodeCache::new();
cache.insert("test".to_string(), make_expr(1), None);
cache.evict_older_than(std::time::Duration::from_secs(10));
assert_eq!(cache.stats().size, 1);
}
#[test]
fn test_evict_older_than_empty_cache() {
let cache = BytecodeCache::new();
cache.evict_older_than(std::time::Duration::from_secs(1));
assert_eq!(cache.stats().size, 0);
}
#[test]
fn test_lru_eviction_on_full_cache() {
let cache = BytecodeCache::with_capacity(3);
cache.insert("a".to_string(), make_expr(1), None);
cache.insert("b".to_string(), make_expr(2), None);
cache.insert("c".to_string(), make_expr(3), None);
assert_eq!(cache.stats().size, 3);
cache.insert("d".to_string(), make_expr(4), None);
assert!(cache.get("a").is_none()); assert!(cache.get("b").is_some());
assert!(cache.get("c").is_some());
assert!(cache.get("d").is_some());
}
#[test]
fn test_cache_key_hash_in_hashmap() {
use std::collections::HashMap;
let mut map: HashMap<CacheKey, i32> = HashMap::new();
let key1 = CacheKey::new("test".to_string());
let key2 = CacheKey::new("test".to_string());
map.insert(key1, 42);
assert_eq!(map.get(&key2), Some(&42));
}
}