1use std::collections::HashSet;
4use std::path::PathBuf;
5
6use crate::ast::Program;
7use crate::diagnostics::format_scalar_type;
8use xlog_core::ScalarType;
9
10pub(crate) type ModulePath = Vec<String>;
12
13pub(crate) fn module_path_to_string(path: &[String]) -> String {
15 path.join("/")
16}
17
18#[derive(Debug)]
20pub struct LoadedModule {
21 pub path: ModulePath,
26 pub source_file: PathBuf,
31 pub exports: HashSet<String>,
33 pub function_exports: HashSet<String>,
35 pub program: Program,
37}
38
39impl LoadedModule {
40 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#[derive(Debug, Clone)]
54#[non_exhaustive]
55pub enum ModuleError {
56 NotFound {
58 path: ModulePath,
60 searched: Vec<PathBuf>,
62 },
63 CircularImport {
65 cycle: Vec<ModulePath>,
67 },
68 ImportConflict {
70 name: String,
72 module1: ModulePath,
74 module2: ModulePath,
76 },
77 PrivatePredicate {
79 name: String,
81 module: ModulePath,
83 },
84 PredicateNotFound {
86 name: String,
88 module: ModulePath,
90 },
91 UnsupportedImportedContent {
93 module: ModulePath,
95 constructs: Vec<String>,
97 },
98 HiddenDependency {
100 module: ModulePath,
102 export: String,
104 dependency: String,
106 },
107 AmbiguousModulePath {
109 path: ModulePath,
111 candidates: Vec<PathBuf>,
113 },
114 IncompatiblePredicateDeclaration {
117 name: String,
119 module1: ModulePath,
121 module2: ModulePath,
123 },
124 IncompatibleDomainDeclaration {
127 name: String,
129 module1: ModulePath,
131 module2: ModulePath,
133 },
134 DuplicateImportedFunction {
136 name: String,
138 module: ModulePath,
140 },
141 ConflictingPredicateVisibility {
143 name: String,
145 module: ModulePath,
147 },
148 IncompatibleInferredPredicateSchema {
151 name: String,
153 arity: usize,
155 column: usize,
157 type1: ScalarType,
159 type2: ScalarType,
161 module1: ModulePath,
163 module2: ModulePath,
165 source1: Box<PathBuf>,
168 source2: Box<PathBuf>,
171 },
172 PredicateSchemaInferenceFailed {
175 name: String,
177 arity: usize,
179 module: ModulePath,
181 source: Box<PathBuf>,
183 message: String,
185 },
186 ParseError {
188 path: PathBuf,
190 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}