Skip to main content

xlog_logic/
module.rs

1//! Module system types for XLOG.
2
3use std::collections::HashSet;
4use std::path::PathBuf;
5
6use crate::ast::Program;
7use crate::diagnostics::format_scalar_type;
8use xlog_core::ScalarType;
9
10/// A module path like ["utils", "math"]
11pub(crate) type ModulePath = Vec<String>;
12
13/// Convert module path to string for display
14pub(crate) fn module_path_to_string(path: &[String]) -> String {
15    path.join("/")
16}
17
18/// A loaded module with metadata
19#[derive(Debug)]
20pub struct LoadedModule {
21    /// Logical path recorded when this canonical source was first loaded.
22    ///
23    /// A later alias returned by `ModuleResolver::get_module` can differ from
24    /// this representative path.
25    pub path: ModulePath,
26    /// Filesystem spelling used when this canonical source was first loaded.
27    ///
28    /// This path is not guaranteed to be canonical when the module was reached
29    /// through a relative path or symbolic link.
30    pub source_file: PathBuf,
31    /// Public predicate names
32    pub exports: HashSet<String>,
33    /// Public function names
34    pub function_exports: HashSet<String>,
35    /// The parsed program content
36    pub program: Program,
37}
38
39impl LoadedModule {
40    /// Create a new loaded module (exports initially empty).
41    pub fn new(path: ModulePath, source_file: PathBuf, program: Program) -> Self {
42        Self {
43            path,
44            source_file,
45            exports: HashSet::new(),
46            function_exports: HashSet::new(),
47            program,
48        }
49    }
50}
51
52/// Errors that can occur during module resolution
53#[derive(Debug, Clone)]
54#[non_exhaustive]
55pub enum ModuleError {
56    /// Module file not found
57    NotFound {
58        /// Logical module path that failed to resolve.
59        path: ModulePath,
60        /// Filesystem locations that were searched.
61        searched: Vec<PathBuf>,
62    },
63    /// Circular import detected
64    CircularImport {
65        /// Ordered import cycle that was discovered.
66        cycle: Vec<ModulePath>,
67    },
68    /// Conflicting definitions for an imported function.
69    ImportConflict {
70        /// Function name that has multiple definitions.
71        name: String,
72        /// Module containing the first definition.
73        module1: ModulePath,
74        /// Module containing the conflicting definition.
75        module2: ModulePath,
76    },
77    /// Attempted to import private predicate
78    PrivatePredicate {
79        /// Predicate name that is not exported.
80        name: String,
81        /// Module that owns the private predicate.
82        module: ModulePath,
83    },
84    /// Selected item not exported by a module
85    PredicateNotFound {
86        /// Predicate or function name that is not exported.
87        name: String,
88        /// Module that was expected to export the item.
89        module: ModulePath,
90    },
91    /// An imported module contains program-level constructs that are entry-only.
92    UnsupportedImportedContent {
93        /// Module containing the unsupported constructs.
94        module: ModulePath,
95        /// Deterministically ordered construct categories.
96        constructs: Vec<String>,
97    },
98    /// An exported item depends on module-local support that the import filters out.
99    HiddenDependency {
100        /// Module containing the exported item.
101        module: ModulePath,
102        /// Exported predicate or function whose implementation is incomplete.
103        export: String,
104        /// Private or selectively omitted dependency.
105        dependency: String,
106    },
107    /// A context-free API request used a logical path that names several loaded files.
108    AmbiguousModulePath {
109        /// Logical module path whose source cannot be inferred.
110        path: ModulePath,
111        /// Canonical source files registered for the logical path.
112        candidates: Vec<PathBuf>,
113    },
114    /// A declaration in the entry program or a selected public declaration in
115    /// its resolved imports conflicts with another participating declaration.
116    IncompatiblePredicateDeclaration {
117        /// Predicate whose declarations differ.
118        name: String,
119        /// Module containing the first declaration.
120        module1: ModulePath,
121        /// Module containing the incompatible declaration.
122        module2: ModulePath,
123    },
124    /// The entry program or imported modules define one domain alias with
125    /// incompatible scalar types.
126    IncompatibleDomainDeclaration {
127        /// Domain alias whose declarations differ.
128        name: String,
129        /// Module containing the first declaration.
130        module1: ModulePath,
131        /// Module containing the incompatible declaration.
132        module2: ModulePath,
133    },
134    /// An imported module defines one exported function name more than once.
135    DuplicateImportedFunction {
136        /// Duplicate function name.
137        name: String,
138        /// Module containing the duplicate definitions.
139        module: ModulePath,
140    },
141    /// An imported module declares one predicate as both public and private.
142    ConflictingPredicateVisibility {
143        /// Predicate with contradictory visibility declarations.
144        name: String,
145        /// Module containing the declarations.
146        module: ModulePath,
147    },
148    /// Separate source programs contribute clauses for one undeclared
149    /// predicate signature with incompatible inferred column types.
150    IncompatibleInferredPredicateSchema {
151        /// Predicate whose inferred schemas differ.
152        name: String,
153        /// Predicate arity used to distinguish same-name signatures.
154        arity: usize,
155        /// One-based index of the incompatible column.
156        column: usize,
157        /// Type inferred from the first contribution.
158        type1: ScalarType,
159        /// Type inferred from the conflicting contribution.
160        type2: ScalarType,
161        /// Module containing the first contribution.
162        module1: ModulePath,
163        /// Module containing the conflicting contribution.
164        module2: ModulePath,
165        /// Source identity for the first contribution, or `<program>` when no
166        /// loaded source anchors the caller's program.
167        source1: Box<PathBuf>,
168        /// Source identity for the conflicting contribution, or `<program>`
169        /// when no loaded source anchors the caller's program.
170        source2: Box<PathBuf>,
171    },
172    /// A clause contains invalid type evidence while module resolution is
173    /// inferring an undeclared predicate signature.
174    PredicateSchemaInferenceFailed {
175        /// Predicate whose contribution could not be inferred.
176        name: String,
177        /// Predicate arity used to identify the signature.
178        arity: usize,
179        /// Module containing the invalid contribution.
180        module: ModulePath,
181        /// Source containing the invalid contribution.
182        source: Box<PathBuf>,
183        /// Diagnostic produced by the authoritative schema inference path.
184        message: String,
185    },
186    /// Parse error in module
187    ParseError {
188        /// Source file path that failed to parse.
189        path: PathBuf,
190        /// Human-readable parse failure message.
191        message: String,
192    },
193}
194
195impl std::fmt::Display for ModuleError {
196    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197        match self {
198            ModuleError::NotFound { path, searched } => {
199                writeln!(
200                    f,
201                    "error[E0400]: module not found: `{}`",
202                    module_path_to_string(path)
203                )?;
204                writeln!(f, "  = note: searched in:")?;
205                for s in searched {
206                    writeln!(f, "          - {}", s.display())?;
207                }
208                write!(
209                    f,
210                    "  = help: check the module path spelling or add to --module-path"
211                )
212            }
213            ModuleError::CircularImport { cycle } => {
214                writeln!(f, "error[E0401]: circular import detected")?;
215                for (i, path) in cycle.iter().enumerate() {
216                    if i < cycle.len() - 1 {
217                        writeln!(
218                            f,
219                            "  {} imports {}",
220                            module_path_to_string(path),
221                            module_path_to_string(&cycle[i + 1])
222                        )?;
223                    }
224                }
225                write!(f, "  = help: extract shared predicates into a third module")
226            }
227            ModuleError::ImportConflict {
228                name,
229                module1,
230                module2,
231            } => {
232                writeln!(
233                    f,
234                    "error[E0402]: conflicting definitions for imported function `{name}`"
235                )?;
236                writeln!(
237                    f,
238                    "  function `{}` is defined by module `{}`",
239                    name,
240                    module_path_to_string(module1)
241                )?;
242                writeln!(
243                    f,
244                    "  function `{}` is also defined by module `{}`",
245                    name,
246                    module_path_to_string(module2)
247                )?;
248                write!(
249                    f,
250                    "  = help: import only one definition of function `{name}` with selective `use` declarations"
251                )
252            }
253            ModuleError::PrivatePredicate { name, module } => {
254                write!(
255                    f,
256                    "error[E0403]: cannot import private predicate `{}` from {}",
257                    name,
258                    module_path_to_string(module)
259                )
260            }
261            ModuleError::PredicateNotFound { name, module } => {
262                write!(
263                    f,
264                    "error[E0404]: item `{}` is not exported by module {}",
265                    name,
266                    module_path_to_string(module)
267                )
268            }
269            ModuleError::UnsupportedImportedContent { module, constructs } => {
270                writeln!(
271                    f,
272                    "error[E0405]: imported module `{}` contains unsupported program-level constructs: {}",
273                    module_path_to_string(module),
274                    constructs.join(", ")
275                )?;
276                write!(f, "  = help: declare these constructs in the entry file")
277            }
278            ModuleError::HiddenDependency {
279                module,
280                export,
281                dependency,
282            } => {
283                writeln!(
284                    f,
285                    "error[E0406]: exported item `{}` in module `{}` depends on hidden item `{}`",
286                    export,
287                    module_path_to_string(module),
288                    dependency
289                )?;
290                write!(
291                    f,
292                    "  = help: imported exports cannot depend on private or selectively omitted module items"
293                )
294            }
295            ModuleError::AmbiguousModulePath { path, candidates } => {
296                writeln!(
297                    f,
298                    "error[E0407]: module path `{}` identifies multiple loaded files",
299                    module_path_to_string(path)
300                )?;
301                writeln!(f, "  = note: loaded candidates:")?;
302                for candidate in candidates {
303                    writeln!(f, "          - {}", candidate.display())?;
304                }
305                write!(
306                    f,
307                    "  = help: load the entry file or root module before validating or merging its imports"
308                )
309            }
310            ModuleError::IncompatiblePredicateDeclaration {
311                name,
312                module1,
313                module2,
314            } => {
315                writeln!(
316                    f,
317                    "error[E0408]: incompatible declarations for predicate `{name}`"
318                )?;
319                writeln!(
320                    f,
321                    "  `{name}` is declared by {} and {} with different schemas",
322                    module_path_to_string(module1),
323                    module_path_to_string(module2)
324                )?;
325                write!(
326                    f,
327                    "  = help: every declaration in the entry program and every public declaration selected by the resolved imports must use identical arity, column names, and resolved types"
328                )
329            }
330            ModuleError::IncompatibleDomainDeclaration {
331                name,
332                module1,
333                module2,
334            } => {
335                writeln!(
336                    f,
337                    "error[E0409]: incompatible declarations for domain alias `{name}`"
338                )?;
339                writeln!(
340                    f,
341                    "  `{name}` is declared by {} and {} with different scalar types",
342                    module_path_to_string(module1),
343                    module_path_to_string(module2)
344                )?;
345                write!(
346                    f,
347                    "  = help: a domain alias must resolve to one scalar type throughout the entry program and resolved import closure"
348                )
349            }
350            ModuleError::DuplicateImportedFunction { name, module } => {
351                writeln!(
352                    f,
353                    "error[E0410]: imported module `{}` defines function `{name}` more than once",
354                    module_path_to_string(module)
355                )?;
356                write!(f, "  = help: keep exactly one definition for each function")
357            }
358            ModuleError::ConflictingPredicateVisibility { name, module } => {
359                writeln!(
360                    f,
361                    "error[E0411]: imported module `{}` declares predicate `{name}` as both public and private",
362                    module_path_to_string(module)
363                )?;
364                write!(
365                    f,
366                    "  = help: use one visibility for every declaration of a predicate"
367                )
368            }
369            ModuleError::IncompatibleInferredPredicateSchema {
370                name,
371                arity,
372                column,
373                type1,
374                type2,
375                module1,
376                module2,
377                source1,
378                source2,
379            } => {
380                writeln!(
381                    f,
382                    "error[E0412]: incompatible inferred schemas for undeclared predicate `{name}/{arity}`"
383                )?;
384                if module1 == module2 {
385                    writeln!(
386                        f,
387                        "  column {column} is inferred as {} by `{}` and {} by `{}` (both resolved as module `{}`)",
388                        format_scalar_type(*type1),
389                        source1.display(),
390                        format_scalar_type(*type2),
391                        source2.display(),
392                        module_path_to_string(module1)
393                    )?;
394                } else {
395                    writeln!(
396                        f,
397                        "  column {column} is inferred as {} by module `{}` and {} by module `{}`",
398                        format_scalar_type(*type1),
399                        module_path_to_string(module1),
400                        format_scalar_type(*type2),
401                        module_path_to_string(module2)
402                    )?;
403                }
404                write!(
405                    f,
406                    "  = help: add a `pred {name}(...)` declaration that defines the shared schema, or make the contributing clauses use the same column types"
407                )
408            }
409            ModuleError::PredicateSchemaInferenceFailed {
410                name,
411                arity,
412                module,
413                source,
414                message,
415            } => {
416                writeln!(
417                    f,
418                    "error[E0413]: cannot infer schema for predicate `{name}/{arity}` from module `{}`",
419                    module_path_to_string(module)
420                )?;
421                writeln!(f, "  source: {}", source.display())?;
422                writeln!(f, "  cause: {message}")?;
423                write!(
424                    f,
425                    "  = help: fix the clause's type error or add a `pred {name}(...)` declaration with the intended schema"
426                )
427            }
428            ModuleError::ParseError { path, message } => {
429                write!(f, "error: parse error in {:?}: {}", path, message)
430            }
431        }
432    }
433}
434
435impl std::error::Error for ModuleError {}
436
437impl From<ModuleError> for xlog_core::XlogError {
438    fn from(e: ModuleError) -> Self {
439        xlog_core::XlogError::Compilation(e.to_string())
440    }
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446
447    #[test]
448    fn test_module_path_to_string() {
449        assert_eq!(
450            module_path_to_string(&["utils".into(), "math".into()]),
451            "utils/math"
452        );
453        assert_eq!(module_path_to_string(&["single".into()]), "single");
454    }
455
456    #[test]
457    fn test_loaded_module_new() {
458        let module = LoadedModule::new(
459            vec!["test".to_string()],
460            PathBuf::from("/test.xlog"),
461            Program::default(),
462        );
463        assert_eq!(module.path, vec!["test"]);
464        assert!(module.exports.is_empty());
465    }
466
467    #[test]
468    fn test_module_error_display() {
469        let err = ModuleError::NotFound {
470            path: vec!["missing".to_string()],
471            searched: vec![PathBuf::from("/a/missing.xlog")],
472        };
473        let msg = err.to_string();
474        assert!(msg.contains("module not found"));
475        assert!(msg.contains("missing"));
476    }
477
478    #[test]
479    fn ambiguous_module_path_error_lists_candidates() {
480        let err = ModuleError::AmbiguousModulePath {
481            path: vec!["support".to_string()],
482            candidates: vec![
483                PathBuf::from("/modules/left/support.xlog"),
484                PathBuf::from("/modules/right/support.xlog"),
485            ],
486        };
487
488        let message = err.to_string();
489
490        assert!(message.contains("error[E0407]"));
491        assert!(message.contains("module path `support`"));
492        assert!(message.contains("/modules/left/support.xlog"));
493        assert!(message.contains("/modules/right/support.xlog"));
494    }
495
496    #[test]
497    fn incompatible_predicate_declaration_error_names_both_modules() {
498        let err = ModuleError::IncompatiblePredicateDeclaration {
499            name: "external".to_string(),
500            module1: vec!["first".to_string()],
501            module2: vec!["second".to_string()],
502        };
503
504        let message = err.to_string();
505
506        assert!(message.contains("error[E0408]"));
507        assert!(message.contains("predicate `external`"));
508        assert!(message.contains("first and second"));
509        assert!(message.contains(
510            "every declaration in the entry program and every public declaration selected by the resolved imports"
511        ));
512    }
513
514    #[test]
515    fn incompatible_domain_declaration_error_names_both_modules() {
516        let err = ModuleError::IncompatibleDomainDeclaration {
517            name: "key".to_string(),
518            module1: vec!["first".to_string()],
519            module2: vec!["second".to_string()],
520        };
521
522        let message = err.to_string();
523
524        assert!(message.contains("error[E0409]"));
525        assert!(message.contains("domain alias `key`"));
526        assert!(message.contains("first and second"));
527    }
528
529    #[test]
530    fn duplicate_imported_function_error_names_module() {
531        let err = ModuleError::DuplicateImportedFunction {
532            name: "normalize".to_string(),
533            module: vec!["library".to_string()],
534        };
535
536        let message = err.to_string();
537
538        assert!(message.contains("error[E0410]"));
539        assert!(message.contains("function `normalize`"));
540        assert!(message.contains("module `library`"));
541    }
542
543    #[test]
544    fn conflicting_predicate_visibility_error_names_module() {
545        let err = ModuleError::ConflictingPredicateVisibility {
546            name: "shared".to_string(),
547            module: vec!["library".to_string()],
548        };
549
550        let message = err.to_string();
551
552        assert!(message.contains("error[E0411]"));
553        assert!(message.contains("predicate `shared`"));
554        assert!(message.contains("module `library`"));
555        assert!(message.contains("both public and private"));
556    }
557
558    #[test]
559    fn incompatible_inferred_predicate_schema_error_explains_the_conflict() {
560        let err = ModuleError::IncompatibleInferredPredicateSchema {
561            name: "shared".to_string(),
562            arity: 2,
563            column: 2,
564            type1: ScalarType::U32,
565            type2: ScalarType::Symbol,
566            module1: vec!["first".to_string()],
567            module2: vec!["second".to_string()],
568            source1: Box::new(PathBuf::from("first.xlog")),
569            source2: Box::new(PathBuf::from("second.xlog")),
570        };
571
572        let message = err.to_string();
573
574        assert!(message.contains("error[E0412]"));
575        assert!(message.contains("undeclared predicate `shared/2`"));
576        assert!(message.contains("column 2"));
577        assert!(message.contains("u32 by module `first`"));
578        assert!(message.contains("symbol by module `second`"));
579        assert!(message.contains("add a `pred shared(...)` declaration"));
580    }
581
582    #[test]
583    fn predicate_schema_inference_error_names_its_source() {
584        let err = ModuleError::PredicateSchemaInferenceFailed {
585            name: "shared".to_string(),
586            arity: 1,
587            module: vec!["library".to_string()],
588            source: Box::new(PathBuf::from("library.xlog")),
589            message: "Type mismatch in arithmetic: U32 vs U64".to_string(),
590        };
591
592        let message = err.to_string();
593
594        assert!(message.contains("error[E0413]"));
595        assert!(message.contains("predicate `shared/1`"));
596        assert!(message.contains("module `library`"));
597        assert!(message.contains("library.xlog"));
598        assert!(message.contains("Type mismatch in arithmetic"));
599    }
600
601    #[test]
602    fn imported_function_conflict_error_names_both_definitions() {
603        let err = ModuleError::ImportConflict {
604            name: "normalize".to_string(),
605            module1: vec!["first".to_string()],
606            module2: vec!["second".to_string()],
607        };
608
609        let message = err.to_string();
610
611        assert!(message
612            .contains("error[E0402]: conflicting definitions for imported function `normalize`"));
613        assert!(message.contains("function `normalize` is defined by module `first`"));
614        assert!(message.contains("function `normalize` is also defined by module `second`"));
615        assert!(message.contains("import only one definition of function `normalize`"));
616    }
617
618    #[test]
619    fn test_module_error_into_xlog() {
620        let err = ModuleError::ParseError {
621            path: std::path::PathBuf::from("/test.xlog"),
622            message: "unexpected EOF".to_string(),
623        };
624        let xlog_err: xlog_core::XlogError = err.into();
625        let msg = xlog_err.to_string();
626        assert!(
627            msg.contains("unexpected EOF"),
628            "Expected 'unexpected EOF' in: {msg}"
629        );
630    }
631}