use std::collections::hash_map::DefaultHasher;
use std::collections::HashMap;
use std::hash::{Hash, Hasher};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, PoisonError};
use harn_modules::project_config::LintConfig;
use harn_modules::ModuleGraph;
use harn_parser::{Node, SNode};
struct CachedGraph {
imports_key: u64,
graph: Arc<ModuleGraph>,
valid: bool,
}
#[derive(Default)]
pub(crate) struct WorkspaceCache {
graphs: Mutex<HashMap<PathBuf, CachedGraph>>,
lint_configs: Mutex<HashMap<PathBuf, Arc<LintConfig>>>,
buffers: Mutex<HashMap<PathBuf, String>>,
generation: AtomicU64,
#[cfg(test)]
graph_probe: Mutex<Option<Arc<dyn Fn() + Send + Sync>>>,
}
impl WorkspaceCache {
pub(crate) fn module_graph(
&self,
path: &Path,
source: &str,
program: &[SNode],
) -> Arc<ModuleGraph> {
let imports_key = imports_key(source, program);
loop {
let (generation, mut sources) = {
let graphs = lock(&self.graphs);
if let Some(cached) = graphs.get(path) {
if cached.valid && cached.imports_key == imports_key {
return Arc::clone(&cached.graph);
}
}
(self.generation(), lock(&self.buffers).clone())
};
sources.insert(path.to_path_buf(), source.to_string());
let graph = Arc::new(harn_modules::build_with_sources(path, &sources));
#[cfg(test)]
if let Some(probe) = lock(&self.graph_probe).clone() {
probe();
}
let mut graphs = lock(&self.graphs);
if generation != self.generation() {
continue;
}
graphs.insert(
path.to_path_buf(),
CachedGraph {
imports_key,
graph: Arc::clone(&graph),
valid: true,
},
);
return graph;
}
}
pub(crate) fn generation(&self) -> u64 {
self.generation.load(Ordering::Acquire)
}
pub(crate) fn lint_config(&self, path: &Path) -> Arc<LintConfig> {
let dir = path.parent().unwrap_or(path).to_path_buf();
loop {
let generation = self.generation();
if let Some(config) = lock(&self.lint_configs).get(&dir) {
return Arc::clone(config);
}
let config = Arc::new(
harn_modules::project_config::load_for_path(path)
.map(|config| config.lint)
.unwrap_or_default(),
);
let mut configs = lock(&self.lint_configs);
if generation != self.generation() {
continue;
}
configs.insert(dir, Arc::clone(&config));
return config;
}
}
pub(crate) fn set_buffer(&self, path: PathBuf, source: String) {
let mut graphs = lock(&self.graphs);
lock(&self.buffers).insert(path.clone(), source);
self.generation.fetch_add(1, Ordering::Release);
invalidate_graphs(&mut graphs, &path);
}
pub(crate) fn remove_buffer(&self, path: &Path) {
let mut graphs = lock(&self.graphs);
lock(&self.buffers).remove(path);
graphs.remove(path);
self.generation.fetch_add(1, Ordering::Release);
invalidate_graphs(&mut graphs, path);
}
pub(crate) fn invalidate_module(&self, changed: &Path) -> Vec<PathBuf> {
let mut graphs = lock(&self.graphs);
self.generation.fetch_add(1, Ordering::Release);
invalidate_graphs(&mut graphs, changed)
}
pub(crate) fn invalidate_all(&self) {
let mut graphs = lock(&self.graphs);
graphs.clear();
lock(&self.lint_configs).clear();
self.generation.fetch_add(1, Ordering::Release);
}
}
fn invalidate_graphs(graphs: &mut HashMap<PathBuf, CachedGraph>, changed: &Path) -> Vec<PathBuf> {
graphs
.iter()
.filter(|(root, cached)| root.as_path() != changed && cached.graph.contains_module(changed))
.map(|(root, _)| root.clone())
.collect::<Vec<_>>()
.into_iter()
.inspect(|root| graphs.get_mut(root).unwrap().valid = false)
.collect()
}
fn lock<T>(mutex: &Mutex<T>) -> std::sync::MutexGuard<'_, T> {
mutex.lock().unwrap_or_else(PoisonError::into_inner)
}
fn imports_key(source: &str, program: &[SNode]) -> u64 {
let mut hasher = DefaultHasher::new();
for node in program {
if matches!(
node.node,
Node::ImportDecl { .. } | Node::SelectiveImport { .. } | Node::NamespaceImport { .. }
) {
(node.span.start, source.get(node.span.start..node.span.end)).hash(&mut hasher);
}
}
hasher.finish()
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tower_lsp::lsp_types::Url;
use super::WorkspaceCache;
use crate::document::DocumentState;
fn analyze(state: &mut DocumentState, uri: &Url, workspace: Option<&WorkspaceCache>) {
state.dirty = true;
state.reparse_if_dirty_with_rules(Some(uri), None, workspace);
}
#[test]
fn edits_reuse_the_module_graph_until_an_imported_module_changes() {
let dir = tempfile::tempdir().unwrap();
let lib = dir.path().join("lib.harn");
let app = dir.path().join("app.harn");
std::fs::write(&lib, "pub fn helper() -> int {\n return 1\n}\n").unwrap();
let source = "import { helper } from \"./lib\"\n\npipeline main() {\n log(helper())\n}\n";
std::fs::write(&app, source).unwrap();
let uri = Url::from_file_path(&app).unwrap();
let workspace = WorkspaceCache::default();
let mut state = DocumentState::new_for_language(source.to_string(), "harn", &uri);
let reads = harn_modules::module_source_disk_reads;
let start = reads();
analyze(&mut state, &uri, Some(&workspace));
assert_eq!(
reads() - start,
1,
"the first analysis reads the imported module"
);
let edited = source.replace("log(helper())", "log(helper() + 1)");
state.update_source(edited);
let start = reads();
analyze(&mut state, &uri, Some(&workspace));
assert_eq!(reads() - start, 0, "a body edit reuses the cached graph");
let start = reads();
analyze(&mut state, &uri, None);
assert_eq!(reads() - start, 1);
std::fs::write(&lib, "pub fn helper() -> string {\n return \"one\"\n}\n").unwrap();
let importers = workspace.invalidate_module(&lib);
assert_eq!(importers, vec![app]);
let start = reads();
analyze(&mut state, &uri, Some(&workspace));
assert_eq!(
reads() - start,
1,
"the importer re-reads the changed module"
);
}
#[test]
fn a_graph_built_during_a_dependency_edit_is_rebuilt_before_publication() {
let dir = tempfile::tempdir().unwrap();
let lib = dir.path().join("lib.harn");
let app = dir.path().join("app.harn");
std::fs::write(&lib, "pub fn helper() -> int { return 1 }\n").unwrap();
let source =
"import { helper } from \"./lib\"\npub fn total() -> int { return helper() }\n";
std::fs::write(&app, source).unwrap();
let uri = Url::from_file_path(&app).unwrap();
let workspace = Arc::new(WorkspaceCache::default());
let reached = Arc::new(AtomicBool::new(false));
let weak = Arc::downgrade(&workspace);
let observed = Arc::clone(&reached);
*super::lock(&workspace.graph_probe) = Some(Arc::new(move || {
if !observed.swap(true, Ordering::SeqCst) {
weak.upgrade().unwrap().set_buffer(
lib.clone(),
"pub fn helper() -> string { return \"changed\" }\n".into(),
);
}
}));
let mut state = DocumentState::new_for_language(source.into(), "harn", &uri);
analyze(&mut state, &uri, Some(&workspace));
assert!(
reached.load(Ordering::SeqCst),
"the edit occurred after a real graph build"
);
assert!(state
.type_diagnostics
.iter()
.any(|diagnostic| diagnostic.severity == harn_parser::DiagnosticSeverity::Error));
assert!(workspace
.invalidate_module(&dir.path().join("lib.harn"))
.contains(&app));
}
}