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