use std::collections::VecDeque;
use std::sync::Arc;
use dashmap::DashMap;
use mir_codebase::StubSlice;
use mir_issues::Issue;
use parking_lot::Mutex;
pub const DEFAULT_CAPACITY: usize = 6144;
type ParseCacheKey = ([u8; 32], u8);
#[derive(Clone)]
pub struct CachedParse {
pub slice: Arc<StubSlice>,
pub issues: Arc<Vec<Issue>>,
}
pub struct ParseCache {
map: DashMap<ParseCacheKey, CachedParse>,
order: Mutex<VecDeque<ParseCacheKey>>,
capacity: usize,
}
impl Default for ParseCache {
fn default() -> Self {
Self::with_capacity(DEFAULT_CAPACITY)
}
}
impl ParseCache {
pub fn with_capacity(capacity: usize) -> Self {
Self {
map: DashMap::new(),
order: Mutex::new(VecDeque::new()),
capacity: capacity.max(1),
}
}
pub fn get(&self, hash: &[u8; 32], php_v: u8) -> Option<CachedParse> {
self.map.get(&(*hash, php_v)).map(|r| r.clone())
}
pub fn insert(
&self,
hash: [u8; 32],
php_v: u8,
slice: Arc<StubSlice>,
issues: Arc<Vec<Issue>>,
) {
let key = (hash, php_v);
let is_new = self
.map
.insert(key, CachedParse { slice, issues })
.is_none();
if !is_new {
return;
}
let mut order = self.order.lock();
order.push_back(key);
while self.map.len() > self.capacity {
match order.pop_front() {
Some(old) => {
self.map.remove(&old);
}
None => break,
}
}
}
pub fn remove(&self, hash: &[u8; 32], php_v: u8) {
self.map.remove(&(*hash, php_v));
}
pub fn len(&self) -> usize {
self.map.len()
}
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn empty_issues() -> Arc<Vec<Issue>> {
Arc::new(Vec::new())
}
#[test]
fn get_is_isolated_per_php_version() {
let cache = ParseCache::with_capacity(8);
let hash = [1u8; 32];
let slice_80 = Arc::new(StubSlice::default());
cache.insert(hash, 80, slice_80, empty_issues());
assert!(
cache.get(&hash, 80).is_some(),
"same content hash + same PHP version must hit"
);
assert!(
cache.get(&hash, 81).is_none(),
"same content hash but a DIFFERENT PHP version must miss — a \
version-specific collected StubSlice is not interchangeable"
);
}
#[test]
fn two_versions_of_the_same_content_coexist() {
let cache = ParseCache::with_capacity(8);
let hash = [2u8; 32];
cache.insert(hash, 80, Arc::new(StubSlice::default()), empty_issues());
cache.insert(hash, 81, Arc::new(StubSlice::default()), empty_issues());
assert!(cache.get(&hash, 80).is_some());
assert!(cache.get(&hash, 81).is_some());
assert_eq!(
cache.len(),
2,
"both version-specific entries must be retained"
);
}
#[test]
fn get_returns_the_issues_stored_alongside_the_slice() {
let cache = ParseCache::with_capacity(8);
let hash = [3u8; 32];
let issue = Issue::new(
mir_issues::IssueKind::UndefinedVariable {
name: "x".to_string(),
},
mir_types::Location {
file: Arc::from("a.php"),
line: 1,
line_end: 1,
col_start: 0,
col_end: 1,
},
);
cache.insert(
hash,
80,
Arc::new(StubSlice::default()),
Arc::new(vec![issue.clone()]),
);
let cached = cache.get(&hash, 80).expect("hit");
assert_eq!(cached.issues.len(), 1);
assert_eq!(cached.issues[0], issue);
}
}