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