Skip to main content

bock_air/
registry.rs

1//! Cross-file module registry for the Bock compiler.
2//!
3//! The [`ModuleRegistry`] is built incrementally as modules are compiled in
4//! dependency order. Each module's public symbols are collected into a
5//! [`ModuleExports`] entry after compilation, and downstream modules query
6//! the registry during name resolution and type checking to resolve imports.
7//!
8//! # Type Representation
9//!
10//! This module uses [`TypeRef`] (a lightweight string-based handle) rather
11//! than the full `Type` algebra from `bock-types`, because `bock-air` sits
12//! upstream of `bock-types` in the crate dependency chain. The actual type
13//! system integration happens in later compilation passes.
14
15use std::collections::HashMap;
16
17use bock_ast::Visibility;
18
19use crate::stubs::TypeRef;
20
21// ─── Module identifier ───────────────────────────────────────────────────────
22
23/// Unique module identifier: dot-separated path (e.g., `"app.models"`).
24pub type ModuleId = String;
25
26// ─── Error types ─────────────────────────────────────────────────────────────
27
28/// Errors returned by registry lookup operations.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum RegistryError {
31    /// No module with the given ID has been registered.
32    ModuleNotFound { module_id: String },
33    /// The module exists but does not export a symbol with this name.
34    SymbolNotFound { module_id: String, name: String },
35    /// The symbol exists but is not visible from the requesting context.
36    NotVisible { module_id: String, name: String },
37}
38
39impl std::fmt::Display for RegistryError {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        match self {
42            RegistryError::ModuleNotFound { module_id } => {
43                write!(f, "module not found: `{module_id}`")
44            }
45            RegistryError::SymbolNotFound { module_id, name } => {
46                write!(f, "symbol `{name}` not found in module `{module_id}`")
47            }
48            RegistryError::NotVisible { module_id, name } => {
49                write!(
50                    f,
51                    "symbol `{name}` in module `{module_id}` is not visible"
52                )
53            }
54        }
55    }
56}
57
58impl std::error::Error for RegistryError {}
59
60// ─── Registry ────────────────────────────────────────────────────────────────
61
62/// The central cross-file symbol registry.
63///
64/// Built incrementally as modules are compiled in dependency order.
65/// Queried by `resolve.rs` and `checker.rs` when processing imports.
66#[derive(Debug, Default)]
67pub struct ModuleRegistry {
68    /// Per-module export tables, keyed by dot-separated module path.
69    modules: HashMap<ModuleId, ModuleExports>,
70}
71
72impl ModuleRegistry {
73    /// Creates an empty registry.
74    #[must_use]
75    pub fn new() -> Self {
76        Self::default()
77    }
78
79    /// Registers a module's exports after it has been fully compiled.
80    ///
81    /// If a module with the same ID was already registered, it is replaced.
82    pub fn register(&mut self, exports: ModuleExports) {
83        self.modules.insert(exports.module_id.clone(), exports);
84    }
85
86    /// Checks whether a module with the given ID has been registered.
87    #[must_use]
88    pub fn has_module(&self, module_id: &str) -> bool {
89        self.modules.contains_key(module_id)
90    }
91
92    /// Looks up a module by its dot-path ID.
93    #[must_use]
94    pub fn get_module(&self, module_id: &str) -> Option<&ModuleExports> {
95        self.modules.get(module_id)
96    }
97
98    /// Resolves a specific symbol from a module.
99    ///
100    /// Returns only symbols that are visible outside the module
101    /// (`Public` or `Internal`). Private symbols produce a
102    /// [`RegistryError::NotVisible`] error.
103    ///
104    /// Re-exports are followed transitively.
105    pub fn resolve_symbol(
106        &self,
107        module_id: &str,
108        name: &str,
109    ) -> Result<&ExportedSymbol, RegistryError> {
110        let exports = self
111            .modules
112            .get(module_id)
113            .ok_or_else(|| RegistryError::ModuleNotFound {
114                module_id: module_id.to_string(),
115            })?;
116
117        // Check direct exports first.
118        if let Some(sym) = exports.symbols.get(name) {
119            return if sym.visibility == Visibility::Private {
120                Err(RegistryError::NotVisible {
121                    module_id: module_id.to_string(),
122                    name: name.to_string(),
123                })
124            } else {
125                Ok(sym)
126            };
127        }
128
129        // Check re-exports: follow the chain to the source module.
130        if let Some((source_module, original_name)) = exports.reexports.get(name) {
131            return self.resolve_symbol(source_module, original_name);
132        }
133
134        Err(RegistryError::SymbolNotFound {
135            module_id: module_id.to_string(),
136            name: name.to_string(),
137        })
138    }
139
140    /// Returns all publicly visible symbols from a module (for glob imports).
141    ///
142    /// Includes both direct exports and re-exports that have `Public` or
143    /// `Internal` visibility.
144    pub fn resolve_glob(
145        &self,
146        module_id: &str,
147    ) -> Result<Vec<(&str, &ExportedSymbol)>, RegistryError> {
148        let exports = self
149            .modules
150            .get(module_id)
151            .ok_or_else(|| RegistryError::ModuleNotFound {
152                module_id: module_id.to_string(),
153            })?;
154
155        let mut result: Vec<(&str, &ExportedSymbol)> = exports
156            .symbols
157            .iter()
158            .filter(|(_, sym)| sym.visibility != Visibility::Private)
159            .map(|(name, sym)| (name.as_str(), sym))
160            .collect();
161
162        // Resolve re-exports and include them.
163        for (local_name, (source_module, original_name)) in &exports.reexports {
164            if let Ok(sym) = self.resolve_symbol(source_module, original_name) {
165                result.push((local_name.as_str(), sym));
166            }
167        }
168
169        result.sort_by_key(|(name, _)| *name);
170        Ok(result)
171    }
172
173    /// Gets the type reference for a specific exported symbol.
174    ///
175    /// Convenience wrapper around [`resolve_symbol`](Self::resolve_symbol)
176    /// that returns just the [`TypeRef`].
177    pub fn get_type(
178        &self,
179        module_id: &str,
180        name: &str,
181    ) -> Result<&TypeRef, RegistryError> {
182        self.resolve_symbol(module_id, name).map(|sym| &sym.ty)
183    }
184
185    /// Returns the number of registered modules.
186    #[must_use]
187    pub fn module_count(&self) -> usize {
188        self.modules.len()
189    }
190}
191
192// ─── Module exports ──────────────────────────────────────────────────────────
193
194/// Everything a downstream module needs to know about an upstream module.
195#[derive(Debug, Clone)]
196pub struct ModuleExports {
197    /// The module's dot-separated path (e.g., `"app.models"`).
198    pub module_id: ModuleId,
199    /// Source file path (for diagnostics).
200    pub source_path: String,
201    /// Exported symbols keyed by name.
202    pub symbols: HashMap<String, ExportedSymbol>,
203    /// Re-exports: names this module re-exports from other modules.
204    /// Key = local name, Value = (source_module_id, original_name).
205    pub reexports: HashMap<String, (ModuleId, String)>,
206}
207
208impl ModuleExports {
209    /// Creates a new, empty export table for a module.
210    #[must_use]
211    pub fn new(module_id: impl Into<String>, source_path: impl Into<String>) -> Self {
212        Self {
213            module_id: module_id.into(),
214            source_path: source_path.into(),
215            symbols: HashMap::new(),
216            reexports: HashMap::new(),
217        }
218    }
219
220    /// Adds a symbol to the export table.
221    pub fn add_symbol(&mut self, name: impl Into<String>, symbol: ExportedSymbol) {
222        self.symbols.insert(name.into(), symbol);
223    }
224
225    /// Adds a re-export entry.
226    pub fn add_reexport(
227        &mut self,
228        local_name: impl Into<String>,
229        source_module: impl Into<String>,
230        original_name: impl Into<String>,
231    ) {
232        self.reexports
233            .insert(local_name.into(), (source_module.into(), original_name.into()));
234    }
235}
236
237// ─── Exported symbol ─────────────────────────────────────────────────────────
238
239/// A single exported symbol from a module.
240#[derive(Debug, Clone)]
241pub struct ExportedSymbol {
242    /// What kind of entity this is.
243    pub kind: ExportKind,
244    /// Declared visibility (`Public`, `Internal`, or `Private`).
245    pub visibility: Visibility,
246    /// Lightweight type reference for this symbol.
247    ///
248    /// Uses [`TypeRef`] (a string handle) rather than the full `Type` from
249    /// `bock-types`, since `bock-air` is upstream in the dependency chain.
250    pub ty: TypeRef,
251    /// Additional type information needed by importers.
252    pub detail: ExportDetail,
253}
254
255// ─── Export kind ─────────────────────────────────────────────────────────────
256
257/// Classification of an exported symbol.
258#[derive(Debug, Clone, Copy, PartialEq, Eq)]
259pub enum ExportKind {
260    /// A function or method.
261    Function,
262    /// A record (value-type) declaration.
263    Record,
264    /// An enum (algebraic data type) declaration.
265    Enum,
266    /// A trait declaration.
267    Trait,
268    /// An algebraic effect declaration.
269    Effect,
270    /// A type alias.
271    TypeAlias,
272    /// A constant declaration.
273    Constant,
274}
275
276// ─── Export detail ───────────────────────────────────────────────────────────
277
278/// Type-level details that importers need beyond the primary [`TypeRef`].
279#[derive(Debug, Clone)]
280pub enum ExportDetail {
281    /// No additional detail needed (functions, constants).
282    None,
283
284    /// Record: field names, types, generic parameters, and inherent methods.
285    Record {
286        /// (field_name, field_type_ref) pairs.
287        fields: Vec<(String, TypeRef)>,
288        /// Names of generic type parameters.
289        generic_params: Vec<String>,
290        /// Inherent impl methods: method_name → method_type_ref.
291        methods: HashMap<String, TypeRef>,
292    },
293
294    /// Enum: variant constructors and their types.
295    Enum {
296        /// Variant definitions.
297        variants: Vec<EnumVariantExport>,
298        /// Names of generic type parameters.
299        generic_params: Vec<String>,
300    },
301
302    /// Trait: method signatures.
303    Trait {
304        /// method_name → method_type_ref.
305        methods: HashMap<String, TypeRef>,
306    },
307
308    /// Effect: operation signatures and component effects.
309    Effect {
310        /// (operation_name, operation_type_ref) pairs.
311        operations: Vec<(String, TypeRef)>,
312        /// Component effect names (for composite effects).
313        components: Vec<String>,
314    },
315
316    /// Type alias: the underlying type.
317    TypeAlias {
318        /// The type this alias expands to.
319        underlying: TypeRef,
320    },
321}
322
323// ─── Enum variant export ─────────────────────────────────────────────────────
324
325/// An exported enum variant's constructor information.
326#[derive(Debug, Clone)]
327pub struct EnumVariantExport {
328    /// Variant name (e.g., `"Some"`, `"None"`).
329    pub name: String,
330    /// For tuple variants: the constructor function type.
331    /// `None` for unit variants.
332    pub constructor_type: Option<TypeRef>,
333    /// For struct variants: (field_name, field_type_ref) pairs.
334    /// `None` for unit and tuple variants.
335    pub fields: Option<Vec<(String, TypeRef)>>,
336}
337
338// ─── Tests ───────────────────────────────────────────────────────────────────
339
340#[cfg(test)]
341mod tests {
342    use super::*;
343
344    // ── Helpers ───────────────────────────────────────────────────────────
345
346    fn make_fn_symbol(name: &str, vis: Visibility) -> (String, ExportedSymbol) {
347        (
348            name.to_string(),
349            ExportedSymbol {
350                kind: ExportKind::Function,
351                visibility: vis,
352                ty: TypeRef(format!("Fn() -> Void")),
353                detail: ExportDetail::None,
354            },
355        )
356    }
357
358    fn make_record_symbol(
359        name: &str,
360        vis: Visibility,
361        fields: Vec<(&str, &str)>,
362        generics: Vec<&str>,
363    ) -> (String, ExportedSymbol) {
364        (
365            name.to_string(),
366            ExportedSymbol {
367                kind: ExportKind::Record,
368                visibility: vis,
369                ty: TypeRef(name.to_string()),
370                detail: ExportDetail::Record {
371                    fields: fields
372                        .into_iter()
373                        .map(|(n, t)| (n.to_string(), TypeRef(t.to_string())))
374                        .collect(),
375                    generic_params: generics.into_iter().map(String::from).collect(),
376                    methods: HashMap::new(),
377                },
378            },
379        )
380    }
381
382    fn make_enum_symbol(
383        name: &str,
384        vis: Visibility,
385        variants: Vec<EnumVariantExport>,
386        generics: Vec<&str>,
387    ) -> (String, ExportedSymbol) {
388        (
389            name.to_string(),
390            ExportedSymbol {
391                kind: ExportKind::Enum,
392                visibility: vis,
393                ty: TypeRef(name.to_string()),
394                detail: ExportDetail::Enum {
395                    variants,
396                    generic_params: generics.into_iter().map(String::from).collect(),
397                },
398            },
399        )
400    }
401
402    fn sample_module() -> ModuleExports {
403        let mut exports = ModuleExports::new("app.models", "src/app/models.bock");
404
405        // Public function
406        let (name, sym) = make_fn_symbol("create_user", Visibility::Public);
407        exports.add_symbol(name, sym);
408
409        // Public record
410        let (name, sym) = make_record_symbol(
411            "User",
412            Visibility::Public,
413            vec![("id", "Int"), ("name", "String")],
414            vec![],
415        );
416        exports.add_symbol(name, sym);
417
418        // Public enum
419        let (name, sym) = make_enum_symbol(
420            "Status",
421            Visibility::Public,
422            vec![
423                EnumVariantExport {
424                    name: "Active".to_string(),
425                    constructor_type: None,
426                    fields: None,
427                },
428                EnumVariantExport {
429                    name: "Suspended".to_string(),
430                    constructor_type: Some(TypeRef("Fn(String) -> Status".to_string())),
431                    fields: None,
432                },
433            ],
434            vec![],
435        );
436        exports.add_symbol(name, sym);
437
438        // Private (internal) helper — should NOT be visible outside
439        let (name, sym) = make_fn_symbol("hash_password", Visibility::Private);
440        exports.add_symbol(name, sym);
441
442        // Internal function — visible within the package
443        let (name, sym) = make_fn_symbol("validate_email", Visibility::Internal);
444        exports.add_symbol(name, sym);
445
446        exports
447    }
448
449    // ── Registration and basic lookup ────────────────────────────────────
450
451    #[test]
452    fn register_and_lookup_module() {
453        let mut reg = ModuleRegistry::new();
454        assert!(!reg.has_module("app.models"));
455        assert_eq!(reg.module_count(), 0);
456
457        reg.register(sample_module());
458
459        assert!(reg.has_module("app.models"));
460        assert_eq!(reg.module_count(), 1);
461
462        let m = reg.get_module("app.models").unwrap();
463        assert_eq!(m.module_id, "app.models");
464        assert_eq!(m.source_path, "src/app/models.bock");
465    }
466
467    // ── Resolve a specific name ──────────────────────────────────────────
468
469    #[test]
470    fn resolve_public_function() {
471        let mut reg = ModuleRegistry::new();
472        reg.register(sample_module());
473
474        let sym = reg.resolve_symbol("app.models", "create_user").unwrap();
475        assert_eq!(sym.kind, ExportKind::Function);
476        assert_eq!(sym.visibility, Visibility::Public);
477    }
478
479    #[test]
480    fn resolve_public_record() {
481        let mut reg = ModuleRegistry::new();
482        reg.register(sample_module());
483
484        let sym = reg.resolve_symbol("app.models", "User").unwrap();
485        assert_eq!(sym.kind, ExportKind::Record);
486        match &sym.detail {
487            ExportDetail::Record { fields, generic_params, .. } => {
488                assert_eq!(fields.len(), 2);
489                assert_eq!(fields[0].0, "id");
490                assert!(generic_params.is_empty());
491            }
492            _ => panic!("expected Record detail"),
493        }
494    }
495
496    #[test]
497    fn resolve_public_enum() {
498        let mut reg = ModuleRegistry::new();
499        reg.register(sample_module());
500
501        let sym = reg.resolve_symbol("app.models", "Status").unwrap();
502        assert_eq!(sym.kind, ExportKind::Enum);
503        match &sym.detail {
504            ExportDetail::Enum { variants, .. } => {
505                assert_eq!(variants.len(), 2);
506                assert_eq!(variants[0].name, "Active");
507                assert!(variants[0].constructor_type.is_none());
508                assert_eq!(variants[1].name, "Suspended");
509                assert!(variants[1].constructor_type.is_some());
510            }
511            _ => panic!("expected Enum detail"),
512        }
513    }
514
515    #[test]
516    fn resolve_internal_symbol() {
517        let mut reg = ModuleRegistry::new();
518        reg.register(sample_module());
519
520        let sym = reg.resolve_symbol("app.models", "validate_email").unwrap();
521        assert_eq!(sym.kind, ExportKind::Function);
522        assert_eq!(sym.visibility, Visibility::Internal);
523    }
524
525    // ── Resolve glob imports ─────────────────────────────────────────────
526
527    #[test]
528    fn resolve_glob_excludes_private() {
529        let mut reg = ModuleRegistry::new();
530        reg.register(sample_module());
531
532        let syms = reg.resolve_glob("app.models").unwrap();
533        let names: Vec<&str> = syms.iter().map(|(n, _)| *n).collect();
534
535        // Should include public and internal, but NOT private
536        assert!(names.contains(&"create_user"));
537        assert!(names.contains(&"User"));
538        assert!(names.contains(&"Status"));
539        assert!(names.contains(&"validate_email"));
540        assert!(!names.contains(&"hash_password"));
541    }
542
543    #[test]
544    fn resolve_glob_includes_reexports() {
545        let mut reg = ModuleRegistry::new();
546
547        // Register upstream module
548        let mut upstream = ModuleExports::new("lib.utils", "src/lib/utils.bock");
549        let (name, sym) = make_fn_symbol("format_date", Visibility::Public);
550        upstream.add_symbol(name, sym);
551        reg.register(upstream);
552
553        // Register downstream module that re-exports from upstream
554        let mut downstream = ModuleExports::new("app.helpers", "src/app/helpers.bock");
555        let (name, sym) = make_fn_symbol("helper_fn", Visibility::Public);
556        downstream.add_symbol(name, sym);
557        downstream.add_reexport("format_date", "lib.utils", "format_date");
558        reg.register(downstream);
559
560        let syms = reg.resolve_glob("app.helpers").unwrap();
561        let names: Vec<&str> = syms.iter().map(|(n, _)| *n).collect();
562
563        assert!(names.contains(&"helper_fn"));
564        assert!(names.contains(&"format_date"));
565    }
566
567    // ── Missing module → error ───────────────────────────────────────────
568
569    #[test]
570    fn missing_module_error() {
571        let reg = ModuleRegistry::new();
572
573        let err = reg.resolve_symbol("no.such.module", "foo").unwrap_err();
574        assert_eq!(
575            err,
576            RegistryError::ModuleNotFound {
577                module_id: "no.such.module".to_string(),
578            }
579        );
580    }
581
582    #[test]
583    fn missing_module_glob_error() {
584        let reg = ModuleRegistry::new();
585
586        let err = reg.resolve_glob("no.such.module").unwrap_err();
587        assert_eq!(
588            err,
589            RegistryError::ModuleNotFound {
590                module_id: "no.such.module".to_string(),
591            }
592        );
593    }
594
595    #[test]
596    fn missing_module_get_type_error() {
597        let reg = ModuleRegistry::new();
598
599        let err = reg.get_type("no.such.module", "Foo").unwrap_err();
600        assert!(matches!(err, RegistryError::ModuleNotFound { .. }));
601    }
602
603    // ── Missing name → error ─────────────────────────────────────────────
604
605    #[test]
606    fn missing_name_error() {
607        let mut reg = ModuleRegistry::new();
608        reg.register(sample_module());
609
610        let err = reg
611            .resolve_symbol("app.models", "nonexistent")
612            .unwrap_err();
613        assert_eq!(
614            err,
615            RegistryError::SymbolNotFound {
616                module_id: "app.models".to_string(),
617                name: "nonexistent".to_string(),
618            }
619        );
620    }
621
622    // ── Visibility: private names not visible outside ────────────────────
623
624    #[test]
625    fn private_symbol_not_visible() {
626        let mut reg = ModuleRegistry::new();
627        reg.register(sample_module());
628
629        let err = reg
630            .resolve_symbol("app.models", "hash_password")
631            .unwrap_err();
632        assert_eq!(
633            err,
634            RegistryError::NotVisible {
635                module_id: "app.models".to_string(),
636                name: "hash_password".to_string(),
637            }
638        );
639    }
640
641    // ── get_type convenience ─────────────────────────────────────────────
642
643    #[test]
644    fn get_type_returns_type_ref() {
645        let mut reg = ModuleRegistry::new();
646        reg.register(sample_module());
647
648        let ty = reg.get_type("app.models", "User").unwrap();
649        assert_eq!(ty.0, "User");
650    }
651
652    // ── Re-export resolution ─────────────────────────────────────────────
653
654    #[test]
655    fn resolve_reexport_transitively() {
656        let mut reg = ModuleRegistry::new();
657
658        // Module A exports "greet"
659        let mut mod_a = ModuleExports::new("mod_a", "a.bock");
660        let (name, sym) = make_fn_symbol("greet", Visibility::Public);
661        mod_a.add_symbol(name, sym);
662        reg.register(mod_a);
663
664        // Module B re-exports "greet" from A
665        let mut mod_b = ModuleExports::new("mod_b", "b.bock");
666        mod_b.add_reexport("greet", "mod_a", "greet");
667        reg.register(mod_b);
668
669        // Module C re-exports "greet" from B (transitive chain)
670        let mut mod_c = ModuleExports::new("mod_c", "c.bock");
671        mod_c.add_reexport("greet", "mod_b", "greet");
672        reg.register(mod_c);
673
674        // Should resolve through B → A
675        let sym = reg.resolve_symbol("mod_c", "greet").unwrap();
676        assert_eq!(sym.kind, ExportKind::Function);
677        assert_eq!(sym.visibility, Visibility::Public);
678    }
679
680    // ── Multiple modules ─────────────────────────────────────────────────
681
682    #[test]
683    fn multiple_modules_independent() {
684        let mut reg = ModuleRegistry::new();
685
686        let mut m1 = ModuleExports::new("pkg.alpha", "alpha.bock");
687        let (name, sym) = make_fn_symbol("alpha_fn", Visibility::Public);
688        m1.add_symbol(name, sym);
689
690        let mut m2 = ModuleExports::new("pkg.beta", "beta.bock");
691        let (name, sym) = make_fn_symbol("beta_fn", Visibility::Public);
692        m2.add_symbol(name, sym);
693
694        reg.register(m1);
695        reg.register(m2);
696
697        assert_eq!(reg.module_count(), 2);
698        assert!(reg.resolve_symbol("pkg.alpha", "alpha_fn").is_ok());
699        assert!(reg.resolve_symbol("pkg.beta", "beta_fn").is_ok());
700        assert!(reg.resolve_symbol("pkg.alpha", "beta_fn").is_err());
701        assert!(reg.resolve_symbol("pkg.beta", "alpha_fn").is_err());
702    }
703
704    // ── Replace existing module ──────────────────────────────────────────
705
706    #[test]
707    fn register_replaces_existing() {
708        let mut reg = ModuleRegistry::new();
709
710        let mut m1 = ModuleExports::new("app.core", "core.bock");
711        let (name, sym) = make_fn_symbol("old_fn", Visibility::Public);
712        m1.add_symbol(name, sym);
713        reg.register(m1);
714
715        assert!(reg.resolve_symbol("app.core", "old_fn").is_ok());
716
717        // Re-register with different exports
718        let mut m2 = ModuleExports::new("app.core", "core.bock");
719        let (name, sym) = make_fn_symbol("new_fn", Visibility::Public);
720        m2.add_symbol(name, sym);
721        reg.register(m2);
722
723        assert!(reg.resolve_symbol("app.core", "old_fn").is_err());
724        assert!(reg.resolve_symbol("app.core", "new_fn").is_ok());
725    }
726}