use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use salsa::Setter;
use smol_str::SmolStr;
use crate::bib::semantic::Model as BibModel;
use crate::bib::syntax::SyntaxNode as BibSyntaxNode;
use crate::declarations::ResolvedDeclarations;
use crate::file_discovery::file_kind_or_tex;
use crate::parser::{
Edit, LexConfig, ParseCtx, ReparseBase, ReparseTier, SyntaxError,
parse_with_declarations_resolved, reparse_edits,
};
use crate::project::citations::document_cite_names;
use crate::project::labels::{
document_glossary_keys, document_label_names, document_ref_names, is_document_root,
};
use crate::project::options::resolved_package_options;
use crate::project::{
BibTarget, IncludeEdgeKey, PackageEdgeKey, PackageOptionFacts, Project, ProjectMember,
ResolvedCitations, ResolvedLabels, ResolvedPackageOptions, collect_bib_resource_targets,
collect_include_edge_keys, collect_package_edge_keys, package_graph, package_option_facts,
resolved_citations, resolved_labels,
};
use crate::semantic::{
DocAssociation, SemanticModel, SignatureDb, doc_associations as build_doc_associations,
scan_definitions,
};
use crate::syntax::SyntaxNode;
#[salsa::input]
pub struct SourceFile {
#[returns(ref)]
pub path: PathBuf,
#[returns(ref)]
pub text: Arc<str>,
}
#[salsa::input(singleton)]
pub struct DeclarationsInput {
#[returns(ref)]
pub declarations: ResolvedDeclarations,
}
fn declarations_of(db: &dyn IncrementalDb) -> &ResolvedDeclarations {
DeclarationsInput::get(db).declarations(db)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum QueryKind {
ParsedDocument,
SemanticModel,
DocumentSignatures,
DocAssociations,
IncludeEdges,
PackageEdges,
FileLabels,
FileRefs,
FileGlossaryKeys,
FileIsDocumentRoot,
ProjectGraph,
PackageGraph,
ScopeSignatures,
ResolvedLabels,
ParsedBibDocument,
BibSemanticModel,
FileCiteNames,
FileCiteFacts,
ResolvedCitations,
FilePackageOptionFacts,
ResolvedPackageOptions,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct QueryLogEntry {
pub kind: QueryKind,
pub file: Option<SourceFile>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseDiagnosticData {
pub message: String,
pub start: usize,
pub end: usize,
}
#[derive(Debug, Clone)]
pub struct ParsedDocument {
pub green: rowan::GreenNode,
pub diagnostics: Vec<ParseDiagnosticData>,
}
#[derive(Debug, Clone)]
pub struct ParsedBibDocument {
pub green: rowan::GreenNode,
pub diagnostics: Vec<ParseDiagnosticData>,
}
#[derive(Debug, Clone)]
pub struct PrevParse {
pub text: Arc<str>,
pub green: rowan::GreenNode,
pub errors: Vec<SyntaxError>,
pub ctx: ParseCtx,
pub config: LexConfig,
pub declared: ResolvedDeclarations,
}
impl PrevParse {
fn is_current(
&self,
text: &Arc<str>,
config: LexConfig,
declared: &ResolvedDeclarations,
) -> bool {
self.config == config
&& &self.declared == declared
&& (Arc::ptr_eq(&self.text, text) || *self.text == **text)
}
fn as_reparse_base<'a>(&'a self, declared: &'a ResolvedDeclarations) -> ReparseBase<'a> {
ReparseBase::from_parts(
&self.text,
&self.green,
&self.errors,
&self.ctx,
self.config,
declared,
)
}
}
const MAX_REPARSE_BASES: usize = 64;
const MAX_CHAIN_EDITS: usize = 16;
const MAX_CHAIN_INSERT_BYTES: usize = 64 * 1024;
#[salsa::db]
pub trait IncrementalDb: salsa::Database {
fn record_query(&self, entry: QueryLogEntry);
fn reparse_prev(&self, _file: SourceFile) -> Option<Arc<PrevParse>> {
None
}
fn reparse_stage_edits(&self, _file: SourceFile, _edits: Option<Vec<Edit>>) {}
fn reparse_pending_edits(&self, _file: SourceFile) -> Vec<Edit> {
Vec::new()
}
fn reparse_store(
&self,
_file: SourceFile,
_prev: PrevParse,
_tier: Option<ReparseTier>,
_consumed: usize,
) {
}
fn reparse_evict(&self, _file: SourceFile) {}
}
#[salsa::tracked(returns(ref), no_eq, unsafe(non_salsa_values))]
pub fn parsed_document(db: &dyn IncrementalDb, file: SourceFile) -> ParsedDocument {
db.record_query(QueryLogEntry {
kind: QueryKind::ParsedDocument,
file: Some(file),
});
let config = file_kind_or_tex(file.path(db)).lex_config();
let declared = declarations_of(db);
let text = file.text(db);
let staged = db.reparse_pending_edits(file);
let prev = db.reparse_prev(file);
if let Some(prev) = prev
.as_ref()
.filter(|prev| prev.is_current(text, config, declared))
{
return ParsedDocument {
green: prev.green.clone(),
diagnostics: to_diagnostics(&prev.errors),
};
}
let reparsed = prev
.as_ref()
.and_then(|prev| reparse_edits(&prev.as_reparse_base(declared), &staged, text));
let tier = reparsed.as_ref().map(|r| r.tier);
let (green, errors) = match reparsed {
Some(r) => (r.green, r.errors),
None => {
let (parsed, ctx) = parse_with_declarations_resolved(text, config, declared);
let (green, errors) = (parsed.green, parsed.errors);
db.reparse_store(
file,
PrevParse {
text: text.clone(),
green: green.clone(),
errors: errors.clone(),
ctx,
config,
declared: declared.clone(),
},
tier,
staged.len(),
);
return ParsedDocument {
diagnostics: to_diagnostics(&errors),
green,
};
}
};
let ctx = prev
.as_ref()
.map(|prev| prev.ctx.clone())
.unwrap_or_default();
db.reparse_store(
file,
PrevParse {
text: text.clone(),
green: green.clone(),
errors: errors.clone(),
ctx,
config,
declared: declared.clone(),
},
tier,
staged.len(),
);
ParsedDocument {
green,
diagnostics: to_diagnostics(&errors),
}
}
fn to_diagnostics(errors: &[SyntaxError]) -> Vec<ParseDiagnosticData> {
errors
.iter()
.map(|error| ParseDiagnosticData {
message: error.message.clone(),
start: error.start,
end: error.end,
})
.collect()
}
pub fn parse_diagnostics(db: &dyn IncrementalDb, file: SourceFile) -> &[ParseDiagnosticData] {
&parsed_document(db, file).diagnostics
}
pub fn parsed_tree_root(db: &dyn IncrementalDb, file: SourceFile) -> SyntaxNode {
SyntaxNode::new_root(parsed_document(db, file).green.clone())
}
#[salsa::tracked(returns(ref))]
pub fn semantic_model(db: &dyn IncrementalDb, file: SourceFile) -> SemanticModel {
db.record_query(QueryLogEntry {
kind: QueryKind::SemanticModel,
file: Some(file),
});
SemanticModel::build(&parsed_tree_root(db, file))
}
#[salsa::tracked(returns(ref))]
pub fn document_signatures(db: &dyn IncrementalDb, file: SourceFile) -> SignatureDb {
db.record_query(QueryLogEntry {
kind: QueryKind::DocumentSignatures,
file: Some(file),
});
scan_definitions(&parsed_tree_root(db, file))
}
#[salsa::tracked(returns(ref))]
pub fn scope_signatures<'db>(
db: &'db dyn IncrementalDb,
project: Project<'db>,
file: SourceFile,
) -> SignatureDb {
db.record_query(QueryLogEntry {
kind: QueryKind::ScopeSignatures,
file: Some(file),
});
let graph = package_graph(db, project);
let by_path: HashMap<&Path, SourceFile> = project
.members(db)
.iter()
.map(|member| (member.path.as_path(), member.file))
.collect();
let mut merged = SignatureDb::default();
for loaded in graph.transitively_loaded(file.path(db)) {
if let Some(&member) = by_path.get(loaded.as_path()) {
match loaded.file_stem().and_then(|s| s.to_str()) {
Some(origin) => {
merged.merge_from_package(document_signatures(db, member), origin);
}
None => merged.merge_from(document_signatures(db, member)),
}
}
}
merged.merge_from(document_signatures(db, file));
merged.merge_declarations(declarations_of(db));
merged
}
#[salsa::tracked(returns(ref))]
pub fn doc_associations(db: &dyn IncrementalDb, file: SourceFile) -> Vec<DocAssociation> {
db.record_query(QueryLogEntry {
kind: QueryKind::DocAssociations,
file: Some(file),
});
build_doc_associations(&parsed_tree_root(db, file))
}
#[salsa::tracked(returns(ref))]
pub fn include_edges(db: &dyn IncrementalDb, file: SourceFile) -> Vec<IncludeEdgeKey> {
db.record_query(QueryLogEntry {
kind: QueryKind::IncludeEdges,
file: Some(file),
});
let root = parsed_tree_root(db, file);
collect_include_edge_keys(&root, file.path(db).parent())
}
#[salsa::tracked(returns(ref))]
pub fn package_edges(db: &dyn IncrementalDb, file: SourceFile) -> Vec<PackageEdgeKey> {
db.record_query(QueryLogEntry {
kind: QueryKind::PackageEdges,
file: Some(file),
});
let root = parsed_tree_root(db, file);
collect_package_edge_keys(&root, file.path(db).parent())
}
#[salsa::tracked(returns(ref))]
pub fn file_labels(db: &dyn IncrementalDb, file: SourceFile) -> Vec<SmolStr> {
db.record_query(QueryLogEntry {
kind: QueryKind::FileLabels,
file: Some(file),
});
document_label_names(semantic_model(db, file))
}
#[salsa::tracked(returns(ref))]
pub fn file_refs(db: &dyn IncrementalDb, file: SourceFile) -> Vec<SmolStr> {
db.record_query(QueryLogEntry {
kind: QueryKind::FileRefs,
file: Some(file),
});
document_ref_names(semantic_model(db, file))
}
#[salsa::tracked(returns(ref))]
pub fn file_glossary_keys(db: &dyn IncrementalDb, file: SourceFile) -> Vec<SmolStr> {
db.record_query(QueryLogEntry {
kind: QueryKind::FileGlossaryKeys,
file: Some(file),
});
document_glossary_keys(semantic_model(db, file))
}
#[salsa::tracked(returns(ref))]
pub fn file_is_document_root(db: &dyn IncrementalDb, file: SourceFile) -> bool {
db.record_query(QueryLogEntry {
kind: QueryKind::FileIsDocumentRoot,
file: Some(file),
});
is_document_root(&parsed_tree_root(db, file))
}
#[salsa::tracked(returns(ref))]
pub fn file_package_option_facts(
db: &dyn IncrementalDb,
file: SourceFile,
) -> Option<PackageOptionFacts> {
db.record_query(QueryLogEntry {
kind: QueryKind::FilePackageOptionFacts,
file: Some(file),
});
package_option_facts(
file.path(db),
&parsed_tree_root(db, file),
semantic_model(db, file),
)
}
#[salsa::tracked(returns(ref), no_eq, unsafe(non_salsa_values))]
pub fn parsed_bib_document(db: &dyn IncrementalDb, file: SourceFile) -> ParsedBibDocument {
db.record_query(QueryLogEntry {
kind: QueryKind::ParsedBibDocument,
file: Some(file),
});
let parsed = crate::bib::parse(file.text(db));
let diagnostics = parsed
.errors
.into_iter()
.map(|error| ParseDiagnosticData {
message: error.message,
start: error.start,
end: error.end,
})
.collect();
ParsedBibDocument {
green: parsed.green,
diagnostics,
}
}
pub fn bib_parse_diagnostics(db: &dyn IncrementalDb, file: SourceFile) -> &[ParseDiagnosticData] {
&parsed_bib_document(db, file).diagnostics
}
pub fn parsed_bib_tree_root(db: &dyn IncrementalDb, file: SourceFile) -> BibSyntaxNode {
BibSyntaxNode::new_root(parsed_bib_document(db, file).green.clone())
}
#[salsa::tracked(returns(ref))]
pub fn bib_semantic_model(db: &dyn IncrementalDb, file: SourceFile) -> BibModel {
db.record_query(QueryLogEntry {
kind: QueryKind::BibSemanticModel,
file: Some(file),
});
BibModel::build(&parsed_bib_tree_root(db, file))
}
#[salsa::tracked(returns(ref))]
pub fn file_cite_names(db: &dyn IncrementalDb, file: SourceFile) -> Vec<SmolStr> {
db.record_query(QueryLogEntry {
kind: QueryKind::FileCiteNames,
file: Some(file),
});
document_cite_names(bib_semantic_model(db, file))
}
#[derive(Debug, Clone, PartialEq, Eq, salsa::SalsaValue)]
pub struct FileCiteFacts {
pub bib_targets: Vec<BibTarget>,
pub nocite_all: bool,
}
#[salsa::tracked(returns(ref))]
pub fn file_cite_facts(db: &dyn IncrementalDb, file: SourceFile) -> FileCiteFacts {
db.record_query(QueryLogEntry {
kind: QueryKind::FileCiteFacts,
file: Some(file),
});
let root = parsed_tree_root(db, file);
FileCiteFacts {
bib_targets: collect_bib_resource_targets(&root, file.path(db).parent()),
nocite_all: semantic_model(db, file).has_wildcard_nocite(),
}
}
#[derive(Default)]
struct FileReparseState {
prev: Option<Arc<PrevParse>>,
pending: Vec<Edit>,
used: u64,
hot: bool,
}
#[derive(Default)]
struct ReparseCache {
files: HashMap<SourceFile, FileReparseState>,
clock: u64,
}
impl ReparseCache {
fn touch(&mut self) -> u64 {
self.clock += 1;
self.clock
}
fn evict_if_full(&mut self) {
if self.files.len() <= MAX_REPARSE_BASES {
return;
}
let mut stamps: Vec<(bool, u64, SourceFile)> = self
.files
.iter()
.map(|(&file, state)| (state.hot, state.used, file))
.collect();
stamps.sort_unstable_by_key(|&(hot, used, _)| (hot, used));
let excess = self.files.len() - MAX_REPARSE_BASES;
for (_, _, file) in stamps.into_iter().take(excess) {
self.files.remove(&file);
}
}
}
#[salsa::db]
pub struct IncrementalDatabase {
storage: salsa::Storage<Self>,
query_log: Arc<Mutex<Vec<QueryLogEntry>>>,
files: Arc<Mutex<HashMap<PathBuf, SourceFile>>>,
reparse_cache: Arc<Mutex<ReparseCache>>,
}
impl Default for IncrementalDatabase {
fn default() -> Self {
let db = Self {
storage: salsa::Storage::new(None),
query_log: Arc::new(Mutex::new(Vec::new())),
files: Arc::new(Mutex::new(HashMap::new())),
reparse_cache: Arc::new(Mutex::new(ReparseCache::default())),
};
let _ = DeclarationsInput::builder(ResolvedDeclarations::default())
.declarations_durability(salsa::Durability::HIGH)
.new(&db);
db
}
}
impl Clone for IncrementalDatabase {
fn clone(&self) -> Self {
Self {
storage: self.storage.clone(),
query_log: Arc::clone(&self.query_log),
files: Arc::clone(&self.files),
reparse_cache: Arc::clone(&self.reparse_cache),
}
}
}
impl std::fmt::Debug for IncrementalDatabase {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("IncrementalDatabase")
.finish_non_exhaustive()
}
}
fn recover_poison<T>(err: std::sync::PoisonError<T>) -> T {
err.into_inner()
}
pub(crate) fn normalize_path(path: &Path) -> PathBuf {
use std::path::Component;
let absolute = std::path::absolute(path).unwrap_or_else(|_| path.to_path_buf());
let mut out = PathBuf::new();
for component in absolute.components() {
match component {
Component::CurDir => {}
Component::ParentDir
if matches!(out.components().next_back(), Some(Component::Normal(_))) =>
{
out.pop();
}
other => out.push(other.as_os_str()),
}
}
out
}
static MEM_FILE_COUNTER: AtomicU64 = AtomicU64::new(0);
impl IncrementalDatabase {
pub fn add_file(&self, text: impl Into<Arc<str>>) -> SourceFile {
let n = MEM_FILE_COUNTER.fetch_add(1, Ordering::Relaxed);
let path = PathBuf::from(format!("<mem>/{n}.tex"));
SourceFile::builder(path, text.into())
.path_durability(salsa::Durability::HIGH)
.new(self)
}
pub fn set_file_text(&mut self, file: SourceFile, text: impl Into<Arc<str>>) {
file.set_text(self).to(text.into());
}
pub fn declarations(&self) -> &ResolvedDeclarations {
DeclarationsInput::get(self).declarations(self)
}
pub fn set_declarations(&mut self, declared: ResolvedDeclarations) -> bool {
let input = DeclarationsInput::get(self);
if input.declarations(self) == &declared {
return false;
}
input
.set_declarations(self)
.with_durability(salsa::Durability::HIGH)
.to(declared);
true
}
pub fn upsert_file(&mut self, path: &Path, text: impl Into<Arc<str>>) -> SourceFile {
let key = normalize_path(path);
let text = text.into();
let existing = self
.files
.lock()
.unwrap_or_else(recover_poison)
.get(&key)
.copied();
match existing {
Some(file) => {
let unchanged = {
let tracked = file.text(self);
Arc::ptr_eq(tracked, &text) || **tracked == *text
};
if !unchanged {
file.set_text(self).to(text);
}
file
}
None => {
let file = SourceFile::builder(key.clone(), text)
.path_durability(salsa::Durability::HIGH)
.new(self);
self.files
.lock()
.unwrap_or_else(recover_poison)
.insert(key, file);
file
}
}
}
pub fn tracked_files(&self) -> Vec<(PathBuf, SourceFile)> {
let mut files: Vec<(PathBuf, SourceFile)> = self
.files
.lock()
.unwrap_or_else(recover_poison)
.iter()
.map(|(path, &file)| (path.clone(), file))
.collect();
files.sort_by(|a, b| a.0.cmp(&b.0));
files
}
pub fn lookup_file(&self, path: &Path) -> Option<SourceFile> {
self.files
.lock()
.unwrap_or_else(recover_poison)
.get(&normalize_path(path))
.copied()
}
pub fn remove_file(&mut self, path: &Path) -> Option<SourceFile> {
let removed = self
.files
.lock()
.unwrap_or_else(recover_poison)
.remove(&normalize_path(path));
if let Some(file) = removed {
self.reparse_evict(file);
}
removed
}
pub fn reparse_cache_len(&self) -> usize {
self.reparse_cache
.lock()
.unwrap_or_else(recover_poison)
.files
.len()
}
pub fn file_text(&self, file: SourceFile) -> &str {
file.text(self)
}
pub fn text_is_current(&self, file: SourceFile, text: &str) -> bool {
let tracked: &str = file.text(self);
let same_bytes = std::ptr::eq(tracked.as_ptr(), text.as_ptr());
(same_bytes && tracked.len() == text.len()) || tracked == text
}
pub fn file_path(&self, file: SourceFile) -> &Path {
file.path(self)
}
pub fn parse_diagnostics(&self, file: SourceFile) -> &[ParseDiagnosticData] {
parse_diagnostics(self, file)
}
pub fn parsed_tree(&self, file: SourceFile) -> SyntaxNode {
parsed_tree_root(self, file)
}
pub fn include_edges(&self, file: SourceFile) -> &[IncludeEdgeKey] {
include_edges(self, file)
}
pub fn semantic_model(&self, file: SourceFile) -> &SemanticModel {
semantic_model(self, file)
}
pub fn document_signatures(&self, file: SourceFile) -> &SignatureDb {
document_signatures(self, file)
}
pub fn doc_associations(&self, file: SourceFile) -> &[DocAssociation] {
doc_associations(self, file)
}
pub fn file_labels(&self, file: SourceFile) -> &[SmolStr] {
file_labels(self, file)
}
pub fn file_refs(&self, file: SourceFile) -> &[SmolStr] {
file_refs(self, file)
}
pub fn file_glossary_keys(&self, file: SourceFile) -> &[SmolStr] {
file_glossary_keys(self, file)
}
pub fn file_is_document_root(&self, file: SourceFile) -> bool {
*file_is_document_root(self, file)
}
pub fn bib_parse_diagnostics(&self, file: SourceFile) -> &[ParseDiagnosticData] {
bib_parse_diagnostics(self, file)
}
pub fn parsed_bib_tree(&self, file: SourceFile) -> BibSyntaxNode {
parsed_bib_tree_root(self, file)
}
pub fn bib_semantic_model(&self, file: SourceFile) -> &BibModel {
bib_semantic_model(self, file)
}
pub fn clear_query_log(&self) {
self.query_log.lock().unwrap_or_else(recover_poison).clear();
}
pub fn query_log(&self) -> Vec<QueryLogEntry> {
self.query_log.lock().unwrap_or_else(recover_poison).clone()
}
pub fn snapshot(&self) -> Analysis {
Analysis(self.clone())
}
}
pub struct Analysis(IncrementalDatabase);
impl Analysis {
pub fn lookup_file(&self, path: &Path) -> Option<SourceFile> {
self.0.lookup_file(path)
}
pub fn file_text(&self, file: SourceFile) -> &str {
self.0.file_text(file)
}
pub fn text_is_current(&self, file: SourceFile, text: &str) -> bool {
self.0.text_is_current(file, text)
}
pub fn file_path(&self, file: SourceFile) -> &Path {
self.0.file_path(file)
}
pub fn tracked_files(&self) -> Vec<(PathBuf, SourceFile)> {
self.0.tracked_files()
}
pub fn declarations(&self) -> &ResolvedDeclarations {
self.0.declarations()
}
pub fn parse_diagnostics(&self, file: SourceFile) -> &[ParseDiagnosticData] {
self.0.parse_diagnostics(file)
}
pub fn parsed_tree(&self, file: SourceFile) -> SyntaxNode {
self.0.parsed_tree(file)
}
pub fn semantic_model(&self, file: SourceFile) -> &SemanticModel {
self.0.semantic_model(file)
}
pub fn file_is_document_root(&self, file: SourceFile) -> bool {
self.0.file_is_document_root(file)
}
pub fn document_signatures(&self, file: SourceFile) -> &SignatureDb {
self.0.document_signatures(file)
}
pub fn file_glossary_keys(&self, file: SourceFile) -> &[SmolStr] {
self.0.file_glossary_keys(file)
}
pub fn bib_parse_diagnostics(&self, file: SourceFile) -> &[ParseDiagnosticData] {
self.0.bib_parse_diagnostics(file)
}
pub fn parsed_bib_tree(&self, file: SourceFile) -> BibSyntaxNode {
self.0.parsed_bib_tree(file)
}
pub fn bib_semantic_model(&self, file: SourceFile) -> &BibModel {
self.0.bib_semantic_model(file)
}
fn intern_project(&self, mut members: Vec<ProjectMember>) -> Project<'_> {
crate::project::graph::normalize_members(&mut members);
Project::new(&self.0, members)
}
pub fn resolve_project(
&self,
members: Vec<ProjectMember>,
) -> (&ResolvedLabels, &ResolvedCitations) {
let project = self.intern_project(members);
(
resolved_labels(&self.0, project),
resolved_citations(&self.0, project),
)
}
pub fn scope_signatures(&self, members: Vec<ProjectMember>, file: SourceFile) -> &SignatureDb {
let project = self.intern_project(members);
scope_signatures(&self.0, project, file)
}
pub fn package_graph(&self, members: Vec<ProjectMember>) -> &crate::project::PackageGraph {
let project = self.intern_project(members);
package_graph(&self.0, project)
}
pub fn resolve_package_options(&self, members: Vec<ProjectMember>) -> &ResolvedPackageOptions {
let project = self.intern_project(members);
resolved_package_options(&self.0, project)
}
}
#[salsa::db]
impl salsa::Database for IncrementalDatabase {}
#[salsa::db]
impl IncrementalDb for IncrementalDatabase {
fn record_query(&self, entry: QueryLogEntry) {
self.query_log
.lock()
.unwrap_or_else(recover_poison)
.push(entry);
}
fn reparse_prev(&self, file: SourceFile) -> Option<Arc<PrevParse>> {
self.reparse_cache
.lock()
.unwrap_or_else(recover_poison)
.files
.get(&file)
.and_then(|state| state.prev.clone())
}
fn reparse_stage_edits(&self, file: SourceFile, edits: Option<Vec<Edit>>) {
let mut cache = self.reparse_cache.lock().unwrap_or_else(recover_poison);
let Some(edits) = edits else {
if let Some(state) = cache.files.get_mut(&file) {
state.pending.clear();
}
return;
};
let state = cache.files.entry(file).or_default();
state.hot = true;
state.pending.extend(edits);
let inserted: usize = state.pending.iter().map(|e| e.insert.len()).sum();
if state.pending.len() > MAX_CHAIN_EDITS || inserted > MAX_CHAIN_INSERT_BYTES {
state.pending.clear();
}
}
fn reparse_pending_edits(&self, file: SourceFile) -> Vec<Edit> {
self.reparse_cache
.lock()
.unwrap_or_else(recover_poison)
.files
.get(&file)
.map(|state| state.pending.clone())
.unwrap_or_default()
}
fn reparse_store(
&self,
file: SourceFile,
prev: PrevParse,
tier: Option<ReparseTier>,
consumed: usize,
) {
let mut cache = self.reparse_cache.lock().unwrap_or_else(recover_poison);
let used = cache.touch();
let state = cache.files.entry(file).or_default();
state.prev = Some(Arc::new(prev));
state.used = used;
state.hot |= tier.is_some();
let consumed = consumed.min(state.pending.len());
state.pending.drain(..consumed);
cache.evict_if_full();
}
fn reparse_evict(&self, file: SourceFile) {
self.reparse_cache
.lock()
.unwrap_or_else(recover_poison)
.files
.remove(&file);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn base_for(text: &str) -> PrevParse {
let text: Arc<str> = Arc::from(text);
let declared = ResolvedDeclarations::default();
let config = LexConfig::default();
let (parse, ctx) = parse_with_declarations_resolved(&text, config, &declared);
PrevParse {
text,
green: parse.green,
errors: parse.errors,
ctx,
config,
declared,
}
}
#[test]
fn a_base_is_current_only_for_the_inputs_it_was_parsed_under() {
let base = base_for("\\section{Hi}\n");
let declared = ResolvedDeclarations::default();
let same: Arc<str> = Arc::from("\\section{Hi}\n");
let other: Arc<str> = Arc::from("\\section{Ho}\n");
assert!(base.is_current(&same, LexConfig::default(), &declared));
assert!(base.is_current(&base.text.clone(), LexConfig::default(), &declared));
assert!(!base.is_current(&other, LexConfig::default(), &declared));
assert!(
!base.is_current(
&same,
LexConfig {
flavor: crate::parser::LatexFlavor::Package,
dtx: false,
},
&declared,
),
"a `.sty` reads `@` as a letter, so the same bytes are a different parse"
);
}
#[test]
fn eviction_prefers_cold_entries() {
let mut db = IncrementalDatabase::default();
let hot = db.upsert_file(Path::new("hot.tex"), "hot\n".to_owned());
db.reparse_store(hot, base_for("hot\n"), Some(ReparseTier::Token), 0);
for n in 0..MAX_REPARSE_BASES + 10 {
let cold = db.upsert_file(Path::new(&format!("cold{n}.tex")), "cold\n".to_owned());
db.reparse_store(cold, base_for("cold\n"), None, 0);
}
assert!(db.reparse_cache_len() <= MAX_REPARSE_BASES);
assert!(
db.reparse_prev(hot).is_some(),
"the hot entry outlived every cold one"
);
}
#[test]
fn a_store_drains_only_the_prefix_it_consumed() {
let mut db = IncrementalDatabase::default();
let file = db.upsert_file(Path::new("a.tex"), "x\n".to_owned());
db.reparse_stage_edits(
file,
Some(vec![Edit {
range: 0..0,
insert: "a".to_string(),
}]),
);
let peeked = db.reparse_pending_edits(file).len();
db.reparse_stage_edits(
file,
Some(vec![Edit {
range: 0..0,
insert: "b".to_string(),
}]),
);
db.reparse_store(file, base_for("ax\n"), None, peeked);
let left = db.reparse_pending_edits(file);
assert_eq!(left.len(), 1, "the late stage must survive");
assert_eq!(left[0].insert, "b");
}
#[test]
fn poisoned_files_lock_recovers() {
let mut db = IncrementalDatabase::default();
db.upsert_file(Path::new("a.tex"), "before".to_owned());
let files = Arc::clone(&db.files);
let poisoned = std::thread::spawn(move || {
let _guard = files.lock().expect("first lock is unpoisoned");
panic!("boom while holding the files lock");
})
.join();
assert!(poisoned.is_err(), "the helper thread should have panicked");
assert!(db.files.is_poisoned(), "the lock should now be poisoned");
assert!(db.lookup_file(Path::new("a.tex")).is_some());
db.upsert_file(Path::new("b.tex"), "after".to_owned());
assert_eq!(db.tracked_files().len(), 2);
}
}