Skip to main content

php_lsp/db/
parse.rs

1//! The `parsed_doc` salsa query: parses a `SourceFile` into an `Arc<ParsedDoc>`
2//! under salsa memoization. Downstream queries (file_index, method_returns,
3//! semantic diagnostics) depend on this one, so each file is parsed at most
4//! once per revision.
5//!
6//! `ParsedDoc` owns a self-referential bumpalo arena and cannot safely
7//! implement the structural `Update` trait — instead we wrap in a `ParsedArc`
8//! newtype whose `Update` impl uses `Arc::ptr_eq`. Every reparse produces a
9//! new `Arc`, so pointer equality is a correct (if conservative) "changed"
10//! signal: salsa never falsely backdates, and downstream queries re-run after
11//! every input text change.
12
13use std::sync::Arc;
14
15use salsa::Database;
16
17use crate::ast::ParsedDoc;
18use crate::db::input::SourceFile;
19
20/// Opaque handle to a parsed document. Cheap to clone (refcount bump); never
21/// compared structurally. See module docs for the `Update` contract.
22///
23/// No `Debug` impl because `ParsedDoc` isn't `Debug` (it owns raw pointers
24/// into a bumpalo arena). Salsa doesn't require `Debug` on tracked returns
25/// when `no_eq` is used.
26#[derive(Clone)]
27pub struct ParsedArc(pub Arc<ParsedDoc>);
28
29impl ParsedArc {
30    pub fn get(&self) -> &ParsedDoc {
31        &self.0
32    }
33}
34
35// SAFETY: The `ptr_eq` short-circuit returns `false` without writing, matching
36// salsa's "no observable change" contract. `ParsedDoc` is already `Send + Sync`
37// (see `ast.rs:98`).
38crate::impl_arc_update!(ParsedArc);
39
40/// Parse the file's source text. `no_eq` because `ParsedArc` has no
41/// structural equality — invalidation is driven entirely by input changes,
42/// not by comparing the new value against the old one.
43///
44/// Phase F: `lru = 2048` bounds the number of cached ASTs. Parsed docs own
45/// bumpalo arenas and are the largest memoized values in the db; dropping
46/// older entries caps resident memory at roughly 2048 × avg_ast_size.
47/// Re-reads after eviction reparse from the live `SourceFile::text` input
48/// (cheap `Arc<str>` clone). This replaces the hand-written
49/// `DocumentStore::indexed_order` LRU that used to bound `Document` entries.
50#[salsa::tracked(no_eq, lru = 2048)]
51pub fn parsed_doc(db: &dyn Database, file: SourceFile) -> ParsedArc {
52    // Pass Arc<str> directly — refcount bump, no heap allocation on the hot path.
53    let doc = ParsedDoc::parse(file.text(db));
54    ParsedArc(Arc::new(doc))
55}
56
57/// Parse-error count, derived from `parsed_doc`. Kept as a separate query so
58/// callers that only need the diagnostic count don't clone the parsed AST.
59#[salsa::tracked]
60pub fn parse_error_count(db: &dyn Database, file: SourceFile) -> usize {
61    parsed_doc(db, file).get().errors.len()
62}
63
64#[cfg(test)]
65mod tests {
66    use std::sync::Arc;
67    use std::sync::atomic::{AtomicUsize, Ordering};
68
69    use super::*;
70    use crate::db::analysis::AnalysisHost;
71    use crate::db::input::{FileId, SourceFile};
72    use salsa::Setter;
73
74    static CALLS: AtomicUsize = AtomicUsize::new(0);
75
76    #[salsa::tracked]
77    fn counted_parse(db: &dyn Database, file: SourceFile) -> usize {
78        CALLS.fetch_add(1, Ordering::SeqCst);
79        parsed_doc(db, file).get().errors.len()
80    }
81
82    #[test]
83    fn parsed_doc_returns_ast() {
84        let host = AnalysisHost::new();
85        let file = SourceFile::new(
86            host.db(),
87            FileId(0),
88            Arc::<str>::from("file:///t.php"),
89            Arc::<str>::from("<?php\nfunction greet() {}"),
90            None,
91        );
92        let arc = parsed_doc(host.db(), file);
93        assert!(arc.get().errors.is_empty());
94        assert!(!arc.get().program().stmts.is_empty());
95    }
96
97    #[test]
98    fn parsed_doc_memoizes_and_invalidates() {
99        CALLS.store(0, Ordering::SeqCst);
100        let mut host = AnalysisHost::new();
101        let file = SourceFile::new(
102            host.db(),
103            FileId(1),
104            Arc::<str>::from("file:///t.php"),
105            Arc::<str>::from("<?php\nfunction a() {}"),
106            None,
107        );
108
109        let _ = counted_parse(host.db(), file);
110        let _ = counted_parse(host.db(), file);
111        assert_eq!(
112            CALLS.load(Ordering::SeqCst),
113            1,
114            "salsa should memoize the second call with unchanged input"
115        );
116
117        file.set_text(host.db_mut())
118            .to(Arc::<str>::from("<?php\nclass {"));
119        let _ = counted_parse(host.db(), file);
120        assert_eq!(
121            CALLS.load(Ordering::SeqCst),
122            2,
123            "downstream query should re-run after input text changes"
124        );
125    }
126
127    #[test]
128    fn parse_error_count_reflects_diagnostics() {
129        let host = AnalysisHost::new();
130        let file = SourceFile::new(
131            host.db(),
132            FileId(2),
133            Arc::<str>::from("file:///t.php"),
134            Arc::<str>::from("<?php\nclass {"),
135            None,
136        );
137        assert!(parse_error_count(host.db(), file) > 0);
138    }
139}