Skip to main content

brokk_bifrost_cpp/graph/
namespace_alias.rs

1//! Namespace-alias binding shared by forward and inverse C++ type lookup.
2//!
3//! `namespace name = target;` introduces no declaration of its own: it rebinds
4//! a qualifier. Each definition is a typed source fact of the file that writes
5//! it ([`CppNamespaceAliasFact`]); this module is the one resolver that reads
6//! those facts. Both lookup directions reach it through
7//! `resolve_type_components_in_authoritative_scope`, so a forward edge and an
8//! inverse reference follow the same binding.
9//!
10//! A definition binds a written qualifier component when, at the reference:
11//! - its name and scope match: a namespace-scope alias at the lexical tier
12//!   that unqualified lookup reaches (or inside the namespace a qualified
13//!   lookup names), or a block-scope alias whose block contains the reference;
14//! - it is earlier in source order (for a chained alias, earlier than the
15//!   alias that names it);
16//! - its guards hold at the reference by the declaration-site rule.
17//!
18//! A definition whose guards the reference neither proves nor contradicts,
19//! whose target has no structured path, or whose sites disagree leaves the
20//! binding explicitly unresolved rather than falling back to the written
21//! spelling.
22//!
23//! [`CppNamespaceAliasFact`]: brokk_bifrost_core::analyzer::cpp_facts::CppNamespaceAliasFact
24
25use super::resolver::{
26    PreprocessorGuard, VisibilityIndex, merge_preprocessor_guards, preprocessor_guard_environment,
27};
28use crate::source_context::cpp_guard_set_to_runtime;
29use brokk_bifrost_core::analyzer::ProjectFile;
30use brokk_bifrost_core::hash::{HashMap, HashSet};
31use std::sync::{Arc, OnceLock};
32use tree_sitter::Node;
33
34/// One namespace alias definition in a source file visible from the root.
35#[derive(Debug)]
36pub struct NamespaceAliasSite {
37    source: ProjectFile,
38    start: usize,
39    end: usize,
40    target: Option<Vec<String>>,
41    target_global: bool,
42    namespace: Vec<String>,
43    block: Option<(usize, usize)>,
44    guards: Option<HashSet<PreprocessorGuard>>,
45}
46
47impl NamespaceAliasSite {
48    fn same_target(&self, other: &Self) -> bool {
49        self.target == other.target
50            && self.target_global == other.target_global
51            && self.namespace == other.namespace
52    }
53}
54
55/// The namespace aliases of one root's visible source files, by alias name.
56pub type NamespaceAliasCell = Arc<OnceLock<HashMap<String, Vec<NamespaceAliasSite>>>>;
57
58/// How the namespace aliases in effect at a reference rebind its qualifier.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum NamespaceQualifierBinding {
61    /// No namespace alias binds a qualifier component at the reference.
62    Unaliased,
63    /// The qualifier names an alias whose target, followed through any alias
64    /// chain, is established: look `components` up from `lexical_scope`, the
65    /// scope of the last alias in the chain.
66    Bound {
67        components: Vec<String>,
68        global: bool,
69        lexical_scope: Vec<String>,
70        depth: usize,
71    },
72    /// The qualifier names an alias whose target cannot be established here.
73    Unresolved { depth: usize },
74}
75
76impl NamespaceQualifierBinding {
77    /// The lexical depth at which the first alias binds: the length of the
78    /// scope that declares it, or one more than the reference's scope for a
79    /// block-scope alias. An ordinary name found at a deeper tier shadows it.
80    pub fn depth(&self) -> Option<usize> {
81        match self {
82            Self::Unaliased => None,
83            Self::Bound { depth, .. } | Self::Unresolved { depth } => Some(*depth),
84        }
85    }
86}
87
88/// How many alias hops one binding follows before it reports the chain as
89/// unresolved; a cycle (`namespace a = b; namespace b = a;`) ends here.
90const MAX_NAMESPACE_ALIAS_HOPS: usize = 32;
91
92enum SiteState {
93    InEffect,
94    Possible,
95    Hidden,
96}
97
98struct AliasLookup<'r, 'tree> {
99    file: &'r ProjectFile,
100    reference: Node<'tree>,
101    sites: &'r HashMap<String, Vec<NamespaceAliasSite>>,
102    raw_reference_guards: Option<HashSet<PreprocessorGuard>>,
103}
104
105impl VisibilityIndex<'_> {
106    /// Rebind the qualifier of the type name `components`, written at
107    /// `reference` in `file` with the given lexical scope, through the
108    /// namespace aliases in effect there.
109    pub fn bind_namespace_qualifier(
110        &self,
111        file: &ProjectFile,
112        reference: Node<'_>,
113        components: &[String],
114        global: bool,
115        lexical_scope: &[String],
116    ) -> NamespaceQualifierBinding {
117        if components.len() < 2 {
118            return NamespaceQualifierBinding::Unaliased;
119        }
120        let cell = self.namespace_alias_cell(file);
121        let sites = cell.get_or_init(|| self.collect_namespace_alias_sites(file));
122        if !components[..components.len() - 1]
123            .iter()
124            .any(|component| sites.contains_key(component))
125        {
126            return NamespaceQualifierBinding::Unaliased;
127        }
128        let lookup = AliasLookup {
129            file,
130            reference,
131            sites,
132            raw_reference_guards: self
133                .cpp()
134                .prepared_syntax(self.token(), file)
135                .and_then(|prepared| preprocessor_guard_environment(reference, prepared.source())),
136        };
137        self.bind_qualifier(
138            &lookup,
139            components,
140            global,
141            lexical_scope,
142            (file, reference.start_byte()),
143            MAX_NAMESPACE_ALIAS_HOPS,
144        )
145    }
146
147    fn collect_namespace_alias_sites(
148        &self,
149        root: &ProjectFile,
150    ) -> HashMap<String, Vec<NamespaceAliasSite>> {
151        let mut sites: HashMap<String, Vec<NamespaceAliasSite>> = HashMap::default();
152        for source in self.visible_source_files(root) {
153            let Some(facts) = self.cpp().declaration_source_facts(self.token(), &source) else {
154                continue;
155            };
156            for alias in &facts.facts.namespace_aliases {
157                let range = facts.source.occurrence(alias.occurrence).range;
158                sites
159                    .entry(alias.name.clone())
160                    .or_default()
161                    .push(NamespaceAliasSite {
162                        source: source.clone(),
163                        start: range.start_byte,
164                        end: range.end_byte,
165                        target: alias.target.clone(),
166                        target_global: alias.target_global,
167                        namespace: alias.namespace.clone(),
168                        block: alias.block.map(|block| {
169                            let range = facts.source.occurrence(block).range;
170                            (range.start_byte, range.end_byte)
171                        }),
172                        guards: alias.guards.as_ref().and_then(cpp_guard_set_to_runtime),
173                    });
174            }
175        }
176        sites
177    }
178
179    /// Bind the first alias among the qualifier components of `components`,
180    /// looked up from `lexical_scope` at `position`, the reference itself or
181    /// the alias definition whose target is being followed.
182    fn bind_qualifier(
183        &self,
184        lookup: &AliasLookup<'_, '_>,
185        components: &[String],
186        global: bool,
187        lexical_scope: &[String],
188        position: (&ProjectFile, usize),
189        hops: usize,
190    ) -> NamespaceQualifierBinding {
191        let qualifier = &components[..components.len() - 1];
192        let precedes =
193            |site: &NamespaceAliasSite| site.source != *position.0 || site.end <= position.1;
194        if !global {
195            // A block-scope alias is closer than any namespace-scope name.
196            let block_sites = lookup
197                .sites
198                .get(&qualifier[0])
199                .into_iter()
200                .flatten()
201                .filter(|site| {
202                    site.block.is_some_and(|(start, end)| {
203                        site.source == *position.0 && start <= position.1 && position.1 < end
204                    }) && precedes(site)
205                        && lexical_scope.starts_with(&site.namespace)
206                });
207            if let Some(innermost) = block_sites.clone().filter_map(|site| site.block).max() {
208                let candidates = block_sites
209                    .filter(|site| site.block == Some(innermost))
210                    .collect::<Vec<_>>();
211                return self.bind_through(
212                    lookup,
213                    &candidates,
214                    components,
215                    0,
216                    lexical_scope.len() + 1,
217                    hops,
218                );
219            }
220        }
221        let innermost_tier = if global { 0 } else { lexical_scope.len() };
222        for tier in (0..=innermost_tier).rev() {
223            let mut owner = lexical_scope[..tier].to_vec();
224            for (index, name) in qualifier.iter().enumerate() {
225                let candidates = lookup
226                    .sites
227                    .get(name)
228                    .into_iter()
229                    .flatten()
230                    .filter(|site| {
231                        site.block.is_none() && site.namespace == owner && precedes(site)
232                    })
233                    .collect::<Vec<_>>();
234                if !candidates.is_empty() {
235                    match self.bind_through(lookup, &candidates, components, index, tier, hops) {
236                        NamespaceQualifierBinding::Unaliased => {}
237                        binding => return binding,
238                    }
239                }
240                owner.push(name.clone());
241            }
242        }
243        NamespaceQualifierBinding::Unaliased
244    }
245
246    /// Bind `components[index]` through the same-scope definitions in
247    /// `candidates`, then follow the target's own qualifier from the alias's
248    /// scope and position.
249    fn bind_through(
250        &self,
251        lookup: &AliasLookup<'_, '_>,
252        candidates: &[&NamespaceAliasSite],
253        components: &[String],
254        index: usize,
255        depth: usize,
256        hops: usize,
257    ) -> NamespaceQualifierBinding {
258        let mut bound: Option<&NamespaceAliasSite> = None;
259        let mut unresolved = false;
260        for site in candidates {
261            match self.namespace_alias_site_state(lookup, site) {
262                SiteState::Hidden => {}
263                SiteState::Possible => unresolved = true,
264                SiteState::InEffect => match bound {
265                    None => bound = Some(site),
266                    Some(previous) if previous.same_target(site) => {}
267                    Some(_) => unresolved = true,
268                },
269            }
270        }
271        if unresolved {
272            return NamespaceQualifierBinding::Unresolved { depth };
273        }
274        let Some(site) = bound else {
275            return NamespaceQualifierBinding::Unaliased;
276        };
277        let (Some(target), Some(hops)) = (&site.target, hops.checked_sub(1)) else {
278            return NamespaceQualifierBinding::Unresolved { depth };
279        };
280        let mut rebound = target.clone();
281        rebound.extend_from_slice(&components[index + 1..]);
282        match self.bind_qualifier(
283            lookup,
284            &rebound,
285            site.target_global,
286            &site.namespace,
287            (&site.source, site.start),
288            hops,
289        ) {
290            NamespaceQualifierBinding::Unaliased => NamespaceQualifierBinding::Bound {
291                components: rebound,
292                global: site.target_global,
293                lexical_scope: site.namespace.clone(),
294                depth,
295            },
296            NamespaceQualifierBinding::Bound {
297                components,
298                global,
299                lexical_scope,
300                ..
301            } => NamespaceQualifierBinding::Bound {
302                components,
303                global,
304                lexical_scope,
305                depth,
306            },
307            NamespaceQualifierBinding::Unresolved { .. } => {
308                NamespaceQualifierBinding::Unresolved { depth }
309            }
310        }
311    }
312
313    fn namespace_alias_site_state(
314        &self,
315        lookup: &AliasLookup<'_, '_>,
316        site: &NamespaceAliasSite,
317    ) -> SiteState {
318        let Some(guards) = &site.guards else {
319            return SiteState::Possible;
320        };
321        if self.binding_site_visible_at_reference(
322            lookup.file,
323            &site.source,
324            site.end,
325            guards,
326            lookup.reference,
327        ) {
328            return SiteState::InEffect;
329        }
330        // A definition the reference reaches only under guards it neither
331        // proves nor contradicts is a live alternative, not an absence.
332        let reachable_in_order =
333            site.source != *lookup.file || site.end <= lookup.reference.start_byte();
334        if reachable_in_order
335            && !guards.is_empty()
336            && lookup
337                .raw_reference_guards
338                .as_ref()
339                .is_some_and(|active| merge_preprocessor_guards(guards, active).is_some())
340        {
341            SiteState::Possible
342        } else {
343            SiteState::Hidden
344        }
345    }
346}