Skip to main content

brokk_bifrost_core/analyzer/
capabilities.rs

1use crate::analyzer::code_unit_index::CodeUnitIndex;
2use crate::analyzer::model::{CodeUnit, ImportInfo, ProjectFile};
3use crate::analyzer::pool_memo::PoolSafeMemo;
4use crate::compact_graph::{CompactRows, CompactRowsBuilder};
5use crate::hash::{HashMap, HashSet};
6use std::any::Any;
7use std::collections::{BTreeSet, VecDeque};
8use std::sync::Arc;
9
10use rayon::prelude::*;
11
12pub trait CapabilityProvider: Any {
13    fn as_any(&self) -> &dyn Any;
14}
15
16impl<T: Any> CapabilityProvider for T {
17    fn as_any(&self) -> &dyn Any {
18        self
19    }
20}
21
22pub trait ImportAnalysisProvider: CapabilityProvider + Send + Sync {
23    /// Shared, not owned: every memoizing implementation already stores the set
24    /// behind an `Arc` in its per-file cache, and the hottest consumers only
25    /// read it (a membership test in candidate discovery, a projection to
26    /// source files in the reverse-import index). Returning the `Arc` removes a
27    /// whole-set clone per cache hit and per insert.
28    fn imported_code_units_of(&self, file: &ProjectFile) -> Arc<HashSet<CodeUnit>>;
29    fn referencing_files_of(&self, file: &ProjectFile) -> HashSet<ProjectFile>;
30
31    /// Return import facts for a group of files without requiring each caller
32    /// to hydrate a complete per-file analyzer state. `None` preserves the
33    /// existing file-at-a-time behavior for providers without a bulk read model.
34    fn import_infos_for_files(
35        &self,
36        _files: &[ProjectFile],
37    ) -> Option<HashMap<ProjectFile, Vec<ImportInfo>>> {
38        None
39    }
40
41    fn import_info_of(&self, _file: &ProjectFile) -> Vec<ImportInfo> {
42        Vec::new()
43    }
44
45    /// Resolve imported source units from already-loaded import facts. Providers
46    /// that cannot do this cheaply return `None` and use `imported_code_units_of`.
47    fn imported_code_units_from_infos(
48        &self,
49        _file: &ProjectFile,
50        _imports: &[ImportInfo],
51    ) -> Option<Arc<HashSet<CodeUnit>>> {
52        None
53    }
54
55    /// Resolve directly imported project files from already-loaded import facts.
56    /// Providers that do not expose file-level edges return `None` and callers
57    /// can derive a conservative approximation from imported declarations.
58    fn imported_files_from_infos(
59        &self,
60        _file: &ProjectFile,
61        _imports: &[ImportInfo],
62    ) -> Option<HashSet<ProjectFile>> {
63        None
64    }
65
66    fn relevant_imports_for(&self, _code_unit: &CodeUnit) -> HashSet<String> {
67        HashSet::default()
68    }
69
70    /// Whether `source_file` can reference a declaration of `target`. A `true`
71    /// is an answer; a `false` is only an absence of evidence, which is why
72    /// callers still run the expansion backstop after one. Providers that can
73    /// prove the negative override [`Self::import_reachability`] instead and
74    /// derive this from it.
75    fn could_import_file(
76        &self,
77        _source_file: &ProjectFile,
78        _imports: &[ImportInfo],
79        _target: &ProjectFile,
80    ) -> bool {
81        false
82    }
83
84    /// The three-valued form of [`Self::could_import_file`], which lets a
85    /// provider that has a completeness proof retire the caller's backstop.
86    ///
87    /// The default preserves the historical contract exactly by mapping the
88    /// bool spelling's `true` to [`ImportReachability::Reaches`] and its
89    /// `false` to [`ImportReachability::Unknown`] -- never to `DoesNotReach`,
90    /// because the bool contract never distinguished "no" from "I did not
91    /// find one".
92    ///
93    /// Bridging runs in this direction only. A provider overrides this method
94    /// and defines `could_import_file` over it, so the two spellings cannot
95    /// drift apart.
96    fn import_reachability(
97        &self,
98        source_file: &ProjectFile,
99        imports: &[ImportInfo],
100        target: &ProjectFile,
101    ) -> ImportReachability {
102        if self.could_import_file(source_file, imports, target) {
103            ImportReachability::Reaches
104        } else {
105            ImportReachability::Unknown
106        }
107    }
108}
109
110/// How completely a provider can answer "can `source_file` reference a
111/// declaration of `target`?".
112///
113/// Candidate discovery asks this per (candidate, target) pair and, when it
114/// gets no positive answer, materializes every declaration the candidate
115/// imports as a recall backstop. For a namespace-import language that
116/// expansion is the whole workspace's top-level types per using directive,
117/// which is the shape that burned #1194. The backstop exists only because the
118/// bool contract could not distinguish a proven "no" from an unproven one.
119///
120/// A provider must return `DoesNotReach` only from a proof that covers every
121/// way its language can name a declaration without importing it. Anything less
122/// is `Unknown`: the cost of an unnecessary expansion is time, the cost of a
123/// wrong `DoesNotReach` is a missing usage.
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum ImportReachability {
126    /// The candidate can reference the target. Accept it without expanding.
127    Reaches,
128    /// The candidate provably cannot. Reject it without expanding.
129    DoesNotReach,
130    /// Undecided. The caller runs its own backstop, exactly as before this
131    /// verdict existed.
132    Unknown,
133}
134
135/// Resolve direct project-file edges from structured import facts. Prefer a
136/// provider's file-level resolver so imports whose target has no declarations
137/// remain visible; otherwise conservatively project resolved declaration
138/// identities back to their source files.
139pub fn resolve_imported_files_from_infos(
140    provider: &dyn ImportAnalysisProvider,
141    file: &ProjectFile,
142    imports: &[ImportInfo],
143) -> HashSet<ProjectFile> {
144    provider
145        .imported_files_from_infos(file, imports)
146        .unwrap_or_else(|| {
147            provider
148                .imported_code_units_from_infos(file, imports)
149                .unwrap_or_else(|| provider.imported_code_units_of(file))
150                .iter()
151                .map(|unit| unit.source().clone())
152                .collect()
153        })
154}
155
156pub fn build_reverse_import_index<F>(
157    files: &[ProjectFile],
158    resolve_imported: F,
159    parallel: bool,
160) -> HashMap<ProjectFile, Arc<HashSet<ProjectFile>>>
161where
162    F: Fn(&ProjectFile) -> Arc<HashSet<CodeUnit>> + Sync,
163{
164    build_reverse_file_index(
165        files,
166        |file| {
167            resolve_imported(file)
168                .iter()
169                .map(|code_unit| code_unit.source().clone())
170                .collect::<Vec<_>>()
171        },
172        parallel,
173    )
174}
175
176pub type ReverseFileIndex = HashMap<ProjectFile, Arc<HashSet<ProjectFile>>>;
177
178pub fn memoized_reverse_import_index<F, Files>(
179    memo: &PoolSafeMemo<ReverseFileIndex>,
180    files: Files,
181    resolve_imported: F,
182) -> Arc<ReverseFileIndex>
183where
184    F: Fn(&ProjectFile) -> Arc<HashSet<CodeUnit>> + Sync + Copy,
185    Files: Fn() -> Vec<ProjectFile> + Copy,
186{
187    memoized_reverse_file_index(memo, files, |file| {
188        resolve_imported(file)
189            .iter()
190            .map(|code_unit| code_unit.source().clone())
191            .collect::<Vec<_>>()
192    })
193}
194
195pub fn memoized_reverse_file_index<F, I, Files>(
196    memo: &PoolSafeMemo<ReverseFileIndex>,
197    files: Files,
198    resolve_targets: F,
199) -> Arc<ReverseFileIndex>
200where
201    F: Fn(&ProjectFile) -> I + Sync + Copy,
202    I: IntoIterator<Item = ProjectFile>,
203    Files: Fn() -> Vec<ProjectFile> + Copy,
204{
205    memo.get_or_build(
206        || {
207            let files = files();
208            build_reverse_file_index(&files, resolve_targets, true)
209        },
210        || {
211            let files = files();
212            build_reverse_file_index(&files, resolve_targets, false)
213        },
214    )
215}
216
217pub fn build_reverse_file_index<F, I>(
218    files: &[ProjectFile],
219    resolve_targets: F,
220    parallel: bool,
221) -> HashMap<ProjectFile, Arc<HashSet<ProjectFile>>>
222where
223    F: Fn(&ProjectFile) -> I + Sync,
224    I: IntoIterator<Item = ProjectFile>,
225{
226    let collect_edges = |file: &ProjectFile| {
227        let source = file.clone();
228        resolve_targets(file)
229            .into_iter()
230            .filter_map(move |target| (target != source).then(|| (target, source.clone())))
231            .collect::<Vec<_>>()
232    };
233    let edges: Vec<_> = if parallel {
234        files.par_iter().flat_map(collect_edges).collect()
235    } else {
236        files.iter().flat_map(collect_edges).collect()
237    };
238
239    let mut reverse: HashMap<ProjectFile, HashSet<ProjectFile>> = HashMap::default();
240    for (target, source) in edges {
241        reverse.entry(target).or_default().insert(source);
242    }
243    reverse
244        .into_iter()
245        .map(|(file, refs)| (file, Arc::new(refs)))
246        .collect()
247}
248
249/// `Send + Sync` like its three sibling capabilities: every implementor is an
250/// analyzer, and a parallel whole-workspace scan holds the provider across its
251/// fan-out (`kotlin_graph`'s edge builder does exactly that).
252pub trait TypeAliasProvider: CapabilityProvider + Send + Sync {
253    fn is_type_alias(&self, _code_unit: &CodeUnit) -> bool {
254        false
255    }
256}
257
258pub trait TestDetectionProvider: CapabilityProvider {}
259
260pub trait TypeHierarchyProvider: CapabilityProvider + Send + Sync {
261    fn get_direct_ancestors(&self, code_unit: &CodeUnit) -> Vec<CodeUnit>;
262    fn get_direct_descendants(&self, code_unit: &CodeUnit) -> HashSet<CodeUnit>;
263
264    fn supports_type_hierarchy(&self, code_unit: &CodeUnit) -> bool {
265        code_unit.is_class()
266    }
267
268    fn get_ancestors(&self, code_unit: &CodeUnit) -> Vec<CodeUnit> {
269        traverse_hierarchy(code_unit, |next| self.get_direct_ancestors(next))
270    }
271
272    fn get_descendants(&self, code_unit: &CodeUnit) -> Vec<CodeUnit> {
273        traverse_hierarchy(code_unit, |next| {
274            self.get_direct_descendants(next).into_iter().collect()
275        })
276    }
277
278    fn get_polymorphic_matches<T: CodeUnitIndex>(
279        &self,
280        target: &CodeUnit,
281        analyzer: &T,
282    ) -> Vec<CodeUnit>
283    where
284        Self: Sized,
285    {
286        if !target.is_function() {
287            return Vec::new();
288        }
289
290        let Some(parent) = analyzer.parent_of(target) else {
291            return Vec::new();
292        };
293
294        self.get_descendants(&parent)
295    }
296}
297
298/// Exact declaration identities plus compact ancestor-to-descendant rows.
299pub struct DirectDescendantIndex {
300    nodes: Box<[CodeUnit]>,
301    row_by_ancestor: HashMap<CodeUnit, u32>,
302    descendants: CompactRows<u32>,
303}
304
305impl DirectDescendantIndex {
306    pub fn from_indexed_nodes(
307        nodes: Vec<CodeUnit>,
308        index_by_node: HashMap<CodeUnit, u32>,
309        mut edges: Vec<(u32, u32)>,
310    ) -> Self {
311        assert_eq!(nodes.len(), index_by_node.len());
312        assert!(nodes.iter().enumerate().all(|(index, node)| {
313            index_by_node.get(node).copied()
314                == Some(
315                    u32::try_from(index).expect("hierarchy index declarations must fit in a u32"),
316                )
317        }));
318        assert!(edges.iter().all(|(ancestor, descendant)| {
319            (*ancestor as usize) < nodes.len() && (*descendant as usize) < nodes.len()
320        }));
321        edges.sort_unstable();
322        edges.dedup();
323
324        let row_count = usize::from(!edges.is_empty())
325            + edges
326                .windows(2)
327                .filter(|pair| pair[0].0 != pair[1].0)
328                .count();
329        let mut row_by_ancestor = HashMap::default();
330        let mut descendants = CompactRowsBuilder::with_capacity(row_count, edges.len());
331        let mut cursor = 0;
332        while cursor < edges.len() {
333            let ancestor = edges[cursor].0;
334            let start = cursor;
335            while cursor < edges.len() && edges[cursor].0 == ancestor {
336                cursor += 1;
337            }
338            let row =
339                u32::try_from(descendants.rows()).expect("hierarchy index rows must fit in a u32");
340            row_by_ancestor.insert(nodes[ancestor as usize].clone(), row);
341            descendants.push_row(
342                edges[start..cursor]
343                    .iter()
344                    .map(|(_, descendant)| *descendant),
345            );
346        }
347        Self {
348            nodes: nodes.into_boxed_slice(),
349            row_by_ancestor,
350            descendants: descendants.finish(),
351        }
352    }
353
354    pub fn descendants(&self, ancestor: &CodeUnit) -> HashSet<CodeUnit> {
355        let Some(row) = self.row_by_ancestor.get(ancestor).copied() else {
356            return HashSet::default();
357        };
358        self.descendants
359            .row(row as usize)
360            .iter()
361            .map(|descendant| self.nodes[*descendant as usize].clone())
362            .collect()
363    }
364}
365
366pub fn build_direct_descendant_index<A, P>(analyzer: &A, provider: &P) -> DirectDescendantIndex
367where
368    A: CodeUnitIndex,
369    P: TypeHierarchyProvider + ?Sized,
370{
371    build_direct_descendant_index_from_candidates(
372        analyzer
373            .all_declarations()
374            .filter(|candidate| candidate.is_class())
375            .collect(),
376        |candidate| provider.get_direct_ancestors(candidate),
377    )
378}
379
380pub fn build_direct_descendant_index_from_candidates<F>(
381    mut candidates: Vec<CodeUnit>,
382    mut direct_ancestors: F,
383) -> DirectDescendantIndex
384where
385    F: FnMut(&CodeUnit) -> Vec<CodeUnit>,
386{
387    candidates.sort();
388    candidates.dedup();
389    let mut types_by_fq_name: HashMap<String, Vec<CodeUnit>> = HashMap::default();
390    for candidate in &candidates {
391        types_by_fq_name
392            .entry(candidate.fq_name())
393            .or_default()
394            .push(candidate.clone());
395    }
396    let mut nodes = candidates.clone();
397    let mut index_by_node: HashMap<_, _> = nodes
398        .iter()
399        .enumerate()
400        .map(|(index, node)| {
401            (
402                node.clone(),
403                u32::try_from(index).expect("hierarchy index declarations must fit in a u32"),
404            )
405        })
406        .collect();
407    let mut edges = Vec::new();
408    for candidate in candidates {
409        let descendant = index_by_node[&candidate];
410        for ancestor in direct_ancestors(&candidate) {
411            let ancestor = types_by_fq_name
412                .get(&ancestor.fq_name())
413                .and_then(|same_name| {
414                    let mut same_source = same_name
415                        .iter()
416                        .filter(|unit| unit.source() == candidate.source());
417                    let exact = same_source.next()?;
418                    same_source.next().is_none().then(|| exact.clone())
419                })
420                .unwrap_or(ancestor);
421            let ancestor = *index_by_node.entry(ancestor.clone()).or_insert_with(|| {
422                let index = u32::try_from(nodes.len())
423                    .expect("hierarchy index declarations must fit in a u32");
424                nodes.push(ancestor);
425                index
426            });
427            edges.push((ancestor, descendant));
428        }
429    }
430    DirectDescendantIndex::from_indexed_nodes(nodes, index_by_node, edges)
431}
432
433fn traverse_hierarchy<F>(root: &CodeUnit, mut next: F) -> Vec<CodeUnit>
434where
435    F: FnMut(&CodeUnit) -> Vec<CodeUnit>,
436{
437    let direct = next(root);
438    if direct.is_empty() {
439        return Vec::new();
440    }
441
442    let mut seen = BTreeSet::new();
443    let mut result = Vec::new();
444    let mut queue = VecDeque::new();
445
446    for item in direct {
447        if seen.insert(item.fq_name()) {
448            queue.push_back(item.clone());
449            result.push(item);
450        }
451    }
452
453    while let Some(current) = queue.pop_front() {
454        for item in next(&current) {
455            if seen.insert(item.fq_name()) {
456                queue.push_back(item.clone());
457                result.push(item);
458            }
459        }
460    }
461
462    result
463}