Skip to main content

brokk_bifrost_cpp/
graph_support.rs

1//! The analyzer-resident products C++'s language logic resolves through.
2//!
3//! `CppAnalyzer` owns five moka caches, three `Arc<OnceLock<..>>` cells and one
4//! `PoolSafeMemo`; every one of them stays in `brokk-bifrost-analysis` because
5//! `IAnalyzer::update`/`update_all` rebuild the analyzer wholesale through
6//! `Self::from_inner`. What crosses the crate line is the *decision* that fills
7//! each cell -- the builders in [`crate::hierarchy`], [`crate::identity`] and
8//! [`crate::imports`] -- plus this trait, which is how a free function reaches
9//! back for a memoized product without naming the analyzer type.
10//!
11//! The census found no re-entrancy among these accessors, so this is a single
12//! tier rather than the two-tier split some fleet languages needed: the #1134
13//! reconciliation reads `visible_type_units`, which reads `include_target_index`
14//! and the supertrait's `import_statements`, and none of those reads back into
15//! reconciliation.
16//!
17//! The visible-type table remains analyzer-owned:
18//!
19//! * [`CppSource::visible_type_units`] is the moka-cached include-closure
20//!   class table. Its *builder* is [`crate::hierarchy::build_cpp_visible_type_units`];
21//!   the cell and its `test-support` build counter stay analyzer-side, so this
22//!   accessor is the only way the reconciler reaches a warm table.
23
24use crate::compile_context::CppCompileContext;
25use crate::declarations::CppRecoveredExportClassIndex;
26use crate::graph::CppWorkspaceSource;
27use crate::graph::resolver::{
28    CppClassDeclarationStrength, OrphanedNamespaceScopeIndex, SourceUsingIndex,
29};
30use crate::identity::CppCallableUnitRole;
31use crate::imports::{IncludeTargetIndex, canonical_include_paths, canonical_include_statements};
32use brokk_bifrost_core::analyzer::capabilities::{TypeAliasProvider, TypeHierarchyProvider};
33use brokk_bifrost_core::analyzer::model::{CppFieldLinkage, CppTemplateMetadata};
34use brokk_bifrost_core::analyzer::prepared_syntax::PreparedSyntaxTree;
35use brokk_bifrost_core::analyzer::query_token::QueryToken;
36use brokk_bifrost_core::analyzer::{CodeUnit, CodeUnitIndex, ProjectFile};
37use brokk_bifrost_core::cancellation::CancellationToken;
38use brokk_bifrost_core::hash::{HashMap, HashSet};
39use std::collections::BTreeSet;
40use std::sync::Arc;
41
42pub trait CppSource:
43    CodeUnitIndex + TypeAliasProvider + TypeHierarchyProvider + CppWorkspaceSource
44{
45    /// Canonical declaration syntax for the file's primary compilation reading.
46    fn declaration_source_facts(
47        &self,
48        _token: QueryToken<'_>,
49        _file: &ProjectFile,
50    ) -> Option<Arc<crate::source_facts::CppFileSourceFacts>> {
51        None
52    }
53
54    /// Canonical AST-owned include facts for the file's primary reading.
55    /// `None` means source-facts publication is unavailable; an empty vector
56    /// is an authoritative include-free file.
57    fn canonical_include_facts(
58        &self,
59        token: QueryToken<'_>,
60        file: &ProjectFile,
61    ) -> Option<Vec<brokk_bifrost_core::analyzer::cpp_facts::CppIncludeFact>> {
62        self.declaration_source_facts(token, file)
63            .map(|source| source.facts.includes.clone())
64    }
65
66    /// Canonical include paths for a file's primary reading.
67    fn canonical_include_paths(
68        &self,
69        token: QueryToken<'_>,
70        file: &ProjectFile,
71    ) -> Option<Vec<String>> {
72        self.declaration_source_facts(token, file)
73            .map(|source| canonical_include_paths(&source))
74    }
75
76    /// Exact source alternatives for a mounted declaration. Unavailable source
77    /// publication stays unknown; consumers must never reparse the candidate.
78    fn declaration_source_properties(
79        &self,
80        token: QueryToken<'_>,
81        unit: &CodeUnit,
82    ) -> Option<Vec<brokk_bifrost_core::analyzer::cpp_facts::CppDeclarationSourceFact>> {
83        let source = self.declaration_source_facts(token, unit.source())?;
84        let facts: Vec<_> = source.for_unit(unit).cloned().collect();
85        (!facts.is_empty()).then_some(facts)
86    }
87
88    fn declaration_source_occurrences(
89        &self,
90        token: QueryToken<'_>,
91        unit: &CodeUnit,
92    ) -> Option<Vec<crate::source_facts::CppDeclarationOccurrence>> {
93        let source = self.declaration_source_facts(token, unit.source())?;
94        let occurrences = source.declaration_occurrences(unit);
95        (!occurrences.is_empty()).then_some(occurrences)
96    }
97
98    /// Navigation alternatives come from the same canonical occurrences as
99    /// declaration properties. An analyzer that selects alternate dialects must
100    /// override this method to join against the selected dialect's own arena.
101    fn declaration_navigation_occurrences(
102        &self,
103        token: QueryToken<'_>,
104        unit: &CodeUnit,
105    ) -> Option<Vec<crate::source_facts::CppNavigationOccurrence>> {
106        let source = self.declaration_source_facts(token, unit.source())?;
107        let facts = self.declaration_source_properties(token, unit)?;
108        Some(
109            facts
110                .into_iter()
111                .map(|fact| {
112                    let declaration = source.source.declaration(fact.declaration);
113                    let range = source.source.occurrence(declaration.occurrence).range;
114                    let conditional_family = fact.conditional_family.map(|id| {
115                        let range = source.source.occurrence(id).range;
116                        (range.start_byte, range.end_byte)
117                    });
118                    crate::source_facts::CppNavigationOccurrence {
119                        range,
120                        role: fact.occurrence_role,
121                        conditional_family,
122                    }
123                })
124                .collect(),
125        )
126    }
127
128    /// Structured includes for a disposable visibility traversal.
129    fn visibility_import_statements(
130        &self,
131        token: QueryToken<'_>,
132        file: &ProjectFile,
133    ) -> Vec<String> {
134        self.declaration_source_facts(token, file)
135            .map(|source| canonical_include_statements(&source))
136            .unwrap_or_default()
137    }
138
139    /// Physical identifier candidates used only to discover source files for
140    /// a bounded visibility traversal. Re-keyed out-of-line definitions do not
141    /// add an includable source, so analyzer implementations may answer this
142    /// from their stored declaration index without global identity
143    /// reconciliation.
144    fn visibility_identifier_candidates(&self, identifier: &str) -> BTreeSet<CodeUnit> {
145        self.lookup_candidates_by_identifier(identifier)
146    }
147
148    /// Batched form of [`Self::visibility_identifier_candidates`].
149    fn visibility_identifier_candidates_batch(
150        &self,
151        identifiers: &HashSet<String>,
152        cancellation: Option<&CancellationToken>,
153    ) -> HashMap<String, BTreeSet<CodeUnit>> {
154        identifiers
155            .iter()
156            .take_while(|_| !cancellation.is_some_and(CancellationToken::is_cancelled))
157            .map(|identifier| {
158                (
159                    identifier.clone(),
160                    self.visibility_identifier_candidates(identifier),
161                )
162            })
163            .collect()
164    }
165
166    /// The callable role recorded for a physical stored declaration, without
167    /// consulting reconciliation fallbacks that may themselves be building.
168    fn stored_callable_unit_role(&self, callable: &CodeUnit) -> CppCallableUnitRole;
169
170    /// The workspace-wide `#include` resolution table, built once per analyzer
171    /// generation from [`IncludeTargetIndex::build`].
172    fn include_target_index(&self) -> &IncludeTargetIndex;
173
174    /// Every class-like or alias declaration reachable from `file` through its
175    /// `#include` closure, memoized per file. See this module's note.
176    fn visible_type_units(&self, file: &ProjectFile) -> Arc<Vec<CodeUnit>>;
177
178    /// [`Self::visible_type_units`] under a caller's deadline.
179    ///
180    /// `None` means the include-closure walk stopped short. Nothing is
181    /// memoized in that case: a truncated class table is indistinguishable
182    /// from a file that simply sees fewer types, so every later base-specifier
183    /// resolution reading it would silently lose ancestors.
184    ///
185    /// This is the shape issue #1748 needed. The closure walk is individually
186    /// cheap -- a fraction of a millisecond to about 180 ms -- but the
187    /// descendant-index build above it runs one per class in the workspace,
188    /// which on a large tree is tens of thousands of them inside a single
189    /// request that asked for thirty seconds.
190    fn visible_type_units_while(
191        &self,
192        file: &ProjectFile,
193        keep_going: &dyn Fn() -> bool,
194    ) -> Option<Arc<Vec<CodeUnit>>>;
195
196    /// The per-file structured using index (ordinary using declarations and
197    /// using-directives with their guard environments), memoized per file.
198    /// Built by [`crate::graph::extractor::build_source_using_index`], which is
199    /// a pure function of the file's parsed content.
200    ///
201    /// Memoized on the analyzer rather than on `VisibilityIndex` because a
202    /// fresh visibility index is built per usage query: rebuilding the index
203    /// per query re-walked a 9.5 MB amalgamation's AST once per candidate
204    /// (issue #1927).
205    fn source_using_index(
206        &self,
207        token: QueryToken<'_>,
208        file: &ProjectFile,
209    ) -> Arc<SourceUsingIndex>;
210
211    /// The indexed source of `file` (`TreeSitterAnalyzer::file_source`).
212    fn file_source(&self, file: &ProjectFile) -> Option<String>;
213
214    /// The parsed tree and its source backing for `file`, from the analyzer's
215    /// query read cache.
216    ///
217    /// The single hottest member of this trait: the usage graph reaches it at
218    /// 27 call sites. The cache is *not* rebuilt per call -- `VisibilityIndex`
219    /// borrows the analyzer rather than cloning it precisely so this stays warm
220    /// across a scan (#1175), so an implementor must forward to the same
221    /// analyzer the query is running against.
222    ///
223    /// The [`QueryToken`] is proof that a request scope is open, so the cache
224    /// this reads is live (issue #2414 step 3).
225    fn prepared_syntax(
226        &self,
227        token: QueryToken<'_>,
228        file: &ProjectFile,
229    ) -> Option<Arc<PreparedSyntaxTree>>;
230
231    /// The persisted linkage fact for one C++ field, when the parser recorded
232    /// it. A missing fact is unavailable linkage evidence.
233    fn cpp_field_linkage(&self, code_unit: &CodeUnit) -> Option<CppFieldLinkage>;
234
235    /// The cached result of a preprocessor-visible include-reachability walk.
236    ///
237    /// The reference language affects only `__cplusplus` guards. Callers pass
238    /// that fact as a Boolean so cache keys do not retain the full reference
239    /// path.
240    fn cached_unconditional_include_reachability(
241        &self,
242        first: &ProjectFile,
243        donor_source: &ProjectFile,
244        reference_is_c: bool,
245    ) -> Option<bool>;
246
247    /// Store a completed preprocessor-visible include-reachability walk.
248    fn cache_unconditional_include_reachability(
249        &self,
250        first: &ProjectFile,
251        donor_source: &ProjectFile,
252        reference_is_c: bool,
253        reaches: bool,
254    );
255
256    /// The file's embedded export-macro class recovery, resolved once per file.
257    ///
258    /// Built by [`crate::declarations::CppRecoveredExportClassIndex::build`],
259    /// which is a pure function of the file's parsed content. Memoized on the
260    /// analyzer for the same reason as [`Self::source_using_index`]: the
261    /// declaration-strength question asks it once per class-like unit, and
262    /// re-deriving it walks and sorts every `ERROR` subtree in the file (#1496).
263    fn recovered_export_class_index(
264        &self,
265        token: QueryToken<'_>,
266        file: &ProjectFile,
267    ) -> Arc<CppRecoveredExportClassIndex>;
268
269    /// The namespaces C++ parse recovery dropped from `file`'s tree, resolved
270    /// once per file.
271    ///
272    /// Built by [`OrphanedNamespaceScopeIndex::build`], a pure function of the
273    /// file's parsed content, and empty for a file without parse errors. Both
274    /// lookup directions derive every lexical scope in the file through it, so
275    /// it is memoized on the analyzer like [`Self::source_using_index`] rather
276    /// than rebuilt per query or per reference (issue #1537).
277    fn orphaned_namespace_scopes(
278        &self,
279        token: QueryToken<'_>,
280        file: &ProjectFile,
281    ) -> Arc<OrphanedNamespaceScopeIndex>;
282
283    /// The memoized answer of
284    /// [`crate::graph::resolver::cpp_class_declaration_strength`] for one
285    /// class-like unit, when a previous ask stored it.
286    ///
287    /// The answer is a pure function of the unit's declaration ranges and its
288    /// file's parse tree, so it is stable for an analyzer generation. Memoized
289    /// on the analyzer for the same reason as [`Self::source_using_index`]: the
290    /// inverse scan asks it once per declaration seed, and on a translation
291    /// unit the parser could not fully recover each ask re-derives the
292    /// export-macro recovery shapes from the file's `ERROR` subtrees, which is
293    /// quadratic in the file's size (#1496).
294    fn cached_class_declaration_strength(
295        &self,
296        candidate: &CodeUnit,
297    ) -> Option<CppClassDeclarationStrength>;
298
299    /// Store a completed declaration-strength answer.
300    fn cache_class_declaration_strength(
301        &self,
302        candidate: &CodeUnit,
303        strength: CppClassDeclarationStrength,
304    );
305
306    /// The declaration's syntactic owner, which unlike
307    /// [`CodeUnitIndex::parent_of`] never falls back to a definition-row lookup.
308    fn structural_parent_of(&self, code_unit: &CodeUnit) -> Option<CodeUnit>;
309
310    /// The persisted C++ template metadata side table's row for `code_unit`.
311    fn template_metadata(&self, code_unit: &CodeUnit) -> Option<CppTemplateMetadata>;
312
313    /// Whether a reference written in the header `file` reads the declarations
314    /// it can see with C semantics: every workspace translation unit that
315    /// provably compiles this header compiles it as C (issue #1970).
316    ///
317    /// Always false for a translation unit -- its own extension settles its
318    /// dialect, which [`crate::graph::resolver::is_c_source_file`] answers
319    /// without asking the analyzer. Both halves are combined by
320    /// [`crate::graph::resolver::reference_uses_c_semantics`], the one helper
321    /// resolution asks.
322    ///
323    /// A header both languages provably include reports false: forward
324    /// resolution from it reports the C++ identity, and the site-equivalence
325    /// union below is what keeps inverse in agreement.
326    fn header_uses_c_semantics(&self, file: &ProjectFile) -> bool;
327
328    /// The declarations of `file` as one of its two readings sees them.
329    ///
330    /// `c_semantics` false is the C++ reading, which is exactly
331    /// [`CodeUnitIndex::declarations`]. True is the C reading: the stored
332    /// `cpp:c` row-set when the blob has one, and otherwise the same C++
333    /// row-set, because "no C rows" means the two readings agree.
334    ///
335    /// Candidate enumeration selects a reading here and nowhere else: include
336    /// activation, preprocessor-guard compatibility, block-scope shadowing and
337    /// ambiguity all run unchanged over whichever set comes back.
338    fn declarations_in_reading(&self, file: &ProjectFile, c_semantics: bool) -> BTreeSet<CodeUnit>;
339
340    /// The identities of the other reading that share `code_unit`'s
341    /// declaration site -- the same source file and the same declaration byte
342    /// range -- empty when the two readings agree about it.
343    ///
344    /// Keyed on the range, never on a name. Inverse results are unioned across
345    /// these so a query against either identity of one declaration reports
346    /// every reference found under both readings.
347    fn site_equivalent_units(&self, code_unit: &CodeUnit) -> Vec<CodeUnit>;
348
349    /// Every distinct `compile_commands.json` configuration governing `file`,
350    /// empty when the workspace has no compile database entry naming it. The
351    /// only analyzer-resident product [`crate::diagnostics`] needs.
352    ///
353    /// A file the build compiles in several configurations yields several
354    /// contexts, because their include closures can disagree about a name.
355    fn compile_contexts_for(&self, file: &ProjectFile) -> &[CppCompileContext];
356
357    /// Every workspace translation unit whose `#include` closure transitively
358    /// reaches `file` -- the header-to-TU attribution map from #1970's
359    /// Milestone 2, which #2011 phase 2 reads for header-sited guard proof.
360    /// Empty when nothing reaches `file` or the implementor has no include
361    /// graph, which conservatively yields no compile-context facts.
362    fn reaching_translation_units(&self, _file: &ProjectFile) -> Vec<ProjectFile> {
363        Vec::new()
364    }
365
366    /// Count a precise-parent resolution against the analyzer's counter.
367    ///
368    /// Called from production code in [`crate::graph::resolver`], which is why
369    /// it is on the trait at all; the counter itself stays on the analyzer.
370    ///
371    /// The default is a no-op because the two gates are not the same gate:
372    /// Cargo turns this crate's `test-support` on for the whole build graph
373    /// whenever `brokk-bifrost-analysis`'s dev-dependencies are in play, while
374    /// the analyzer-side counter field is `#[cfg(any(test, feature =
375    /// "test-support"))]` on *that* crate. The implementor overrides this
376    /// exactly when it has a counter to record into -- which is the build the
377    /// tests reading the counter run in.
378    #[cfg(any(test, feature = "test-support"))]
379    fn record_cpp_parent_resolution_for_test(&self) {}
380
381    /// Count a class-declaration-strength parse. See
382    /// [`Self::record_cpp_parent_resolution_for_test`].
383    #[cfg(any(test, feature = "test-support"))]
384    fn record_cpp_class_strength_parse_for_test(&self) {}
385
386    /// Count a guard-ancestry inspection during a per-file using-index build.
387    /// See [`Self::record_cpp_parent_resolution_for_test`].
388    #[cfg(any(test, feature = "test-support"))]
389    fn record_using_guard_context_inspection_for_test(&self) {}
390}