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//! Three members are load-bearing beyond their signature:
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//! * [`CppSource::raw_supertypes_of`] is
24//! `TreeSitterAnalyzer::raw_supertypes_of`, whose rows are crate-private to
25//! analysis; the analyzer hands the decoded base-specifier strings across.
26
27use crate::compile_context::CppCompileContext;
28use crate::declarations::CppRecoveredExportClassIndex;
29use crate::graph::CppWorkspaceSource;
30use crate::graph::resolver::{
31 CppClassDeclarationStrength, OrphanedNamespaceScopeIndex, SourceUsingIndex,
32};
33use crate::identity::CppCallableUnitRole;
34use crate::imports::IncludeTargetIndex;
35use brokk_bifrost_core::analyzer::capabilities::{TypeAliasProvider, TypeHierarchyProvider};
36use brokk_bifrost_core::analyzer::model::{CppFieldLinkage, CppTemplateMetadata};
37use brokk_bifrost_core::analyzer::prepared_syntax::PreparedSyntaxTree;
38use brokk_bifrost_core::analyzer::query_token::QueryToken;
39use brokk_bifrost_core::analyzer::{CodeUnit, CodeUnitIndex, ProjectFile};
40use brokk_bifrost_core::cancellation::CancellationToken;
41use brokk_bifrost_core::hash::{HashMap, HashSet};
42use std::collections::BTreeSet;
43use std::sync::Arc;
44
45pub trait CppSource:
46 CodeUnitIndex + TypeAliasProvider + TypeHierarchyProvider + CppWorkspaceSource
47{
48 /// Structured includes for a disposable visibility traversal.
49 fn visibility_import_statements(
50 &self,
51 _token: QueryToken<'_>,
52 file: &ProjectFile,
53 ) -> Vec<String> {
54 self.import_statements(file)
55 }
56
57 /// Physical identifier candidates used only to discover source files for
58 /// a bounded visibility traversal. Re-keyed out-of-line definitions do not
59 /// add an includable source, so analyzer implementations may answer this
60 /// from their stored declaration index without global identity
61 /// reconciliation.
62 fn visibility_identifier_candidates(&self, identifier: &str) -> BTreeSet<CodeUnit> {
63 self.lookup_candidates_by_identifier(identifier)
64 }
65
66 /// Batched form of [`Self::visibility_identifier_candidates`].
67 fn visibility_identifier_candidates_batch(
68 &self,
69 identifiers: &HashSet<String>,
70 cancellation: Option<&CancellationToken>,
71 ) -> HashMap<String, BTreeSet<CodeUnit>> {
72 identifiers
73 .iter()
74 .take_while(|_| !cancellation.is_some_and(CancellationToken::is_cancelled))
75 .map(|identifier| {
76 (
77 identifier.clone(),
78 self.visibility_identifier_candidates(identifier),
79 )
80 })
81 .collect()
82 }
83
84 /// The callable role recorded for a physical stored declaration, without
85 /// consulting reconciliation fallbacks that may themselves be building.
86 fn stored_callable_unit_role(&self, callable: &CodeUnit) -> CppCallableUnitRole;
87
88 /// The workspace-wide `#include` resolution table, built once per analyzer
89 /// generation from [`IncludeTargetIndex::build`].
90 fn include_target_index(&self) -> &IncludeTargetIndex;
91
92 /// The declared base specifiers of `code_unit`, as written
93 /// (`TreeSitterAnalyzer::raw_supertypes_of`).
94 fn raw_supertypes_of(&self, code_unit: &CodeUnit) -> Vec<String>;
95
96 /// Every class-like or alias declaration reachable from `file` through its
97 /// `#include` closure, memoized per file. See this module's note.
98 fn visible_type_units(&self, file: &ProjectFile) -> Arc<Vec<CodeUnit>>;
99
100 /// [`Self::visible_type_units`] under a caller's deadline.
101 ///
102 /// `None` means the include-closure walk stopped short. Nothing is
103 /// memoized in that case: a truncated class table is indistinguishable
104 /// from a file that simply sees fewer types, so every later base-specifier
105 /// resolution reading it would silently lose ancestors.
106 ///
107 /// This is the shape issue #1748 needed. The closure walk is individually
108 /// cheap -- a fraction of a millisecond to about 180 ms -- but the
109 /// descendant-index build above it runs one per class in the workspace,
110 /// which on a large tree is tens of thousands of them inside a single
111 /// request that asked for thirty seconds.
112 fn visible_type_units_while(
113 &self,
114 file: &ProjectFile,
115 keep_going: &dyn Fn() -> bool,
116 ) -> Option<Arc<Vec<CodeUnit>>>;
117
118 /// The per-file structured using index (ordinary using declarations and
119 /// using-directives with their guard environments), memoized per file.
120 /// Built by [`crate::graph::extractor::build_source_using_index`], which is
121 /// a pure function of the file's parsed content.
122 ///
123 /// Memoized on the analyzer rather than on `VisibilityIndex` because a
124 /// fresh visibility index is built per usage query: rebuilding the index
125 /// per query re-walked a 9.5 MB amalgamation's AST once per candidate
126 /// (issue #1927).
127 fn source_using_index(
128 &self,
129 token: QueryToken<'_>,
130 file: &ProjectFile,
131 ) -> Arc<SourceUsingIndex>;
132
133 /// The indexed source of `file` (`TreeSitterAnalyzer::file_source`).
134 fn file_source(&self, file: &ProjectFile) -> Option<String>;
135
136 /// The parsed tree and its source backing for `file`, from the analyzer's
137 /// query read cache.
138 ///
139 /// The single hottest member of this trait: the usage graph reaches it at
140 /// 27 call sites. The cache is *not* rebuilt per call -- `VisibilityIndex`
141 /// borrows the analyzer rather than cloning it precisely so this stays warm
142 /// across a scan (#1175), so an implementor must forward to the same
143 /// analyzer the query is running against.
144 ///
145 /// The [`QueryToken`] is proof that a request scope is open, so the cache
146 /// this reads is live (issue #2414 step 3).
147 fn prepared_syntax(
148 &self,
149 token: QueryToken<'_>,
150 file: &ProjectFile,
151 ) -> Option<Arc<PreparedSyntaxTree>>;
152
153 /// The persisted linkage fact for one C++ field, when the parser recorded
154 /// it. A missing fact requires the resolver's syntax fallback.
155 fn cpp_field_linkage(&self, code_unit: &CodeUnit) -> Option<CppFieldLinkage>;
156
157 /// The cached result of a preprocessor-visible include-reachability walk.
158 ///
159 /// The reference language affects only `__cplusplus` guards. Callers pass
160 /// that fact as a Boolean so cache keys do not retain the full reference
161 /// path.
162 fn cached_unconditional_include_reachability(
163 &self,
164 first: &ProjectFile,
165 donor_source: &ProjectFile,
166 reference_is_c: bool,
167 ) -> Option<bool>;
168
169 /// Store a completed preprocessor-visible include-reachability walk.
170 fn cache_unconditional_include_reachability(
171 &self,
172 first: &ProjectFile,
173 donor_source: &ProjectFile,
174 reference_is_c: bool,
175 reaches: bool,
176 );
177
178 /// The file's embedded export-macro class recovery, resolved once per file.
179 ///
180 /// Built by [`crate::declarations::CppRecoveredExportClassIndex::build`],
181 /// which is a pure function of the file's parsed content. Memoized on the
182 /// analyzer for the same reason as [`Self::source_using_index`]: the
183 /// declaration-strength question asks it once per class-like unit, and
184 /// re-deriving it walks and sorts every `ERROR` subtree in the file (#1496).
185 fn recovered_export_class_index(
186 &self,
187 token: QueryToken<'_>,
188 file: &ProjectFile,
189 ) -> Arc<CppRecoveredExportClassIndex>;
190
191 /// The namespaces C++ parse recovery dropped from `file`'s tree, resolved
192 /// once per file.
193 ///
194 /// Built by [`OrphanedNamespaceScopeIndex::build`], a pure function of the
195 /// file's parsed content, and empty for a file without parse errors. Both
196 /// lookup directions derive every lexical scope in the file through it, so
197 /// it is memoized on the analyzer like [`Self::source_using_index`] rather
198 /// than rebuilt per query or per reference (issue #1537).
199 fn orphaned_namespace_scopes(
200 &self,
201 token: QueryToken<'_>,
202 file: &ProjectFile,
203 ) -> Arc<OrphanedNamespaceScopeIndex>;
204
205 /// The memoized answer of
206 /// [`crate::graph::resolver::cpp_class_declaration_strength`] for one
207 /// class-like unit, when a previous ask stored it.
208 ///
209 /// The answer is a pure function of the unit's declaration ranges and its
210 /// file's parse tree, so it is stable for an analyzer generation. Memoized
211 /// on the analyzer for the same reason as [`Self::source_using_index`]: the
212 /// inverse scan asks it once per declaration seed, and on a translation
213 /// unit the parser could not fully recover each ask re-derives the
214 /// export-macro recovery shapes from the file's `ERROR` subtrees, which is
215 /// quadratic in the file's size (#1496).
216 fn cached_class_declaration_strength(
217 &self,
218 candidate: &CodeUnit,
219 ) -> Option<CppClassDeclarationStrength>;
220
221 /// Store a completed declaration-strength answer.
222 fn cache_class_declaration_strength(
223 &self,
224 candidate: &CodeUnit,
225 strength: CppClassDeclarationStrength,
226 );
227
228 /// The declaration's syntactic owner, which unlike
229 /// [`CodeUnitIndex::parent_of`] never falls back to a definition-row lookup.
230 fn structural_parent_of(&self, code_unit: &CodeUnit) -> Option<CodeUnit>;
231
232 /// The persisted C++ template metadata side table's row for `code_unit`.
233 fn template_metadata(&self, code_unit: &CodeUnit) -> Option<CppTemplateMetadata>;
234
235 /// Whether a reference written in the header `file` reads the declarations
236 /// it can see with C semantics: every workspace translation unit that
237 /// provably compiles this header compiles it as C (issue #1970).
238 ///
239 /// Always false for a translation unit -- its own extension settles its
240 /// dialect, which [`crate::graph::resolver::is_c_source_file`] answers
241 /// without asking the analyzer. Both halves are combined by
242 /// [`crate::graph::resolver::reference_uses_c_semantics`], the one helper
243 /// resolution asks.
244 ///
245 /// A header both languages provably include reports false: forward
246 /// resolution from it reports the C++ identity, and the site-equivalence
247 /// union below is what keeps inverse in agreement.
248 fn header_uses_c_semantics(&self, file: &ProjectFile) -> bool;
249
250 /// The declarations of `file` as one of its two readings sees them.
251 ///
252 /// `c_semantics` false is the C++ reading, which is exactly
253 /// [`CodeUnitIndex::declarations`]. True is the C reading: the stored
254 /// `cpp:c` row-set when the blob has one, and otherwise the same C++
255 /// row-set, because "no C rows" means the two readings agree.
256 ///
257 /// Candidate enumeration selects a reading here and nowhere else: include
258 /// activation, preprocessor-guard compatibility, block-scope shadowing and
259 /// ambiguity all run unchanged over whichever set comes back.
260 fn declarations_in_reading(&self, file: &ProjectFile, c_semantics: bool) -> BTreeSet<CodeUnit>;
261
262 /// The identities of the other reading that share `code_unit`'s
263 /// declaration site -- the same source file and the same declaration byte
264 /// range -- empty when the two readings agree about it.
265 ///
266 /// Keyed on the range, never on a name. Inverse results are unioned across
267 /// these so a query against either identity of one declaration reports
268 /// every reference found under both readings.
269 fn site_equivalent_units(&self, code_unit: &CodeUnit) -> Vec<CodeUnit>;
270
271 /// Every distinct `compile_commands.json` configuration governing `file`,
272 /// empty when the workspace has no compile database entry naming it. The
273 /// only analyzer-resident product [`crate::diagnostics`] needs.
274 ///
275 /// A file the build compiles in several configurations yields several
276 /// contexts, because their include closures can disagree about a name.
277 fn compile_contexts_for(&self, file: &ProjectFile) -> &[CppCompileContext];
278
279 /// Every workspace translation unit whose `#include` closure transitively
280 /// reaches `file` -- the header-to-TU attribution map from #1970's
281 /// Milestone 2, which #2011 phase 2 reads for header-sited guard proof.
282 /// Empty when nothing reaches `file` or the implementor has no include
283 /// graph, which conservatively yields no compile-context facts.
284 fn reaching_translation_units(&self, _file: &ProjectFile) -> Vec<ProjectFile> {
285 Vec::new()
286 }
287
288 /// Count a precise-parent resolution against the analyzer's counter.
289 ///
290 /// Called from production code in [`crate::graph::resolver`], which is why
291 /// it is on the trait at all; the counter itself stays on the analyzer.
292 ///
293 /// The default is a no-op because the two gates are not the same gate:
294 /// Cargo turns this crate's `test-support` on for the whole build graph
295 /// whenever `brokk-bifrost-analysis`'s dev-dependencies are in play, while
296 /// the analyzer-side counter field is `#[cfg(any(test, feature =
297 /// "test-support"))]` on *that* crate. The implementor overrides this
298 /// exactly when it has a counter to record into -- which is the build the
299 /// tests reading the counter run in.
300 #[cfg(any(test, feature = "test-support"))]
301 fn record_cpp_parent_resolution_for_test(&self) {}
302
303 /// Count a class-declaration-strength parse. See
304 /// [`Self::record_cpp_parent_resolution_for_test`].
305 #[cfg(any(test, feature = "test-support"))]
306 fn record_cpp_class_strength_parse_for_test(&self) {}
307
308 /// Count a guard-ancestry inspection during a per-file using-index build.
309 /// See [`Self::record_cpp_parent_resolution_for_test`].
310 #[cfg(any(test, feature = "test-support"))]
311 fn record_using_guard_context_inspection_for_test(&self) {}
312}