use std::collections::HashMap;
use std::sync::Arc;
use crate::config::Config;
use crate::parser::{RefdefMap, SyntaxError};
use crate::salsa::{FileConfig, FileText};
pub type ReparseKey = (FileText, FileConfig);
#[derive(Debug, Clone)]
pub struct PrevParse {
pub text: Arc<str>,
pub green: rowan::GreenNode,
pub errors: Vec<SyntaxError>,
pub refdefs: RefdefMap,
pub config: Config,
}
pub enum ReparseAdmission {
Refused,
Admitted(Option<Arc<PrevParse>>),
}
#[derive(Debug, Default)]
struct FileReparseState {
prev: Option<Arc<PrevParse>>,
used: u64,
}
const MAX_REPARSE_BASES: usize = 64;
#[derive(Default)]
pub struct ReparseCache {
files: HashMap<ReparseKey, FileReparseState>,
clock: u64,
}
impl ReparseCache {
fn touch(&mut self, key: ReparseKey) -> Option<&mut FileReparseState> {
self.clock += 1;
let clock = self.clock;
let state = self.files.get_mut(&key)?;
state.used = clock;
Some(state)
}
pub fn base(&mut self, key: ReparseKey) -> ReparseAdmission {
match self.touch(key) {
Some(state) => ReparseAdmission::Admitted(state.prev.clone()),
None => ReparseAdmission::Refused,
}
}
pub fn store(&mut self, key: ReparseKey, prev: PrevParse) {
let Some(state) = self.touch(key) else {
return;
};
state.prev = Some(Arc::new(prev));
self.evict_over_budget();
}
pub fn admit(&mut self, key: ReparseKey) {
self.files
.retain(|(file, config), _| *file != key.0 || *config == key.1);
self.clock += 1;
let clock = self.clock;
self.files.entry(key).or_default().used = clock;
self.evict_over_budget();
}
pub fn retire_file(&mut self, file: FileText) {
self.files.retain(|(entry, _), _| *entry != file);
}
pub fn clear(&mut self) {
self.files.clear();
}
fn evict_over_budget(&mut self) {
if self.files.len() <= MAX_REPARSE_BASES {
return;
}
let over = self.files.len() - MAX_REPARSE_BASES;
let mut stamps: Vec<u64> = self.files.values().map(|state| state.used).collect();
stamps.select_nth_unstable(over - 1);
let threshold = stamps[over - 1];
self.files.retain(|_, state| state.used > threshold);
}
}
#[cfg(debug_assertions)]
pub fn assert_reuse_matches_full_parse(
reused: &crate::salsa::ParsedDocument,
text: &str,
config: &Config,
refdefs: &RefdefMap,
) {
use std::sync::OnceLock;
static ARMED: OnceLock<bool> = OnceLock::new();
if !*ARMED.get_or_init(|| std::env::var("PANACHE_REPARSE_ORACLE").as_deref() == Ok("1")) {
return;
}
let (full, full_errors) =
crate::parser::parse_with_refdefs_and_errors(text, Some(config.clone()), refdefs.clone());
let reused_root = crate::syntax::SyntaxNode::new_root(reused.green.clone());
assert_eq!(
panache_parser::parser::fingerprint(&reused_root),
panache_parser::parser::fingerprint(&full),
"reused parse diverged from a full parse of the same text",
);
assert_eq!(
reused.errors, full_errors,
"reused parse diverged from a full parse on syntax errors",
);
}
#[cfg(not(debug_assertions))]
#[inline]
pub fn assert_reuse_matches_full_parse(
_reused: &crate::salsa::ParsedDocument,
_text: &str,
_config: &Config,
_refdefs: &RefdefMap,
) {
}
#[cfg(test)]
mod tests {
use super::*;
use crate::salsa::SalsaDb;
fn base(text: &str) -> PrevParse {
let tree = crate::parser::parse(text, None);
PrevParse {
text: Arc::from(text),
green: tree.green().to_owned(),
errors: Vec::new(),
refdefs: crate::parser::collect_refdef_labels(
text,
panache_parser::Dialect::for_flavor(Config::default().flavor),
),
config: Config::default(),
}
}
fn key(db: &SalsaDb, text: &str) -> ReparseKey {
(
FileText::from_str(db, text),
FileConfig::new(db, Config::default()),
)
}
#[test]
fn a_key_that_was_never_admitted_is_refused_and_stores_nothing() {
let db = SalsaDb::default();
let mut cache = ReparseCache::default();
let k = key(&db, "# Title\n");
assert!(matches!(cache.base(k), ReparseAdmission::Refused));
cache.store(k, base("# Title\n"));
assert!(matches!(cache.base(k), ReparseAdmission::Refused));
}
#[test]
fn admission_enables_storing_and_reading_a_base() {
let db = SalsaDb::default();
let mut cache = ReparseCache::default();
let k = key(&db, "# Title\n");
cache.admit(k);
assert!(matches!(cache.base(k), ReparseAdmission::Admitted(None)));
cache.store(k, base("# Title\n"));
let ReparseAdmission::Admitted(Some(prev)) = cache.base(k) else {
panic!("a stored base must come back");
};
assert_eq!(&*prev.text, "# Title\n");
}
#[test]
fn admitting_a_new_config_drops_the_same_file_under_the_old_one() {
let db = SalsaDb::default();
let mut cache = ReparseCache::default();
let file = FileText::from_str(&db, "# Title\n");
let old_config = FileConfig::new(&db, Config::default());
let new_config = FileConfig::new(&db, Config::default());
cache.admit((file, old_config));
cache.store((file, old_config), base("# Title\n"));
cache.admit((file, new_config));
assert!(matches!(
cache.base((file, old_config)),
ReparseAdmission::Refused
));
assert!(matches!(
cache.base((file, new_config)),
ReparseAdmission::Admitted(None)
));
}
#[test]
fn retiring_a_file_forgets_every_config_it_was_parsed_under() {
let db = SalsaDb::default();
let mut cache = ReparseCache::default();
let file = FileText::from_str(&db, "# Title\n");
let other = FileText::from_str(&db, "# Other\n");
let config = FileConfig::new(&db, Config::default());
cache.admit((file, config));
cache.admit((other, config));
cache.retire_file(file);
assert!(matches!(
cache.base((file, config)),
ReparseAdmission::Refused
));
assert!(matches!(
cache.base((other, config)),
ReparseAdmission::Admitted(None)
));
}
#[test]
fn eviction_keeps_the_budget_and_spares_the_most_recently_used() {
let db = SalsaDb::default();
let mut cache = ReparseCache::default();
let config = FileConfig::new(&db, Config::default());
let keys: Vec<ReparseKey> = (0..MAX_REPARSE_BASES + 8)
.map(|index| (FileText::from_str(&db, format!("# {index}\n")), config))
.collect();
for key in &keys {
cache.admit(*key);
}
assert!(cache.files.len() <= MAX_REPARSE_BASES);
assert!(matches!(
cache.base(*keys.last().unwrap()),
ReparseAdmission::Admitted(_)
));
}
#[test]
fn clearing_empties_the_channel() {
let db = SalsaDb::default();
let mut cache = ReparseCache::default();
let k = key(&db, "# Title\n");
cache.admit(k);
cache.store(k, base("# Title\n"));
cache.clear();
assert!(matches!(cache.base(k), ReparseAdmission::Refused));
}
}