Skip to main content

bock_air/
resolve.rs

1//! Name resolution pass — S-AIR layer.
2//!
3//! Resolves all identifier references in an AST [`Module`] to their
4//! definitions, populating the provided [`SymbolTable`].  Diagnostics are
5//! emitted for undefined names and unused imports.
6//!
7//! # Two-pass algorithm
8//! 1. **Collect** — all top-level declarations and import bindings are entered
9//!    into the module scope.
10//! 2. **Resolve** — the AST is walked; every [`Expr::Identifier`] is looked up
11//!    in the scope stack and recorded in [`SymbolTable::resolutions`].
12//!
13//! Inner scopes shadow outer ones (standard lexical scoping).
14
15use std::collections::{HashMap, HashSet};
16
17use bock_ast::{
18    Block, EnumVariant, Expr, FnDecl, ForLoop, GuardStmt, HandlingBlock, ImplBlock, ImportItems,
19    InterpolationPart, Item, LetStmt, LoopStmt, MatchArm, Module, ModulePath, NodeId, Param,
20    Pattern, Stmt, Visibility, WhileLoop,
21};
22use bock_errors::{DiagnosticBag, DiagnosticCode, Span};
23
24use crate::registry::{ExportDetail, ExportKind, ExportedSymbol, ModuleRegistry, RegistryError};
25
26// ─── Diagnostic codes ─────────────────────────────────────────────────────────
27
28const E_UNDEFINED: DiagnosticCode = DiagnosticCode {
29    prefix: 'E',
30    number: 1001,
31};
32const E_MODULE_NOT_FOUND: DiagnosticCode = DiagnosticCode {
33    prefix: 'E',
34    number: 1005,
35};
36const E_SYMBOL_NOT_FOUND: DiagnosticCode = DiagnosticCode {
37    prefix: 'E',
38    number: 1006,
39};
40const E_NOT_VISIBLE: DiagnosticCode = DiagnosticCode {
41    prefix: 'E',
42    number: 1007,
43};
44const W_UNUSED_IMPORT: DiagnosticCode = DiagnosticCode {
45    prefix: 'W',
46    number: 1001,
47};
48
49// ─── Public types ─────────────────────────────────────────────────────────────
50
51/// Classification of what a resolved name refers to.
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum NameKind {
54    Variable,
55    Function,
56    Type,
57    Trait,
58    Effect,
59    Module,
60    /// A prelude/builtin name (function, type, trait, or constructor)
61    /// that is always in scope without an explicit import.
62    Builtin,
63    /// The actual kind is not yet known (e.g. a named import before
64    /// the imported module has been analyzed).
65    Unresolved,
66}
67
68/// A fully-resolved name reference: the definition site and its kind.
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct ResolvedName {
71    /// NodeId of the declaration this name refers to.
72    pub def_id: NodeId,
73    /// What kind of entity this name refers to.
74    pub kind: NameKind,
75}
76
77/// A name binding within a [`Scope`].
78#[derive(Debug, Clone)]
79pub struct Binding {
80    /// The name as written in source.
81    pub name: String,
82    /// The resolved entity.
83    pub resolved: ResolvedName,
84    /// Declared visibility of the definition.
85    pub visibility: Visibility,
86    /// Source span of the definition site.
87    pub span: Span,
88    /// Whether this binding has been referenced at least once.
89    pub used: bool,
90    /// `true` for bindings introduced by `use` import declarations.
91    pub is_import: bool,
92}
93
94/// Information about an effect declaration's operations, stored during
95/// the collection phase so that `with` clauses can inject operations
96/// into function scopes.
97#[derive(Debug, Clone, Default)]
98pub struct EffectInfo {
99    /// Direct operations declared in this effect: `(name, def_id, span)`.
100    pub operations: Vec<(String, NodeId, Span)>,
101    /// Component effect names (for composite effects like `effect IO = Log + Clock`).
102    pub components: Vec<String>,
103}
104
105/// A single lexical scope (one entry on the scope stack).
106#[derive(Debug, Default)]
107pub struct Scope {
108    /// Bindings defined in this scope, keyed by name.
109    pub bindings: HashMap<String, Binding>,
110}
111
112impl Scope {
113    fn new() -> Self {
114        Self::default()
115    }
116}
117
118/// A hierarchical symbol table: module scope → nested scopes → bindings.
119///
120/// The scope stack grows when blocks are entered and shrinks when they exit.
121/// Lookups walk from the innermost scope outward; the first match wins
122/// (lexical shadowing).
123pub struct SymbolTable {
124    /// Live scope stack.  Index 0 is the module (outermost) scope.
125    scopes: Vec<Scope>,
126    /// Resolution map: usage-site NodeId → resolved binding.
127    pub resolutions: HashMap<NodeId, ResolvedName>,
128    /// Effect declarations: effect name → operations and components.
129    /// Populated during the collection phase, read during resolution to
130    /// inject effect operations into `with`-annotated function scopes.
131    pub effect_info: HashMap<String, EffectInfo>,
132    /// Maps enum variant names to their parent enum import name.
133    /// Used to propagate "used" status from variants to the enum import.
134    variant_parent: HashMap<String, String>,
135}
136
137impl Default for SymbolTable {
138    fn default() -> Self {
139        Self::new()
140    }
141}
142
143impl SymbolTable {
144    /// Creates a new symbol table with one empty module scope.
145    #[must_use]
146    pub fn new() -> Self {
147        Self {
148            scopes: vec![Scope::new()],
149            resolutions: HashMap::new(),
150            effect_info: HashMap::new(),
151            variant_parent: HashMap::new(),
152        }
153    }
154
155    /// Seed the root scope with prelude/builtin names that are always
156    /// available without an explicit import.
157    ///
158    /// Uses a synthetic `NodeId` range starting at `PRELUDE_BASE_ID` to
159    /// avoid collisions with parser-assigned IDs.
160    fn seed_prelude(&mut self) {
161        const PRELUDE_BASE_ID: NodeId = u32::MAX / 4;
162
163        use crate::prelude_vocab::{
164            PRELUDE_CONSTRUCTORS, PRELUDE_FUNCTIONS as PRELUDE_FNS, PRELUDE_TRAITS, PRELUDE_TYPES,
165        };
166
167        let mut id = PRELUDE_BASE_ID;
168
169        let dummy_span = Span {
170            file: bock_errors::FileId(0),
171            start: 0,
172            end: 0,
173        };
174
175        for &name in PRELUDE_FNS {
176            self.define(
177                name.to_string(),
178                Binding {
179                    name: name.to_string(),
180                    resolved: ResolvedName {
181                        def_id: id,
182                        kind: NameKind::Builtin,
183                    },
184                    visibility: Visibility::Public,
185                    span: dummy_span,
186                    used: true, // never warn about unused builtins
187                    is_import: false,
188                },
189            );
190            id += 1;
191        }
192
193        for &name in PRELUDE_TYPES {
194            self.define(
195                name.to_string(),
196                Binding {
197                    name: name.to_string(),
198                    resolved: ResolvedName {
199                        def_id: id,
200                        kind: NameKind::Builtin,
201                    },
202                    visibility: Visibility::Public,
203                    span: dummy_span,
204                    used: true,
205                    is_import: false,
206                },
207            );
208            id += 1;
209        }
210
211        for &name in PRELUDE_CONSTRUCTORS {
212            self.define(
213                name.to_string(),
214                Binding {
215                    name: name.to_string(),
216                    resolved: ResolvedName {
217                        def_id: id,
218                        kind: NameKind::Builtin,
219                    },
220                    visibility: Visibility::Public,
221                    span: dummy_span,
222                    used: true,
223                    is_import: false,
224                },
225            );
226            id += 1;
227        }
228
229        for &name in PRELUDE_TRAITS {
230            self.define(
231                name.to_string(),
232                Binding {
233                    name: name.to_string(),
234                    resolved: ResolvedName {
235                        def_id: id,
236                        kind: NameKind::Builtin,
237                    },
238                    visibility: Visibility::Public,
239                    span: dummy_span,
240                    used: true,
241                    is_import: false,
242                },
243            );
244            id += 1;
245        }
246    }
247
248    /// Defines `name` in the current (innermost) scope.
249    pub fn define(&mut self, name: String, binding: Binding) {
250        if let Some(scope) = self.scopes.last_mut() {
251            scope.bindings.insert(name, binding);
252        }
253    }
254
255    /// Pushes a new empty scope onto the stack.
256    pub fn push_scope(&mut self) {
257        self.scopes.push(Scope::new());
258    }
259
260    /// Pops and returns the innermost scope.  Returns `None` if only the
261    /// module scope remains (it is never popped).
262    pub fn pop_scope(&mut self) -> Option<Scope> {
263        if self.scopes.len() > 1 {
264            self.scopes.pop()
265        } else {
266            None
267        }
268    }
269
270    /// Looks up `name`, walking from innermost to outermost scope.
271    ///
272    /// Marks the binding as used and returns a clone of the resolved name.
273    pub fn lookup(&mut self, name: &str) -> Option<ResolvedName> {
274        for scope in self.scopes.iter_mut().rev() {
275            if let Some(binding) = scope.bindings.get_mut(name) {
276                binding.used = true;
277                return Some(binding.resolved.clone());
278            }
279        }
280        None
281    }
282
283    /// Marks the binding for `name` as used without returning a resolution.
284    /// Used for type names that appear in constructor or annotation position.
285    pub fn mark_used(&mut self, name: &str) {
286        for scope in self.scopes.iter_mut().rev() {
287            if let Some(binding) = scope.bindings.get_mut(name) {
288                binding.used = true;
289                return;
290            }
291        }
292    }
293
294    /// Immutable lookup — does **not** mark the binding as used.
295    #[must_use]
296    pub fn lookup_peek(&self, name: &str) -> Option<&Binding> {
297        for scope in self.scopes.iter().rev() {
298            if let Some(b) = scope.bindings.get(name) {
299                return Some(b);
300            }
301        }
302        None
303    }
304
305    /// Records that the node at `use_id` resolves to `resolved`.
306    pub fn record_resolution(&mut self, use_id: NodeId, resolved: ResolvedName) {
307        self.resolutions.insert(use_id, resolved);
308    }
309
310    /// Collects every name visible in the current scope stack.
311    ///
312    /// Used for "did you mean X?" suggestions when a lookup fails. Names
313    /// from inner scopes shadow outer scopes, but for suggestion purposes
314    /// we include all reachable binders — duplicates are de-duplicated.
315    #[must_use]
316    pub fn visible_names(&self) -> Vec<String> {
317        let mut seen: HashSet<String> = HashSet::new();
318        let mut out = Vec::new();
319        for scope in self.scopes.iter().rev() {
320            for name in scope.bindings.keys() {
321                if seen.insert(name.clone()) {
322                    out.push(name.clone());
323                }
324            }
325        }
326        out
327    }
328
329    /// Returns `true` if any wildcard (`*`) import exists in the module scope.
330    #[must_use]
331    pub fn has_wildcard_import(&self) -> bool {
332        self.scopes
333            .first()
334            .map(|s| {
335                s.bindings
336                    .values()
337                    .any(|b| b.is_import && b.name.ends_with(".*"))
338            })
339            .unwrap_or(false)
340    }
341
342    /// Returns all import bindings from the module scope that were never used.
343    #[must_use]
344    pub fn unused_imports(&self) -> Vec<&Binding> {
345        self.scopes
346            .first()
347            .map(|s| {
348                s.bindings
349                    .values()
350                    .filter(|b| b.is_import && !b.used)
351                    .collect()
352            })
353            .unwrap_or_default()
354    }
355}
356
357// ─── Resolver (private) ───────────────────────────────────────────────────────
358
359struct Resolver<'a> {
360    symbols: &'a mut SymbolTable,
361    diag: &'a mut DiagnosticBag,
362    /// Counter for synthetic NodeIds (e.g. shorthand record pattern bindings).
363    /// Starts high to avoid collision with parser-assigned IDs.
364    synthetic_id: NodeId,
365    /// Optional cross-file module registry for resolving imports.
366    /// When `Some`, named and glob imports are resolved from registered modules.
367    registry: Option<&'a ModuleRegistry>,
368}
369
370impl<'a> Resolver<'a> {
371    fn new(symbols: &'a mut SymbolTable, diag: &'a mut DiagnosticBag) -> Self {
372        Self {
373            symbols,
374            diag,
375            synthetic_id: u32::MAX / 2,
376            registry: None,
377        }
378    }
379
380    /// Returns the next unique synthetic [`NodeId`].
381    fn next_synthetic_id(&mut self) -> NodeId {
382        let id = self.synthetic_id;
383        self.synthetic_id += 1;
384        id
385    }
386
387    // ── Module entry point ────────────────────────────────────────────────────
388
389    fn resolve_module(&mut self, module: &Module) {
390        // Pass 0: seed prelude/builtin names into the root scope.
391        // These are shadowed by imports and local declarations.
392        self.symbols.seed_prelude();
393        // Pass 1a: imports first — local declarations can shadow them.
394        self.collect_imports(module);
395        // Pass 1b: collect all top-level declarations (shadows imports if same name).
396        self.collect_items(&module.items);
397        // Pass 2: resolve all identifier references.
398        for item in &module.items {
399            self.resolve_item(item);
400        }
401        // Pass 3: warn about unused imports.
402        self.check_unused_imports();
403    }
404
405    // ── Collection passes ─────────────────────────────────────────────────────
406
407    fn collect_imports(&mut self, module: &Module) {
408        for import in &module.imports {
409            let module_id = module_path_str(&import.path);
410            match &import.items {
411                ImportItems::Module => {
412                    // `use foo.bar` — bind the last segment as a module alias.
413                    let name = import
414                        .path
415                        .segments
416                        .last()
417                        .map(|s| s.name.clone())
418                        .unwrap_or_default();
419                    self.symbols.define(
420                        name.clone(),
421                        Binding {
422                            name,
423                            resolved: ResolvedName {
424                                def_id: import.id,
425                                kind: NameKind::Module,
426                            },
427                            visibility: Visibility::Private,
428                            span: import.span,
429                            used: false,
430                            is_import: true,
431                        },
432                    );
433                }
434                ImportItems::Named(names) => {
435                    for imported in names {
436                        let local = imported.alias.as_ref().unwrap_or(&imported.name);
437                        // Try to resolve from the registry if available.
438                        let kind = if let Some(registry) = self.registry {
439                            match registry.resolve_symbol(&module_id, &imported.name.name) {
440                                Ok(sym) => {
441                                    // If this is an effect, seed effect_info so `with`
442                                    // clauses can inject its operations into scope.
443                                    if sym.kind == ExportKind::Effect {
444                                        seed_effect_info_from_registry(
445                                            self.symbols,
446                                            &local.name,
447                                            sym,
448                                            import.id,
449                                            import.span,
450                                        );
451                                    }
452                                    // If this is an enum, also seed its variant
453                                    // constructors so bare names like `Red` work.
454                                    if sym.kind == ExportKind::Enum {
455                                        seed_enum_variants_from_registry(
456                                            self.symbols,
457                                            &local.name,
458                                            sym,
459                                            import.id,
460                                            import.span,
461                                        );
462                                    }
463                                    export_kind_to_name_kind(sym.kind)
464                                }
465                                Err(RegistryError::ModuleNotFound { .. }) => {
466                                    self.diag.error(
467                                        E_MODULE_NOT_FOUND,
468                                        format!("module `{module_id}` not found"),
469                                        import.span,
470                                    );
471                                    NameKind::Unresolved
472                                }
473                                Err(RegistryError::SymbolNotFound { name, .. }) => {
474                                    self.diag.error(
475                                        E_SYMBOL_NOT_FOUND,
476                                        format!(
477                                            "`{name}` is not exported by module `{module_id}`"
478                                        ),
479                                        imported.span,
480                                    );
481                                    NameKind::Unresolved
482                                }
483                                Err(RegistryError::NotVisible { name, .. }) => {
484                                    self.diag.error(
485                                        E_NOT_VISIBLE,
486                                        format!(
487                                            "`{name}` in module `{module_id}` is private"
488                                        ),
489                                        imported.span,
490                                    );
491                                    NameKind::Unresolved
492                                }
493                            }
494                        } else {
495                            // No registry — single-file mode; kind is unknown.
496                            NameKind::Unresolved
497                        };
498                        self.symbols.define(
499                            local.name.clone(),
500                            Binding {
501                                name: local.name.clone(),
502                                resolved: ResolvedName {
503                                    def_id: import.id,
504                                    kind,
505                                },
506                                visibility: Visibility::Private,
507                                span: imported.span,
508                                used: false,
509                                is_import: true,
510                            },
511                        );
512                    }
513                }
514                ImportItems::Glob => {
515                    // If the registry knows this module, enumerate its exports
516                    // and define each one individually.
517                    if let Some(registry) = self.registry {
518                        match registry.resolve_glob(&module_id) {
519                            Ok(exports) => {
520                                for (name, sym) in exports {
521                                    // Seed effect_info for imported effects.
522                                    if sym.kind == ExportKind::Effect {
523                                        seed_effect_info_from_registry(
524                                            self.symbols,
525                                            name,
526                                            sym,
527                                            import.id,
528                                            import.span,
529                                        );
530                                    }
531                                    // Seed enum variant constructors for
532                                    // glob-imported enums.
533                                    if sym.kind == ExportKind::Enum {
534                                        seed_enum_variants_from_registry(
535                                            self.symbols,
536                                            name,
537                                            sym,
538                                            import.id,
539                                            import.span,
540                                        );
541                                    }
542                                    self.symbols.define(
543                                        name.to_string(),
544                                        Binding {
545                                            name: name.to_string(),
546                                            resolved: ResolvedName {
547                                                def_id: import.id,
548                                                kind: export_kind_to_name_kind(sym.kind),
549                                            },
550                                            visibility: Visibility::Private,
551                                            span: import.span,
552                                            used: false,
553                                            is_import: true,
554                                        },
555                                    );
556                                }
557                            }
558                            Err(RegistryError::ModuleNotFound { .. }) => {
559                                self.diag.error(
560                                    E_MODULE_NOT_FOUND,
561                                    format!("module `{module_id}` not found"),
562                                    import.span,
563                                );
564                            }
565                            Err(e) => {
566                                self.diag.error(
567                                    E_SYMBOL_NOT_FOUND,
568                                    format!("{e}"),
569                                    import.span,
570                                );
571                            }
572                        }
573                    }
574                    // Always add the sentinel so has_wildcard_import() works
575                    // for unresolved glob imports (backward compat).
576                    let sentinel = format!("{}.*", module_id);
577                    self.symbols.define(
578                        sentinel.clone(),
579                        Binding {
580                            name: sentinel,
581                            resolved: ResolvedName {
582                                def_id: import.id,
583                                kind: NameKind::Module,
584                            },
585                            visibility: Visibility::Private,
586                            span: import.span,
587                            used: true, // wildcards are never "unused"
588                            is_import: true,
589                        },
590                    );
591                }
592            }
593        }
594    }
595
596    fn collect_items(&mut self, items: &[Item]) {
597        for item in items {
598            match item {
599                Item::Fn(d) => {
600                    self.symbols.define(
601                        d.name.name.clone(),
602                        Binding {
603                            name: d.name.name.clone(),
604                            resolved: ResolvedName {
605                                def_id: d.id,
606                                kind: NameKind::Function,
607                            },
608                            visibility: d.visibility,
609                            span: d.span,
610                            used: false,
611                            is_import: false,
612                        },
613                    );
614                }
615                Item::Record(d) => {
616                    self.symbols.define(
617                        d.name.name.clone(),
618                        Binding {
619                            name: d.name.name.clone(),
620                            resolved: ResolvedName {
621                                def_id: d.id,
622                                kind: NameKind::Type,
623                            },
624                            visibility: d.visibility,
625                            span: d.span,
626                            used: false,
627                            is_import: false,
628                        },
629                    );
630                }
631                Item::Enum(d) => {
632                    self.symbols.define(
633                        d.name.name.clone(),
634                        Binding {
635                            name: d.name.name.clone(),
636                            resolved: ResolvedName {
637                                def_id: d.id,
638                                kind: NameKind::Type,
639                            },
640                            visibility: d.visibility,
641                            span: d.span,
642                            used: false,
643                            is_import: false,
644                        },
645                    );
646                    // Register each variant as a constructor in scope.
647                    for variant in &d.variants {
648                        let (vname, vid, vspan) = match variant {
649                            EnumVariant::Unit { name, id, span } => (name, id, span),
650                            EnumVariant::Struct { name, id, span, .. } => (name, id, span),
651                            EnumVariant::Tuple { name, id, span, .. } => (name, id, span),
652                        };
653                        self.symbols.define(
654                            vname.name.clone(),
655                            Binding {
656                                name: vname.name.clone(),
657                                resolved: ResolvedName {
658                                    def_id: *vid,
659                                    kind: NameKind::Function,
660                                },
661                                visibility: d.visibility,
662                                span: *vspan,
663                                used: false,
664                                is_import: false,
665                            },
666                        );
667                    }
668                }
669                Item::Class(d) => {
670                    self.symbols.define(
671                        d.name.name.clone(),
672                        Binding {
673                            name: d.name.name.clone(),
674                            resolved: ResolvedName {
675                                def_id: d.id,
676                                kind: NameKind::Type,
677                            },
678                            visibility: d.visibility,
679                            span: d.span,
680                            used: false,
681                            is_import: false,
682                        },
683                    );
684                }
685                Item::Trait(d) | Item::PlatformTrait(d) => {
686                    self.symbols.define(
687                        d.name.name.clone(),
688                        Binding {
689                            name: d.name.name.clone(),
690                            resolved: ResolvedName {
691                                def_id: d.id,
692                                kind: NameKind::Trait,
693                            },
694                            visibility: d.visibility,
695                            span: d.span,
696                            used: false,
697                            is_import: false,
698                        },
699                    );
700                }
701                Item::Effect(d) => {
702                    self.symbols.define(
703                        d.name.name.clone(),
704                        Binding {
705                            name: d.name.name.clone(),
706                            resolved: ResolvedName {
707                                def_id: d.id,
708                                kind: NameKind::Effect,
709                            },
710                            visibility: d.visibility,
711                            span: d.span,
712                            used: false,
713                            is_import: false,
714                        },
715                    );
716                    // Store effect operations so `with` clauses can inject
717                    // them into function scopes during resolution.
718                    let ops: Vec<(String, NodeId, Span)> = d
719                        .operations
720                        .iter()
721                        .map(|op| (op.name.name.clone(), op.id, op.span))
722                        .collect();
723                    let components: Vec<String> = d
724                        .components
725                        .iter()
726                        .map(|tp| {
727                            tp.segments
728                                .iter()
729                                .map(|s| s.name.as_str())
730                                .collect::<Vec<_>>()
731                                .join(".")
732                        })
733                        .collect();
734                    self.symbols.effect_info.insert(
735                        d.name.name.clone(),
736                        EffectInfo {
737                            operations: ops,
738                            components,
739                        },
740                    );
741                }
742                Item::TypeAlias(d) => {
743                    self.symbols.define(
744                        d.name.name.clone(),
745                        Binding {
746                            name: d.name.name.clone(),
747                            resolved: ResolvedName {
748                                def_id: d.id,
749                                kind: NameKind::Type,
750                            },
751                            visibility: d.visibility,
752                            span: d.span,
753                            used: false,
754                            is_import: false,
755                        },
756                    );
757                }
758                Item::Const(d) => {
759                    self.symbols.define(
760                        d.name.name.clone(),
761                        Binding {
762                            name: d.name.name.clone(),
763                            resolved: ResolvedName {
764                                def_id: d.id,
765                                kind: NameKind::Variable,
766                            },
767                            visibility: d.visibility,
768                            span: d.span,
769                            used: false,
770                            is_import: false,
771                        },
772                    );
773                }
774                // Impl blocks, module handles, and property tests don't
775                // introduce names at the module level.
776                Item::Impl(_)
777                | Item::ModuleHandle(_)
778                | Item::PropertyTest(_)
779                | Item::Error { .. } => {}
780            }
781        }
782    }
783
784    // ── Item resolution ───────────────────────────────────────────────────────
785
786    fn resolve_item(&mut self, item: &Item) {
787        match item {
788            Item::Fn(d) => self.resolve_fn(d),
789            Item::Impl(d) => self.resolve_impl(d),
790            Item::Class(d) => {
791                for m in &d.methods {
792                    self.resolve_fn(m);
793                }
794            }
795            Item::Trait(d) | Item::PlatformTrait(d) => {
796                for m in &d.methods {
797                    self.resolve_fn(m);
798                }
799            }
800            Item::Effect(d) => {
801                for op in &d.operations {
802                    self.resolve_fn(op);
803                }
804            }
805            Item::Const(d) => self.resolve_expr(&d.value),
806            Item::ModuleHandle(d) => self.resolve_expr(&d.handler),
807            Item::PropertyTest(d) => self.resolve_block(&d.body),
808            // Types / aliases: no expressions to resolve yet.
809            Item::Record(_) | Item::Enum(_) | Item::TypeAlias(_) | Item::Error { .. } => {}
810        }
811    }
812
813    fn resolve_fn(&mut self, d: &FnDecl) {
814        self.symbols.push_scope();
815        for param in &d.params {
816            self.resolve_param(param);
817        }
818        if let Some(ret) = &d.return_type {
819            self.resolve_type_expr(ret);
820        }
821        // Inject effect operations from the `with` clause into scope so
822        // that calls like `log("msg")` resolve inside effectful functions.
823        self.inject_effect_operations(&d.effect_clause);
824        if let Some(ref body) = d.body {
825            self.resolve_block_body(body);
826        }
827        self.symbols.pop_scope();
828    }
829
830    /// For each effect in the `with` clause, look up its operations and
831    /// define them as function bindings in the current scope.
832    fn inject_effect_operations(&mut self, effect_clause: &[bock_ast::TypePath]) {
833        let mut visited = HashSet::new();
834        for effect_path in effect_clause {
835            let effect_name = effect_path
836                .segments
837                .iter()
838                .map(|s| s.name.as_str())
839                .collect::<Vec<_>>()
840                .join(".");
841            self.inject_ops_for_effect(&effect_name, &mut visited);
842        }
843    }
844
845    /// Recursively inject operations for a single effect (handles composites).
846    fn inject_ops_for_effect(&mut self, effect_name: &str, visited: &mut HashSet<String>) {
847        if !visited.insert(effect_name.to_string()) {
848            return; // avoid cycles
849        }
850        // Clone to avoid borrow conflict with self.symbols.define below.
851        let info = self.symbols.effect_info.get(effect_name).cloned();
852        if let Some(info) = info {
853            for (op_name, op_id, op_span) in &info.operations {
854                self.symbols.define(
855                    op_name.clone(),
856                    Binding {
857                        name: op_name.clone(),
858                        resolved: ResolvedName {
859                            def_id: *op_id,
860                            kind: NameKind::Function,
861                        },
862                        visibility: Visibility::Public,
863                        span: *op_span,
864                        used: true, // never warn about unused effect ops
865                        is_import: false,
866                    },
867                );
868            }
869            // Resolve composite effects transitively.
870            let components = info.components.clone();
871            for component in &components {
872                self.inject_ops_for_effect(component, visited);
873            }
874        }
875    }
876
877    fn resolve_param(&mut self, param: &Param) {
878        self.collect_pattern_bindings(&param.pattern, NameKind::Variable, Visibility::Private);
879        if let Some(ty) = &param.ty {
880            self.resolve_type_expr(ty);
881        }
882        if let Some(default) = &param.default {
883            self.resolve_expr(default);
884        }
885    }
886
887    fn resolve_impl(&mut self, d: &ImplBlock) {
888        for m in &d.methods {
889            // Check whether the method already declares `self` as a parameter.
890            let has_self = m.params.iter().any(|p| {
891                matches!(&p.pattern, Pattern::Bind { name, .. } if name.name == "self")
892            });
893
894            if has_self || d.trait_path.is_none() {
895                // Inherent impl or method with explicit `self` — resolve normally.
896                self.resolve_fn(m);
897            } else {
898                // Effect impl method without explicit `self` parameter.
899                // Inject a synthetic `self` binding so the body can access
900                // the implementing record's fields.
901                self.symbols.push_scope();
902                let syn_id = self.next_synthetic_id();
903                self.symbols.define(
904                    "self".to_string(),
905                    Binding {
906                        name: "self".to_string(),
907                        resolved: ResolvedName {
908                            def_id: syn_id,
909                            kind: NameKind::Variable,
910                        },
911                        visibility: Visibility::Private,
912                        span: m.span,
913                        used: false,
914                        is_import: false,
915                    },
916                );
917                for param in &m.params {
918                    self.resolve_param(param);
919                }
920                self.inject_effect_operations(&m.effect_clause);
921                if let Some(ref body) = m.body {
922                    self.resolve_block_body(body);
923                }
924                self.symbols.pop_scope();
925            }
926        }
927    }
928
929    // ── Block / statement resolution ──────────────────────────────────────────
930
931    /// Resolve a block, pushing and popping a fresh scope.
932    fn resolve_block(&mut self, block: &Block) {
933        self.symbols.push_scope();
934        self.resolve_block_body(block);
935        self.symbols.pop_scope();
936    }
937
938    /// Resolve a block's contents in the *current* scope (no scope push/pop).
939    ///
940    /// Used when the caller has already pushed a scope for the block (e.g. for
941    /// `for` loops where the loop variable is in the same scope as the body).
942    fn resolve_block_body(&mut self, block: &Block) {
943        for stmt in &block.stmts {
944            self.resolve_stmt(stmt);
945        }
946        if let Some(tail) = &block.tail {
947            self.resolve_expr(tail);
948        }
949    }
950
951    fn resolve_stmt(&mut self, stmt: &Stmt) {
952        match stmt {
953            Stmt::Let(s) => self.resolve_let(s),
954            Stmt::Expr(e) => self.resolve_expr(e),
955            Stmt::For(f) => self.resolve_for(f),
956            Stmt::While(w) => self.resolve_while(w),
957            Stmt::Loop(l) => self.resolve_loop(l),
958            Stmt::Guard(g) => self.resolve_guard(g),
959            Stmt::Handling(h) => self.resolve_handling(h),
960            Stmt::Empty => {}
961        }
962    }
963
964    fn resolve_let(&mut self, s: &LetStmt) {
965        // Resolve the initialiser before binding the name (no self-referential defs).
966        self.resolve_expr(&s.value);
967        if let Some(ty) = &s.ty {
968            self.resolve_type_expr(ty);
969        }
970        self.collect_pattern_bindings(&s.pattern, NameKind::Variable, Visibility::Private);
971    }
972
973    fn resolve_for(&mut self, f: &ForLoop) {
974        self.resolve_expr(&f.iterable);
975        // The loop variable and the body share one scope.
976        self.symbols.push_scope();
977        self.collect_pattern_bindings(&f.pattern, NameKind::Variable, Visibility::Private);
978        self.resolve_block_body(&f.body);
979        self.symbols.pop_scope();
980    }
981
982    fn resolve_while(&mut self, w: &WhileLoop) {
983        self.resolve_expr(&w.condition);
984        self.resolve_block(&w.body);
985    }
986
987    fn resolve_loop(&mut self, l: &LoopStmt) {
988        self.resolve_block(&l.body);
989    }
990
991    fn resolve_guard(&mut self, g: &GuardStmt) {
992        self.resolve_expr(&g.condition);
993        if let Some(pat) = &g.let_pattern {
994            self.collect_pattern_bindings(pat, NameKind::Variable, Visibility::Private);
995        }
996        self.resolve_block(&g.else_block);
997    }
998
999    fn resolve_handling(&mut self, h: &HandlingBlock) {
1000        for pair in &h.handlers {
1001            self.resolve_expr(&pair.handler);
1002        }
1003        self.resolve_block(&h.body);
1004    }
1005
1006    // ── Expression resolution ─────────────────────────────────────────────────
1007
1008    fn resolve_expr(&mut self, expr: &Expr) {
1009        match expr {
1010            Expr::Identifier { id, name, .. } => {
1011                if let Some(resolved) = self.symbols.lookup(&name.name) {
1012                    self.symbols.record_resolution(*id, resolved);
1013                } else if !self.symbols.has_wildcard_import() {
1014                    let visible = self.symbols.visible_names();
1015                    let diag = self.diag.error(
1016                        E_UNDEFINED,
1017                        format!("undefined name `{}`", name.name),
1018                        name.span,
1019                    );
1020                    if let Some(hint) = keyword_hint(&name.name) {
1021                        diag.note(hint);
1022                    } else if let Some(suggestion) =
1023                        bock_errors::suggest_similar(&name.name, visible, 2)
1024                    {
1025                        diag.note(format!("did you mean `{suggestion}`?"));
1026                    }
1027                }
1028            }
1029
1030            // Terminals with no sub-expressions.
1031            Expr::Literal { .. }
1032            | Expr::Continue { .. }
1033            | Expr::Unreachable { .. }
1034            | Expr::Placeholder { .. } => {}
1035
1036            Expr::Binary { left, right, .. } => {
1037                self.resolve_expr(left);
1038                self.resolve_expr(right);
1039            }
1040            Expr::Unary { operand, .. } => self.resolve_expr(operand),
1041            Expr::Assign { target, value, .. } => {
1042                self.resolve_expr(target);
1043                self.resolve_expr(value);
1044            }
1045            Expr::Call { callee, args, .. } => {
1046                self.resolve_expr(callee);
1047                for arg in args {
1048                    self.resolve_expr(&arg.value);
1049                }
1050            }
1051            Expr::MethodCall { receiver, args, .. } => {
1052                self.resolve_expr(receiver);
1053                for arg in args {
1054                    self.resolve_expr(&arg.value);
1055                }
1056            }
1057            Expr::FieldAccess { object, .. } => self.resolve_expr(object),
1058            Expr::Index { object, index, .. } => {
1059                self.resolve_expr(object);
1060                self.resolve_expr(index);
1061            }
1062            Expr::Try { expr, .. } => self.resolve_expr(expr),
1063            Expr::Lambda { params, body, .. } => {
1064                self.symbols.push_scope();
1065                for p in params {
1066                    self.resolve_param(p);
1067                }
1068                self.resolve_expr(body);
1069                self.symbols.pop_scope();
1070            }
1071            Expr::Pipe { left, right, .. } | Expr::Compose { left, right, .. } => {
1072                self.resolve_expr(left);
1073                self.resolve_expr(right);
1074            }
1075            Expr::If {
1076                condition,
1077                let_pattern,
1078                then_block,
1079                else_block,
1080                ..
1081            } => {
1082                self.resolve_expr(condition);
1083                // `if let pat = expr { ... }` — pattern bindings live in the
1084                // then-branch scope.
1085                self.symbols.push_scope();
1086                if let Some(pat) = let_pattern {
1087                    self.collect_pattern_bindings(pat, NameKind::Variable, Visibility::Private);
1088                }
1089                self.resolve_block_body(then_block);
1090                self.symbols.pop_scope();
1091                if let Some(eb) = else_block {
1092                    self.resolve_expr(eb);
1093                }
1094            }
1095            Expr::Match {
1096                scrutinee, arms, ..
1097            } => {
1098                self.resolve_expr(scrutinee);
1099                for arm in arms {
1100                    self.resolve_match_arm(arm);
1101                }
1102            }
1103            Expr::Loop { body, .. } => self.resolve_block(body),
1104            Expr::Block { block, .. } => self.resolve_block(block),
1105            Expr::RecordConstruct {
1106                path,
1107                fields,
1108                spread,
1109                ..
1110            } => {
1111                if let Some(first) = path.segments.first() {
1112                    self.symbols.mark_used(&first.name);
1113                }
1114                for f in fields {
1115                    if let Some(v) = &f.value {
1116                        self.resolve_expr(v);
1117                    }
1118                }
1119                if let Some(s) = spread {
1120                    self.resolve_expr(&s.expr);
1121                }
1122            }
1123            Expr::ListLiteral { elems, .. }
1124            | Expr::SetLiteral { elems, .. }
1125            | Expr::TupleLiteral { elems, .. } => {
1126                for e in elems {
1127                    self.resolve_expr(e);
1128                }
1129            }
1130            Expr::MapLiteral { entries, .. } => {
1131                for (k, v) in entries {
1132                    self.resolve_expr(k);
1133                    self.resolve_expr(v);
1134                }
1135            }
1136            Expr::Range { lo, hi, .. } => {
1137                self.resolve_expr(lo);
1138                self.resolve_expr(hi);
1139            }
1140            Expr::Await { expr, .. } => self.resolve_expr(expr),
1141            Expr::Return { value, .. } | Expr::Break { value, .. } => {
1142                if let Some(v) = value {
1143                    self.resolve_expr(v);
1144                }
1145            }
1146            Expr::Interpolation { parts, .. } => {
1147                for part in parts {
1148                    if let InterpolationPart::Expr(e) = part {
1149                        self.resolve_expr(e);
1150                    }
1151                }
1152            }
1153            Expr::Is { expr, .. } => {
1154                self.resolve_expr(expr);
1155            }
1156        }
1157    }
1158
1159    /// Marks type names referenced in a type expression as used imports.
1160    fn resolve_type_expr(&mut self, ty: &bock_ast::TypeExpr) {
1161        match ty {
1162            bock_ast::TypeExpr::Named { path, args, .. } => {
1163                if let Some(first) = path.segments.first() {
1164                    self.symbols.mark_used(&first.name);
1165                }
1166                for arg in args {
1167                    self.resolve_type_expr(arg);
1168                }
1169            }
1170            bock_ast::TypeExpr::Tuple { elems, .. } => {
1171                for e in elems {
1172                    self.resolve_type_expr(e);
1173                }
1174            }
1175            bock_ast::TypeExpr::Function {
1176                params, ret, effects, ..
1177            } => {
1178                for p in params {
1179                    self.resolve_type_expr(p);
1180                }
1181                self.resolve_type_expr(ret);
1182                for eff in effects {
1183                    if let Some(first) = eff.segments.first() {
1184                        self.symbols.mark_used(&first.name);
1185                    }
1186                }
1187            }
1188            bock_ast::TypeExpr::Optional { inner, .. } => {
1189                self.resolve_type_expr(inner);
1190            }
1191            bock_ast::TypeExpr::SelfType { .. } => {}
1192        }
1193    }
1194
1195    fn resolve_match_arm(&mut self, arm: &MatchArm) {
1196        self.symbols.push_scope();
1197        self.collect_pattern_bindings(&arm.pattern, NameKind::Variable, Visibility::Private);
1198        if let Some(g) = &arm.guard {
1199            self.resolve_expr(g);
1200        }
1201        self.resolve_expr(&arm.body);
1202        self.symbols.pop_scope();
1203    }
1204
1205    // ── Pattern binding collection ────────────────────────────────────────────
1206
1207    fn collect_pattern_bindings(
1208        &mut self,
1209        pattern: &Pattern,
1210        kind: NameKind,
1211        visibility: Visibility,
1212    ) {
1213        match pattern {
1214            // Terminals that bind nothing.
1215            Pattern::Wildcard { .. } | Pattern::Literal { .. } | Pattern::Rest { .. } => {}
1216
1217            Pattern::Bind { id, span, name } | Pattern::MutBind { id, span, name } => {
1218                self.symbols.define(
1219                    name.name.clone(),
1220                    Binding {
1221                        name: name.name.clone(),
1222                        resolved: ResolvedName { def_id: *id, kind },
1223                        visibility,
1224                        span: *span,
1225                        used: false,
1226                        is_import: false,
1227                    },
1228                );
1229            }
1230
1231            Pattern::Constructor { path, fields, .. } => {
1232                if let Some(first) = path.segments.first() {
1233                    self.symbols.mark_used(&first.name);
1234                }
1235                for f in fields {
1236                    self.collect_pattern_bindings(f, kind, visibility);
1237                }
1238            }
1239            Pattern::Tuple { elems, .. } => {
1240                for e in elems {
1241                    self.collect_pattern_bindings(e, kind, visibility);
1242                }
1243            }
1244            Pattern::Record { path, fields, .. } => {
1245                if let Some(first) = path.segments.first() {
1246                    self.symbols.mark_used(&first.name);
1247                }
1248                for f in fields {
1249                    if let Some(p) = &f.pattern {
1250                        self.collect_pattern_bindings(p, kind, visibility);
1251                    } else {
1252                        // Shorthand `{ field }` — bind `field` to itself.
1253                        let syn_id = self.next_synthetic_id();
1254                        self.symbols.define(
1255                            f.name.name.clone(),
1256                            Binding {
1257                                name: f.name.name.clone(),
1258                                resolved: ResolvedName {
1259                                    def_id: syn_id,
1260                                    kind,
1261                                },
1262                                visibility,
1263                                span: f.span,
1264                                used: false,
1265                                is_import: false,
1266                            },
1267                        );
1268                    }
1269                }
1270            }
1271            Pattern::List { elems, rest, .. } => {
1272                for e in elems {
1273                    self.collect_pattern_bindings(e, kind, visibility);
1274                }
1275                if let Some(r) = rest {
1276                    self.collect_pattern_bindings(r, kind, visibility);
1277                }
1278            }
1279            Pattern::Or { alternatives, .. } => {
1280                // All alternatives must bind the same names (enforced by the
1281                // type checker).  Collect bindings from the first alternative.
1282                if let Some(first) = alternatives.first() {
1283                    self.collect_pattern_bindings(first, kind, visibility);
1284                }
1285            }
1286            Pattern::Range { lo, hi, .. } => {
1287                self.collect_pattern_bindings(lo, kind, visibility);
1288                self.collect_pattern_bindings(hi, kind, visibility);
1289            }
1290        }
1291    }
1292
1293    // ── Unused-import check ───────────────────────────────────────────────────
1294
1295    fn check_unused_imports(&mut self) {
1296        // Propagate "used" from enum variants to their parent enum import.
1297        // If a variant like `Red` was used, mark `Color` as used too.
1298        if let Some(scope) = self.symbols.scopes.first() {
1299            let used_parents: Vec<String> = self
1300                .symbols
1301                .variant_parent
1302                .iter()
1303                .filter(|(variant, _)| {
1304                    scope
1305                        .bindings
1306                        .get(variant.as_str())
1307                        .is_some_and(|b| b.used)
1308                })
1309                .map(|(_, parent)| parent.clone())
1310                .collect();
1311            for parent in used_parents {
1312                self.symbols.mark_used(&parent);
1313            }
1314        }
1315
1316        // Collect into an owned vec to avoid borrowing `self.symbols` and
1317        // `self.diag` simultaneously.
1318        let unused: Vec<(String, Span)> = self
1319            .symbols
1320            .scopes
1321            .first()
1322            .map(|s| {
1323                s.bindings
1324                    .values()
1325                    .filter(|b| b.is_import && !b.used)
1326                    .map(|b| (b.name.clone(), b.span))
1327                    .collect()
1328            })
1329            .unwrap_or_default();
1330
1331        for (name, span) in unused {
1332            self.diag
1333                .warning(W_UNUSED_IMPORT, format!("unused import `{name}`"), span);
1334        }
1335    }
1336}
1337
1338// ─── Helpers ──────────────────────────────────────────────────────────────────
1339
1340fn module_path_str(path: &ModulePath) -> String {
1341    path.segments
1342        .iter()
1343        .map(|s| s.name.as_str())
1344        .collect::<Vec<_>>()
1345        .join(".")
1346}
1347
1348/// Converts a registry [`ExportKind`] to a resolver [`NameKind`].
1349fn export_kind_to_name_kind(kind: ExportKind) -> NameKind {
1350    match kind {
1351        ExportKind::Function => NameKind::Function,
1352        ExportKind::Record | ExportKind::Enum | ExportKind::TypeAlias => NameKind::Type,
1353        ExportKind::Trait => NameKind::Trait,
1354        ExportKind::Effect => NameKind::Effect,
1355        ExportKind::Constant => NameKind::Variable,
1356    }
1357}
1358
1359/// Seeds the symbol table's `effect_info` for an imported effect, so that
1360/// `with` clauses in the importing module can inject its operations into scope.
1361fn seed_effect_info_from_registry(
1362    symbols: &mut SymbolTable,
1363    local_name: &str,
1364    sym: &ExportedSymbol,
1365    import_id: NodeId,
1366    import_span: Span,
1367) {
1368    if let ExportDetail::Effect {
1369        operations,
1370        components,
1371    } = &sym.detail
1372    {
1373        let ops: Vec<(String, NodeId, Span)> = operations
1374            .iter()
1375            .map(|(name, _type_ref)| (name.clone(), import_id, import_span))
1376            .collect();
1377        symbols.effect_info.insert(
1378            local_name.to_string(),
1379            EffectInfo {
1380                operations: ops,
1381                components: components.clone(),
1382            },
1383        );
1384    }
1385}
1386
1387/// Seeds enum variant constructors into the symbol table when an enum type
1388/// is imported (named or glob). This mirrors what `collect_items` does for
1389/// locally-defined enums: each variant name becomes a `NameKind::Function`
1390/// binding so bare constructor names like `Red` or `Circle { ... }` work.
1391fn seed_enum_variants_from_registry(
1392    symbols: &mut SymbolTable,
1393    enum_name: &str,
1394    sym: &ExportedSymbol,
1395    import_id: NodeId,
1396    import_span: Span,
1397) {
1398    if let ExportDetail::Enum { variants, .. } = &sym.detail {
1399        for variant in variants {
1400            symbols.variant_parent.insert(
1401                variant.name.clone(),
1402                enum_name.to_string(),
1403            );
1404            symbols.define(
1405                variant.name.clone(),
1406                Binding {
1407                    name: variant.name.clone(),
1408                    resolved: ResolvedName {
1409                        def_id: import_id,
1410                        kind: NameKind::Function,
1411                    },
1412                    visibility: Visibility::Private,
1413                    span: import_span,
1414                    used: false,
1415                    // Not marked as an import — auto-seeded variants should
1416                    // not produce individual "unused import" warnings.
1417                    is_import: false,
1418                },
1419            );
1420        }
1421    }
1422}
1423
1424// ─── Common-mistake keyword hints ────────────────────────────────────────────
1425
1426/// Maps foreign-language keywords that users commonly reach for in Bock to a
1427/// one-line hint suggesting the Bock equivalent.
1428///
1429/// Returned as a static hint string when a name lookup fails on one of these
1430/// keywords. This is preferred over an edit-distance suggestion because the
1431/// user is almost certainly using vocabulary from another language.
1432fn keyword_hint(name: &str) -> Option<&'static str> {
1433    match name {
1434        "pub" => Some("Bock uses `public` for visibility, not `pub`"),
1435        "var" => Some("Bock uses `let mut` for mutable bindings, not `var`"),
1436        "func" | "def" => Some("Bock uses `fn` to declare functions"),
1437        "interface" => Some("Bock uses `trait` for interfaces"),
1438        "struct" => Some("Bock uses `record` for value types"),
1439        "class" => Some("Bock uses `record` for data and `trait` for behavior — there is no `class`"),
1440        "None_" | "nil" | "null" | "undefined" => {
1441            Some("Bock uses `None` (from `Optional[T]`) to represent absent values")
1442        }
1443        "true_" | "false_" => Some("Bock boolean literals are `true` and `false`"),
1444        _ => None,
1445    }
1446}
1447
1448// ─── Public entry points ─────────────────────────────────────────────────────
1449
1450/// Resolve all names in `ast`, populating `symbols` and returning diagnostics.
1451///
1452/// This is the single-file entry point. Imports are registered with
1453/// [`NameKind::Unresolved`] since no cross-file registry is available.
1454///
1455/// After this call:
1456/// - `symbols.resolutions` maps each identifier's usage NodeId to its definition.
1457/// - Diagnostics include `E1001` errors for undefined names and `W1001`
1458///   warnings for unused imports.
1459pub fn resolve_names(ast: &Module, symbols: &mut SymbolTable) -> DiagnosticBag {
1460    let mut diag = DiagnosticBag::new();
1461    let mut resolver = Resolver::new(symbols, &mut diag);
1462    resolver.resolve_module(ast);
1463    diag
1464}
1465
1466/// Resolve all names in `ast` with cross-file import resolution.
1467///
1468/// Named imports (`use a.b.{X}`) and glob imports (`use a.b.*`) are resolved
1469/// against the `registry`. Modules not found in the registry produce `E1005`
1470/// diagnostics; missing or private symbols produce `E1006`/`E1007`.
1471///
1472/// When the registry is empty (no modules registered), this behaves identically
1473/// to [`resolve_names`] — single-file mode is preserved.
1474pub fn resolve_names_with_registry(
1475    ast: &Module,
1476    symbols: &mut SymbolTable,
1477    registry: &ModuleRegistry,
1478) -> DiagnosticBag {
1479    let mut diag = DiagnosticBag::new();
1480    let mut resolver = Resolver::new(symbols, &mut diag);
1481    resolver.registry = Some(registry);
1482    resolver.resolve_module(ast);
1483    diag
1484}
1485
1486// ─── Tests ────────────────────────────────────────────────────────────────────
1487
1488#[cfg(test)]
1489mod tests {
1490    use super::*;
1491    use bock_ast::{
1492        Block, FnDecl, Ident, ImportDecl, ImportItems, ImportedName, Item, Literal, Module,
1493        ModulePath, Param, Pattern, Stmt, Visibility,
1494    };
1495    use bock_errors::{FileId, Span};
1496
1497    fn sp() -> Span {
1498        Span {
1499            file: FileId(0),
1500            start: 0,
1501            end: 1,
1502        }
1503    }
1504
1505    fn ident(name: &str) -> Ident {
1506        Ident {
1507            name: name.to_string(),
1508            span: sp(),
1509        }
1510    }
1511
1512    fn mpath(segments: &[&str]) -> ModulePath {
1513        ModulePath {
1514            segments: segments.iter().map(|s| ident(s)).collect(),
1515            span: sp(),
1516        }
1517    }
1518
1519    fn empty_block(id: NodeId) -> Block {
1520        Block {
1521            id,
1522            span: sp(),
1523            stmts: vec![],
1524            tail: None,
1525        }
1526    }
1527
1528    fn simple_module(imports: Vec<ImportDecl>, items: Vec<Item>) -> Module {
1529        Module {
1530            id: 0,
1531            span: sp(),
1532            doc: vec![],
1533            path: None,
1534            imports,
1535            items,
1536        }
1537    }
1538
1539    fn fn_item(id: NodeId, name: &str, vis: Visibility) -> Item {
1540        Item::Fn(FnDecl {
1541            id,
1542            span: sp(),
1543            annotations: vec![],
1544            visibility: vis,
1545            is_async: false,
1546            name: ident(name),
1547            generic_params: vec![],
1548            params: vec![],
1549            return_type: None,
1550            effect_clause: vec![],
1551            where_clause: vec![],
1552            body: Some(empty_block(id + 100)),
1553        })
1554    }
1555
1556    // ── SymbolTable basics ────────────────────────────────────────────────────
1557
1558    #[test]
1559    fn symbol_table_define_and_lookup() {
1560        let mut st = SymbolTable::new();
1561        st.define(
1562            "foo".into(),
1563            Binding {
1564                name: "foo".into(),
1565                resolved: ResolvedName {
1566                    def_id: 1,
1567                    kind: NameKind::Function,
1568                },
1569                visibility: Visibility::Public,
1570                span: sp(),
1571                used: false,
1572                is_import: false,
1573            },
1574        );
1575        let r = st.lookup("foo").unwrap();
1576        assert_eq!(r.def_id, 1);
1577        assert_eq!(r.kind, NameKind::Function);
1578    }
1579
1580    #[test]
1581    fn symbol_table_lookup_marks_used() {
1582        let mut st = SymbolTable::new();
1583        st.define(
1584            "x".into(),
1585            Binding {
1586                name: "x".into(),
1587                resolved: ResolvedName {
1588                    def_id: 5,
1589                    kind: NameKind::Variable,
1590                },
1591                visibility: Visibility::Private,
1592                span: sp(),
1593                used: false,
1594                is_import: false,
1595            },
1596        );
1597        st.lookup("x");
1598        assert!(st.lookup_peek("x").unwrap().used);
1599    }
1600
1601    #[test]
1602    fn symbol_table_inner_scope_shadows_outer() {
1603        let mut st = SymbolTable::new();
1604        st.define(
1605            "x".into(),
1606            Binding {
1607                name: "x".into(),
1608                resolved: ResolvedName {
1609                    def_id: 1,
1610                    kind: NameKind::Variable,
1611                },
1612                visibility: Visibility::Private,
1613                span: sp(),
1614                used: false,
1615                is_import: false,
1616            },
1617        );
1618        st.push_scope();
1619        st.define(
1620            "x".into(),
1621            Binding {
1622                name: "x".into(),
1623                resolved: ResolvedName {
1624                    def_id: 2,
1625                    kind: NameKind::Variable,
1626                },
1627                visibility: Visibility::Private,
1628                span: sp(),
1629                used: false,
1630                is_import: false,
1631            },
1632        );
1633        // Inner binding (def_id=2) shadows outer (def_id=1).
1634        assert_eq!(st.lookup("x").unwrap().def_id, 2);
1635        st.pop_scope();
1636        // After popping, outer binding is visible again.
1637        assert_eq!(st.lookup("x").unwrap().def_id, 1);
1638    }
1639
1640    #[test]
1641    fn symbol_table_lookup_unknown_returns_none() {
1642        let mut st = SymbolTable::new();
1643        assert!(st.lookup("unknown").is_none());
1644    }
1645
1646    #[test]
1647    fn symbol_table_module_scope_never_popped() {
1648        let mut st = SymbolTable::new();
1649        assert!(st.pop_scope().is_none()); // only module scope remains
1650    }
1651
1652    // ── resolve_names: simple cases ───────────────────────────────────────────
1653
1654    #[test]
1655    fn resolve_defined_identifier() {
1656        // fn foo() {}
1657        // fn bar() { foo }
1658        let module = simple_module(
1659            vec![],
1660            vec![
1661                fn_item(1, "foo", Visibility::Private),
1662                Item::Fn(FnDecl {
1663                    id: 2,
1664                    span: sp(),
1665                    annotations: vec![],
1666                    visibility: Visibility::Private,
1667                    is_async: false,
1668                    name: ident("bar"),
1669                    generic_params: vec![],
1670                    params: vec![],
1671                    return_type: None,
1672                    effect_clause: vec![],
1673                    where_clause: vec![],
1674                    body: Some(Block {
1675                        id: 200,
1676                        span: sp(),
1677                        stmts: vec![],
1678                        tail: Some(Box::new(Expr::Identifier {
1679                            id: 99,
1680                            span: sp(),
1681                            name: ident("foo"),
1682                        })),
1683                    }),
1684                }),
1685            ],
1686        );
1687        let mut st = SymbolTable::new();
1688        let diag = resolve_names(&module, &mut st);
1689        assert!(
1690            !diag.has_errors(),
1691            "unexpected errors: {:?}",
1692            diag.iter().collect::<Vec<_>>()
1693        );
1694        let resolved = st
1695            .resolutions
1696            .get(&99)
1697            .expect("identifier should be resolved");
1698        assert_eq!(resolved.def_id, 1);
1699        assert_eq!(resolved.kind, NameKind::Function);
1700    }
1701
1702    #[test]
1703    fn resolve_undefined_identifier_produces_error() {
1704        let module = simple_module(
1705            vec![],
1706            vec![Item::Fn(FnDecl {
1707                id: 1,
1708                span: sp(),
1709                annotations: vec![],
1710                visibility: Visibility::Private,
1711                is_async: false,
1712                name: ident("bar"),
1713                generic_params: vec![],
1714                params: vec![],
1715                return_type: None,
1716                effect_clause: vec![],
1717                where_clause: vec![],
1718                body: Some(Block {
1719                    id: 100,
1720                    span: sp(),
1721                    stmts: vec![],
1722                    tail: Some(Box::new(Expr::Identifier {
1723                        id: 42,
1724                        span: sp(),
1725                        name: ident("undefined_thing"),
1726                    })),
1727                }),
1728            })],
1729        );
1730        let mut st = SymbolTable::new();
1731        let diag = resolve_names(&module, &mut st);
1732        assert!(diag.has_errors());
1733        let msgs: Vec<_> = diag.iter().map(|d| d.message.as_str()).collect();
1734        assert!(msgs.iter().any(|m| m.contains("undefined_thing")));
1735    }
1736
1737    // ── Import resolution ─────────────────────────────────────────────────────
1738
1739    #[test]
1740    fn named_import_creates_binding() {
1741        let import = ImportDecl {
1742            id: 10,
1743            span: sp(),
1744            visibility: Visibility::Private,
1745            path: mpath(&["core", "collections"]),
1746            items: ImportItems::Named(vec![
1747                ImportedName {
1748                    span: sp(),
1749                    name: ident("List"),
1750                    alias: None,
1751                },
1752                ImportedName {
1753                    span: sp(),
1754                    name: ident("Map"),
1755                    alias: None,
1756                },
1757            ]),
1758        };
1759        let module = simple_module(vec![import], vec![]);
1760        let mut st = SymbolTable::new();
1761        resolve_names(&module, &mut st);
1762        // Both List and Map should be in the module scope.
1763        assert!(st.lookup_peek("List").is_some());
1764        assert!(st.lookup_peek("Map").is_some());
1765    }
1766
1767    #[test]
1768    fn named_import_with_alias() {
1769        let import = ImportDecl {
1770            id: 10,
1771            span: sp(),
1772            visibility: Visibility::Private,
1773            path: mpath(&["core"]),
1774            items: ImportItems::Named(vec![ImportedName {
1775                span: sp(),
1776                name: ident("FooBar"),
1777                alias: Some(ident("FB")),
1778            }]),
1779        };
1780        let module = simple_module(vec![import], vec![]);
1781        let mut st = SymbolTable::new();
1782        resolve_names(&module, &mut st);
1783        // Local name is the alias.
1784        assert!(st.lookup_peek("FB").is_some());
1785        assert!(st.lookup_peek("FooBar").is_none());
1786    }
1787
1788    #[test]
1789    fn module_import_creates_binding() {
1790        let import = ImportDecl {
1791            id: 10,
1792            span: sp(),
1793            visibility: Visibility::Private,
1794            path: mpath(&["app", "models"]),
1795            items: ImportItems::Module,
1796        };
1797        let module = simple_module(vec![import], vec![]);
1798        let mut st = SymbolTable::new();
1799        resolve_names(&module, &mut st);
1800        // Last path segment is bound.
1801        let b = st.lookup_peek("models").expect("models should be bound");
1802        assert_eq!(b.resolved.kind, NameKind::Module);
1803    }
1804
1805    #[test]
1806    fn wildcard_import_suppresses_undefined_errors() {
1807        // `use some.module.*` — we can't enumerate names, so identifiers that
1808        // aren't locally defined should NOT produce an error.
1809        let import = ImportDecl {
1810            id: 10,
1811            span: sp(),
1812            visibility: Visibility::Private,
1813            path: mpath(&["some", "module"]),
1814            items: ImportItems::Glob,
1815        };
1816        let module = simple_module(
1817            vec![import],
1818            vec![Item::Fn(FnDecl {
1819                id: 1,
1820                span: sp(),
1821                annotations: vec![],
1822                visibility: Visibility::Private,
1823                is_async: false,
1824                name: ident("test"),
1825                generic_params: vec![],
1826                params: vec![],
1827                return_type: None,
1828                effect_clause: vec![],
1829                where_clause: vec![],
1830                body: Some(Block {
1831                    id: 100,
1832                    span: sp(),
1833                    stmts: vec![],
1834                    tail: Some(Box::new(Expr::Identifier {
1835                        id: 99,
1836                        span: sp(),
1837                        name: ident("SomethingFromWildcard"),
1838                    })),
1839                }),
1840            })],
1841        );
1842        let mut st = SymbolTable::new();
1843        let diag = resolve_names(&module, &mut st);
1844        assert!(
1845            !diag.has_errors(),
1846            "wildcard import should suppress undefined errors"
1847        );
1848    }
1849
1850    // ── Unused import warnings ────────────────────────────────────────────────
1851
1852    #[test]
1853    fn unused_named_import_produces_warning() {
1854        let import = ImportDecl {
1855            id: 10,
1856            span: sp(),
1857            visibility: Visibility::Private,
1858            path: mpath(&["core"]),
1859            items: ImportItems::Named(vec![ImportedName {
1860                span: sp(),
1861                name: ident("Unused"),
1862                alias: None,
1863            }]),
1864        };
1865        let module = simple_module(vec![import], vec![]);
1866        let mut st = SymbolTable::new();
1867        let diag = resolve_names(&module, &mut st);
1868        assert!(!diag.has_errors());
1869        let warnings: Vec<_> = diag
1870            .iter()
1871            .filter(|d| d.severity == bock_errors::Severity::Warning)
1872            .collect();
1873        assert!(!warnings.is_empty(), "expected unused import warning");
1874        assert!(warnings.iter().any(|w| w.message.contains("Unused")));
1875    }
1876
1877    #[test]
1878    fn used_import_no_warning() {
1879        let import = ImportDecl {
1880            id: 10,
1881            span: sp(),
1882            visibility: Visibility::Private,
1883            path: mpath(&["core"]),
1884            items: ImportItems::Named(vec![ImportedName {
1885                span: sp(),
1886                name: ident("Used"),
1887                alias: None,
1888            }]),
1889        };
1890        let module = simple_module(
1891            vec![import],
1892            vec![Item::Fn(FnDecl {
1893                id: 1,
1894                span: sp(),
1895                annotations: vec![],
1896                visibility: Visibility::Private,
1897                is_async: false,
1898                name: ident("test"),
1899                generic_params: vec![],
1900                params: vec![],
1901                return_type: None,
1902                effect_clause: vec![],
1903                where_clause: vec![],
1904                body: Some(Block {
1905                    id: 100,
1906                    span: sp(),
1907                    stmts: vec![],
1908                    tail: Some(Box::new(Expr::Identifier {
1909                        id: 99,
1910                        span: sp(),
1911                        name: ident("Used"),
1912                    })),
1913                }),
1914            })],
1915        );
1916        let mut st = SymbolTable::new();
1917        let diag = resolve_names(&module, &mut st);
1918        assert!(!diag.has_errors());
1919        let warnings: Vec<_> = diag
1920            .iter()
1921            .filter(|d| d.severity == bock_errors::Severity::Warning)
1922            .collect();
1923        assert!(warnings.is_empty(), "no warning expected for used import");
1924    }
1925
1926    // ── Shadowing ─────────────────────────────────────────────────────────────
1927
1928    #[test]
1929    fn let_binding_shadows_outer() {
1930        // fn test() {
1931        //   let x = 1
1932        //   let x = 2  ← shadows
1933        //   x          ← resolves to inner x (id=20)
1934        // }
1935        use bock_ast::LetStmt;
1936        let outer_let = Stmt::Let(LetStmt {
1937            id: 10,
1938            span: sp(),
1939            pattern: Pattern::Bind {
1940                id: 10,
1941                span: sp(),
1942                name: ident("x"),
1943            },
1944            ty: None,
1945            value: Expr::Literal {
1946                id: 11,
1947                span: sp(),
1948                lit: Literal::Int("1".into()),
1949            },
1950        });
1951        let inner_let = Stmt::Let(LetStmt {
1952            id: 20,
1953            span: sp(),
1954            pattern: Pattern::Bind {
1955                id: 20,
1956                span: sp(),
1957                name: ident("x"),
1958            },
1959            ty: None,
1960            value: Expr::Literal {
1961                id: 21,
1962                span: sp(),
1963                lit: Literal::Int("2".into()),
1964            },
1965        });
1966        let use_x = Expr::Identifier {
1967            id: 99,
1968            span: sp(),
1969            name: ident("x"),
1970        };
1971
1972        let module = simple_module(
1973            vec![],
1974            vec![Item::Fn(FnDecl {
1975                id: 1,
1976                span: sp(),
1977                annotations: vec![],
1978                visibility: Visibility::Private,
1979                is_async: false,
1980                name: ident("test"),
1981                generic_params: vec![],
1982                params: vec![],
1983                return_type: None,
1984                effect_clause: vec![],
1985                where_clause: vec![],
1986                body: Some(Block {
1987                    id: 100,
1988                    span: sp(),
1989                    stmts: vec![outer_let, inner_let],
1990                    tail: Some(Box::new(use_x)),
1991                }),
1992            })],
1993        );
1994        let mut st = SymbolTable::new();
1995        let diag = resolve_names(&module, &mut st);
1996        assert!(!diag.has_errors());
1997        // The use of `x` (id=99) should resolve to the inner let (def_id=20).
1998        let resolved = st.resolutions.get(&99).expect("x should be resolved");
1999        assert_eq!(resolved.def_id, 20);
2000    }
2001
2002    // ── Param binding ─────────────────────────────────────────────────────────
2003
2004    #[test]
2005    fn function_param_is_in_scope() {
2006        let module = simple_module(
2007            vec![],
2008            vec![Item::Fn(FnDecl {
2009                id: 1,
2010                span: sp(),
2011                annotations: vec![],
2012                visibility: Visibility::Private,
2013                is_async: false,
2014                name: ident("id"),
2015                generic_params: vec![],
2016                params: vec![Param {
2017                    id: 5,
2018                    span: sp(),
2019                    pattern: Pattern::Bind {
2020                        id: 5,
2021                        span: sp(),
2022                        name: ident("n"),
2023                    },
2024                    ty: None,
2025                    default: None,
2026                }],
2027                return_type: None,
2028                effect_clause: vec![],
2029                where_clause: vec![],
2030                body: Some(Block {
2031                    id: 100,
2032                    span: sp(),
2033                    stmts: vec![],
2034                    tail: Some(Box::new(Expr::Identifier {
2035                        id: 99,
2036                        span: sp(),
2037                        name: ident("n"),
2038                    })),
2039                }),
2040            })],
2041        );
2042        let mut st = SymbolTable::new();
2043        let diag = resolve_names(&module, &mut st);
2044        assert!(!diag.has_errors());
2045        let resolved = st.resolutions.get(&99).expect("param n should resolve");
2046        assert_eq!(resolved.def_id, 5);
2047        assert_eq!(resolved.kind, NameKind::Variable);
2048    }
2049
2050    // ── Visibility tracked ────────────────────────────────────────────────────
2051
2052    #[test]
2053    fn visibility_is_stored_in_binding() {
2054        let module = simple_module(vec![], vec![fn_item(1, "pub_fn", Visibility::Public)]);
2055        let mut st = SymbolTable::new();
2056        resolve_names(&module, &mut st);
2057        let b = st.lookup_peek("pub_fn").expect("pub_fn should be bound");
2058        assert_eq!(b.visibility, Visibility::Public);
2059    }
2060
2061    // ── Registry-backed import resolution ────────────────────────────────────
2062
2063    use crate::registry::{
2064        EnumVariantExport, ExportDetail, ExportKind, ExportedSymbol, ModuleExports,
2065        ModuleRegistry,
2066    };
2067    use crate::stubs::TypeRef;
2068
2069    /// Build a registry with a sample "app.models" module exporting
2070    /// User (record), Role (enum), and default_user (function).
2071    fn sample_registry() -> ModuleRegistry {
2072        let mut reg = ModuleRegistry::new();
2073        let mut exports = ModuleExports::new("app.models", "src/app/models.bock");
2074        exports.add_symbol(
2075            "User",
2076            ExportedSymbol {
2077                kind: ExportKind::Record,
2078                visibility: Visibility::Public,
2079                ty: TypeRef("User".to_string()),
2080                detail: ExportDetail::Record {
2081                    fields: vec![
2082                        ("name".to_string(), TypeRef("String".to_string())),
2083                        ("age".to_string(), TypeRef("Int".to_string())),
2084                    ],
2085                    generic_params: vec![],
2086                    methods: HashMap::new(),
2087                },
2088            },
2089        );
2090        exports.add_symbol(
2091            "Role",
2092            ExportedSymbol {
2093                kind: ExportKind::Enum,
2094                visibility: Visibility::Public,
2095                ty: TypeRef("Role".to_string()),
2096                detail: ExportDetail::Enum {
2097                    variants: vec![],
2098                    generic_params: vec![],
2099                },
2100            },
2101        );
2102        exports.add_symbol(
2103            "default_user",
2104            ExportedSymbol {
2105                kind: ExportKind::Function,
2106                visibility: Visibility::Public,
2107                ty: TypeRef("Fn() -> User".to_string()),
2108                detail: ExportDetail::None,
2109            },
2110        );
2111        exports.add_symbol(
2112            "internal_helper",
2113            ExportedSymbol {
2114                kind: ExportKind::Function,
2115                visibility: Visibility::Internal,
2116                ty: TypeRef("Fn() -> Void".to_string()),
2117                detail: ExportDetail::None,
2118            },
2119        );
2120        exports.add_symbol(
2121            "private_secret",
2122            ExportedSymbol {
2123                kind: ExportKind::Function,
2124                visibility: Visibility::Private,
2125                ty: TypeRef("Fn() -> Void".to_string()),
2126                detail: ExportDetail::None,
2127            },
2128        );
2129        reg.register(exports);
2130        reg
2131    }
2132
2133    #[test]
2134    fn registry_named_import_resolves_kind() {
2135        let registry = sample_registry();
2136        let import = ImportDecl {
2137            id: 10,
2138            span: sp(),
2139            visibility: Visibility::Private,
2140            path: mpath(&["app", "models"]),
2141            items: ImportItems::Named(vec![
2142                ImportedName {
2143                    span: sp(),
2144                    name: ident("User"),
2145                    alias: None,
2146                },
2147                ImportedName {
2148                    span: sp(),
2149                    name: ident("default_user"),
2150                    alias: None,
2151                },
2152            ]),
2153        };
2154        let module = simple_module(vec![import], vec![]);
2155        let mut st = SymbolTable::new();
2156        let diag = resolve_names_with_registry(&module, &mut st, &registry);
2157        assert!(
2158            !diag.has_errors(),
2159            "unexpected errors: {:?}",
2160            diag.iter().collect::<Vec<_>>()
2161        );
2162        let user = st.lookup_peek("User").expect("User should be bound");
2163        assert_eq!(user.resolved.kind, NameKind::Type);
2164        assert!(user.is_import);
2165        let dfn = st
2166            .lookup_peek("default_user")
2167            .expect("default_user should be bound");
2168        assert_eq!(dfn.resolved.kind, NameKind::Function);
2169    }
2170
2171    #[test]
2172    fn registry_named_import_with_alias_resolves_kind() {
2173        let registry = sample_registry();
2174        let import = ImportDecl {
2175            id: 10,
2176            span: sp(),
2177            visibility: Visibility::Private,
2178            path: mpath(&["app", "models"]),
2179            items: ImportItems::Named(vec![ImportedName {
2180                span: sp(),
2181                name: ident("User"),
2182                alias: Some(ident("AppUser")),
2183            }]),
2184        };
2185        let module = simple_module(vec![import], vec![]);
2186        let mut st = SymbolTable::new();
2187        let diag = resolve_names_with_registry(&module, &mut st, &registry);
2188        assert!(!diag.has_errors());
2189        assert!(st.lookup_peek("AppUser").is_some());
2190        assert!(st.lookup_peek("User").is_none());
2191        assert_eq!(
2192            st.lookup_peek("AppUser").unwrap().resolved.kind,
2193            NameKind::Type
2194        );
2195    }
2196
2197    #[test]
2198    fn registry_named_import_missing_symbol_produces_error() {
2199        let registry = sample_registry();
2200        let import = ImportDecl {
2201            id: 10,
2202            span: sp(),
2203            visibility: Visibility::Private,
2204            path: mpath(&["app", "models"]),
2205            items: ImportItems::Named(vec![ImportedName {
2206                span: sp(),
2207                name: ident("NonExistent"),
2208                alias: None,
2209            }]),
2210        };
2211        let module = simple_module(vec![import], vec![]);
2212        let mut st = SymbolTable::new();
2213        let diag = resolve_names_with_registry(&module, &mut st, &registry);
2214        assert!(diag.has_errors());
2215        let msgs: Vec<_> = diag.iter().map(|d| d.message.clone()).collect();
2216        assert!(msgs.iter().any(|m| m.contains("NonExistent")));
2217        assert!(msgs.iter().any(|m| m.contains("not exported")));
2218    }
2219
2220    #[test]
2221    fn registry_named_import_private_symbol_produces_error() {
2222        let registry = sample_registry();
2223        let import = ImportDecl {
2224            id: 10,
2225            span: sp(),
2226            visibility: Visibility::Private,
2227            path: mpath(&["app", "models"]),
2228            items: ImportItems::Named(vec![ImportedName {
2229                span: sp(),
2230                name: ident("private_secret"),
2231                alias: None,
2232            }]),
2233        };
2234        let module = simple_module(vec![import], vec![]);
2235        let mut st = SymbolTable::new();
2236        let diag = resolve_names_with_registry(&module, &mut st, &registry);
2237        assert!(diag.has_errors());
2238        let msgs: Vec<_> = diag.iter().map(|d| d.message.clone()).collect();
2239        assert!(msgs.iter().any(|m| m.contains("private")));
2240    }
2241
2242    #[test]
2243    fn registry_named_import_module_not_found_produces_error() {
2244        let registry = sample_registry();
2245        let import = ImportDecl {
2246            id: 10,
2247            span: sp(),
2248            visibility: Visibility::Private,
2249            path: mpath(&["no", "such", "module"]),
2250            items: ImportItems::Named(vec![ImportedName {
2251                span: sp(),
2252                name: ident("Foo"),
2253                alias: None,
2254            }]),
2255        };
2256        let module = simple_module(vec![import], vec![]);
2257        let mut st = SymbolTable::new();
2258        let diag = resolve_names_with_registry(&module, &mut st, &registry);
2259        assert!(diag.has_errors());
2260        let msgs: Vec<_> = diag.iter().map(|d| d.message.clone()).collect();
2261        assert!(msgs.iter().any(|m| m.contains("not found")));
2262    }
2263
2264    #[test]
2265    fn registry_glob_import_defines_all_public_names() {
2266        let registry = sample_registry();
2267        let import = ImportDecl {
2268            id: 10,
2269            span: sp(),
2270            visibility: Visibility::Private,
2271            path: mpath(&["app", "models"]),
2272            items: ImportItems::Glob,
2273        };
2274        let module = simple_module(vec![import], vec![]);
2275        let mut st = SymbolTable::new();
2276        let diag = resolve_names_with_registry(&module, &mut st, &registry);
2277        assert!(
2278            !diag.has_errors(),
2279            "unexpected errors: {:?}",
2280            diag.iter().collect::<Vec<_>>()
2281        );
2282        // Public symbols should be defined.
2283        assert!(st.lookup_peek("User").is_some());
2284        assert!(st.lookup_peek("Role").is_some());
2285        assert!(st.lookup_peek("default_user").is_some());
2286        // Internal is also visible (within-package access).
2287        assert!(st.lookup_peek("internal_helper").is_some());
2288        // Private must NOT be imported.
2289        assert!(
2290            st.lookup_peek("private_secret").is_none()
2291                || st.lookup_peek("private_secret").unwrap().resolved.kind == NameKind::Builtin
2292        );
2293        // Sentinel still present for backward compat.
2294        assert!(st.has_wildcard_import());
2295    }
2296
2297    #[test]
2298    fn registry_glob_import_module_not_found_produces_error() {
2299        let registry = sample_registry();
2300        let import = ImportDecl {
2301            id: 10,
2302            span: sp(),
2303            visibility: Visibility::Private,
2304            path: mpath(&["no", "such", "module"]),
2305            items: ImportItems::Glob,
2306        };
2307        let module = simple_module(vec![import], vec![]);
2308        let mut st = SymbolTable::new();
2309        let diag = resolve_names_with_registry(&module, &mut st, &registry);
2310        assert!(diag.has_errors());
2311        let msgs: Vec<_> = diag.iter().map(|d| d.message.clone()).collect();
2312        assert!(msgs.iter().any(|m| m.contains("not found")));
2313    }
2314
2315    #[test]
2316    fn registry_resolved_import_used_in_body_no_errors() {
2317        // Simulates: use app.models.{User, default_user}
2318        // fn main() { default_user() }
2319        let registry = sample_registry();
2320        let import = ImportDecl {
2321            id: 10,
2322            span: sp(),
2323            visibility: Visibility::Private,
2324            path: mpath(&["app", "models"]),
2325            items: ImportItems::Named(vec![
2326                ImportedName {
2327                    span: sp(),
2328                    name: ident("User"),
2329                    alias: None,
2330                },
2331                ImportedName {
2332                    span: sp(),
2333                    name: ident("default_user"),
2334                    alias: None,
2335                },
2336            ]),
2337        };
2338        let module = simple_module(
2339            vec![import],
2340            vec![Item::Fn(FnDecl {
2341                id: 1,
2342                span: sp(),
2343                annotations: vec![],
2344                visibility: Visibility::Private,
2345                is_async: false,
2346                name: ident("main"),
2347                generic_params: vec![],
2348                params: vec![],
2349                return_type: None,
2350                effect_clause: vec![],
2351                where_clause: vec![],
2352                body: Some(Block {
2353                    id: 100,
2354                    span: sp(),
2355                    stmts: vec![],
2356                    tail: Some(Box::new(Expr::Call {
2357                        id: 50,
2358                        span: sp(),
2359                        callee: Box::new(Expr::Identifier {
2360                            id: 51,
2361                            span: sp(),
2362                            name: ident("default_user"),
2363                        }),
2364                        type_args: vec![],
2365                        args: vec![],
2366                    })),
2367                }),
2368            })],
2369        );
2370        let mut st = SymbolTable::new();
2371        let diag = resolve_names_with_registry(&module, &mut st, &registry);
2372        assert!(
2373            !diag.has_errors(),
2374            "unexpected errors: {:?}",
2375            diag.iter().collect::<Vec<_>>()
2376        );
2377        // default_user call resolves to the import's def_id.
2378        let resolved = st
2379            .resolutions
2380            .get(&51)
2381            .expect("default_user should resolve");
2382        assert_eq!(resolved.kind, NameKind::Function);
2383        // User import is unused — should produce a warning.
2384        let warnings: Vec<_> = diag
2385            .iter()
2386            .filter(|d| d.severity == bock_errors::Severity::Warning)
2387            .collect();
2388        assert!(
2389            warnings.iter().any(|w| w.message.contains("User")),
2390            "expected unused import warning for User"
2391        );
2392    }
2393
2394    #[test]
2395    fn empty_registry_behaves_like_single_file() {
2396        // With an empty registry, named imports stay Unresolved (no error
2397        // since the module simply isn't registered).
2398        let registry = ModuleRegistry::new();
2399        let import = ImportDecl {
2400            id: 10,
2401            span: sp(),
2402            visibility: Visibility::Private,
2403            path: mpath(&["unknown", "module"]),
2404            items: ImportItems::Named(vec![ImportedName {
2405                span: sp(),
2406                name: ident("Thing"),
2407                alias: None,
2408            }]),
2409        };
2410        let module = simple_module(vec![import], vec![]);
2411        let mut st = SymbolTable::new();
2412        let diag = resolve_names_with_registry(&module, &mut st, &registry);
2413        // Empty registry triggers module-not-found for the import.
2414        assert!(diag.has_errors());
2415        // But the binding is still defined (as Unresolved) so the compiler
2416        // can continue downstream.
2417        let b = st.lookup_peek("Thing").expect("Thing should still be bound");
2418        assert_eq!(b.resolved.kind, NameKind::Unresolved);
2419    }
2420
2421    #[test]
2422    fn no_registry_leaves_imports_unresolved() {
2423        // Plain resolve_names (no registry) keeps the old behavior.
2424        let import = ImportDecl {
2425            id: 10,
2426            span: sp(),
2427            visibility: Visibility::Private,
2428            path: mpath(&["app", "models"]),
2429            items: ImportItems::Named(vec![ImportedName {
2430                span: sp(),
2431                name: ident("User"),
2432                alias: None,
2433            }]),
2434        };
2435        let module = simple_module(vec![import], vec![]);
2436        let mut st = SymbolTable::new();
2437        let diag = resolve_names(&module, &mut st);
2438        // No errors: single-file mode doesn't validate imports.
2439        assert!(!diag.has_errors());
2440        let b = st.lookup_peek("User").expect("User should be bound");
2441        assert_eq!(b.resolved.kind, NameKind::Unresolved);
2442    }
2443
2444    #[test]
2445    fn registry_internal_symbol_is_importable() {
2446        let registry = sample_registry();
2447        let import = ImportDecl {
2448            id: 10,
2449            span: sp(),
2450            visibility: Visibility::Private,
2451            path: mpath(&["app", "models"]),
2452            items: ImportItems::Named(vec![ImportedName {
2453                span: sp(),
2454                name: ident("internal_helper"),
2455                alias: None,
2456            }]),
2457        };
2458        let module = simple_module(vec![import], vec![]);
2459        let mut st = SymbolTable::new();
2460        let diag = resolve_names_with_registry(&module, &mut st, &registry);
2461        assert!(
2462            !diag.has_errors(),
2463            "internal symbols should be importable: {:?}",
2464            diag.iter().collect::<Vec<_>>()
2465        );
2466        let b = st
2467            .lookup_peek("internal_helper")
2468            .expect("internal_helper should be bound");
2469        assert_eq!(b.resolved.kind, NameKind::Function);
2470    }
2471
2472    #[test]
2473    fn registry_named_enum_import_seeds_variant_constructors() {
2474        // Build a registry with a module exporting an enum with variants.
2475        let mut reg = ModuleRegistry::new();
2476        let mut exports = ModuleExports::new("colors", "colors.bock");
2477        exports.add_symbol(
2478            "Color",
2479            ExportedSymbol {
2480                kind: ExportKind::Enum,
2481                visibility: Visibility::Public,
2482                ty: TypeRef("Color".to_string()),
2483                detail: ExportDetail::Enum {
2484                    variants: vec![
2485                        EnumVariantExport {
2486                            name: "Red".to_string(),
2487                            constructor_type: None,
2488                            fields: None,
2489                        },
2490                        EnumVariantExport {
2491                            name: "Green".to_string(),
2492                            constructor_type: None,
2493                            fields: None,
2494                        },
2495                        EnumVariantExport {
2496                            name: "Blue".to_string(),
2497                            constructor_type: None,
2498                            fields: None,
2499                        },
2500                    ],
2501                    generic_params: vec![],
2502                },
2503            },
2504        );
2505        reg.register(exports);
2506
2507        // use colors.{Color}
2508        let import = ImportDecl {
2509            id: 10,
2510            span: sp(),
2511            visibility: Visibility::Private,
2512            path: mpath(&["colors"]),
2513            items: ImportItems::Named(vec![ImportedName {
2514                span: sp(),
2515                name: ident("Color"),
2516                alias: None,
2517            }]),
2518        };
2519        let module = simple_module(vec![import], vec![]);
2520        let mut st = SymbolTable::new();
2521        let diag = resolve_names_with_registry(&module, &mut st, &reg);
2522        assert!(
2523            !diag.has_errors(),
2524            "unexpected errors: {:?}",
2525            diag.iter().collect::<Vec<_>>()
2526        );
2527        // The enum type itself should be defined.
2528        assert!(st.lookup_peek("Color").is_some());
2529        // Each variant constructor should also be in scope.
2530        let red = st.lookup_peek("Red").expect("Red should be in scope");
2531        assert_eq!(red.resolved.kind, NameKind::Function);
2532        // Auto-seeded variants are not marked as imports to avoid
2533        // spurious "unused import" warnings.
2534        assert!(!red.is_import);
2535        assert!(st.lookup_peek("Green").is_some());
2536        assert!(st.lookup_peek("Blue").is_some());
2537    }
2538
2539    #[test]
2540    fn registry_glob_enum_import_seeds_variant_constructors() {
2541        // Same registry as above but using glob import.
2542        let mut reg = ModuleRegistry::new();
2543        let mut exports = ModuleExports::new("colors", "colors.bock");
2544        exports.add_symbol(
2545            "Color",
2546            ExportedSymbol {
2547                kind: ExportKind::Enum,
2548                visibility: Visibility::Public,
2549                ty: TypeRef("Color".to_string()),
2550                detail: ExportDetail::Enum {
2551                    variants: vec![
2552                        EnumVariantExport {
2553                            name: "Red".to_string(),
2554                            constructor_type: None,
2555                            fields: None,
2556                        },
2557                    ],
2558                    generic_params: vec![],
2559                },
2560            },
2561        );
2562        reg.register(exports);
2563
2564        // use colors.*
2565        let import = ImportDecl {
2566            id: 10,
2567            span: sp(),
2568            visibility: Visibility::Private,
2569            path: mpath(&["colors"]),
2570            items: ImportItems::Glob,
2571        };
2572        let module = simple_module(vec![import], vec![]);
2573        let mut st = SymbolTable::new();
2574        let diag = resolve_names_with_registry(&module, &mut st, &reg);
2575        assert!(
2576            !diag.has_errors(),
2577            "unexpected errors: {:?}",
2578            diag.iter().collect::<Vec<_>>()
2579        );
2580        assert!(st.lookup_peek("Color").is_some());
2581        let red = st.lookup_peek("Red").expect("Red should be in scope via glob");
2582        assert_eq!(red.resolved.kind, NameKind::Function);
2583        assert!(!red.is_import);
2584    }
2585
2586    #[test]
2587    fn registry_glob_with_body_resolves_names() {
2588        // use app.models.*
2589        // fn test() { User }
2590        let registry = sample_registry();
2591        let import = ImportDecl {
2592            id: 10,
2593            span: sp(),
2594            visibility: Visibility::Private,
2595            path: mpath(&["app", "models"]),
2596            items: ImportItems::Glob,
2597        };
2598        let module = simple_module(
2599            vec![import],
2600            vec![Item::Fn(FnDecl {
2601                id: 1,
2602                span: sp(),
2603                annotations: vec![],
2604                visibility: Visibility::Private,
2605                is_async: false,
2606                name: ident("test"),
2607                generic_params: vec![],
2608                params: vec![],
2609                return_type: None,
2610                effect_clause: vec![],
2611                where_clause: vec![],
2612                body: Some(Block {
2613                    id: 100,
2614                    span: sp(),
2615                    stmts: vec![],
2616                    tail: Some(Box::new(Expr::Identifier {
2617                        id: 99,
2618                        span: sp(),
2619                        name: ident("User"),
2620                    })),
2621                }),
2622            })],
2623        );
2624        let mut st = SymbolTable::new();
2625        let diag = resolve_names_with_registry(&module, &mut st, &registry);
2626        assert!(
2627            !diag.has_errors(),
2628            "unexpected errors: {:?}",
2629            diag.iter().collect::<Vec<_>>()
2630        );
2631        let resolved = st
2632            .resolutions
2633            .get(&99)
2634            .expect("User should resolve from glob import");
2635        assert_eq!(resolved.kind, NameKind::Type);
2636    }
2637}