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;
7
8/// A module path like ["utils", "math"]
9pub(crate) type ModulePath = Vec<String>;
10
11/// Convert module path to string for display
12pub(crate) fn module_path_to_string(path: &[String]) -> String {
13    path.join("/")
14}
15
16/// A loaded module with metadata
17#[derive(Debug)]
18pub struct LoadedModule {
19    /// Module path
20    pub path: ModulePath,
21    /// Source file location
22    pub source_file: PathBuf,
23    /// Public predicate names
24    pub exports: HashSet<String>,
25    /// Public function names
26    pub function_exports: HashSet<String>,
27    /// The parsed program content
28    pub program: Program,
29}
30
31impl LoadedModule {
32    /// Create a new loaded module (exports initially empty).
33    pub fn new(path: ModulePath, source_file: PathBuf, program: Program) -> Self {
34        Self {
35            path,
36            source_file,
37            exports: HashSet::new(),
38            function_exports: HashSet::new(),
39            program,
40        }
41    }
42}
43
44/// Errors that can occur during module resolution
45#[derive(Debug, Clone)]
46#[non_exhaustive]
47pub enum ModuleError {
48    /// Module file not found
49    NotFound {
50        /// Logical module path that failed to resolve.
51        path: ModulePath,
52        /// Filesystem locations that were searched.
53        searched: Vec<PathBuf>,
54    },
55    /// Circular import detected
56    CircularImport {
57        /// Ordered import cycle that was discovered.
58        cycle: Vec<ModulePath>,
59    },
60    /// Name conflict between imports
61    ImportConflict {
62        /// Imported symbol name that collided.
63        name: String,
64        /// First module providing the name.
65        module1: ModulePath,
66        /// Second module providing the same name.
67        module2: ModulePath,
68    },
69    /// Attempted to import private predicate
70    PrivatePredicate {
71        /// Predicate name that is not exported.
72        name: String,
73        /// Module that owns the private predicate.
74        module: ModulePath,
75    },
76    /// Predicate not found in module
77    PredicateNotFound {
78        /// Predicate name that could not be found.
79        name: String,
80        /// Module that was expected to export the predicate.
81        module: ModulePath,
82    },
83    /// Parse error in module
84    ParseError {
85        /// Source file path that failed to parse.
86        path: PathBuf,
87        /// Human-readable parse failure message.
88        message: String,
89    },
90}
91
92impl std::fmt::Display for ModuleError {
93    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94        match self {
95            ModuleError::NotFound { path, searched } => {
96                writeln!(
97                    f,
98                    "error[E0400]: module not found: `{}`",
99                    module_path_to_string(path)
100                )?;
101                writeln!(f, "  = note: searched in:")?;
102                for s in searched {
103                    writeln!(f, "          - {}", s.display())?;
104                }
105                write!(
106                    f,
107                    "  = help: check the module path spelling or add to --module-path"
108                )
109            }
110            ModuleError::CircularImport { cycle } => {
111                writeln!(f, "error[E0401]: circular import detected")?;
112                for (i, path) in cycle.iter().enumerate() {
113                    if i < cycle.len() - 1 {
114                        writeln!(
115                            f,
116                            "  {} imports {}",
117                            module_path_to_string(path),
118                            module_path_to_string(&cycle[i + 1])
119                        )?;
120                    }
121                }
122                write!(f, "  = help: extract shared predicates into a third module")
123            }
124            ModuleError::ImportConflict {
125                name,
126                module1,
127                module2,
128            } => {
129                writeln!(f, "error[E0402]: ambiguous import `{}`", name)?;
130                writeln!(
131                    f,
132                    "  `{}` first imported from {}",
133                    name,
134                    module_path_to_string(module1)
135                )?;
136                writeln!(
137                    f,
138                    "  `{}` also exported by {}",
139                    name,
140                    module_path_to_string(module2)
141                )?;
142                write!(
143                    f,
144                    "  = help: use selective imports: `use {}::{{...}}.`",
145                    module_path_to_string(module1)
146                )
147            }
148            ModuleError::PrivatePredicate { name, module } => {
149                write!(
150                    f,
151                    "error[E0403]: cannot import private predicate `{}` from {}",
152                    name,
153                    module_path_to_string(module)
154                )
155            }
156            ModuleError::PredicateNotFound { name, module } => {
157                write!(
158                    f,
159                    "error[E0404]: predicate `{}` not found in module {}",
160                    name,
161                    module_path_to_string(module)
162                )
163            }
164            ModuleError::ParseError { path, message } => {
165                write!(f, "error: parse error in {:?}: {}", path, message)
166            }
167        }
168    }
169}
170
171impl std::error::Error for ModuleError {}
172
173impl From<ModuleError> for xlog_core::XlogError {
174    fn from(e: ModuleError) -> Self {
175        xlog_core::XlogError::Compilation(e.to_string())
176    }
177}
178
179/// Generate internal qualified name for a predicate
180/// E.g., (["utils", "math"], "abs") -> "__utils_math__abs"
181#[allow(dead_code)] // reserved API: module system not yet wired
182pub(crate) fn internal_name(module_path: &[String], predicate: &str) -> String {
183    if module_path.is_empty() {
184        predicate.to_string()
185    } else {
186        format!("__{}__{}", module_path.join("_"), predicate)
187    }
188}
189
190/// Extract module and predicate from internal name
191/// E.g., "__utils_math__abs" -> (["utils", "math"], "abs")
192#[allow(dead_code)] // reserved API: module system not yet wired
193pub(crate) fn parse_internal_name(internal: &str) -> (Vec<String>, String) {
194    if internal.starts_with("__") {
195        if let Some(pos) = internal.rfind("__") {
196            if pos > 2 {
197                let module_part = &internal[2..pos];
198                let pred_part = &internal[pos + 2..];
199                let modules: Vec<String> = module_part.split('_').map(String::from).collect();
200                return (modules, pred_part.to_string());
201            }
202        }
203    }
204    (vec![], internal.to_string())
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn test_module_path_to_string() {
213        assert_eq!(
214            module_path_to_string(&["utils".into(), "math".into()]),
215            "utils/math"
216        );
217        assert_eq!(module_path_to_string(&["single".into()]), "single");
218    }
219
220    #[test]
221    fn test_loaded_module_new() {
222        let module = LoadedModule::new(
223            vec!["test".to_string()],
224            PathBuf::from("/test.xlog"),
225            Program::default(),
226        );
227        assert_eq!(module.path, vec!["test"]);
228        assert!(module.exports.is_empty());
229    }
230
231    #[test]
232    fn test_module_error_display() {
233        let err = ModuleError::NotFound {
234            path: vec!["missing".to_string()],
235            searched: vec![PathBuf::from("/a/missing.xlog")],
236        };
237        let msg = err.to_string();
238        assert!(msg.contains("module not found"));
239        assert!(msg.contains("missing"));
240    }
241
242    #[test]
243    fn test_internal_name() {
244        assert_eq!(internal_name(&[], "foo"), "foo");
245        assert_eq!(
246            internal_name(&["utils".into(), "math".into()], "abs"),
247            "__utils_math__abs"
248        );
249        assert_eq!(internal_name(&["single".into()], "pred"), "__single__pred");
250    }
251
252    #[test]
253    fn test_parse_internal_name() {
254        assert_eq!(parse_internal_name("foo"), (vec![], "foo".to_string()));
255        assert_eq!(
256            parse_internal_name("__utils_math__abs"),
257            (
258                vec!["utils".to_string(), "math".to_string()],
259                "abs".to_string()
260            )
261        );
262        assert_eq!(
263            parse_internal_name("__single__pred"),
264            (vec!["single".to_string()], "pred".to_string())
265        );
266    }
267
268    #[test]
269    fn test_module_error_into_xlog() {
270        let err = ModuleError::ParseError {
271            path: std::path::PathBuf::from("/test.xlog"),
272            message: "unexpected EOF".to_string(),
273        };
274        let xlog_err: xlog_core::XlogError = err.into();
275        let msg = xlog_err.to_string();
276        assert!(
277            msg.contains("unexpected EOF"),
278            "Expected 'unexpected EOF' in: {msg}"
279        );
280    }
281}