use std::collections::VecDeque;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use i_slint_compiler::CompilerConfiguration;
use i_slint_compiler::parser::Token;
const CAPACITY: usize = 64;
pub fn is_rust_analyzer() -> bool {
std::env::var_os("RUST_ANALYZER_INTERNALS_DO_NOT_USE").is_some()
}
pub fn enabled() -> bool {
is_rust_analyzer()
|| matches!(std::env::var("SLINT_MACRO_CACHE").as_deref(), Ok("1") | Ok("true"))
}
struct Entry {
key: String,
output: String,
deps: Vec<(PathBuf, u64)>,
}
#[derive(Default)]
struct Cache {
entries: VecDeque<Entry>,
}
impl Cache {
fn candidate(&self, key: &str) -> Option<(String, Vec<(PathBuf, u64)>)> {
self.entries.iter().find(|e| e.key == key).map(|e| (e.output.clone(), e.deps.clone()))
}
fn put(&mut self, key: String, output: String, deps: Vec<(PathBuf, u64)>) {
if let Some(existing) = self.entries.iter_mut().find(|e| e.key == key) {
existing.output = output;
existing.deps = deps;
return;
}
if self.entries.len() >= CAPACITY {
self.entries.pop_front();
}
self.entries.push_back(Entry { key, output, deps });
}
}
fn deps_match(deps: &[(PathBuf, u64)]) -> bool {
deps.iter().all(|(path, expected)| content_hash(path) == Some(*expected))
}
fn cache() -> &'static Mutex<Cache> {
static CACHE: OnceLock<Mutex<Cache>> = OnceLock::new();
CACHE.get_or_init(|| Mutex::new(Cache::default()))
}
fn hash64<H: Hash>(value: &H) -> u64 {
let mut hasher = DefaultHasher::new();
value.hash(&mut hasher);
hasher.finish()
}
fn content_hash(path: &Path) -> Option<u64> {
std::fs::read(path).ok().map(|bytes| hash64(&bytes))
}
pub fn key_material(
tokens: &[Token],
config: &CompilerConfiguration,
source_path: &Path,
) -> String {
use std::fmt::Write;
let mut key = String::new();
let _ = write!(key, "src={source_path:?};");
let _ = write!(key, "style={:?};", config.style);
let _ = write!(key, "inc={:?};", config.include_paths);
let mut libs: Vec<_> = config.library_paths.iter().collect();
libs.sort_by(|a, b| a.0.cmp(b.0));
let _ = write!(key, "lib={libs:?};");
let _ = write!(key, "td={:?};", config.translation_domain);
let _ = write!(key, "dtc={:?};", config.default_translation_context);
let _ = write!(key, "exp={};", config.enable_experimental);
let _ = write!(key, "inl={};", config.inline_all_elements);
key.push_str("tokens=");
for t in tokens {
let _ = write!(key, "{:?}:{};", t.kind, t.text);
}
key
}
#[cfg(test)]
static HIT_COUNT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
#[inline]
fn note_hit() {
#[cfg(test)]
HIT_COUNT.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
pub fn lookup(key: &str) -> Option<String> {
let (output, deps) = cache().lock().ok()?.candidate(key)?;
if deps_match(&deps) {
note_hit();
Some(output)
} else {
None
}
}
pub fn store(key: String, output: String, loaded_files: &[PathBuf]) {
let mut deps = Vec::with_capacity(loaded_files.len());
for path in loaded_files {
match content_hash(path) {
Some(h) => deps.push((path.clone(), h)),
None => return,
}
}
if let Ok(mut guard) = cache().lock() {
guard.put(key, output, deps);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn hit_count() -> u64 {
HIT_COUNT.load(std::sync::atomic::Ordering::Relaxed)
}
#[test]
fn store_then_lookup_is_a_recorded_hit() {
let key = "expansion_cache::store_then_lookup_is_a_recorded_hit";
assert_eq!(lookup(key), None, "absent key misses");
store(key.into(), "OUTPUT".into(), &[]);
let before = hit_count();
assert_eq!(lookup(key).as_deref(), Some("OUTPUT"), "stored key hits");
assert!(hit_count() > before, "the hit is recorded");
}
#[test]
fn changed_dependency_turns_a_hit_into_a_miss() {
let key = "expansion_cache::changed_dependency_turns_a_hit_into_a_miss";
let dir = std::env::temp_dir().join("slint_macro_cache_test_dep");
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("dep.slint");
std::fs::write(&file, b"export component A {}").unwrap();
store(key.into(), "OUTPUT".into(), std::slice::from_ref(&file));
assert_eq!(lookup(key).as_deref(), Some("OUTPUT"), "unchanged dep hits");
std::fs::write(&file, b"export component A { width: 1px; }").unwrap();
assert_eq!(lookup(key), None, "changed dep invalidates");
std::fs::remove_file(&file).unwrap();
assert_eq!(lookup(key), None, "missing dep invalidates");
}
#[test]
fn put_replaces_an_existing_key() {
let mut cache = Cache::default();
cache.put("k".into(), "v1".into(), vec![]);
cache.put("k".into(), "v2".into(), vec![]);
assert_eq!(cache.candidate("k").map(|(o, _)| o).as_deref(), Some("v2"));
assert_eq!(cache.entries.len(), 1);
}
#[test]
fn put_evicts_the_oldest_when_full() {
let mut cache = Cache::default();
for i in 0..CAPACITY + 10 {
cache.put(format!("k{i}"), format!("v{i}"), vec![]);
}
assert_eq!(cache.entries.len(), CAPACITY, "size is bounded");
assert!(cache.candidate("k0").is_none());
assert!(cache.candidate("k9").is_none());
assert_eq!(cache.candidate("k10").map(|(o, _)| o).as_deref(), Some("v10"));
}
}