Skip to main content

graphcal_compiler/syntax/
module_resolve.rs

1//! Module-aware symbol tables backing HIR name resolution.
2//!
3//! This module is the first HIR/resolver-oriented layer after the syntax-first
4//! name refactor. It does **not** rewrite the AST yet. Instead it builds typed
5//! symbol tables for loaded DAG/module identities and resolves syntactic
6//! [`NamePath`] / [`IdentPath`] references to canonical [`ResolvedName`] values.
7//!
8//! The important invariant is that source qualifier text is used only to look up
9//! a module alias in the current module scope. The result of a successful lookup
10//! carries the canonical [`DagId`] owner, not the textual alias.
11
12use std::collections::{HashMap, HashSet};
13
14use thiserror::Error;
15
16use crate::dag_id::DagId;
17use crate::desugar::desugared_ast as ast;
18use crate::syntax::ast::{IdentPath, ImportItem, ImportItemNamespace, ImportKind, ModulePath};
19use crate::syntax::names::{
20    ConstructorName, DeclName, DimName, IndexName, IndexVariantName, ModuleAliasName, NameAtom,
21    NameDef, NameNamespace, NamePath, ResolvedIndexVariant, ResolvedName, StructTypeName, UnitName,
22    namespace,
23};
24use crate::syntax::non_empty::NonEmpty;
25use crate::syntax::phase::never;
26use crate::syntax::span::{Span, Spanned};
27
28/// Visibility of a symbol across module boundaries.
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
30pub enum SymbolVisibility {
31    /// Visible only inside the owning module.
32    Private,
33    /// Publicly visible to importers.
34    Public,
35    /// Public and bindable by include-time type/index/dimension bindings.
36    PublicBind,
37}
38
39impl SymbolVisibility {
40    /// Returns whether the symbol is visible outside its owning module.
41    #[must_use]
42    pub const fn is_public(self) -> bool {
43        matches!(self, Self::Public | Self::PublicBind)
44    }
45
46    /// Returns whether the symbol can be rebound by include-time bindings.
47    #[must_use]
48    pub const fn is_bindable(self) -> bool {
49        matches!(self, Self::PublicBind)
50    }
51}
52
53impl From<ast::Visibility> for SymbolVisibility {
54    fn from(visibility: ast::Visibility) -> Self {
55        match visibility {
56            ast::Visibility::Private => Self::Private,
57            ast::Visibility::Public => Self::Public,
58        }
59    }
60}
61
62impl From<ast::BindableVisibility> for SymbolVisibility {
63    fn from(visibility: ast::BindableVisibility) -> Self {
64        match visibility {
65            ast::BindableVisibility::Private => Self::Private,
66            ast::BindableVisibility::Public => Self::Public,
67            ast::BindableVisibility::PublicBind => Self::PublicBind,
68        }
69    }
70}
71
72/// Semantic kind of a value/declaration namespace symbol.
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
74pub enum DeclSymbolKind {
75    Const,
76    Param,
77    Node,
78    Assert,
79    Plot,
80    Figure,
81    Layer,
82    Dag,
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
86enum ExclusiveNameKind {
87    Value,
88    Dimension,
89    StructType,
90    Index,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94struct ExclusiveNameBinding {
95    kind: ExclusiveNameKind,
96    span: Span,
97}
98
99impl DeclSymbolKind {
100    /// Returns whether this declaration can be referenced from const-like
101    /// expression positions.
102    #[must_use]
103    pub const fn is_const(self) -> bool {
104        matches!(self, Self::Const)
105    }
106}
107
108impl std::fmt::Display for DeclSymbolKind {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        let label = match self {
111            Self::Const => "const",
112            Self::Param => "param",
113            Self::Node => "node",
114            Self::Assert => "assert",
115            Self::Plot => "plot",
116            Self::Figure => "figure",
117            Self::Layer => "layer",
118            Self::Dag => "dag",
119        };
120        f.write_str(label)
121    }
122}
123
124/// Visibility rule applied by a module alias or selective import edge.
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
126pub enum ModuleAccess {
127    /// Cross-module import/include boundary: only public target symbols are accessible.
128    PublicOnly,
129}
130
131impl ModuleAccess {
132    const fn requires_public(self) -> bool {
133        matches!(self, Self::PublicOnly)
134    }
135}
136
137/// A declaration symbol in one semantic namespace.
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct ModuleSymbol<Ns: NameNamespace> {
140    resolved: ResolvedName<Ns>,
141    visibility: SymbolVisibility,
142    span: Span,
143}
144
145impl<Ns: NameNamespace> ModuleSymbol<Ns> {
146    fn new(owner: &DagId, name: NameDef<Ns>, visibility: SymbolVisibility, span: Span) -> Self {
147        Self {
148            resolved: ResolvedName::from_def(owner.clone(), name),
149            visibility,
150            span,
151        }
152    }
153
154    /// Canonical resolved identity for this symbol.
155    #[must_use]
156    pub const fn resolved(&self) -> &ResolvedName<Ns> {
157        &self.resolved
158    }
159
160    /// Visibility of this symbol across module boundaries.
161    #[must_use]
162    pub const fn visibility(&self) -> SymbolVisibility {
163        self.visibility
164    }
165
166    /// Source span of the definition-site name.
167    #[must_use]
168    pub const fn span(&self) -> Span {
169        self.span
170    }
171}
172
173trait ModuleSymbolLookup<Ns: NameNamespace> {
174    fn resolved(&self) -> &ResolvedName<Ns>;
175    fn visibility(&self) -> SymbolVisibility;
176    fn span(&self) -> Span;
177}
178
179impl<Ns: NameNamespace> ModuleSymbolLookup<Ns> for ModuleSymbol<Ns> {
180    fn resolved(&self) -> &ResolvedName<Ns> {
181        self.resolved()
182    }
183
184    fn visibility(&self) -> SymbolVisibility {
185        self.visibility()
186    }
187
188    fn span(&self) -> Span {
189        self.span()
190    }
191}
192
193/// Value/declaration symbol plus its semantic declaration kind.
194#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct ModuleDeclSymbol {
196    symbol: ModuleSymbol<namespace::Decl>,
197    kind: DeclSymbolKind,
198}
199
200impl ModuleDeclSymbol {
201    fn new(
202        owner: &DagId,
203        name: DeclName,
204        visibility: SymbolVisibility,
205        span: Span,
206        kind: DeclSymbolKind,
207    ) -> Self {
208        Self {
209            symbol: ModuleSymbol::new(owner, name, visibility, span),
210            kind,
211        }
212    }
213
214    /// Canonical resolved identity for this declaration.
215    #[must_use]
216    pub const fn resolved(&self) -> &ResolvedName<namespace::Decl> {
217        self.symbol.resolved()
218    }
219
220    /// Visibility of this declaration across module boundaries.
221    #[must_use]
222    pub const fn visibility(&self) -> SymbolVisibility {
223        self.symbol.visibility()
224    }
225
226    /// Source span of the definition-site name.
227    #[must_use]
228    pub const fn span(&self) -> Span {
229        self.symbol.span()
230    }
231
232    /// Semantic declaration kind.
233    #[must_use]
234    pub const fn kind(&self) -> DeclSymbolKind {
235        self.kind
236    }
237}
238
239impl ModuleSymbolLookup<namespace::Decl> for ModuleDeclSymbol {
240    fn resolved(&self) -> &ResolvedName<namespace::Decl> {
241        self.resolved()
242    }
243
244    fn visibility(&self) -> SymbolVisibility {
245        self.visibility()
246    }
247
248    fn span(&self) -> Span {
249        self.span()
250    }
251}
252
253/// Index symbol plus the variants declared by that index.
254#[derive(Debug, Clone, PartialEq, Eq)]
255pub struct ModuleIndexSymbol {
256    symbol: ModuleSymbol<namespace::Index>,
257    variants: HashMap<IndexVariantName, Span>,
258}
259
260impl ModuleIndexSymbol {
261    /// Canonical resolved identity for the index type.
262    #[must_use]
263    pub const fn resolved(&self) -> &ResolvedName<namespace::Index> {
264        self.symbol.resolved()
265    }
266
267    /// Visibility of the index declaration.
268    #[must_use]
269    pub const fn visibility(&self) -> SymbolVisibility {
270        self.symbol.visibility()
271    }
272
273    /// Source span of the index definition-site name.
274    #[must_use]
275    pub const fn span(&self) -> Span {
276        self.symbol.span()
277    }
278
279    /// Variant names declared by this index, keyed by leaf name.
280    #[must_use]
281    pub const fn variants(&self) -> &HashMap<IndexVariantName, Span> {
282        &self.variants
283    }
284}
285
286impl ModuleSymbolLookup<namespace::Index> for ModuleIndexSymbol {
287    fn resolved(&self) -> &ResolvedName<namespace::Index> {
288        self.resolved()
289    }
290
291    fn visibility(&self) -> SymbolVisibility {
292        self.visibility()
293    }
294
295    fn span(&self) -> Span {
296        self.span()
297    }
298}
299
300/// Symbols declared by a single DAG/module.
301#[derive(Debug, Clone, PartialEq, Eq)]
302pub struct ModuleSymbols {
303    owner: DagId,
304    decls: HashMap<DeclName, ModuleDeclSymbol>,
305    dimensions: HashMap<DimName, ModuleSymbol<namespace::Dim>>,
306    units: HashMap<UnitName, ModuleSymbol<namespace::Unit>>,
307    struct_types: HashMap<StructTypeName, ModuleSymbol<namespace::StructType>>,
308    indexes: HashMap<IndexName, ModuleIndexSymbol>,
309    constructors: HashMap<ConstructorName, ModuleSymbol<namespace::Constructor>>,
310}
311
312impl ModuleSymbols {
313    /// Build a module symbol table from a declaration list.
314    ///
315    /// The `owner` is the canonical DAG/module identity assigned by the loader.
316    /// The declarations are not modified; this is a pure collection pass.
317    ///
318    /// # Errors
319    ///
320    /// Returns [`ModuleResolveError::DuplicateSymbol`] when two definitions in
321    /// the same namespace share a leaf name.
322    pub fn from_declarations(
323        owner: DagId,
324        declarations: &[ast::Declaration],
325    ) -> Result<Self, ModuleResolveError> {
326        let mut symbols = Self {
327            owner,
328            decls: HashMap::new(),
329            dimensions: HashMap::new(),
330            units: HashMap::new(),
331            struct_types: HashMap::new(),
332            indexes: HashMap::new(),
333            constructors: HashMap::new(),
334        };
335
336        symbols.collect_declarations(declarations)?;
337        Ok(symbols)
338    }
339
340    /// The canonical owner for this table.
341    #[must_use]
342    pub const fn owner(&self) -> &DagId {
343        &self.owner
344    }
345
346    /// Value/declaration namespace symbols.
347    #[must_use]
348    pub const fn decls(&self) -> &HashMap<DeclName, ModuleDeclSymbol> {
349        &self.decls
350    }
351
352    /// Dimension namespace symbols.
353    #[must_use]
354    pub const fn dimensions(&self) -> &HashMap<DimName, ModuleSymbol<namespace::Dim>> {
355        &self.dimensions
356    }
357
358    /// Unit namespace symbols.
359    #[must_use]
360    pub const fn units(&self) -> &HashMap<UnitName, ModuleSymbol<namespace::Unit>> {
361        &self.units
362    }
363
364    /// Struct/tagged-union type namespace symbols.
365    #[must_use]
366    pub const fn struct_types(
367        &self,
368    ) -> &HashMap<StructTypeName, ModuleSymbol<namespace::StructType>> {
369        &self.struct_types
370    }
371
372    /// Index namespace symbols.
373    #[must_use]
374    pub const fn indexes(&self) -> &HashMap<IndexName, ModuleIndexSymbol> {
375        &self.indexes
376    }
377
378    /// Tagged-union constructor namespace symbols.
379    #[must_use]
380    pub const fn constructors(
381        &self,
382    ) -> &HashMap<ConstructorName, ModuleSymbol<namespace::Constructor>> {
383        &self.constructors
384    }
385
386    fn collect_declarations(
387        &mut self,
388        declarations: &[ast::Declaration],
389    ) -> Result<(), ModuleResolveError> {
390        let mut exclusive_names = HashMap::new();
391        for decl in declarations {
392            match &decl.kind {
393                ast::DeclKind::Param(p) => self.insert_value_decl(
394                    &mut exclusive_names,
395                    &p.name,
396                    SymbolVisibility::PublicBind,
397                    DeclSymbolKind::Param,
398                )?,
399                ast::DeclKind::Node(n) => self.insert_value_decl(
400                    &mut exclusive_names,
401                    &n.name,
402                    SymbolVisibility::from(n.visibility),
403                    DeclSymbolKind::Node,
404                )?,
405                ast::DeclKind::ConstNode(c) => self.insert_value_decl(
406                    &mut exclusive_names,
407                    &c.name,
408                    SymbolVisibility::from(c.visibility),
409                    DeclSymbolKind::Const,
410                )?,
411                ast::DeclKind::Assert(a) => self.insert_value_decl(
412                    &mut exclusive_names,
413                    &a.name,
414                    SymbolVisibility::from(a.visibility),
415                    DeclSymbolKind::Assert,
416                )?,
417                ast::DeclKind::Plot(p) => self.insert_value_decl(
418                    &mut exclusive_names,
419                    &p.name,
420                    SymbolVisibility::from(p.visibility),
421                    DeclSymbolKind::Plot,
422                )?,
423                ast::DeclKind::Figure(f) => self.insert_value_decl(
424                    &mut exclusive_names,
425                    &f.name,
426                    SymbolVisibility::from(f.visibility),
427                    DeclSymbolKind::Figure,
428                )?,
429                ast::DeclKind::Layer(l) => self.insert_value_decl(
430                    &mut exclusive_names,
431                    &l.name,
432                    SymbolVisibility::from(l.visibility),
433                    DeclSymbolKind::Layer,
434                )?,
435                ast::DeclKind::Dag(d) => self.insert_value_decl(
436                    &mut exclusive_names,
437                    &d.name,
438                    SymbolVisibility::from(d.visibility),
439                    DeclSymbolKind::Dag,
440                )?,
441                ast::DeclKind::BaseDimension(d) => self.insert_dimension_decl(
442                    &mut exclusive_names,
443                    &d.name,
444                    SymbolVisibility::from(d.visibility),
445                )?,
446                ast::DeclKind::Dimension(d) => self.insert_dimension_decl(
447                    &mut exclusive_names,
448                    &d.name,
449                    SymbolVisibility::from(d.visibility),
450                )?,
451                ast::DeclKind::Unit(u) => self.insert_unit(
452                    &u.name,
453                    SymbolVisibility::from(u.visibility),
454                    namespace::Unit::DISPLAY_NAME,
455                )?,
456                ast::DeclKind::Type(t) => self.insert_type_decl(&mut exclusive_names, t)?,
457                ast::DeclKind::Index(i) => self.insert_index_decl(&mut exclusive_names, i)?,
458                ast::DeclKind::Import(_) | ast::DeclKind::Include(_) => {}
459                #[expect(
460                    clippy::uninhabited_references,
461                    reason = "post-desugar Sugar payload is uninhabited by phase invariant"
462                )]
463                ast::DeclKind::Sugar(s) => never(*s),
464            }
465        }
466        Ok(())
467    }
468
469    fn insert_value_decl(
470        &mut self,
471        exclusive_names: &mut HashMap<NameAtom, ExclusiveNameBinding>,
472        name: &Spanned<DeclName>,
473        visibility: SymbolVisibility,
474        kind: DeclSymbolKind,
475    ) -> Result<(), ModuleResolveError> {
476        self.insert_exclusive_name(
477            exclusive_names,
478            name.value.atom(),
479            ExclusiveNameKind::Value,
480            name.span,
481        )?;
482        self.insert_decl(name, visibility, namespace::Decl::DISPLAY_NAME, kind)
483    }
484
485    fn insert_dimension_decl(
486        &mut self,
487        exclusive_names: &mut HashMap<NameAtom, ExclusiveNameBinding>,
488        name: &Spanned<DimName>,
489        visibility: SymbolVisibility,
490    ) -> Result<(), ModuleResolveError> {
491        self.insert_exclusive_name(
492            exclusive_names,
493            name.value.atom(),
494            ExclusiveNameKind::Dimension,
495            name.span,
496        )?;
497        self.insert_dimension(name, visibility, namespace::Dim::DISPLAY_NAME)
498    }
499
500    fn insert_type_decl(
501        &mut self,
502        exclusive_names: &mut HashMap<NameAtom, ExclusiveNameBinding>,
503        type_decl: &ast::TypeDecl,
504    ) -> Result<(), ModuleResolveError> {
505        let visibility = SymbolVisibility::from(type_decl.visibility);
506        self.insert_exclusive_name(
507            exclusive_names,
508            type_decl.name.value.atom(),
509            ExclusiveNameKind::StructType,
510            type_decl.name.span,
511        )?;
512        self.insert_struct_type(
513            &type_decl.name,
514            visibility,
515            namespace::StructType::DISPLAY_NAME,
516        )?;
517        if let ast::TypeDeclBody::Constructors(members) = &type_decl.body {
518            for member in members {
519                self.insert_constructor(
520                    &member.name,
521                    visibility,
522                    namespace::Constructor::DISPLAY_NAME,
523                )?;
524            }
525        }
526        Ok(())
527    }
528
529    fn insert_index_decl(
530        &mut self,
531        exclusive_names: &mut HashMap<NameAtom, ExclusiveNameBinding>,
532        index: &ast::IndexDecl,
533    ) -> Result<(), ModuleResolveError> {
534        self.insert_exclusive_name(
535            exclusive_names,
536            index.name.value.atom(),
537            ExclusiveNameKind::Index,
538            index.name.span,
539        )?;
540        self.insert_index(index)
541    }
542
543    fn insert_exclusive_name(
544        &self,
545        occupied: &mut HashMap<NameAtom, ExclusiveNameBinding>,
546        atom: &NameAtom,
547        kind: ExclusiveNameKind,
548        span: Span,
549    ) -> Result<(), ModuleResolveError> {
550        match occupied.get(atom) {
551            Some(first) if first.kind != kind => Err(ModuleResolveError::DuplicateSymbol {
552                owner: self.owner.clone(),
553                namespace: "name",
554                name: atom.to_string(),
555                first: first.span,
556                duplicate: span,
557            }),
558            _ => {
559                occupied
560                    .entry(atom.clone())
561                    .or_insert(ExclusiveNameBinding { kind, span });
562                Ok(())
563            }
564        }
565    }
566
567    fn insert_decl(
568        &mut self,
569        name: &Spanned<DeclName>,
570        visibility: SymbolVisibility,
571        namespace_name: &'static str,
572        kind: DeclSymbolKind,
573    ) -> Result<(), ModuleResolveError> {
574        insert_decl_symbol(
575            &self.owner,
576            &mut self.decls,
577            name,
578            visibility,
579            namespace_name,
580            kind,
581        )
582    }
583
584    fn insert_dimension(
585        &mut self,
586        name: &Spanned<DimName>,
587        visibility: SymbolVisibility,
588        namespace_name: &'static str,
589    ) -> Result<(), ModuleResolveError> {
590        insert_symbol(
591            &self.owner,
592            &mut self.dimensions,
593            name,
594            visibility,
595            namespace_name,
596        )
597    }
598
599    fn insert_unit(
600        &mut self,
601        name: &Spanned<UnitName>,
602        visibility: SymbolVisibility,
603        namespace_name: &'static str,
604    ) -> Result<(), ModuleResolveError> {
605        insert_symbol(
606            &self.owner,
607            &mut self.units,
608            name,
609            visibility,
610            namespace_name,
611        )
612    }
613
614    fn insert_struct_type(
615        &mut self,
616        name: &Spanned<StructTypeName>,
617        visibility: SymbolVisibility,
618        namespace_name: &'static str,
619    ) -> Result<(), ModuleResolveError> {
620        insert_symbol(
621            &self.owner,
622            &mut self.struct_types,
623            name,
624            visibility,
625            namespace_name,
626        )
627    }
628
629    fn insert_constructor(
630        &mut self,
631        name: &Spanned<ConstructorName>,
632        visibility: SymbolVisibility,
633        namespace_name: &'static str,
634    ) -> Result<(), ModuleResolveError> {
635        insert_symbol(
636            &self.owner,
637            &mut self.constructors,
638            name,
639            visibility,
640            namespace_name,
641        )
642    }
643
644    fn insert_index(&mut self, index: &ast::IndexDecl) -> Result<(), ModuleResolveError> {
645        if let Some(first) = self.indexes.get(index.name.value.as_str()) {
646            return Err(ModuleResolveError::DuplicateSymbol {
647                owner: self.owner.clone(),
648                namespace: namespace::Index::DISPLAY_NAME,
649                name: index.name.value.to_string(),
650                first: first.span(),
651                duplicate: index.name.span,
652            });
653        }
654
655        let mut variants = HashMap::new();
656        if let ast::IndexDeclKind::Named { variants: declared } = &index.kind {
657            for variant in declared {
658                if let Some(first) = variants.insert(variant.value.clone(), variant.span) {
659                    return Err(ModuleResolveError::DuplicateSymbol {
660                        owner: self.owner.clone(),
661                        namespace: namespace::IndexVariant::DISPLAY_NAME,
662                        name: variant.value.qualified_by(&index.name.value).to_string(),
663                        first,
664                        duplicate: variant.span,
665                    });
666                }
667            }
668        }
669
670        self.indexes.insert(
671            index.name.value.clone(),
672            ModuleIndexSymbol {
673                symbol: ModuleSymbol::new(
674                    &self.owner,
675                    index.name.value.clone(),
676                    SymbolVisibility::from(index.visibility),
677                    index.name.span,
678                ),
679                variants,
680            },
681        );
682        Ok(())
683    }
684}
685
686fn insert_symbol<Ns: NameNamespace>(
687    owner: &DagId,
688    map: &mut HashMap<NameDef<Ns>, ModuleSymbol<Ns>>,
689    name: &Spanned<NameDef<Ns>>,
690    visibility: SymbolVisibility,
691    namespace_name: &'static str,
692) -> Result<(), ModuleResolveError> {
693    if let Some(first) = map.get(name.value.as_str()) {
694        return Err(ModuleResolveError::DuplicateSymbol {
695            owner: owner.clone(),
696            namespace: namespace_name,
697            name: name.value.to_string(),
698            first: first.span(),
699            duplicate: name.span,
700        });
701    }
702    map.insert(
703        name.value.clone(),
704        ModuleSymbol::new(owner, name.value.clone(), visibility, name.span),
705    );
706    Ok(())
707}
708
709fn insert_decl_symbol(
710    owner: &DagId,
711    map: &mut HashMap<DeclName, ModuleDeclSymbol>,
712    name: &Spanned<DeclName>,
713    visibility: SymbolVisibility,
714    namespace_name: &'static str,
715    kind: DeclSymbolKind,
716) -> Result<(), ModuleResolveError> {
717    if let Some(first) = map.get(name.value.as_str()) {
718        return Err(ModuleResolveError::DuplicateSymbol {
719            owner: owner.clone(),
720            namespace: namespace_name,
721            name: name.value.to_string(),
722            first: first.span(),
723            duplicate: name.span,
724        });
725    }
726    map.insert(
727        name.value.clone(),
728        ModuleDeclSymbol::new(owner, name.value.clone(), visibility, name.span, kind),
729    );
730    Ok(())
731}
732
733/// A resolved module alias in one module's import scope.
734#[derive(Debug, Clone, PartialEq, Eq)]
735pub struct ModuleAliasTarget {
736    target: DagId,
737    span: Span,
738    access: ModuleAccess,
739}
740
741impl ModuleAliasTarget {
742    /// Canonical DAG/module targeted by the alias.
743    #[must_use]
744    pub const fn target(&self) -> &DagId {
745        &self.target
746    }
747
748    /// Source span of the local alias name.
749    #[must_use]
750    pub const fn span(&self) -> Span {
751        self.span
752    }
753
754    /// Visibility rule for names reached through this alias.
755    #[must_use]
756    pub const fn access(&self) -> ModuleAccess {
757        self.access
758    }
759}
760
761/// A selective import binding for one namespace.
762#[derive(Debug, Clone, PartialEq, Eq)]
763pub struct ImportedSymbol<Ns: NameNamespace> {
764    resolved: ResolvedName<Ns>,
765    span: Span,
766    visibility: SymbolVisibility,
767}
768
769impl<Ns: NameNamespace> ImportedSymbol<Ns> {
770    const fn new(resolved: ResolvedName<Ns>, span: Span, visibility: SymbolVisibility) -> Self {
771        Self {
772            resolved,
773            span,
774            visibility,
775        }
776    }
777
778    /// Canonical target identity of the imported symbol.
779    #[must_use]
780    pub const fn resolved(&self) -> &ResolvedName<Ns> {
781        &self.resolved
782    }
783
784    /// Source span of the local import name.
785    #[must_use]
786    pub const fn span(&self) -> Span {
787        self.span
788    }
789
790    /// Visibility of this selective import when the importing module is itself imported.
791    #[must_use]
792    pub const fn visibility(&self) -> SymbolVisibility {
793        self.visibility
794    }
795}
796
797impl<Ns: NameNamespace> ModuleSymbolLookup<Ns> for ImportedSymbol<Ns> {
798    fn resolved(&self) -> &ResolvedName<Ns> {
799        self.resolved()
800    }
801
802    fn visibility(&self) -> SymbolVisibility {
803        self.visibility()
804    }
805
806    fn span(&self) -> Span {
807        self.span()
808    }
809}
810
811/// Import scope for a single module.
812#[derive(Debug, Default, Clone, PartialEq, Eq)]
813pub struct ModuleScope {
814    module_aliases: HashMap<ModuleAliasName, ModuleAliasTarget>,
815    selected_decls: HashMap<DeclName, ImportedSymbol<namespace::Decl>>,
816    selected_dimensions: HashMap<DimName, ImportedSymbol<namespace::Dim>>,
817    selected_units: HashMap<UnitName, ImportedSymbol<namespace::Unit>>,
818    selected_struct_types: HashMap<StructTypeName, ImportedSymbol<namespace::StructType>>,
819    selected_indexes: HashMap<IndexName, ImportedSymbol<namespace::Index>>,
820    selected_constructors: HashMap<ConstructorName, ImportedSymbol<namespace::Constructor>>,
821}
822
823impl ModuleScope {
824    /// Module aliases introduced by whole-module imports/includes.
825    #[must_use]
826    pub const fn module_aliases(&self) -> &HashMap<ModuleAliasName, ModuleAliasTarget> {
827        &self.module_aliases
828    }
829}
830
831/// Surface category for diagnostics that cross namespace boundaries.
832#[derive(Debug, Clone, Copy, PartialEq, Eq)]
833pub enum SurfaceNameKind {
834    Value,
835    Dimension,
836    Unit,
837    Type,
838    Index,
839    IndexLabel,
840    Constructor,
841    DefaultImportItem,
842}
843
844impl std::fmt::Display for SurfaceNameKind {
845    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
846        let text = match self {
847            Self::Value => "a value",
848            Self::Dimension => "a dimension",
849            Self::Unit => "a unit",
850            Self::Type => "a type",
851            Self::Index => "an index",
852            Self::IndexLabel => "an index label",
853            Self::Constructor => "a constructor",
854            Self::DefaultImportItem => "a default import item",
855        };
856        f.write_str(text)
857    }
858}
859
860trait ResolvableNamespace: NameNamespace {
861    const SURFACE_KIND: SurfaceNameKind;
862}
863
864impl ResolvableNamespace for namespace::Decl {
865    const SURFACE_KIND: SurfaceNameKind = SurfaceNameKind::Value;
866}
867
868impl ResolvableNamespace for namespace::Dim {
869    const SURFACE_KIND: SurfaceNameKind = SurfaceNameKind::Dimension;
870}
871
872impl ResolvableNamespace for namespace::Unit {
873    const SURFACE_KIND: SurfaceNameKind = SurfaceNameKind::Unit;
874}
875
876impl ResolvableNamespace for namespace::StructType {
877    const SURFACE_KIND: SurfaceNameKind = SurfaceNameKind::Type;
878}
879
880impl ResolvableNamespace for namespace::Index {
881    const SURFACE_KIND: SurfaceNameKind = SurfaceNameKind::Index;
882}
883
884impl ResolvableNamespace for namespace::Constructor {
885    const SURFACE_KIND: SurfaceNameKind = SurfaceNameKind::Constructor;
886}
887
888#[derive(Debug, Clone)]
889enum ImportAddition {
890    ModuleAlias {
891        alias: Spanned<ModuleAliasName>,
892        target: DagId,
893        access: ModuleAccess,
894    },
895    Decl {
896        local: Spanned<DeclName>,
897        target: ResolvedName<namespace::Decl>,
898        visibility: SymbolVisibility,
899    },
900    Dimension {
901        local: Spanned<DimName>,
902        target: ResolvedName<namespace::Dim>,
903        visibility: SymbolVisibility,
904    },
905    Unit {
906        local: Spanned<UnitName>,
907        target: ResolvedName<namespace::Unit>,
908        visibility: SymbolVisibility,
909    },
910    StructType {
911        local: Spanned<StructTypeName>,
912        target: ResolvedName<namespace::StructType>,
913        visibility: SymbolVisibility,
914    },
915    Index {
916        local: Spanned<IndexName>,
917        target: ResolvedName<namespace::Index>,
918        visibility: SymbolVisibility,
919    },
920    Constructor {
921        local: Spanned<ConstructorName>,
922        target: ResolvedName<namespace::Constructor>,
923        visibility: SymbolVisibility,
924    },
925}
926
927/// Project-wide module resolver backed by canonical [`DagId`] identities.
928#[derive(Debug, Default, Clone, PartialEq, Eq)]
929pub struct ModuleResolver {
930    modules: HashMap<DagId, ModuleSymbols>,
931    scopes: HashMap<DagId, ModuleScope>,
932}
933
934#[derive(Debug, Clone, PartialEq, Eq)]
935struct ResolvedModuleQualifier {
936    owner: DagId,
937    access: ModuleAccess,
938}
939
940impl ModuleResolver {
941    /// Build a resolver from `(DagId, File)` pairs without registering any
942    /// import scopes.
943    ///
944    /// Call [`Self::register_import`] / [`Self::register_include`] for each
945    /// loader-resolved edge after all modules have been added.
946    ///
947    /// # Errors
948    ///
949    /// Returns [`ModuleResolveError`] on duplicate modules or duplicate symbols.
950    pub fn from_modules<'a>(
951        modules: impl IntoIterator<Item = (DagId, &'a ast::File)>,
952    ) -> Result<Self, ModuleResolveError> {
953        let mut resolver = Self::default();
954        for (owner, file) in modules {
955            resolver.add_module(owner, &file.declarations)?;
956        }
957        Ok(resolver)
958    }
959
960    /// Add one module's declaration symbols.
961    ///
962    /// # Errors
963    ///
964    /// Returns [`ModuleResolveError::DuplicateModule`] when `owner` already has
965    /// a symbol table, or [`ModuleResolveError::DuplicateSymbol`] for duplicate
966    /// namespace-local definitions inside the module.
967    pub fn add_module(
968        &mut self,
969        owner: DagId,
970        declarations: &[ast::Declaration],
971    ) -> Result<(), ModuleResolveError> {
972        if self.modules.contains_key(&owner) {
973            return Err(ModuleResolveError::DuplicateModule { owner });
974        }
975        let symbols = ModuleSymbols::from_declarations(owner.clone(), declarations)?;
976        self.scopes.entry(owner.clone()).or_default();
977        self.modules.insert(owner, symbols);
978        Ok(())
979    }
980
981    /// Borrow all module symbol tables.
982    #[must_use]
983    pub const fn modules(&self) -> &HashMap<DagId, ModuleSymbols> {
984        &self.modules
985    }
986
987    /// Borrow all module import scopes.
988    #[must_use]
989    pub const fn scopes(&self) -> &HashMap<DagId, ModuleScope> {
990        &self.scopes
991    }
992
993    /// Register one loader-resolved `import` edge in `owner`'s scope.
994    ///
995    /// `path` and `kind` come from the source AST. `target` is the canonical
996    /// module identity chosen by the loader for that path. This function never
997    /// re-resolves filesystem paths.
998    ///
999    /// # Errors
1000    ///
1001    /// Returns [`ModuleResolveError`] if either module is unknown, an imported
1002    /// item is missing/private, or the import introduces a duplicate local name.
1003    pub fn register_import(
1004        &mut self,
1005        owner: &DagId,
1006        path: &ModulePath,
1007        kind: &ImportKind,
1008        target: &DagId,
1009    ) -> Result<(), ModuleResolveError> {
1010        self.register_import_with_access(owner, path, kind, target, ModuleAccess::PublicOnly)
1011    }
1012
1013    /// Register one loader-resolved `include` edge in `owner`'s scope.
1014    ///
1015    /// Instantiated includes embed the dependency DAG body, but the source-level
1016    /// names introduced by the include are still a cross-module boundary and
1017    /// must preserve public visibility.
1018    pub fn register_include(
1019        &mut self,
1020        owner: &DagId,
1021        path: &ModulePath,
1022        kind: &ImportKind,
1023        target: &DagId,
1024    ) -> Result<(), ModuleResolveError> {
1025        self.register_import_with_access(owner, path, kind, target, ModuleAccess::PublicOnly)
1026    }
1027
1028    /// Make an instantiated include's own indexes resolvable in the importer.
1029    ///
1030    /// An instantiated `include` inlines the dependency's declaration bodies
1031    /// into the importer (see `ir::lower::merge_dependency`). Those bodies
1032    /// reference the dependency's own indexes by their bare names (`for s:
1033    /// Step`, `T[Step]`, `Step.A`), which are not bound to any importer symbol.
1034    /// The dependency's declarations live in the synthetic include module
1035    /// `source`; copy each of its index symbols — re-homed onto the importer so
1036    /// they resolve against the flat merged registry that backs the importer's
1037    /// declarations — into the importer's own symbol table, variants included.
1038    ///
1039    /// Indexes named in `bound` (the include's index bindings/overrides) are
1040    /// skipped: a bound index is rewritten to the importer's replacement before
1041    /// resolution, so the dependency's original name must not shadow it.
1042    /// Indexes already declared in the importer are likewise left untouched.
1043    ///
1044    /// # Errors
1045    ///
1046    /// Returns [`ModuleResolveError::UnknownModule`] if either module is absent
1047    /// from the resolver.
1048    pub fn inline_instantiated_include_indexes(
1049        &mut self,
1050        importer: &DagId,
1051        source: &DagId,
1052        bound: &HashSet<&str>,
1053    ) -> Result<(), ModuleResolveError> {
1054        let source_symbols =
1055            self.modules
1056                .get(source)
1057                .ok_or_else(|| ModuleResolveError::UnknownModule {
1058                    owner: source.clone(),
1059                })?;
1060        let injected: Vec<ModuleIndexSymbol> = source_symbols
1061            .indexes
1062            .iter()
1063            .filter(|(name, _)| !bound.contains(name.as_str()))
1064            .map(|(name, symbol)| ModuleIndexSymbol {
1065                symbol: ModuleSymbol::new(
1066                    importer,
1067                    name.clone(),
1068                    symbol.visibility(),
1069                    symbol.span(),
1070                ),
1071                variants: symbol.variants().clone(),
1072            })
1073            .collect();
1074        let target =
1075            self.modules
1076                .get_mut(importer)
1077                .ok_or_else(|| ModuleResolveError::UnknownModule {
1078                    owner: importer.clone(),
1079                })?;
1080        for symbol in injected {
1081            let name = IndexName::from_atom(symbol.resolved().atom().clone());
1082            target.indexes.entry(name).or_insert(symbol);
1083        }
1084        Ok(())
1085    }
1086
1087    fn register_import_with_access(
1088        &mut self,
1089        owner: &DagId,
1090        path: &ModulePath,
1091        kind: &ImportKind,
1092        target: &DagId,
1093        access: ModuleAccess,
1094    ) -> Result<(), ModuleResolveError> {
1095        self.module_symbols(owner)?;
1096        self.module_symbols(target)?;
1097
1098        let additions = self.import_additions(path, kind, target, access)?;
1099        self.check_import_exclusive_name_collisions(owner, &additions)?;
1100        let scope =
1101            self.scopes
1102                .get_mut(owner)
1103                .ok_or_else(|| ModuleResolveError::UnknownModule {
1104                    owner: owner.clone(),
1105                })?;
1106        for addition in additions {
1107            scope.apply_addition(owner, addition)?;
1108        }
1109        Ok(())
1110    }
1111
1112    /// Resolve a syntactic declaration/value path to a canonical owner + leaf.
1113    ///
1114    /// Bare paths first search local declarations, then selective imports.
1115    /// Qualified paths resolve their qualifier through module aliases and then
1116    /// apply that alias boundary's visibility rule.
1117    pub fn resolve_decl_path(
1118        &self,
1119        owner: &DagId,
1120        path: &NamePath,
1121    ) -> Result<ResolvedName<namespace::Decl>, ModuleResolveError> {
1122        self.resolve_symbol_path(owner, path, ModuleSymbols::decls, |scope| {
1123            &scope.selected_decls
1124        })
1125    }
1126
1127    /// Resolve a declaration path and require that it names a const declaration.
1128    pub fn resolve_const_decl_path(
1129        &self,
1130        owner: &DagId,
1131        path: &NamePath,
1132    ) -> Result<ResolvedName<namespace::Decl>, ModuleResolveError> {
1133        let resolved = self.resolve_decl_path(owner, path)?;
1134        let actual = self.decl_symbol_kind(&resolved)?;
1135        if actual.is_const() {
1136            Ok(resolved)
1137        } else {
1138            Err(ModuleResolveError::UnexpectedDeclKind {
1139                name: resolved,
1140                expected: "const",
1141                actual,
1142            })
1143        }
1144    }
1145
1146    /// Return the semantic kind of a resolved declaration symbol.
1147    pub fn decl_symbol_kind(
1148        &self,
1149        name: &ResolvedName<namespace::Decl>,
1150    ) -> Result<DeclSymbolKind, ModuleResolveError> {
1151        let symbols = self.module_symbols(name.owner())?;
1152        let def_name = DeclName::from_atom(name.atom().clone());
1153        symbols
1154            .decls
1155            .get(def_name.as_str())
1156            .map(ModuleDeclSymbol::kind)
1157            .ok_or_else(|| ModuleResolveError::UnknownName {
1158                owner: name.owner().clone(),
1159                namespace: namespace::Decl::DISPLAY_NAME,
1160                name: name.as_str().to_string(),
1161            })
1162    }
1163
1164    /// Resolve a syntactic dimension path to a canonical owner + leaf.
1165    pub fn resolve_dimension_path(
1166        &self,
1167        owner: &DagId,
1168        path: &NamePath,
1169    ) -> Result<ResolvedName<namespace::Dim>, ModuleResolveError> {
1170        self.resolve_symbol_path(owner, path, ModuleSymbols::dimensions, |scope| {
1171            &scope.selected_dimensions
1172        })
1173    }
1174
1175    /// Resolve a syntactic unit path to a canonical owner + leaf.
1176    pub fn resolve_unit_path(
1177        &self,
1178        owner: &DagId,
1179        path: &NamePath,
1180    ) -> Result<ResolvedName<namespace::Unit>, ModuleResolveError> {
1181        self.resolve_symbol_path(owner, path, ModuleSymbols::units, |scope| {
1182            &scope.selected_units
1183        })
1184    }
1185
1186    /// Resolve a syntactic struct/tagged-union type path to a canonical owner + leaf.
1187    pub fn resolve_struct_type_path(
1188        &self,
1189        owner: &DagId,
1190        path: &NamePath,
1191    ) -> Result<ResolvedName<namespace::StructType>, ModuleResolveError> {
1192        self.resolve_symbol_path(owner, path, ModuleSymbols::struct_types, |scope| {
1193            &scope.selected_struct_types
1194        })
1195    }
1196
1197    /// Resolve a syntactic tagged-union constructor path to a canonical owner + leaf.
1198    pub fn resolve_constructor_path(
1199        &self,
1200        owner: &DagId,
1201        path: &NamePath,
1202    ) -> Result<ResolvedName<namespace::Constructor>, ModuleResolveError> {
1203        self.resolve_symbol_path(owner, path, ModuleSymbols::constructors, |scope| {
1204            &scope.selected_constructors
1205        })
1206    }
1207
1208    /// Resolve a span-aware constructor path without losing source path shape at
1209    /// the caller boundary.
1210    pub fn resolve_constructor_ident_path(
1211        &self,
1212        owner: &DagId,
1213        path: &IdentPath,
1214    ) -> Result<ResolvedName<namespace::Constructor>, ModuleResolveError> {
1215        self.resolve_constructor_path(owner, &ident_path_to_name_path(path))
1216    }
1217
1218    /// Resolve a syntactic index path to a canonical owner + leaf.
1219    pub fn resolve_index_path(
1220        &self,
1221        owner: &DagId,
1222        path: &NamePath,
1223    ) -> Result<ResolvedName<namespace::Index>, ModuleResolveError> {
1224        self.resolve_symbol_path(owner, path, ModuleSymbols::indexes, |scope| {
1225            &scope.selected_indexes
1226        })
1227    }
1228
1229    /// Resolve a syntactic index-variant path such as `Index.Variant` or
1230    /// `module.Index.Variant`.
1231    pub fn resolve_index_variant_path(
1232        &self,
1233        owner: &DagId,
1234        path: &NamePath,
1235    ) -> Result<ResolvedIndexVariant, ModuleResolveError> {
1236        let (index_segments, variant_atom) = path.split_last();
1237        let index_path = name_path_from_slice(index_segments).ok_or_else(|| {
1238            ModuleResolveError::ExpectedIndexVariantPath {
1239                owner: owner.clone(),
1240                path: path.display_path(),
1241            }
1242        })?;
1243        self.resolve_index_variant_parts(
1244            owner,
1245            &index_path,
1246            &IndexVariantName::from_atom(variant_atom.clone()),
1247        )
1248    }
1249
1250    /// Resolve an already-split index path plus variant leaf to a canonical
1251    /// index-variant identity.
1252    ///
1253    /// This is the HIR-facing form for parser positions that preserve the
1254    /// index path and variant leaf separately (map keys, index arguments, and
1255    /// match labels). It avoids reconstructing a dotted string or re-parsing
1256    /// source text just to validate the variant against the canonical index.
1257    pub fn resolve_index_variant_parts(
1258        &self,
1259        owner: &DagId,
1260        index_path: &NamePath,
1261        variant: &IndexVariantName,
1262    ) -> Result<ResolvedIndexVariant, ModuleResolveError> {
1263        let resolved_index = self.resolve_index_path(owner, index_path)?;
1264        let index_owner = resolved_index.owner().clone();
1265        let index_name = IndexName::from_atom(resolved_index.atom().clone());
1266        let target_symbols = self.module_symbols(&index_owner)?;
1267        let index_symbol = target_symbols
1268            .indexes
1269            .get(index_name.as_str())
1270            .ok_or_else(|| ModuleResolveError::UnknownName {
1271                owner: index_owner.clone(),
1272                namespace: namespace::Index::DISPLAY_NAME,
1273                name: index_name.to_string(),
1274            })?;
1275        if !index_symbol.variants.contains_key(variant.as_str()) {
1276            return Err(ModuleResolveError::UnknownIndexVariant {
1277                index: resolved_index,
1278                variant: variant.clone(),
1279            });
1280        }
1281        Ok(ResolvedIndexVariant::new(resolved_index, variant.clone()))
1282    }
1283
1284    /// Resolve a bare variant leaf by searching local and selectively imported
1285    /// indexes in the current module scope.
1286    pub fn resolve_bare_index_variant(
1287        &self,
1288        owner: &DagId,
1289        variant: &IndexVariantName,
1290    ) -> Result<ResolvedIndexVariant, ModuleResolveError> {
1291        let local = self.module_symbols(owner)?;
1292        let scope = self.module_scope(owner)?;
1293        let mut candidates = Vec::new();
1294
1295        for symbol in local.indexes.values() {
1296            if symbol.variants().contains_key(variant.as_str()) {
1297                candidates.push(symbol.resolved().clone());
1298            }
1299        }
1300        for imported in scope.selected_indexes.values() {
1301            let resolved = imported.resolved();
1302            let index_owner = resolved.owner().clone();
1303            let index_name = IndexName::from_atom(resolved.atom().clone());
1304            let target_symbols = self.module_symbols(&index_owner)?;
1305            let Some(symbol) = target_symbols.indexes.get(index_name.as_str()) else {
1306                continue;
1307            };
1308            if symbol.variants().contains_key(variant.as_str()) {
1309                candidates.push(resolved.clone());
1310            }
1311        }
1312        match candidates.as_slice() {
1313            [] => Err(ModuleResolveError::UnknownName {
1314                owner: owner.clone(),
1315                namespace: namespace::IndexVariant::DISPLAY_NAME,
1316                name: variant.to_string(),
1317            }),
1318            [index] => Ok(ResolvedIndexVariant::new(index.clone(), variant.clone())),
1319            _ => Err(ModuleResolveError::AmbiguousIndexVariant {
1320                owner: owner.clone(),
1321                variant: variant.clone(),
1322                indexes: candidates,
1323            }),
1324        }
1325    }
1326
1327    /// Resolve a source inline-DAG/module path to its canonical [`DagId`].
1328    ///
1329    /// Single-segment paths first name inline DAG children of `owner`; when
1330    /// called from an inline DAG body and no nested child exists, they may also
1331    /// name sibling DAGs under the parent file. Qualified paths use the first
1332    /// segment as a module alias and append the remaining segments to the alias
1333    /// target. The returned identity is canonical; source qualifier text is not
1334    /// carried beyond this resolver boundary.
1335    pub fn resolve_module_path(
1336        &self,
1337        owner: &DagId,
1338        path: &ModulePath,
1339    ) -> Result<DagId, ModuleResolveError> {
1340        if let [leaf] = path.segments() {
1341            let target = owner.child(leaf.name.as_str());
1342            if self.modules.contains_key(&target) {
1343                return Ok(target);
1344            }
1345            if let Some(parent) = owner.parent() {
1346                let sibling = parent.child(leaf.name.as_str());
1347                if self.modules.contains_key(&sibling) {
1348                    return Ok(sibling);
1349                }
1350            }
1351            return Err(ModuleResolveError::UnknownModule { owner: target });
1352        }
1353
1354        let atoms = path
1355            .segments()
1356            .iter()
1357            .map(|segment| segment.name.clone())
1358            .collect::<Vec<_>>();
1359        let resolved = self.resolve_module_qualifier(owner, &atoms)?;
1360        self.ensure_module_visible(&resolved.owner, resolved.access)?;
1361        Ok(resolved.owner)
1362    }
1363
1364    fn import_additions(
1365        &self,
1366        path: &ModulePath,
1367        kind: &ImportKind,
1368        target: &DagId,
1369        access: ModuleAccess,
1370    ) -> Result<Vec<ImportAddition>, ModuleResolveError> {
1371        match kind {
1372            ImportKind::Module { alias } => {
1373                let alias = alias.clone().unwrap_or_else(|| {
1374                    Spanned::new(
1375                        ModuleAliasName::from_atom(path.leaf().name.clone()),
1376                        path.leaf().span,
1377                    )
1378                });
1379                Ok(vec![ImportAddition::ModuleAlias {
1380                    alias,
1381                    target: target.clone(),
1382                    access,
1383                }])
1384            }
1385            ImportKind::Selective(items) => items
1386                .iter()
1387                .map(|item| self.import_item_additions(target, item, access))
1388                .collect::<Result<Vec<_>, _>>()
1389                .map(|chunks| chunks.into_iter().flatten().collect()),
1390        }
1391    }
1392
1393    fn check_import_exclusive_name_collisions(
1394        &self,
1395        owner: &DagId,
1396        additions: &[ImportAddition],
1397    ) -> Result<(), ModuleResolveError> {
1398        let local = self.module_symbols(owner)?;
1399        let scope = self.module_scope(owner)?;
1400        let mut occupied = HashMap::new();
1401
1402        seed_exclusive_names(&mut occupied, &local.decls, ExclusiveNameKind::Value);
1403        seed_exclusive_names(
1404            &mut occupied,
1405            &local.dimensions,
1406            ExclusiveNameKind::Dimension,
1407        );
1408        seed_exclusive_names(
1409            &mut occupied,
1410            &local.struct_types,
1411            ExclusiveNameKind::StructType,
1412        );
1413        seed_exclusive_names(&mut occupied, &local.indexes, ExclusiveNameKind::Index);
1414        seed_exclusive_names(
1415            &mut occupied,
1416            &scope.selected_decls,
1417            ExclusiveNameKind::Value,
1418        );
1419        seed_exclusive_names(
1420            &mut occupied,
1421            &scope.selected_dimensions,
1422            ExclusiveNameKind::Dimension,
1423        );
1424        seed_exclusive_names(
1425            &mut occupied,
1426            &scope.selected_struct_types,
1427            ExclusiveNameKind::StructType,
1428        );
1429        seed_exclusive_names(
1430            &mut occupied,
1431            &scope.selected_indexes,
1432            ExclusiveNameKind::Index,
1433        );
1434
1435        check_import_addition_exclusive_names(owner, &mut occupied, additions)
1436    }
1437
1438    #[expect(
1439        clippy::too_many_lines,
1440        reason = "import namespace expansion is kept together"
1441    )]
1442    fn import_item_additions(
1443        &self,
1444        target: &DagId,
1445        item: &ImportItem,
1446        access: ModuleAccess,
1447    ) -> Result<Vec<ImportAddition>, ModuleResolveError> {
1448        let source_atom = &item.name.name;
1449        let local_atom = item
1450            .alias
1451            .as_ref()
1452            .map_or_else(|| item.name.name.clone(), |alias| alias.name.clone());
1453        let local_span = item.local_span();
1454        let local_visibility = if item.is_pub {
1455            SymbolVisibility::Public
1456        } else {
1457            SymbolVisibility::Private
1458        };
1459
1460        match item.namespace {
1461            ImportItemNamespace::Type => {
1462                match self.exported_symbol_for_import(
1463                    target,
1464                    source_atom,
1465                    access,
1466                    ModuleSymbols::struct_types,
1467                    |scope| &scope.selected_struct_types,
1468                )? {
1469                    ExportLookup::Public(target_name) => Ok(vec![ImportAddition::StructType {
1470                        local: Spanned::new(StructTypeName::from_atom(local_atom), local_span),
1471                        target: target_name,
1472                        visibility: local_visibility,
1473                    }]),
1474                    ExportLookup::Private => Err(ModuleResolveError::PrivateName {
1475                        owner: target.clone(),
1476                        namespace: namespace::StructType::DISPLAY_NAME,
1477                        name: source_atom.to_string(),
1478                    }),
1479                    ExportLookup::Missing => {
1480                        if let Some(actual) =
1481                            self.exported_surface_kind_for_import(target, source_atom, access)?
1482                        {
1483                            return Err(ModuleResolveError::WrongUniverseName {
1484                                owner: target.clone(),
1485                                name: source_atom.to_string(),
1486                                expected: SurfaceNameKind::Type,
1487                                actual,
1488                            });
1489                        }
1490                        Err(ModuleResolveError::UnknownName {
1491                            owner: target.clone(),
1492                            namespace: namespace::StructType::DISPLAY_NAME,
1493                            name: source_atom.to_string(),
1494                        })
1495                    }
1496                }
1497            }
1498            ImportItemNamespace::Default => {
1499                let mut additions = Vec::new();
1500                let mut saw_private = false;
1501
1502                match self.exported_symbol_for_import(
1503                    target,
1504                    source_atom,
1505                    access,
1506                    ModuleSymbols::decls,
1507                    |scope| &scope.selected_decls,
1508                )? {
1509                    ExportLookup::Public(target_name) => additions.push(ImportAddition::Decl {
1510                        local: Spanned::new(DeclName::from_atom(local_atom.clone()), local_span),
1511                        target: target_name,
1512                        visibility: local_visibility,
1513                    }),
1514                    ExportLookup::Private => saw_private = true,
1515                    ExportLookup::Missing => {}
1516                }
1517                match self.exported_symbol_for_import(
1518                    target,
1519                    source_atom,
1520                    access,
1521                    ModuleSymbols::dimensions,
1522                    |scope| &scope.selected_dimensions,
1523                )? {
1524                    ExportLookup::Public(target_name) => {
1525                        additions.push(ImportAddition::Dimension {
1526                            local: Spanned::new(DimName::from_atom(local_atom.clone()), local_span),
1527                            target: target_name,
1528                            visibility: local_visibility,
1529                        });
1530                    }
1531                    ExportLookup::Private => saw_private = true,
1532                    ExportLookup::Missing => {}
1533                }
1534                match self.exported_symbol_for_import(
1535                    target,
1536                    source_atom,
1537                    access,
1538                    ModuleSymbols::units,
1539                    |scope| &scope.selected_units,
1540                )? {
1541                    ExportLookup::Public(target_name) => additions.push(ImportAddition::Unit {
1542                        local: Spanned::new(UnitName::from_atom(local_atom.clone()), local_span),
1543                        target: target_name,
1544                        visibility: local_visibility,
1545                    }),
1546                    ExportLookup::Private => saw_private = true,
1547                    ExportLookup::Missing => {}
1548                }
1549                match self.exported_symbol_for_import(
1550                    target,
1551                    source_atom,
1552                    access,
1553                    ModuleSymbols::indexes,
1554                    |scope| &scope.selected_indexes,
1555                )? {
1556                    ExportLookup::Public(target_name) => additions.push(ImportAddition::Index {
1557                        local: Spanned::new(IndexName::from_atom(local_atom.clone()), local_span),
1558                        target: target_name,
1559                        visibility: local_visibility,
1560                    }),
1561                    ExportLookup::Private => saw_private = true,
1562                    ExportLookup::Missing => {}
1563                }
1564                match self.exported_symbol_for_import(
1565                    target,
1566                    source_atom,
1567                    access,
1568                    ModuleSymbols::constructors,
1569                    |scope| &scope.selected_constructors,
1570                )? {
1571                    ExportLookup::Public(target_name) => {
1572                        additions.push(ImportAddition::Constructor {
1573                            local: Spanned::new(ConstructorName::from_atom(local_atom), local_span),
1574                            target: target_name,
1575                            visibility: local_visibility,
1576                        });
1577                    }
1578                    ExportLookup::Private => saw_private = true,
1579                    ExportLookup::Missing => {}
1580                }
1581
1582                if !additions.is_empty() {
1583                    Ok(additions)
1584                } else if saw_private {
1585                    Err(ModuleResolveError::PrivateName {
1586                        owner: target.clone(),
1587                        namespace: "default import namespace",
1588                        name: source_atom.to_string(),
1589                    })
1590                } else if let Some(actual) =
1591                    self.exported_surface_kind_for_import(target, source_atom, access)?
1592                {
1593                    Err(ModuleResolveError::WrongUniverseName {
1594                        owner: target.clone(),
1595                        name: source_atom.to_string(),
1596                        expected: SurfaceNameKind::DefaultImportItem,
1597                        actual,
1598                    })
1599                } else {
1600                    Err(ModuleResolveError::UnknownName {
1601                        owner: target.clone(),
1602                        namespace: "default import namespace",
1603                        name: source_atom.to_string(),
1604                    })
1605                }
1606            }
1607        }
1608    }
1609
1610    fn exported_symbol_for_import<Ns, S>(
1611        &self,
1612        target: &DagId,
1613        atom: &NameAtom,
1614        access: ModuleAccess,
1615        local_symbols: fn(&ModuleSymbols) -> &HashMap<NameDef<Ns>, S>,
1616        selected_symbols: fn(&ModuleScope) -> &HashMap<NameDef<Ns>, ImportedSymbol<Ns>>,
1617    ) -> Result<ExportLookup<Ns>, ModuleResolveError>
1618    where
1619        Ns: ResolvableNamespace,
1620        S: ModuleSymbolLookup<Ns>,
1621    {
1622        let target_symbols = self.module_symbols(target)?;
1623        match exported_symbol(local_symbols(target_symbols), atom, access) {
1624            ExportLookup::Missing => {}
1625            found => return Ok(found),
1626        }
1627
1628        let target_scope = self.module_scope(target)?;
1629        Ok(exported_symbol(
1630            selected_symbols(target_scope),
1631            atom,
1632            access,
1633        ))
1634    }
1635
1636    fn exported_surface_kind_for_import(
1637        &self,
1638        target: &DagId,
1639        atom: &NameAtom,
1640        access: ModuleAccess,
1641    ) -> Result<Option<SurfaceNameKind>, ModuleResolveError> {
1642        macro_rules! probe {
1643            ($kind:expr, $local:expr, $selected:expr) => {
1644                match self.exported_symbol_for_import(target, atom, access, $local, $selected)? {
1645                    ExportLookup::Public(_) | ExportLookup::Private => return Ok(Some($kind)),
1646                    ExportLookup::Missing => {}
1647                }
1648            };
1649        }
1650
1651        probe!(SurfaceNameKind::Value, ModuleSymbols::decls, |scope| &scope
1652            .selected_decls);
1653        probe!(
1654            SurfaceNameKind::Dimension,
1655            ModuleSymbols::dimensions,
1656            |scope| &scope.selected_dimensions
1657        );
1658        probe!(SurfaceNameKind::Unit, ModuleSymbols::units, |scope| &scope
1659            .selected_units);
1660        probe!(
1661            SurfaceNameKind::Type,
1662            ModuleSymbols::struct_types,
1663            |scope| &scope.selected_struct_types
1664        );
1665        probe!(SurfaceNameKind::Index, ModuleSymbols::indexes, |scope| {
1666            &scope.selected_indexes
1667        });
1668        probe!(
1669            SurfaceNameKind::Constructor,
1670            ModuleSymbols::constructors,
1671            |scope| &scope.selected_constructors
1672        );
1673
1674        Ok(None)
1675    }
1676
1677    fn resolve_symbol_path<Ns, S>(
1678        &self,
1679        owner: &DagId,
1680        path: &NamePath,
1681        local_symbols: fn(&ModuleSymbols) -> &HashMap<NameDef<Ns>, S>,
1682        selected_symbols: fn(&ModuleScope) -> &HashMap<NameDef<Ns>, ImportedSymbol<Ns>>,
1683    ) -> Result<ResolvedName<Ns>, ModuleResolveError>
1684    where
1685        Ns: ResolvableNamespace,
1686        S: ModuleSymbolLookup<Ns>,
1687    {
1688        if let Some(atom) = path.as_bare() {
1689            let local = self.module_symbols(owner)?;
1690            if let Some(symbol) = local_symbols(local).get(atom.as_str()) {
1691                return Ok(symbol.resolved().clone());
1692            }
1693            let scope = self.module_scope(owner)?;
1694            if let Some(imported) = selected_symbols(scope).get(atom.as_str()) {
1695                return Ok(imported.resolved().clone());
1696            }
1697            if let Some(actual) = self.visible_surface_kind_for_bare_name(owner, atom)? {
1698                return Err(ModuleResolveError::WrongUniverseName {
1699                    owner: owner.clone(),
1700                    name: atom.to_string(),
1701                    expected: Ns::SURFACE_KIND,
1702                    actual,
1703                });
1704            }
1705            return Err(ModuleResolveError::UnknownName {
1706                owner: owner.clone(),
1707                namespace: Ns::DISPLAY_NAME,
1708                name: atom.to_string(),
1709            });
1710        }
1711
1712        let (qualifier, leaf) = path.split_last();
1713        let target_ref = match self.resolve_module_qualifier(owner, qualifier) {
1714            Ok(target_ref) => target_ref,
1715            Err(
1716                err @ (ModuleResolveError::UnknownModuleAlias { .. }
1717                | ModuleResolveError::UnknownModule { .. }),
1718            ) => {
1719                if let Some(actual) = self.visible_index_variant_path_kind(owner, path)? {
1720                    return Err(ModuleResolveError::WrongUniverseName {
1721                        owner: owner.clone(),
1722                        name: path.display_path(),
1723                        expected: Ns::SURFACE_KIND,
1724                        actual,
1725                    });
1726                }
1727                return Err(err);
1728            }
1729            Err(err) => return Err(err),
1730        };
1731        let target = self.module_symbols(&target_ref.owner)?;
1732        if let Some(symbol) = local_symbols(target).get(leaf.as_str()) {
1733            if target_ref.access.requires_public() && !symbol.visibility().is_public() {
1734                return Err(ModuleResolveError::PrivateName {
1735                    owner: target_ref.owner,
1736                    namespace: Ns::DISPLAY_NAME,
1737                    name: leaf.to_string(),
1738                });
1739            }
1740            return Ok(symbol.resolved().clone());
1741        }
1742
1743        let target_scope = self.module_scope(&target_ref.owner)?;
1744        if let Some(imported) = selected_symbols(target_scope).get(leaf.as_str()) {
1745            if target_ref.access.requires_public() && !imported.visibility().is_public() {
1746                return Err(ModuleResolveError::PrivateName {
1747                    owner: target_ref.owner,
1748                    namespace: Ns::DISPLAY_NAME,
1749                    name: leaf.to_string(),
1750                });
1751            }
1752            return Ok(imported.resolved().clone());
1753        }
1754
1755        if let Some(actual) =
1756            self.visible_surface_kind_for_qualified_leaf(&target_ref, leaf, path)?
1757        {
1758            return Err(ModuleResolveError::WrongUniverseName {
1759                owner: target_ref.owner,
1760                name: path.display_path(),
1761                expected: Ns::SURFACE_KIND,
1762                actual,
1763            });
1764        }
1765
1766        Err(ModuleResolveError::UnknownName {
1767            owner: target_ref.owner,
1768            namespace: Ns::DISPLAY_NAME,
1769            name: leaf.to_string(),
1770        })
1771    }
1772
1773    fn visible_surface_kind_for_bare_name(
1774        &self,
1775        owner: &DagId,
1776        atom: &NameAtom,
1777    ) -> Result<Option<SurfaceNameKind>, ModuleResolveError> {
1778        let local = self.module_symbols(owner)?;
1779        if let Some(kind) = surface_kind_in_local_symbols(local, atom, false) {
1780            return Ok(Some(kind));
1781        }
1782        let scope = self.module_scope(owner)?;
1783        Ok(surface_kind_in_scope(scope, atom, false))
1784    }
1785
1786    fn visible_surface_kind_for_qualified_leaf(
1787        &self,
1788        target_ref: &ResolvedModuleQualifier,
1789        leaf: &NameAtom,
1790        path: &NamePath,
1791    ) -> Result<Option<SurfaceNameKind>, ModuleResolveError> {
1792        if let Some(kind) = self.visible_index_variant_path_kind(&target_ref.owner, path)? {
1793            return Ok(Some(kind));
1794        }
1795
1796        let target = self.module_symbols(&target_ref.owner)?;
1797        if let Some(kind) =
1798            surface_kind_in_local_symbols(target, leaf, target_ref.access.requires_public())
1799        {
1800            return Ok(Some(kind));
1801        }
1802        let target_scope = self.module_scope(&target_ref.owner)?;
1803        Ok(surface_kind_in_scope(
1804            target_scope,
1805            leaf,
1806            target_ref.access.requires_public(),
1807        ))
1808    }
1809
1810    fn visible_index_variant_path_kind(
1811        &self,
1812        owner: &DagId,
1813        path: &NamePath,
1814    ) -> Result<Option<SurfaceNameKind>, ModuleResolveError> {
1815        match self.resolve_index_variant_path(owner, path) {
1816            Ok(_) => Ok(Some(SurfaceNameKind::IndexLabel)),
1817            Err(
1818                ModuleResolveError::UnknownName { .. }
1819                | ModuleResolveError::UnknownIndexVariant { .. }
1820                | ModuleResolveError::ExpectedIndexVariantPath { .. }
1821                | ModuleResolveError::UnknownModuleAlias { .. }
1822                | ModuleResolveError::UnknownModule { .. },
1823            ) => Ok(None),
1824            Err(err) => Err(err),
1825        }
1826    }
1827
1828    fn resolve_module_qualifier(
1829        &self,
1830        owner: &DagId,
1831        qualifier: &[NameAtom],
1832    ) -> Result<ResolvedModuleQualifier, ModuleResolveError> {
1833        let Some((head, rest)) = qualifier.split_first() else {
1834            return Err(ModuleResolveError::UnknownName {
1835                owner: owner.clone(),
1836                namespace: "module",
1837                name: String::new(),
1838            });
1839        };
1840        let scope = self.module_scope(owner)?;
1841        let alias = ModuleAliasName::from_atom(head.clone());
1842        let alias_target = scope.module_aliases.get(alias.as_str()).ok_or_else(|| {
1843            ModuleResolveError::UnknownModuleAlias {
1844                owner: owner.clone(),
1845                alias,
1846            }
1847        })?;
1848        // Descend segment by segment, enforcing dag visibility at every
1849        // step: `lib.helper.symbol` must be rejected when `helper` is a
1850        // private dag, exactly like `resolve_module_path` rejects
1851        // `lib.helper` — previously only the symbol's own visibility was
1852        // checked, never the modules on the path.
1853        let mut target = alias_target.target.clone();
1854        for segment in rest {
1855            target = target.child(segment.as_str());
1856            if !self.modules.contains_key(&target) {
1857                return Err(ModuleResolveError::UnknownModule { owner: target });
1858            }
1859            self.ensure_module_visible(&target, alias_target.access)?;
1860        }
1861        if self.modules.contains_key(&target) {
1862            Ok(ResolvedModuleQualifier {
1863                owner: target,
1864                access: alias_target.access,
1865            })
1866        } else {
1867            Err(ModuleResolveError::UnknownModule { owner: target })
1868        }
1869    }
1870
1871    fn module_symbols(&self, owner: &DagId) -> Result<&ModuleSymbols, ModuleResolveError> {
1872        self.modules
1873            .get(owner)
1874            .ok_or_else(|| ModuleResolveError::UnknownModule {
1875                owner: owner.clone(),
1876            })
1877    }
1878
1879    /// Import scope registered for a module, if any.
1880    ///
1881    /// IDE consumers use this to map canonical owners back to the module
1882    /// aliases a file spelled in its imports.
1883    #[must_use]
1884    pub fn scope(&self, owner: &DagId) -> Option<&ModuleScope> {
1885        self.scopes.get(owner)
1886    }
1887
1888    fn module_scope(&self, owner: &DagId) -> Result<&ModuleScope, ModuleResolveError> {
1889        self.scopes
1890            .get(owner)
1891            .ok_or_else(|| ModuleResolveError::UnknownModule {
1892                owner: owner.clone(),
1893            })
1894    }
1895
1896    fn ensure_module_visible(
1897        &self,
1898        target: &DagId,
1899        access: ModuleAccess,
1900    ) -> Result<(), ModuleResolveError> {
1901        if !access.requires_public() {
1902            return Ok(());
1903        }
1904        let Some(parent) = target.parent() else {
1905            return Ok(());
1906        };
1907        let Some(parent_symbols) = self.modules.get(&parent) else {
1908            return Ok(());
1909        };
1910        let Some(symbol) = parent_symbols.decls.get(target.name()) else {
1911            return Ok(());
1912        };
1913        if symbol.kind() == DeclSymbolKind::Dag && !symbol.visibility().is_public() {
1914            return Err(ModuleResolveError::PrivateName {
1915                owner: parent,
1916                namespace: "dag",
1917                name: target.name().to_string(),
1918            });
1919        }
1920        Ok(())
1921    }
1922}
1923
1924impl ModuleScope {
1925    fn apply_addition(
1926        &mut self,
1927        owner: &DagId,
1928        addition: ImportAddition,
1929    ) -> Result<(), ModuleResolveError> {
1930        match addition {
1931            ImportAddition::ModuleAlias {
1932                alias,
1933                target,
1934                access,
1935            } => insert_module_alias(
1936                owner,
1937                &mut self.module_aliases,
1938                alias,
1939                target,
1940                access,
1941                namespace::ModuleAlias::DISPLAY_NAME,
1942            ),
1943            ImportAddition::Decl {
1944                local,
1945                target,
1946                visibility,
1947            } => insert_imported_symbol(
1948                owner,
1949                &mut self.selected_decls,
1950                local,
1951                target,
1952                visibility,
1953                namespace::Decl::DISPLAY_NAME,
1954            ),
1955            ImportAddition::Dimension {
1956                local,
1957                target,
1958                visibility,
1959            } => insert_imported_symbol(
1960                owner,
1961                &mut self.selected_dimensions,
1962                local,
1963                target,
1964                visibility,
1965                namespace::Dim::DISPLAY_NAME,
1966            ),
1967            ImportAddition::Unit {
1968                local,
1969                target,
1970                visibility,
1971            } => insert_imported_symbol(
1972                owner,
1973                &mut self.selected_units,
1974                local,
1975                target,
1976                visibility,
1977                namespace::Unit::DISPLAY_NAME,
1978            ),
1979            ImportAddition::StructType {
1980                local,
1981                target,
1982                visibility,
1983            } => insert_imported_symbol(
1984                owner,
1985                &mut self.selected_struct_types,
1986                local,
1987                target,
1988                visibility,
1989                namespace::StructType::DISPLAY_NAME,
1990            ),
1991            ImportAddition::Index {
1992                local,
1993                target,
1994                visibility,
1995            } => insert_imported_symbol(
1996                owner,
1997                &mut self.selected_indexes,
1998                local,
1999                target,
2000                visibility,
2001                namespace::Index::DISPLAY_NAME,
2002            ),
2003            ImportAddition::Constructor {
2004                local,
2005                target,
2006                visibility,
2007            } => insert_imported_symbol(
2008                owner,
2009                &mut self.selected_constructors,
2010                local,
2011                target,
2012                visibility,
2013                namespace::Constructor::DISPLAY_NAME,
2014            ),
2015        }
2016    }
2017}
2018
2019fn insert_module_alias(
2020    owner: &DagId,
2021    map: &mut HashMap<ModuleAliasName, ModuleAliasTarget>,
2022    alias: Spanned<ModuleAliasName>,
2023    target: DagId,
2024    access: ModuleAccess,
2025    namespace_name: &'static str,
2026) -> Result<(), ModuleResolveError> {
2027    if let Some(first) = map.get(alias.value.as_str()) {
2028        return Err(ModuleResolveError::DuplicateImportName {
2029            owner: owner.clone(),
2030            namespace: namespace_name,
2031            name: alias.value.to_string(),
2032            first: first.span(),
2033            duplicate: alias.span,
2034        });
2035    }
2036    map.insert(
2037        alias.value,
2038        ModuleAliasTarget {
2039            target,
2040            span: alias.span,
2041            access,
2042        },
2043    );
2044    Ok(())
2045}
2046
2047fn surface_kind_in_local_symbols(
2048    symbols: &ModuleSymbols,
2049    atom: &NameAtom,
2050    requires_public: bool,
2051) -> Option<SurfaceNameKind> {
2052    macro_rules! probe {
2053        ($map:expr, $kind:expr) => {
2054            if let Some(symbol) = $map.get(atom.as_str())
2055                && (!requires_public || symbol.visibility().is_public())
2056            {
2057                return Some($kind);
2058            }
2059        };
2060    }
2061
2062    probe!(symbols.decls, SurfaceNameKind::Value);
2063    probe!(symbols.dimensions, SurfaceNameKind::Dimension);
2064    probe!(symbols.units, SurfaceNameKind::Unit);
2065    probe!(symbols.struct_types, SurfaceNameKind::Type);
2066    probe!(symbols.indexes, SurfaceNameKind::Index);
2067    probe!(symbols.constructors, SurfaceNameKind::Constructor);
2068    None
2069}
2070
2071fn surface_kind_in_scope(
2072    scope: &ModuleScope,
2073    atom: &NameAtom,
2074    requires_public: bool,
2075) -> Option<SurfaceNameKind> {
2076    macro_rules! probe {
2077        ($map:expr, $kind:expr) => {
2078            if let Some(symbol) = $map.get(atom.as_str())
2079                && (!requires_public || symbol.visibility().is_public())
2080            {
2081                return Some($kind);
2082            }
2083        };
2084    }
2085
2086    probe!(scope.selected_decls, SurfaceNameKind::Value);
2087    probe!(scope.selected_dimensions, SurfaceNameKind::Dimension);
2088    probe!(scope.selected_units, SurfaceNameKind::Unit);
2089    probe!(scope.selected_struct_types, SurfaceNameKind::Type);
2090    probe!(scope.selected_indexes, SurfaceNameKind::Index);
2091    probe!(scope.selected_constructors, SurfaceNameKind::Constructor);
2092    None
2093}
2094
2095fn seed_exclusive_names<Ns, S>(
2096    occupied: &mut HashMap<NameAtom, ExclusiveNameBinding>,
2097    symbols: &HashMap<NameDef<Ns>, S>,
2098    kind: ExclusiveNameKind,
2099) where
2100    Ns: NameNamespace,
2101    S: ModuleSymbolLookup<Ns>,
2102{
2103    for (name, symbol) in symbols {
2104        occupied.insert(
2105            name.atom().clone(),
2106            ExclusiveNameBinding {
2107                kind,
2108                span: symbol.span(),
2109            },
2110        );
2111    }
2112}
2113
2114fn check_import_addition_exclusive_names(
2115    owner: &DagId,
2116    occupied: &mut HashMap<NameAtom, ExclusiveNameBinding>,
2117    additions: &[ImportAddition],
2118) -> Result<(), ModuleResolveError> {
2119    for addition in additions {
2120        match addition {
2121            ImportAddition::Decl { local, .. } => register_import_exclusive_name(
2122                owner,
2123                occupied,
2124                local.value.atom(),
2125                ExclusiveNameKind::Value,
2126                local.span,
2127            )?,
2128            ImportAddition::Dimension { local, .. } => register_import_exclusive_name(
2129                owner,
2130                occupied,
2131                local.value.atom(),
2132                ExclusiveNameKind::Dimension,
2133                local.span,
2134            )?,
2135            ImportAddition::StructType { local, .. } => register_import_exclusive_name(
2136                owner,
2137                occupied,
2138                local.value.atom(),
2139                ExclusiveNameKind::StructType,
2140                local.span,
2141            )?,
2142            ImportAddition::Index { local, .. } => register_import_exclusive_name(
2143                owner,
2144                occupied,
2145                local.value.atom(),
2146                ExclusiveNameKind::Index,
2147                local.span,
2148            )?,
2149            ImportAddition::ModuleAlias { .. }
2150            | ImportAddition::Unit { .. }
2151            | ImportAddition::Constructor { .. } => {}
2152        }
2153    }
2154    Ok(())
2155}
2156
2157fn register_import_exclusive_name(
2158    owner: &DagId,
2159    occupied: &mut HashMap<NameAtom, ExclusiveNameBinding>,
2160    atom: &NameAtom,
2161    kind: ExclusiveNameKind,
2162    span: Span,
2163) -> Result<(), ModuleResolveError> {
2164    if let Some(first) = occupied.get(atom) {
2165        return Err(ModuleResolveError::DuplicateImportName {
2166            owner: owner.clone(),
2167            namespace: "name",
2168            name: atom.to_string(),
2169            first: first.span,
2170            duplicate: span,
2171        });
2172    }
2173    occupied.insert(atom.clone(), ExclusiveNameBinding { kind, span });
2174    Ok(())
2175}
2176
2177fn insert_imported_symbol<Ns: NameNamespace>(
2178    owner: &DagId,
2179    map: &mut HashMap<NameDef<Ns>, ImportedSymbol<Ns>>,
2180    local: Spanned<NameDef<Ns>>,
2181    target: ResolvedName<Ns>,
2182    visibility: SymbolVisibility,
2183    namespace_name: &'static str,
2184) -> Result<(), ModuleResolveError> {
2185    if let Some(first) = map.get(local.value.as_str()) {
2186        return Err(ModuleResolveError::DuplicateImportName {
2187            owner: owner.clone(),
2188            namespace: namespace_name,
2189            name: local.value.to_string(),
2190            first: first.span(),
2191            duplicate: local.span,
2192        });
2193    }
2194    map.insert(
2195        local.value,
2196        ImportedSymbol::new(target, local.span, visibility),
2197    );
2198    Ok(())
2199}
2200
2201#[derive(Debug, Clone, PartialEq, Eq)]
2202enum ExportLookup<Ns: NameNamespace> {
2203    Public(ResolvedName<Ns>),
2204    Private,
2205    Missing,
2206}
2207
2208fn exported_symbol<Ns, S>(
2209    map: &HashMap<NameDef<Ns>, S>,
2210    atom: &NameAtom,
2211    access: ModuleAccess,
2212) -> ExportLookup<Ns>
2213where
2214    Ns: NameNamespace,
2215    S: ModuleSymbolLookup<Ns>,
2216{
2217    map.get(atom.as_str())
2218        .map_or(ExportLookup::Missing, |symbol| {
2219            if !access.requires_public() || symbol.visibility().is_public() {
2220                ExportLookup::Public(symbol.resolved().clone())
2221            } else {
2222                ExportLookup::Private
2223            }
2224        })
2225}
2226
2227fn name_path_from_slice(segments: &[NameAtom]) -> Option<NamePath> {
2228    NonEmpty::try_from_vec(segments.to_vec())
2229        .ok()
2230        .map(NamePath::new)
2231}
2232
2233fn ident_path_to_name_path(path: &IdentPath) -> NamePath {
2234    let segments = path.segments();
2235    NamePath::new(NonEmpty::new(
2236        segments[0].name.clone(),
2237        segments[1..]
2238            .iter()
2239            .map(|ident| ident.name.clone())
2240            .collect(),
2241    ))
2242}
2243
2244/// Errors produced while building or using module-aware symbol tables.
2245#[derive(Debug, Clone, PartialEq, Eq, Error)]
2246pub enum ModuleResolveError {
2247    /// A module was added twice.
2248    #[error("duplicate module `{owner}`")]
2249    DuplicateModule { owner: DagId },
2250    /// No symbol table exists for a canonical module identity.
2251    #[error("unknown module `{owner}`")]
2252    UnknownModule { owner: DagId },
2253    /// A module qualifier's first segment is not an alias in the current module.
2254    #[error("module alias `{alias}` is not in scope of `{owner}`")]
2255    UnknownModuleAlias {
2256        owner: DagId,
2257        alias: ModuleAliasName,
2258    },
2259    /// Duplicate definition in one namespace.
2260    #[error("duplicate {namespace} `{name}` in module `{owner}`")]
2261    DuplicateSymbol {
2262        owner: DagId,
2263        namespace: &'static str,
2264        name: String,
2265        first: Span,
2266        duplicate: Span,
2267    },
2268    /// Duplicate local import/alias in one namespace.
2269    #[error("duplicate imported {namespace} `{name}` in module `{owner}`")]
2270    DuplicateImportName {
2271        owner: DagId,
2272        namespace: &'static str,
2273        name: String,
2274        first: Span,
2275        duplicate: Span,
2276    },
2277    /// A name was not found in the requested namespace.
2278    #[error("unknown {namespace} `{name}` in module `{owner}`")]
2279    UnknownName {
2280        owner: DagId,
2281        namespace: &'static str,
2282        name: String,
2283    },
2284    /// A name exists, but in a semantic universe that is not valid here.
2285    #[error("in module `{owner}`, `{name}` is {actual}, not {expected}")]
2286    WrongUniverseName {
2287        owner: DagId,
2288        name: String,
2289        expected: SurfaceNameKind,
2290        actual: SurfaceNameKind,
2291    },
2292    /// A name exists but has the wrong declaration kind for the use site.
2293    #[error("expected {expected} declaration `{name}`, found {actual}")]
2294    UnexpectedDeclKind {
2295        name: ResolvedName<namespace::Decl>,
2296        expected: &'static str,
2297        actual: DeclSymbolKind,
2298    },
2299    /// A name exists but is not public across module boundaries.
2300    #[error("private {namespace} `{name}` in module `{owner}`")]
2301    PrivateName {
2302        owner: DagId,
2303        namespace: &'static str,
2304        name: String,
2305    },
2306    /// A path did not have enough segments to denote `Index.Variant`.
2307    #[error("expected index-variant path in module `{owner}`, got `{path}`")]
2308    ExpectedIndexVariantPath { owner: DagId, path: String },
2309    /// The index exists, but the requested variant is absent.
2310    #[error("unknown variant `{variant}` for index `{index}`")]
2311    UnknownIndexVariant {
2312        index: ResolvedName<namespace::Index>,
2313        variant: IndexVariantName,
2314    },
2315    /// A bare variant exists on more than one visible index.
2316    #[error("ambiguous index label `{variant}` in module `{owner}`; qualify it with an index name")]
2317    AmbiguousIndexVariant {
2318        owner: DagId,
2319        variant: IndexVariantName,
2320        indexes: Vec<ResolvedName<namespace::Index>>,
2321    },
2322}
2323
2324#[cfg(test)]
2325mod tests {
2326    use super::*;
2327    use crate::syntax::ast::Ident;
2328    use crate::syntax::parser::Parser;
2329
2330    fn desugared_source(source: &str) -> ast::File {
2331        let raw = Parser::new(source).parse_file().unwrap();
2332        crate::syntax::desugar::desugar_multi_decls_in_file(raw)
2333    }
2334
2335    fn first_import(file: &ast::File) -> (&ModulePath, &ImportKind) {
2336        file.declarations
2337            .iter()
2338            .find_map(|decl| match &decl.kind {
2339                ast::DeclKind::Import(import) => Some((&import.path, &import.kind)),
2340                _ => None,
2341            })
2342            .expect("source should contain an import")
2343    }
2344
2345    fn imports(file: &ast::File) -> Vec<(&ModulePath, &ImportKind)> {
2346        file.declarations
2347            .iter()
2348            .filter_map(|decl| match &decl.kind {
2349                ast::DeclKind::Import(import) => Some((&import.path, &import.kind)),
2350                _ => None,
2351            })
2352            .collect()
2353    }
2354
2355    fn first_include(file: &ast::File) -> (&ModulePath, &ImportKind) {
2356        file.declarations
2357            .iter()
2358            .find_map(|decl| match &decl.kind {
2359                ast::DeclKind::Include(include) => Some((&include.path, &include.kind)),
2360                _ => None,
2361            })
2362            .expect("source should contain an include")
2363    }
2364
2365    fn atom(s: &str) -> NameAtom {
2366        NameAtom::parse(s).unwrap()
2367    }
2368
2369    fn path(segments: &[&str]) -> NamePath {
2370        let atoms = segments.iter().map(|s| atom(s)).collect::<Vec<_>>();
2371        NamePath::new(NonEmpty::try_from_vec(atoms).unwrap())
2372    }
2373
2374    fn module_path(segments: &[&str]) -> ModulePath {
2375        let idents = segments
2376            .iter()
2377            .map(|s| Ident {
2378                name: atom(s),
2379                span: Span::new(0, 0),
2380            })
2381            .collect::<Vec<_>>();
2382        ModulePath {
2383            segments: NonEmpty::try_from_vec(idents).unwrap(),
2384            span: Span::new(0, 0),
2385        }
2386    }
2387
2388    fn first_dag(file: &ast::File) -> &ast::DagDecl {
2389        file.declarations
2390            .iter()
2391            .find_map(|decl| match &decl.kind {
2392                ast::DeclKind::Dag(dag) => Some(dag),
2393                _ => None,
2394            })
2395            .expect("source should contain a dag")
2396    }
2397
2398    #[test]
2399    fn local_type_index_name_collision_is_rejected() {
2400        let owner = DagId::root("main");
2401        let file = desugared_source("type M { Mk(v: Dimensionless) }\npub index M = { A, B };");
2402
2403        let err = ModuleSymbols::from_declarations(owner.clone(), &file.declarations).unwrap_err();
2404
2405        assert!(matches!(
2406            err,
2407            ModuleResolveError::DuplicateSymbol {
2408                owner: err_owner,
2409                namespace: "name",
2410                name,
2411                ..
2412            } if err_owner == owner && name == "M"
2413        ));
2414    }
2415
2416    #[test]
2417    fn local_dimension_type_name_collision_is_rejected() {
2418        let owner = DagId::root("main");
2419        let file = desugared_source("dim M = Length;\ntype M { Mk(v: Dimensionless) }");
2420
2421        let err = ModuleSymbols::from_declarations(owner.clone(), &file.declarations).unwrap_err();
2422
2423        assert!(matches!(
2424            err,
2425            ModuleResolveError::DuplicateSymbol {
2426                owner: err_owner,
2427                namespace: "name",
2428                name,
2429                ..
2430            } if err_owner == owner && name == "M"
2431        ));
2432    }
2433
2434    #[test]
2435    fn same_named_type_and_constructor_remain_distinct() {
2436        let owner = DagId::root("main");
2437        let file = desugared_source("type T { T }");
2438
2439        let symbols = ModuleSymbols::from_declarations(owner, &file.declarations).unwrap();
2440
2441        assert!(symbols.struct_types().contains_key("T"));
2442        assert!(symbols.constructors().contains_key("T"));
2443    }
2444
2445    #[test]
2446    fn selective_import_cross_universe_name_collision_is_rejected() {
2447        let type_lib_id = DagId::root("type_lib");
2448        let index_lib_id = DagId::root("index_lib");
2449        let main_id = DagId::root("main");
2450        let type_lib = desugared_source("pub type M { Mk(v: Dimensionless) }");
2451        let index_lib = desugared_source("pub index M = { A, B };");
2452        let main = desugared_source(
2453            "import type_lib.{ type M };
2454             import index_lib.{ M };",
2455        );
2456        let imports = imports(&main);
2457
2458        let mut resolver = ModuleResolver::default();
2459        resolver
2460            .add_module(type_lib_id.clone(), &type_lib.declarations)
2461            .unwrap();
2462        resolver
2463            .add_module(index_lib_id.clone(), &index_lib.declarations)
2464            .unwrap();
2465        resolver
2466            .add_module(main_id.clone(), &main.declarations)
2467            .unwrap();
2468        resolver
2469            .register_import(&main_id, imports[0].0, imports[0].1, &type_lib_id)
2470            .unwrap();
2471        let err = resolver
2472            .register_import(&main_id, imports[1].0, imports[1].1, &index_lib_id)
2473            .unwrap_err();
2474
2475        assert!(matches!(
2476            err,
2477            ModuleResolveError::DuplicateImportName {
2478                owner,
2479                namespace: "name",
2480                name,
2481                ..
2482            } if owner == main_id && name == "M"
2483        ));
2484    }
2485
2486    #[test]
2487    fn resolves_qualified_index_variant_to_canonical_owner() {
2488        let lib_id = DagId::root("lib");
2489        let main_id = DagId::root("main");
2490        let lib = desugared_source("pub index Phase = { Burn, Coast };");
2491        let main = desugared_source("import lib as physics;");
2492        let (import_path, import_kind) = first_import(&main);
2493
2494        let mut resolver = ModuleResolver::default();
2495        resolver
2496            .add_module(lib_id.clone(), &lib.declarations)
2497            .unwrap();
2498        resolver
2499            .add_module(main_id.clone(), &main.declarations)
2500            .unwrap();
2501        resolver
2502            .register_import(&main_id, import_path, import_kind, &lib_id)
2503            .unwrap();
2504
2505        let resolved_name = resolver
2506            .resolve_index_variant_path(&main_id, &path(&["physics", "Phase", "Burn"]))
2507            .unwrap();
2508
2509        assert_eq!(resolved_name.index().owner(), &lib_id);
2510        assert_eq!(resolved_name.index().as_str(), "Phase");
2511        assert_eq!(resolved_name.variant().as_str(), "Burn");
2512    }
2513
2514    #[test]
2515    fn selective_type_alias_resolves_to_original_owner_and_leaf() {
2516        let lib_id = DagId::root("lib");
2517        let main_id = DagId::root("main");
2518        let lib = desugared_source("pub type Vec3 { Vec3 }");
2519        let main = desugared_source("import lib.{ type Vec3 as Vector };");
2520        let (import_path, import_kind) = first_import(&main);
2521
2522        let mut resolver = ModuleResolver::default();
2523        resolver
2524            .add_module(lib_id.clone(), &lib.declarations)
2525            .unwrap();
2526        resolver
2527            .add_module(main_id.clone(), &main.declarations)
2528            .unwrap();
2529        resolver
2530            .register_import(&main_id, import_path, import_kind, &lib_id)
2531            .unwrap();
2532
2533        let resolved_name = resolver
2534            .resolve_struct_type_path(&main_id, &path(&["Vector"]))
2535            .unwrap();
2536
2537        assert_eq!(resolved_name.owner(), &lib_id);
2538        assert_eq!(resolved_name.as_str(), "Vec3");
2539    }
2540
2541    #[test]
2542    fn type_import_in_child_dag_does_not_import_same_named_constructor() {
2543        let main_id = DagId::root("main");
2544        let child_id = main_id.child("build_transfer");
2545        let main = desugared_source(
2546            "pub type TransferResult { TransferResult }
2547             dag build_transfer {
2548                 import main.{ type TransferResult };
2549             }",
2550        );
2551        let dag = first_dag(&main);
2552        let import = dag
2553            .body
2554            .iter()
2555            .find_map(|decl| match &decl.kind {
2556                ast::DeclKind::Import(import) => Some((&import.path, &import.kind)),
2557                _ => None,
2558            })
2559            .expect("dag body should contain an import");
2560
2561        let mut resolver = ModuleResolver::default();
2562        resolver
2563            .add_module(main_id.clone(), &main.declarations)
2564            .unwrap();
2565        resolver.add_module(child_id.clone(), &dag.body).unwrap();
2566        resolver
2567            .register_import(&child_id, import.0, import.1, &main_id)
2568            .unwrap();
2569
2570        let resolved_type = resolver
2571            .resolve_struct_type_path(&child_id, &path(&["TransferResult"]))
2572            .unwrap();
2573        assert_eq!(resolved_type.owner(), &main_id);
2574        assert_eq!(resolved_type.as_str(), "TransferResult");
2575
2576        let err = resolver
2577            .resolve_constructor_path(&child_id, &path(&["TransferResult"]))
2578            .unwrap_err();
2579        assert!(matches!(
2580            err,
2581            ModuleResolveError::WrongUniverseName {
2582                owner,
2583                name,
2584                expected: SurfaceNameKind::Constructor,
2585                actual: SurfaceNameKind::Type,
2586            } if owner == child_id && name == "TransferResult"
2587        ));
2588    }
2589
2590    #[test]
2591    fn type_marker_importing_index_reports_wrong_universe() {
2592        let lib_id = DagId::root("lib");
2593        let main_id = DagId::root("main");
2594        let lib = desugared_source("pub index M = { A };");
2595        let main = desugared_source("import lib.{ type M };");
2596        let (import_path, import_kind) = first_import(&main);
2597
2598        let mut resolver = ModuleResolver::default();
2599        resolver
2600            .add_module(lib_id.clone(), &lib.declarations)
2601            .unwrap();
2602        resolver
2603            .add_module(main_id.clone(), &main.declarations)
2604            .unwrap();
2605
2606        let err = resolver
2607            .register_import(&main_id, import_path, import_kind, &lib_id)
2608            .unwrap_err();
2609
2610        assert!(matches!(
2611            err,
2612            ModuleResolveError::WrongUniverseName {
2613                owner,
2614                name,
2615                expected: SurfaceNameKind::Type,
2616                actual: SurfaceNameKind::Index,
2617            } if owner == lib_id && name == "M"
2618        ));
2619    }
2620
2621    #[test]
2622    fn default_importing_type_reports_wrong_universe() {
2623        let lib_id = DagId::root("lib");
2624        let main_id = DagId::root("main");
2625        let lib = desugared_source("pub type Foo { MkFoo }");
2626        let main = desugared_source("import lib.{ Foo };");
2627        let (import_path, import_kind) = first_import(&main);
2628
2629        let mut resolver = ModuleResolver::default();
2630        resolver
2631            .add_module(lib_id.clone(), &lib.declarations)
2632            .unwrap();
2633        resolver
2634            .add_module(main_id.clone(), &main.declarations)
2635            .unwrap();
2636
2637        let err = resolver
2638            .register_import(&main_id, import_path, import_kind, &lib_id)
2639            .unwrap_err();
2640
2641        assert!(matches!(
2642            err,
2643            ModuleResolveError::WrongUniverseName {
2644                owner,
2645                name,
2646                expected: SurfaceNameKind::DefaultImportItem,
2647                actual: SurfaceNameKind::Type,
2648            } if owner == lib_id && name == "Foo"
2649        ));
2650    }
2651
2652    #[test]
2653    fn qualified_private_type_is_rejected() {
2654        let lib_id = DagId::root("lib");
2655        let main_id = DagId::root("main");
2656        let lib = desugared_source("type Secret { Secret }");
2657        let main = desugared_source("import lib as hidden;");
2658        let (import_path, import_kind) = first_import(&main);
2659
2660        let mut resolver = ModuleResolver::default();
2661        resolver
2662            .add_module(lib_id.clone(), &lib.declarations)
2663            .unwrap();
2664        resolver
2665            .add_module(main_id.clone(), &main.declarations)
2666            .unwrap();
2667        resolver
2668            .register_import(&main_id, import_path, import_kind, &lib_id)
2669            .unwrap();
2670
2671        let err = resolver
2672            .resolve_struct_type_path(&main_id, &path(&["hidden", "Secret"]))
2673            .unwrap_err();
2674
2675        assert!(matches!(
2676            err,
2677            ModuleResolveError::PrivateName {
2678                owner,
2679                namespace: "StructTypeName",
2680                name,
2681            } if owner == lib_id && name == "Secret"
2682        ));
2683    }
2684
2685    #[test]
2686    fn include_selective_private_decl_is_rejected() {
2687        let lib_id = DagId::root("lib");
2688        let main_id = DagId::root("main");
2689        let lib = desugared_source("node hidden: Dimensionless = 1.0;");
2690        let main = desugared_source("include lib().{ hidden };");
2691        let (include_path, include_kind) = first_include(&main);
2692
2693        let mut resolver = ModuleResolver::default();
2694        resolver
2695            .add_module(lib_id.clone(), &lib.declarations)
2696            .unwrap();
2697        resolver
2698            .add_module(main_id.clone(), &main.declarations)
2699            .unwrap();
2700
2701        let err = resolver
2702            .register_include(&main_id, include_path, include_kind, &lib_id)
2703            .unwrap_err();
2704
2705        assert!(matches!(
2706            err,
2707            ModuleResolveError::PrivateName {
2708                owner,
2709                namespace: _,
2710                name,
2711            } if owner == lib_id && name == "hidden"
2712        ));
2713    }
2714
2715    #[test]
2716    fn qualified_private_dag_path_is_rejected() {
2717        let lib_id = DagId::root("lib");
2718        let helper_id = lib_id.child("helper");
2719        let main_id = DagId::root("main");
2720        let lib = desugared_source(
2721            "dag helper {
2722                pub node shown: Dimensionless = 1.0;
2723            }",
2724        );
2725        let main = desugared_source("import lib as lib;");
2726        let (import_path, import_kind) = first_import(&main);
2727
2728        let mut resolver = ModuleResolver::default();
2729        resolver
2730            .add_module(lib_id.clone(), &lib.declarations)
2731            .unwrap();
2732        resolver
2733            .add_module(helper_id, &first_dag(&lib).body)
2734            .unwrap();
2735        resolver
2736            .add_module(main_id.clone(), &main.declarations)
2737            .unwrap();
2738        resolver
2739            .register_import(&main_id, import_path, import_kind, &lib_id)
2740            .unwrap();
2741
2742        let err = resolver
2743            .resolve_module_path(&main_id, &module_path(&["lib", "helper"]))
2744            .unwrap_err();
2745
2746        assert!(matches!(
2747            err,
2748            ModuleResolveError::PrivateName {
2749                owner,
2750                namespace: "dag",
2751                name,
2752            } if owner == lib_id && name == "helper"
2753        ));
2754    }
2755
2756    #[test]
2757    fn qualified_symbol_path_through_private_dag_is_rejected() {
2758        // Regression: `resolve_symbol_path` resolved the qualifier without
2759        // the dag-visibility check that `resolve_module_path` enforces, so
2760        // `lib.helper.shown` resolved even though `helper` is a private dag.
2761        let lib_id = DagId::root("lib");
2762        let helper_id = lib_id.child("helper");
2763        let main_id = DagId::root("main");
2764        let lib = desugared_source(
2765            "dag helper {
2766                pub node shown: Dimensionless = 1.0;
2767            }",
2768        );
2769        let main = desugared_source("import lib as lib;");
2770        let (import_path, import_kind) = first_import(&main);
2771
2772        let mut resolver = ModuleResolver::default();
2773        resolver
2774            .add_module(lib_id.clone(), &lib.declarations)
2775            .unwrap();
2776        resolver
2777            .add_module(helper_id, &first_dag(&lib).body)
2778            .unwrap();
2779        resolver
2780            .add_module(main_id.clone(), &main.declarations)
2781            .unwrap();
2782        resolver
2783            .register_import(&main_id, import_path, import_kind, &lib_id)
2784            .unwrap();
2785
2786        let err = resolver
2787            .resolve_decl_path(&main_id, &path(&["lib", "helper", "shown"]))
2788            .unwrap_err();
2789
2790        assert!(
2791            matches!(
2792                err,
2793                ModuleResolveError::PrivateName {
2794                    ref owner,
2795                    namespace: "dag",
2796                    ref name,
2797                } if *owner == lib_id && name == "helper"
2798            ),
2799            "expected PrivateName for dag `helper`, got: {err:?}"
2800        );
2801    }
2802
2803    #[test]
2804    fn qualified_constructor_resolves_to_canonical_owner() {
2805        let lib_id = DagId::root("lib");
2806        let main_id = DagId::root("main");
2807        let lib = desugared_source("pub type BurnKind { Impulsive, Coast }");
2808        let main = desugared_source("import lib as mission;");
2809        let (import_path, import_kind) = first_import(&main);
2810
2811        let mut resolver = ModuleResolver::default();
2812        resolver
2813            .add_module(lib_id.clone(), &lib.declarations)
2814            .unwrap();
2815        resolver
2816            .add_module(main_id.clone(), &main.declarations)
2817            .unwrap();
2818        resolver
2819            .register_import(&main_id, import_path, import_kind, &lib_id)
2820            .unwrap();
2821
2822        let resolved_name = resolver
2823            .resolve_constructor_path(&main_id, &path(&["mission", "Impulsive"]))
2824            .unwrap();
2825
2826        assert_eq!(resolved_name.owner(), &lib_id);
2827        assert_eq!(resolved_name.as_str(), "Impulsive");
2828    }
2829
2830    #[test]
2831    fn selective_pub_reexport_resolves_to_original_owner() {
2832        let leaf_id = DagId::root("leaf");
2833        let middle_id = DagId::root("middle");
2834        let main_id = DagId::root("main");
2835        let leaf = desugared_source("pub dim Acceleration = Length / Time^2;");
2836        let middle = desugared_source("import leaf.{ pub Acceleration };");
2837        let main = desugared_source("import middle.{ Acceleration };");
2838        let (middle_import_path, middle_import_kind) = first_import(&middle);
2839        let (main_import_path, main_import_kind) = first_import(&main);
2840
2841        let mut resolver = ModuleResolver::default();
2842        resolver
2843            .add_module(leaf_id.clone(), &leaf.declarations)
2844            .unwrap();
2845        resolver
2846            .add_module(middle_id.clone(), &middle.declarations)
2847            .unwrap();
2848        resolver
2849            .add_module(main_id.clone(), &main.declarations)
2850            .unwrap();
2851        resolver
2852            .register_import(&middle_id, middle_import_path, middle_import_kind, &leaf_id)
2853            .unwrap();
2854        resolver
2855            .register_import(&main_id, main_import_path, main_import_kind, &middle_id)
2856            .unwrap();
2857
2858        let resolved_name = resolver
2859            .resolve_dimension_path(&main_id, &path(&["Acceleration"]))
2860            .unwrap();
2861
2862        assert_eq!(resolved_name.owner(), &leaf_id);
2863        assert_eq!(resolved_name.as_str(), "Acceleration");
2864    }
2865}