thalir-core 0.1.0

Core IR types and builders for ThalIR smart contract analysis
Documentation
use std::any::{Any, TypeId};
use std::collections::{HashMap, VecDeque};
use std::sync::{Arc, RwLock};
use std::time::{Duration, Instant};

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CacheKey {
    pub analysis_type: TypeId,
    pub target: String,
    pub version: u64,
}

impl CacheKey {
    pub fn new<T: 'static>(target: impl Into<String>, version: u64) -> Self {
        Self {
            analysis_type: TypeId::of::<T>(),
            target: target.into(),
            version,
        }
    }
}

struct CacheEntry {
    value: Box<dyn Any + Send + Sync>,
    last_accessed: Instant,
    size_bytes: usize,
    access_count: u64,
}

pub struct AnalysisCache {
    entries: HashMap<CacheKey, CacheEntry>,
    lru_order: VecDeque<CacheKey>,
    max_size_bytes: usize,
    current_size_bytes: usize,
    max_entries: usize,
    generation: u64,
    stats: CacheStatistics,
}

#[derive(Debug, Clone, Default)]
pub struct CacheStatistics {
    pub hits: u64,
    pub misses: u64,
    pub evictions: u64,
    pub invalidations: u64,
    pub total_compute_time: Duration,
}

impl AnalysisCache {
    pub fn new(max_size_bytes: usize, max_entries: usize) -> Self {
        Self {
            entries: HashMap::new(),
            lru_order: VecDeque::new(),
            max_size_bytes,
            current_size_bytes: 0,
            max_entries,
            generation: 0,
            stats: CacheStatistics::default(),
        }
    }

    pub fn get_or_compute<T, F>(&mut self, key: CacheKey, compute: F) -> Arc<T>
    where
        T: Send + Sync + 'static,
        F: FnOnce() -> T,
    {
        let cached_value = if let Some(entry) = self.entries.get_mut(&key) {
            if let Some(value) = entry.value.downcast_ref::<Arc<T>>() {
                self.stats.hits += 1;
                entry.last_accessed = Instant::now();
                entry.access_count += 1;
                Some(value.clone())
            } else {
                None
            }
        } else {
            None
        };

        if let Some(value) = cached_value {
            self.promote_lru(&key);
            return value;
        }

        self.stats.misses += 1;
        let start = Instant::now();
        let value = Arc::new(compute());
        self.stats.total_compute_time += start.elapsed();

        let size_bytes = std::mem::size_of::<T>() + 64;

        self.evict_if_needed(size_bytes);

        let entry = CacheEntry {
            value: Box::new(value.clone()),
            last_accessed: Instant::now(),
            size_bytes,
            access_count: 1,
        };

        self.entries.insert(key.clone(), entry);
        self.lru_order.push_back(key);
        self.current_size_bytes += size_bytes;

        value
    }

    pub fn get<T>(&mut self, key: &CacheKey) -> Option<Arc<T>>
    where
        T: Send + Sync + 'static,
    {
        let result = if let Some(entry) = self.entries.get_mut(key) {
            if let Some(value) = entry.value.downcast_ref::<Arc<T>>() {
                self.stats.hits += 1;
                entry.last_accessed = Instant::now();
                entry.access_count += 1;
                Some(value.clone())
            } else {
                None
            }
        } else {
            None
        };

        if result.is_some() {
            self.promote_lru(key);
        } else {
            self.stats.misses += 1;
        }

        result
    }

    pub fn insert<T>(&mut self, key: CacheKey, value: T)
    where
        T: Send + Sync + 'static,
    {
        let arc_value = Arc::new(value);
        let size_bytes = std::mem::size_of::<T>() + 64;

        self.evict_if_needed(size_bytes);

        let entry = CacheEntry {
            value: Box::new(arc_value),
            last_accessed: Instant::now(),
            size_bytes,
            access_count: 0,
        };

        self.entries.insert(key.clone(), entry);
        self.lru_order.push_back(key);
        self.current_size_bytes += size_bytes;
    }

    pub fn invalidate<F>(&mut self, predicate: F)
    where
        F: Fn(&CacheKey) -> bool,
    {
        let keys_to_remove: Vec<CacheKey> = self
            .entries
            .keys()
            .filter(|k| predicate(k))
            .cloned()
            .collect();

        for key in keys_to_remove {
            if let Some(entry) = self.entries.remove(&key) {
                self.current_size_bytes -= entry.size_bytes;
                self.stats.invalidations += 1;
            }
            self.lru_order.retain(|k| k != &key);
        }
    }

    pub fn invalidate_target(&mut self, target: &str) {
        self.invalidate(|k| k.target == target);
    }

    pub fn increment_generation(&mut self) {
        let current_gen = self.generation;
        self.generation += 1;
        self.invalidate(|k| k.version < current_gen);
    }

    pub fn clear(&mut self) {
        self.entries.clear();
        self.lru_order.clear();
        self.current_size_bytes = 0;
        self.stats.invalidations += self.entries.len() as u64;
    }

    pub fn statistics(&self) -> &CacheStatistics {
        &self.stats
    }

    pub fn hit_rate(&self) -> f64 {
        let total = self.stats.hits + self.stats.misses;
        if total == 0 {
            0.0
        } else {
            self.stats.hits as f64 / total as f64
        }
    }

    fn promote_lru(&mut self, key: &CacheKey) {
        self.lru_order.retain(|k| k != key);
        self.lru_order.push_back(key.clone());
    }

    fn evict_if_needed(&mut self, needed_bytes: usize) {
        while self.entries.len() >= self.max_entries {
            if let Some(key) = self.lru_order.pop_front() {
                if let Some(entry) = self.entries.remove(&key) {
                    self.current_size_bytes -= entry.size_bytes;
                    self.stats.evictions += 1;
                }
            } else {
                break;
            }
        }

        while self.current_size_bytes + needed_bytes > self.max_size_bytes {
            if let Some(key) = self.lru_order.pop_front() {
                if let Some(entry) = self.entries.remove(&key) {
                    self.current_size_bytes -= entry.size_bytes;
                    self.stats.evictions += 1;
                }
            } else {
                break;
            }
        }
    }
}

impl Default for AnalysisCache {
    fn default() -> Self {
        Self::new(100 * 1024 * 1024, 1000)
    }
}

pub struct SharedAnalysisCache {
    inner: Arc<RwLock<AnalysisCache>>,
}

impl SharedAnalysisCache {
    pub fn new(max_size_bytes: usize, max_entries: usize) -> Self {
        Self {
            inner: Arc::new(RwLock::new(AnalysisCache::new(max_size_bytes, max_entries))),
        }
    }

    pub fn get_or_compute<T, F>(&self, key: CacheKey, compute: F) -> Arc<T>
    where
        T: Send + Sync + 'static,
        F: FnOnce() -> T,
    {
        {
            let mut cache = self.inner.write().unwrap();
            if let Some(value) = cache.get::<T>(&key) {
                return value;
            }
        }

        let mut cache = self.inner.write().unwrap();
        cache.get_or_compute(key, compute)
    }
}

impl Default for SharedAnalysisCache {
    fn default() -> Self {
        Self::new(100 * 1024 * 1024, 1000)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_cache_basic_operations() {
        let mut cache = AnalysisCache::new(1024, 10);

        let key = CacheKey::new::<String>("test", 0);
        let value = cache.get_or_compute(key.clone(), || "hello".to_string());

        assert_eq!(*value, "hello");
        assert_eq!(cache.statistics().hits, 0);
        assert_eq!(cache.statistics().misses, 1);

        let value2 = cache.get::<String>(&key).unwrap();
        assert_eq!(*value2, "hello");
        assert_eq!(cache.statistics().hits, 1);
    }

    #[test]
    fn test_cache_eviction() {
        let mut cache = AnalysisCache::new(1024, 2);

        let key1 = CacheKey::new::<i32>("test1", 0);
        let key2 = CacheKey::new::<i32>("test2", 0);
        let key3 = CacheKey::new::<i32>("test3", 0);

        cache.insert(key1.clone(), 1);
        cache.insert(key2.clone(), 2);
        cache.insert(key3.clone(), 3);

        assert!(cache.get::<i32>(&key1).is_none());
        assert_eq!(cache.statistics().evictions, 1);
    }

    #[test]
    fn test_cache_invalidation() {
        let mut cache = AnalysisCache::new(1024, 10);

        let key1 = CacheKey::new::<String>("target1", 0);
        let key2 = CacheKey::new::<String>("target2", 0);

        cache.insert(key1.clone(), "value1".to_string());
        cache.insert(key2.clone(), "value2".to_string());

        cache.invalidate_target("target1");

        assert!(cache.get::<String>(&key1).is_none());
        assert!(cache.get::<String>(&key2).is_some());
        assert_eq!(cache.statistics().invalidations, 1);
    }
}