1use crate::ast::{
4 ArithExpr, BodyLiteral, DomainDecl, FuncBody, PredDecl, Program, Rule, Term, TypeRef,
5};
6use crate::lower::Lowerer;
7use crate::meta_normalize::static_meta_predicate_dependency;
8use crate::module::{module_path_to_string, LoadedModule, ModuleError, ModulePath};
9use crate::parser::parse_program;
10use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
11use std::fs;
12use std::path::{Path, PathBuf};
13use xlog_core::{ScalarType, XlogError};
14
15fn collect_term_predicate_dependencies(term: &Term, predicates: &mut HashSet<String>) {
16 match term {
17 Term::List(items) => {
18 for item in items {
19 collect_term_predicate_dependencies(item, predicates);
20 }
21 }
22 Term::Cons { head, tail } => {
23 collect_term_predicate_dependencies(head, predicates);
24 collect_term_predicate_dependencies(tail, predicates);
25 }
26 Term::Compound { args, .. } => {
27 for argument in args {
28 collect_term_predicate_dependencies(argument, predicates);
29 }
30 }
31 Term::PredRef(name) => {
32 predicates.insert(name.clone());
33 }
34 Term::Variable(_)
35 | Term::Anonymous
36 | Term::Integer(_)
37 | Term::Float(_)
38 | Term::String(_)
39 | Term::Symbol(_)
40 | Term::Aggregate(_) => {}
41 }
42}
43
44fn collect_arithmetic_function_dependencies(
45 expression: &ArithExpr,
46 functions: &mut HashSet<String>,
47) {
48 match expression {
49 ArithExpr::Add(left, right)
50 | ArithExpr::Sub(left, right)
51 | ArithExpr::Mul(left, right)
52 | ArithExpr::Div(left, right)
53 | ArithExpr::Mod(left, right)
54 | ArithExpr::Min(left, right)
55 | ArithExpr::Max(left, right)
56 | ArithExpr::Pow(left, right) => {
57 collect_arithmetic_function_dependencies(left, functions);
58 collect_arithmetic_function_dependencies(right, functions);
59 }
60 ArithExpr::Abs(inner) | ArithExpr::Cast(inner, _) => {
61 collect_arithmetic_function_dependencies(inner, functions);
62 }
63 ArithExpr::FuncCall { name, args } => {
64 functions.insert(name.clone());
65 for argument in args {
66 collect_arithmetic_function_dependencies(argument, functions);
67 }
68 }
69 ArithExpr::Conditional {
70 cond_left,
71 cond_right,
72 then_expr,
73 else_expr,
74 ..
75 } => {
76 collect_arithmetic_function_dependencies(cond_left, functions);
77 collect_arithmetic_function_dependencies(cond_right, functions);
78 collect_arithmetic_function_dependencies(then_expr, functions);
79 collect_arithmetic_function_dependencies(else_expr, functions);
80 }
81 ArithExpr::Variable(_) | ArithExpr::Integer(_) | ArithExpr::Float(_) => {}
82 }
83}
84
85fn collect_body_dependencies(
86 body: &[BodyLiteral],
87 predicates: &mut HashSet<String>,
88 functions: &mut HashSet<String>,
89) {
90 for literal in body {
91 match literal {
92 BodyLiteral::Positive(atom) => {
93 predicates.insert(atom.predicate.clone());
94 if let Some(dependency) = static_meta_predicate_dependency(atom) {
95 predicates.insert(dependency);
96 }
97 for term in &atom.terms {
98 collect_term_predicate_dependencies(term, predicates);
99 }
100 }
101 BodyLiteral::Negated(atom) => {
102 predicates.insert(atom.predicate.clone());
103 for term in &atom.terms {
104 collect_term_predicate_dependencies(term, predicates);
105 }
106 }
107 BodyLiteral::Epistemic(literal) => {
108 predicates.insert(literal.atom.predicate.clone());
109 for term in &literal.atom.terms {
110 collect_term_predicate_dependencies(term, predicates);
111 }
112 }
113 BodyLiteral::Comparison(comparison) => {
114 collect_term_predicate_dependencies(&comparison.left, predicates);
115 collect_term_predicate_dependencies(&comparison.right, predicates);
116 }
117 BodyLiteral::IsExpr(expression) => {
118 collect_arithmetic_function_dependencies(&expression.expr, functions);
119 }
120 BodyLiteral::Univ(univ) => {
121 collect_term_predicate_dependencies(&univ.term, predicates);
122 collect_term_predicate_dependencies(&univ.parts, predicates);
123 }
124 }
125 }
126}
127
128fn collect_function_body_dependencies(
129 body: &FuncBody,
130 predicates: &mut HashSet<String>,
131 functions: &mut HashSet<String>,
132) {
133 match body {
134 FuncBody::Arithmetic(expression) => {
135 collect_arithmetic_function_dependencies(expression, functions);
136 }
137 FuncBody::Conditional(expression) => {
138 collect_arithmetic_function_dependencies(&expression.cond_left, functions);
139 collect_arithmetic_function_dependencies(&expression.cond_right, functions);
140 collect_function_body_dependencies(&expression.then_branch, predicates, functions);
141 collect_function_body_dependencies(&expression.else_branch, predicates, functions);
142 }
143 FuncBody::Predicate { body, .. } => {
144 collect_body_dependencies(body, predicates, functions);
145 }
146 }
147}
148
149#[derive(Clone, Default)]
151struct ModuleItems {
152 predicates: HashSet<String>,
153 functions: HashSet<String>,
154}
155
156#[derive(Default)]
159struct ImportScope {
160 visible: ModuleItems,
161 hidden: ModuleItems,
162}
163
164#[derive(Clone)]
166struct ImportProvider {
167 module: ModulePath,
168 source: PathBuf,
169}
170
171#[derive(Clone)]
172struct ImportedPredicateDeclaration {
173 module: ModulePath,
174 schema: PredicateDeclarationSchema,
175}
176
177#[derive(Clone, PartialEq, Eq)]
178struct PredicateDeclarationSchema {
179 columns: Vec<(Option<String>, TypeRef)>,
180}
181
182#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
183struct PredicateKey {
184 name: String,
185 arity: usize,
186}
187
188#[derive(Clone)]
189struct InferredPredicateContribution {
190 provider: ImportProvider,
191 rule: Rule,
192}
193
194#[derive(Clone, Copy, PartialEq, Eq)]
195enum InferredColumn {
196 Unknown,
197 Known(ScalarType),
198 Conflicting,
199}
200
201#[derive(Clone)]
202struct ImportedDomainDeclaration {
203 module: ModulePath,
204 declaration: DomainDecl,
205}
206
207#[derive(Default)]
208struct ImportProviders {
209 functions: BTreeMap<String, ImportProvider>,
210 predicate_declarations: BTreeMap<String, ImportedPredicateDeclaration>,
211 inferred_predicate_contributions: BTreeMap<PredicateKey, Vec<InferredPredicateContribution>>,
212 domain_declarations: BTreeMap<String, ImportedDomainDeclaration>,
213}
214
215#[derive(Clone)]
217struct ResolvedImport {
218 source: PathBuf,
219 module_path: ModulePath,
220 imports: Option<Vec<String>>,
221}
222
223struct ResolvedImportGroup {
224 source: PathBuf,
225 module_path: ModulePath,
226 imported_items: Option<HashSet<String>>,
227}
228
229#[derive(Hash, PartialEq, Eq)]
230struct ImportMergeKey {
231 source: PathBuf,
232 imported_items: Option<Vec<String>>,
233}
234
235#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
242pub struct IgnoredImportPragma {
243 pub module: String,
245 pub pragma: &'static str,
247}
248
249impl std::fmt::Display for IgnoredImportPragma {
250 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
251 writeln!(
252 f,
253 "warning[W0510]: `#pragma {}` in imported module `{}` is ignored",
254 self.pragma, self.module
255 )?;
256 write!(
257 f,
258 " = note: pragmas apply only when declared in the entry file"
259 )
260 }
261}
262
263pub struct ModuleResolver {
265 search_paths: Vec<PathBuf>,
267 loaded: HashMap<PathBuf, LoadedModule>,
269 module_aliases: HashMap<String, Vec<PathBuf>>,
273 resolved_imports: HashMap<PathBuf, Vec<ResolvedImport>>,
275 entry: Option<LoadedModule>,
278 entry_source: Option<PathBuf>,
280 entry_resolved_imports: Vec<ResolvedImport>,
281 root_module: Option<PathBuf>,
283 loading: Vec<(PathBuf, ModulePath)>,
285 entry_module: Option<String>,
289}
290
291impl ModuleResolver {
292 pub fn new(search_paths: Vec<PathBuf>) -> Self {
294 Self {
295 search_paths,
296 loaded: HashMap::new(),
297 module_aliases: HashMap::new(),
298 resolved_imports: HashMap::new(),
299 entry: None,
300 entry_source: None,
301 entry_resolved_imports: Vec::new(),
302 root_module: None,
303 loading: Vec::new(),
304 entry_module: None,
305 }
306 }
307
308 pub fn mark_entry_module(&mut self, path_key: &str) {
313 self.entry_module = Some(path_key.to_string());
314 }
315
316 pub fn ignored_import_pragmas(&self) -> Vec<IgnoredImportPragma> {
329 let entry_source = self
330 .entry_module
331 .as_deref()
332 .and_then(|path| self.module_aliases.get(path))
333 .and_then(|sources| sources.first().cloned())
334 .or_else(|| self.entry_source.clone());
335
336 let mut candidates: Vec<(PathBuf, IgnoredImportPragma)> = Vec::new();
337 for (path_key, sources) in &self.module_aliases {
338 for source in sources {
339 if entry_source.as_ref() == Some(source) {
340 continue;
341 }
342 let Some(module) = self.loaded.get(source) else {
343 continue;
344 };
345 for pragma in module.program.directives.set_pragma_names() {
346 candidates.push((
347 source.clone(),
348 IgnoredImportPragma {
349 module: path_key.clone(),
350 pragma,
351 },
352 ));
353 }
354 }
355 }
356
357 candidates.sort_by(|a, b| a.1.cmp(&b.1));
358 let mut seen: HashSet<(PathBuf, &'static str)> = HashSet::new();
359 let mut ignored = Vec::with_capacity(candidates.len());
360 for (source, warning) in candidates {
361 if seen.insert((source, warning.pragma)) {
362 ignored.push(warning);
363 }
364 }
365 ignored
366 }
367
368 fn source_identity(source_file: &Path) -> Result<PathBuf, ModuleError> {
369 fs::canonicalize(source_file).map_err(|error| ModuleError::ParseError {
370 path: source_file.to_path_buf(),
371 message: format!("failed to resolve source-file identity: {error}"),
372 })
373 }
374
375 fn resolve_module_file(
376 &self,
377 base_dir: &Path,
378 module_path: &[String],
379 ) -> Option<(PathBuf, bool)> {
380 let relative_path = format!("{}.xlog", module_path.join("/"));
381 let candidate = base_dir.join(&relative_path);
382 if candidate.exists() {
383 return Some((candidate, true));
384 }
385
386 for search_path in &self.search_paths {
387 let candidate = search_path.join(&relative_path);
388 if candidate.exists() {
389 return Some((candidate, false));
390 }
391 }
392
393 None
394 }
395
396 pub fn find_module_file(&self, base_dir: &Path, module_path: &[String]) -> Option<PathBuf> {
398 self.resolve_module_file(base_dir, module_path)
399 .map(|(path, _)| path)
400 }
401
402 fn searched_paths(&self, base_dir: &Path, module_path: &[String]) -> Vec<PathBuf> {
404 let relative_path = format!("{}.xlog", module_path.join("/"));
405 let mut searched = vec![base_dir.join(&relative_path)];
406 for sp in &self.search_paths {
407 searched.push(sp.join(&relative_path));
408 }
409 searched
410 }
411
412 fn check_cycle(&self, source: &Path, module_path: &[String]) -> Option<Vec<ModulePath>> {
414 for (i, (loading_source, _)) in self.loading.iter().enumerate() {
415 if loading_source == source {
416 let mut cycle: Vec<ModulePath> = self.loading[i..]
417 .iter()
418 .map(|(_, path)| path.clone())
419 .collect();
420 cycle.push(module_path.to_vec());
421 return Some(cycle);
422 }
423 }
424 None
425 }
426
427 pub fn extract_exports(program: &Program) -> (HashSet<String>, HashSet<String>) {
430 let mut pred_exports = HashSet::new();
431 let mut func_exports = HashSet::new();
432
433 for pred in &program.predicates {
435 if !pred.is_private {
436 pred_exports.insert(pred.name.clone());
437 }
438 }
439
440 for rule in &program.rules {
442 let is_private = program
444 .predicates
445 .iter()
446 .any(|p| p.name == rule.head.predicate && p.is_private);
447 if !is_private {
448 pred_exports.insert(rule.head.predicate.clone());
449 }
450 }
451
452 for func in &program.functions {
454 if !func.is_private {
455 func_exports.insert(func.name.clone());
456 }
457 }
458
459 (pred_exports, func_exports)
460 }
461
462 pub fn load_module(
467 &mut self,
468 base_dir: &Path,
469 module_path: &[String],
470 ) -> Result<&LoadedModule, ModuleError> {
471 let (source, _) = self.load_module_resolved(base_dir, None, module_path)?;
472 self.entry = None;
473 self.entry_source = None;
474 self.entry_resolved_imports.clear();
475 self.root_module = Some(source.clone());
476 Ok(self.loaded.get(&source).expect("module was just resolved"))
477 }
478
479 pub fn load_entry_file(&mut self, entry_file: &Path) -> Result<&LoadedModule, ModuleError> {
488 let base_dir = entry_file.parent().unwrap_or(Path::new("."));
489 let module_name = entry_file
490 .file_stem()
491 .and_then(|stem| stem.to_str())
492 .unwrap_or("main")
493 .to_string();
494 let module_path = vec![module_name.clone()];
495
496 let module = Self::parse_module_file(&module_path, entry_file.to_path_buf())?;
497 let entry_source = Self::source_identity(entry_file)?;
498 let module_dir = module.source_file.parent().unwrap_or(base_dir);
499 self.loading
500 .push((entry_source.clone(), module_path.clone()));
501 let resolved_imports = (|| {
502 let mut resolved = Vec::with_capacity(module.program.imports.len());
503 for import in &module.program.imports {
504 let (source, resolved_path) =
505 self.load_module_resolved(module_dir, Some(&module_path), &import.module_path)?;
506 resolved.push(ResolvedImport {
507 source,
508 module_path: resolved_path,
509 imports: import.imports.clone(),
510 });
511 }
512 Ok(resolved)
513 })();
514 self.loading.pop();
515 let resolved_imports = resolved_imports?;
516 self.entry_module = None;
517 self.entry_source = Some(entry_source);
518 self.entry_resolved_imports = resolved_imports;
519 self.root_module = None;
520 self.entry = Some(module);
521 Ok(self.entry.as_ref().expect("entry module was just loaded"))
522 }
523
524 fn contextual_module_path(
525 parent_path: Option<&[String]>,
526 declared_path: &[String],
527 resolved_relative_to_parent: bool,
528 ) -> ModulePath {
529 if !resolved_relative_to_parent {
530 return declared_path.to_vec();
531 }
532
533 let mut resolved = parent_path
534 .and_then(|path| path.split_last().map(|(_, prefix)| prefix.to_vec()))
535 .unwrap_or_default();
536 resolved.extend_from_slice(declared_path);
537 resolved
538 }
539
540 fn record_module_alias(&mut self, module_path: &[String], source: &Path) {
541 let sources = self
542 .module_aliases
543 .entry(module_path_to_string(module_path))
544 .or_default();
545 if !sources.iter().any(|candidate| candidate == source) {
546 sources.push(source.to_path_buf());
547 }
548 }
549
550 fn load_module_resolved(
551 &mut self,
552 base_dir: &Path,
553 parent_path: Option<&[String]>,
554 declared_path: &[String],
555 ) -> Result<(PathBuf, ModulePath), ModuleError> {
556 let (source_file, resolved_relative_to_parent) = self
557 .resolve_module_file(base_dir, declared_path)
558 .ok_or_else(|| ModuleError::NotFound {
559 path: declared_path.to_vec(),
560 searched: self.searched_paths(base_dir, declared_path),
561 })?;
562 let source = Self::source_identity(&source_file)?;
563 let contextual_path =
564 Self::contextual_module_path(parent_path, declared_path, resolved_relative_to_parent);
565
566 if let Some(cycle) = self.check_cycle(&source, &contextual_path) {
567 return Err(ModuleError::CircularImport { cycle });
568 }
569
570 if self.loaded.contains_key(&source) {
571 self.record_module_alias(declared_path, &source);
572 self.record_module_alias(&contextual_path, &source);
573 return Ok((source, contextual_path));
574 }
575
576 self.loading.push((source.clone(), contextual_path.clone()));
577
578 let loaded = (|| {
579 let module = Self::parse_module_file(&contextual_path, source_file)?;
580 let module_dir = source.parent().unwrap_or(base_dir).to_path_buf();
584 let mut resolved_imports = Vec::with_capacity(module.program.imports.len());
585 for import in &module.program.imports {
586 let (target_source, resolved_path) = self.load_module_resolved(
587 &module_dir,
588 Some(&contextual_path),
589 &import.module_path,
590 )?;
591 resolved_imports.push(ResolvedImport {
592 source: target_source,
593 module_path: resolved_path,
594 imports: import.imports.clone(),
595 });
596 }
597 Ok((module, resolved_imports))
598 })();
599
600 self.loading.pop();
601 let (module, resolved_imports) = loaded?;
602 let primary_path = module.path.clone();
603
604 self.record_module_alias(declared_path, &source);
605 self.record_module_alias(&contextual_path, &source);
606 self.resolved_imports
607 .insert(source.clone(), resolved_imports);
608 self.loaded.insert(source.clone(), module);
609 Ok((source, primary_path))
610 }
611
612 fn parse_module_file(
613 module_path: &[String],
614 source_file: PathBuf,
615 ) -> Result<LoadedModule, ModuleError> {
616 let source = fs::read_to_string(&source_file).map_err(|error| ModuleError::ParseError {
617 path: source_file.clone(),
618 message: error.to_string(),
619 })?;
620 let program = parse_program(&source).map_err(|error| ModuleError::ParseError {
621 path: source_file.clone(),
622 message: error.to_string(),
623 })?;
624 let (exports, function_exports) = Self::extract_exports(&program);
625
626 Ok(LoadedModule {
627 path: module_path.to_vec(),
628 source_file,
629 exports,
630 function_exports,
631 program,
632 })
633 }
634
635 pub fn check_import(&self, module_path: &[String], predicate: &str) -> Result<(), ModuleError> {
641 let (_, module) = self
642 .first_loaded_module_for_alias(module_path)
643 .ok_or_else(|| ModuleError::NotFound {
644 path: module_path.to_vec(),
645 searched: vec![],
646 })?;
647
648 Self::validate_consistent_predicate_visibility(&module.program, module_path)?;
649 if !module.exports.contains(predicate) {
650 return Err(ModuleError::PredicateNotFound {
651 name: predicate.to_string(),
652 module: module_path.to_vec(),
653 });
654 }
655
656 Ok(())
657 }
658
659 #[allow(clippy::type_complexity)]
674 pub fn validate_imports(
675 &self,
676 program: &Program,
677 ) -> Result<(HashMap<String, ModulePath>, HashMap<String, ModulePath>), ModuleError> {
678 let imports = self.resolved_imports_for_program(program)?;
679 let validated = self.validate_resolved_imports(&imports)?;
680 self.validate_program_against_imports(program, &imports)?;
681 Ok(validated)
682 }
683
684 #[allow(clippy::type_complexity)]
685 fn validate_resolved_imports(
686 &self,
687 imports: &[ResolvedImport],
688 ) -> Result<(HashMap<String, ModulePath>, HashMap<String, ModulePath>), ModuleError> {
689 let mut imported_predicates: HashMap<String, ModulePath> = HashMap::new();
690 let mut imported_functions: HashMap<String, ModulePath> = HashMap::new();
691 let mut function_providers: HashMap<String, ImportProvider> = HashMap::new();
692
693 for resolved_import in imports {
694 let module =
695 self.loaded
696 .get(&resolved_import.source)
697 .ok_or_else(|| ModuleError::NotFound {
698 path: resolved_import.module_path.clone(),
699 searched: vec![],
700 })?;
701
702 let all_exports: HashSet<String> = module
704 .exports
705 .iter()
706 .chain(module.function_exports.iter())
707 .cloned()
708 .collect();
709
710 let mut names_to_import: Vec<String> = match &resolved_import.imports {
711 Some(specific) => specific.clone(),
712 None => all_exports.iter().cloned().collect(),
713 };
714 names_to_import.sort();
715
716 for name in names_to_import {
717 let is_predicate = module.exports.contains(&name);
719 let is_function = module.function_exports.contains(&name);
720
721 if !is_predicate && !is_function {
722 return Err(ModuleError::PredicateNotFound {
723 name: name.clone(),
724 module: resolved_import.module_path.clone(),
725 });
726 }
727
728 if is_predicate {
729 imported_predicates
730 .entry(name.clone())
731 .or_insert_with(|| resolved_import.module_path.clone());
732 }
733
734 if is_function {
735 if let Some(previous) = function_providers.get(&name) {
736 if previous.source != resolved_import.source {
737 return Err(ModuleError::ImportConflict {
738 name,
739 module1: previous.module.clone(),
740 module2: resolved_import.module_path.clone(),
741 });
742 }
743 } else {
744 function_providers.insert(
745 name.clone(),
746 ImportProvider {
747 module: resolved_import.module_path.clone(),
748 source: resolved_import.source.clone(),
749 },
750 );
751 }
752 imported_functions
753 .entry(name.clone())
754 .or_insert_with(|| resolved_import.module_path.clone());
755 }
756 }
757 }
758
759 Ok((imported_predicates, imported_functions))
760 }
761
762 fn resolved_imports_for_program(
763 &self,
764 program: &Program,
765 ) -> Result<Vec<ResolvedImport>, ModuleError> {
766 if self
767 .entry
768 .as_ref()
769 .is_some_and(|entry| entry.program.imports == program.imports)
770 {
771 return Ok(self.entry_resolved_imports.clone());
772 }
773
774 if let Some(root_source) = &self.root_module {
775 if let Some(root) = self.loaded.get(root_source) {
776 if root.program.imports == program.imports {
777 return Ok(self
778 .resolved_imports
779 .get(root_source)
780 .cloned()
781 .unwrap_or_default());
782 }
783 }
784 }
785
786 program
787 .imports
788 .iter()
789 .map(|use_decl| {
790 let sources = self
791 .module_aliases
792 .get(&module_path_to_string(&use_decl.module_path))
793 .ok_or_else(|| ModuleError::NotFound {
794 path: use_decl.module_path.clone(),
795 searched: vec![],
796 })?;
797 if sources.len() > 1 {
798 let mut candidates = sources.clone();
799 candidates.sort();
800 return Err(ModuleError::AmbiguousModulePath {
801 path: use_decl.module_path.clone(),
802 candidates,
803 });
804 }
805 let source = sources
806 .first()
807 .filter(|source| self.loaded.contains_key(*source))
808 .ok_or_else(|| ModuleError::NotFound {
809 path: use_decl.module_path.clone(),
810 searched: vec![],
811 })?;
812 Ok(ResolvedImport {
813 source: source.clone(),
814 module_path: use_decl.module_path.clone(),
815 imports: use_decl.imports.clone(),
816 })
817 })
818 .collect()
819 }
820
821 fn first_loaded_module_for_alias(
822 &self,
823 module_path: &[String],
824 ) -> Option<(&PathBuf, &LoadedModule)> {
825 let source = self
826 .module_aliases
827 .get(&module_path_to_string(module_path))?
828 .first()?;
829 self.loaded.get_key_value(source)
830 }
831
832 pub fn get_module(&self, module_path: &[String]) -> Option<&LoadedModule> {
838 self.first_loaded_module_for_alias(module_path)
839 .map(|(_, module)| module)
840 }
841
842 pub fn entry(&self) -> Option<&LoadedModule> {
844 self.entry.as_ref()
845 }
846
847 pub fn is_loaded(&self, module_path: &str) -> bool {
849 self.module_aliases.contains_key(module_path)
850 }
851
852 pub fn loaded_modules(&self) -> Vec<&str> {
854 self.module_aliases.keys().map(String::as_str).collect()
855 }
856
857 fn imported_item_set(imports: &Option<Vec<String>>) -> Option<HashSet<String>> {
858 match imports {
859 Some(items) if !items.is_empty() => Some(items.iter().cloned().collect()),
860 _ => None,
861 }
862 }
863
864 fn combined_import_selections(imports: &[ResolvedImport]) -> Vec<ResolvedImportGroup> {
865 let mut combined = Vec::<ResolvedImportGroup>::new();
869 let mut indexes = HashMap::<PathBuf, usize>::new();
870 for resolved_import in imports {
871 let selection = Self::imported_item_set(&resolved_import.imports);
872 if let Some(index) = indexes.get(&resolved_import.source).copied() {
873 let existing = &mut combined[index].imported_items;
874 match (existing.as_mut(), selection) {
875 (Some(existing_names), Some(names)) => existing_names.extend(names),
876 (_, None) => *existing = None,
877 (None, Some(_)) => {}
878 }
879 } else {
880 indexes.insert(resolved_import.source.clone(), combined.len());
881 combined.push(ResolvedImportGroup {
882 source: resolved_import.source.clone(),
883 module_path: resolved_import.module_path.clone(),
884 imported_items: selection,
885 });
886 }
887 }
888 combined
889 }
890
891 fn local_predicate_names(program: &Program) -> HashSet<String> {
892 program
893 .predicates
894 .iter()
895 .map(|predicate| predicate.name.clone())
896 .chain(program.rules.iter().map(|rule| rule.head.predicate.clone()))
897 .collect()
898 }
899
900 fn local_function_names(program: &Program) -> HashSet<String> {
901 program
902 .functions
903 .iter()
904 .map(|function| function.name.clone())
905 .collect()
906 }
907
908 fn hidden_local_function_names(
909 program: &Program,
910 imported_items: Option<&HashSet<String>>,
911 ) -> HashSet<String> {
912 let private_functions = program
913 .functions
914 .iter()
915 .filter(|function| function.is_private)
916 .map(|function| function.name.clone())
917 .collect::<HashSet<_>>();
918 Self::local_function_names(program)
919 .into_iter()
920 .filter(|name| {
921 private_functions.contains(name)
922 || imported_items.is_some_and(|items| !items.contains(name))
923 })
924 .collect()
925 }
926
927 fn hidden_local_items(
928 program: &Program,
929 imported_items: Option<&HashSet<String>>,
930 ) -> ModuleItems {
931 let private_predicates = program
932 .predicates
933 .iter()
934 .filter(|predicate| predicate.is_private)
935 .map(|predicate| predicate.name.clone())
936 .collect::<HashSet<_>>();
937 let predicates = Self::local_predicate_names(program)
938 .into_iter()
939 .filter(|name| {
940 private_predicates.contains(name)
941 || imported_items.is_some_and(|items| !items.contains(name))
942 })
943 .collect();
944
945 let functions = Self::hidden_local_function_names(program, imported_items);
946
947 ModuleItems {
948 predicates,
949 functions,
950 }
951 }
952
953 fn visible_local_functions(
954 program: &Program,
955 imported_items: Option<&HashSet<String>>,
956 ) -> HashSet<String> {
957 let hidden = Self::hidden_local_function_names(program, imported_items);
958 Self::local_function_names(program)
959 .difference(&hidden)
960 .cloned()
961 .collect()
962 }
963
964 fn local_inferred_predicate_contributions(
965 program: &Program,
966 excluded_predicates: &HashSet<String>,
967 provider: ImportProvider,
968 ) -> BTreeMap<PredicateKey, Vec<InferredPredicateContribution>> {
969 let mut contributions = BTreeMap::<PredicateKey, Vec<InferredPredicateContribution>>::new();
970
971 for rule in &program.rules {
972 let key = PredicateKey {
973 name: rule.head.predicate.clone(),
974 arity: rule.head.arity(),
975 };
976 if excluded_predicates.contains(&key.name)
977 || program.predicates.iter().any(|declaration| {
978 declaration.name == key.name && declaration.arity() == key.arity
979 })
980 {
981 continue;
982 }
983
984 let contribution = InferredPredicateContribution {
985 provider: provider.clone(),
986 rule: rule.clone(),
987 };
988 let existing = contributions.entry(key).or_default();
989 if !existing.iter().any(|candidate| {
990 candidate.provider.source == contribution.provider.source
991 && candidate.rule == contribution.rule
992 }) {
993 existing.push(contribution);
994 }
995 }
996
997 contributions
998 }
999
1000 fn merge_inferred_predicate_contributions(
1001 contributions: &mut BTreeMap<PredicateKey, Vec<InferredPredicateContribution>>,
1002 incoming: BTreeMap<PredicateKey, Vec<InferredPredicateContribution>>,
1003 ) {
1004 for (key, incoming_contributions) in incoming {
1005 let existing = contributions.entry(key).or_default();
1006 for contribution in incoming_contributions {
1007 if !existing.iter().any(|candidate| {
1008 candidate.provider.source == contribution.provider.source
1009 && candidate.rule == contribution.rule
1010 }) {
1011 existing.push(contribution);
1012 }
1013 }
1014 }
1015 }
1016
1017 fn storage_scalar_type(typ: &TypeRef) -> Option<ScalarType> {
1018 match typ {
1019 TypeRef::Scalar(typ) => Some(*typ),
1020 TypeRef::List(_) | TypeRef::Term | TypeRef::Compound | TypeRef::PredRef => {
1021 Some(ScalarType::U64)
1022 }
1023 TypeRef::Domain(_) => None,
1024 }
1025 }
1026
1027 fn inferred_rule_columns(
1028 schema_inference: &Lowerer,
1029 rule: &Rule,
1030 schemas: &BTreeMap<PredicateKey, Vec<InferredColumn>>,
1031 ) -> xlog_core::Result<Vec<Option<ScalarType>>> {
1032 schema_inference.infer_rule_head_column_types_before_function_expansion(
1033 rule,
1034 |atom, index| {
1035 schemas
1036 .get(&PredicateKey {
1037 name: atom.predicate.clone(),
1038 arity: atom.arity(),
1039 })
1040 .and_then(|columns| columns.get(index))
1041 .and_then(|column| match column {
1042 InferredColumn::Known(typ) => Some(*typ),
1043 InferredColumn::Unknown | InferredColumn::Conflicting => None,
1044 })
1045 },
1046 )
1047 }
1048
1049 fn predicate_schema_inference_error(
1050 key: &PredicateKey,
1051 contribution: &InferredPredicateContribution,
1052 error: XlogError,
1053 ) -> ModuleError {
1054 ModuleError::PredicateSchemaInferenceFailed {
1055 name: key.name.clone(),
1056 arity: key.arity,
1057 module: contribution.provider.module.clone(),
1058 source: Box::new(contribution.provider.source.clone()),
1059 message: error.to_string(),
1060 }
1061 }
1062
1063 fn inferred_predicate_schemas(
1064 providers: &ImportProviders,
1065 ) -> Result<BTreeMap<PredicateKey, Vec<InferredColumn>>, ModuleError> {
1066 let mut schemas = BTreeMap::<PredicateKey, Vec<InferredColumn>>::new();
1067 let mut declared = BTreeSet::<PredicateKey>::new();
1068
1069 for (name, declaration) in &providers.predicate_declarations {
1070 let key = PredicateKey {
1071 name: name.clone(),
1072 arity: declaration.schema.columns.len(),
1073 };
1074 let columns = declaration
1075 .schema
1076 .columns
1077 .iter()
1078 .map(|(_, typ)| {
1079 Self::storage_scalar_type(typ)
1080 .map(InferredColumn::Known)
1081 .unwrap_or(InferredColumn::Unknown)
1082 })
1083 .collect();
1084 declared.insert(key.clone());
1085 schemas.insert(key, columns);
1086 }
1087
1088 let total_columns = providers
1089 .inferred_predicate_contributions
1090 .keys()
1091 .filter(|key| !declared.contains(*key))
1092 .map(|key| key.arity)
1093 .sum::<usize>();
1094 let max_iterations = total_columns.saturating_mul(2).saturating_add(1);
1095 let schema_inference = Lowerer::new();
1096 let mut converged = false;
1097 for _ in 0..max_iterations {
1098 let mut changed = false;
1099 for (key, contributions) in &providers.inferred_predicate_contributions {
1100 if declared.contains(key) {
1101 continue;
1102 }
1103 for contribution in contributions {
1104 let columns = Self::inferred_rule_columns(
1105 &schema_inference,
1106 &contribution.rule,
1107 &schemas,
1108 )
1109 .map_err(|error| {
1110 Self::predicate_schema_inference_error(key, contribution, error)
1111 })?;
1112 let schema = schemas
1113 .entry(key.clone())
1114 .or_insert_with(|| vec![InferredColumn::Unknown; key.arity]);
1115 for (slot, inferred) in schema.iter_mut().zip(columns) {
1116 let Some(inferred) = inferred else {
1117 continue;
1118 };
1119 let updated = match *slot {
1120 InferredColumn::Unknown => InferredColumn::Known(inferred),
1121 InferredColumn::Known(existing) if existing != inferred => {
1122 InferredColumn::Conflicting
1123 }
1124 InferredColumn::Known(_) | InferredColumn::Conflicting => continue,
1125 };
1126 *slot = updated;
1127 changed = true;
1128 }
1129 }
1130 }
1131 if !changed {
1132 converged = true;
1133 break;
1134 }
1135 }
1136 debug_assert!(
1137 converged,
1138 "predicate schema inference exceeded its monotonic transition bound"
1139 );
1140
1141 Ok(schemas)
1142 }
1143
1144 fn validate_inferred_predicate_contributions(
1145 providers: &ImportProviders,
1146 ) -> Result<(), ModuleError> {
1147 let schemas = Self::inferred_predicate_schemas(providers)?;
1148 let schema_inference = Lowerer::new();
1149 for (key, contributions) in &providers.inferred_predicate_contributions {
1150 if providers
1151 .predicate_declarations
1152 .get(&key.name)
1153 .is_some_and(|declaration| declaration.schema.columns.len() == key.arity)
1154 {
1155 continue;
1156 }
1157
1158 let inferred = contributions
1159 .iter()
1160 .map(|contribution| {
1161 Self::inferred_rule_columns(&schema_inference, &contribution.rule, &schemas)
1162 .map(|columns| (contribution, columns))
1163 .map_err(|error| {
1164 Self::predicate_schema_inference_error(key, contribution, error)
1165 })
1166 })
1167 .collect::<Result<Vec<_>, _>>()?;
1168 for (left_index, (left, left_columns)) in inferred.iter().enumerate() {
1169 for (right, right_columns) in inferred.iter().skip(left_index + 1) {
1170 if left.provider.source == right.provider.source {
1171 continue;
1172 }
1173 for (column_index, (left_type, right_type)) in
1174 left_columns.iter().zip(right_columns).enumerate()
1175 {
1176 if let (Some(type1), Some(type2)) = (left_type, right_type) {
1177 if type1 != type2 {
1178 return Err(ModuleError::IncompatibleInferredPredicateSchema {
1179 name: key.name.clone(),
1180 arity: key.arity,
1181 column: column_index + 1,
1182 type1: *type1,
1183 type2: *type2,
1184 module1: left.provider.module.clone(),
1185 module2: right.provider.module.clone(),
1186 source1: Box::new(left.provider.source.clone()),
1187 source2: Box::new(right.provider.source.clone()),
1188 });
1189 }
1190 }
1191 }
1192 }
1193 }
1194 }
1195
1196 Ok(())
1197 }
1198
1199 fn merge_function_providers(
1200 providers: &mut BTreeMap<String, ImportProvider>,
1201 incoming: BTreeMap<String, ImportProvider>,
1202 ) -> Result<(), ModuleError> {
1203 for (name, provider) in incoming {
1204 if let Some(existing) = providers.get(&name) {
1205 if existing.source != provider.source {
1206 return Err(ModuleError::ImportConflict {
1207 name,
1208 module1: existing.module.clone(),
1209 module2: provider.module,
1210 });
1211 }
1212 } else {
1213 providers.insert(name, provider);
1214 }
1215 }
1216 Ok(())
1217 }
1218
1219 fn record_predicate_declaration(
1220 declarations: &mut BTreeMap<String, ImportedPredicateDeclaration>,
1221 name: String,
1222 incoming: ImportedPredicateDeclaration,
1223 ) -> Result<(), ModuleError> {
1224 if let Some(existing) = declarations.get(&name) {
1225 if existing.schema != incoming.schema {
1226 return Err(ModuleError::IncompatiblePredicateDeclaration {
1227 name,
1228 module1: existing.module.clone(),
1229 module2: incoming.module,
1230 });
1231 }
1232 } else {
1233 declarations.insert(name, incoming);
1234 }
1235 Ok(())
1236 }
1237
1238 fn normalized_type_ref(
1239 domains: &BTreeMap<String, ImportedDomainDeclaration>,
1240 typ: &TypeRef,
1241 ) -> TypeRef {
1242 match typ {
1243 TypeRef::Domain(name) => domains
1244 .get(name)
1245 .map(|domain| TypeRef::Scalar(domain.declaration.typ))
1246 .unwrap_or_else(|| typ.clone()),
1247 TypeRef::List(element) => {
1248 TypeRef::List(Box::new(Self::normalized_type_ref(domains, element)))
1249 }
1250 _ => typ.clone(),
1251 }
1252 }
1253
1254 fn predicate_declaration_schema(
1255 domains: &BTreeMap<String, ImportedDomainDeclaration>,
1256 declaration: &PredDecl,
1257 ) -> PredicateDeclarationSchema {
1258 PredicateDeclarationSchema {
1259 columns: declaration
1260 .schema_columns()
1261 .iter()
1262 .map(|column| {
1263 (
1264 column.name.clone(),
1265 Self::normalized_type_ref(domains, &column.typ),
1266 )
1267 })
1268 .collect(),
1269 }
1270 }
1271
1272 fn merge_predicate_declaration_map(
1273 declarations: &mut BTreeMap<String, ImportedPredicateDeclaration>,
1274 incoming: BTreeMap<String, ImportedPredicateDeclaration>,
1275 ) -> Result<(), ModuleError> {
1276 for (name, declaration) in incoming {
1277 Self::record_predicate_declaration(declarations, name, declaration)?;
1278 }
1279 Ok(())
1280 }
1281
1282 fn record_domain_declaration(
1283 declarations: &mut BTreeMap<String, ImportedDomainDeclaration>,
1284 name: String,
1285 incoming: ImportedDomainDeclaration,
1286 ) -> Result<(), ModuleError> {
1287 if let Some(existing) = declarations.get(&name) {
1288 if existing.declaration.typ != incoming.declaration.typ {
1289 return Err(ModuleError::IncompatibleDomainDeclaration {
1290 name,
1291 module1: existing.module.clone(),
1292 module2: incoming.module,
1293 });
1294 }
1295 } else {
1296 declarations.insert(name, incoming);
1297 }
1298 Ok(())
1299 }
1300
1301 fn merge_domain_declaration_map(
1302 declarations: &mut BTreeMap<String, ImportedDomainDeclaration>,
1303 incoming: BTreeMap<String, ImportedDomainDeclaration>,
1304 ) -> Result<(), ModuleError> {
1305 for (name, declaration) in incoming {
1306 Self::record_domain_declaration(declarations, name, declaration)?;
1307 }
1308 Ok(())
1309 }
1310
1311 fn validate_unique_imported_functions(
1312 program: &Program,
1313 module_path: &[String],
1314 ) -> Result<(), ModuleError> {
1315 let mut counts = HashMap::<&str, usize>::new();
1316 for function in &program.functions {
1317 *counts.entry(&function.name).or_default() += 1;
1318 }
1319 for function in program
1320 .functions
1321 .iter()
1322 .filter(|function| !function.is_private)
1323 {
1324 if counts.get(function.name.as_str()).copied().unwrap_or(0) > 1 {
1325 return Err(ModuleError::DuplicateImportedFunction {
1326 name: function.name.clone(),
1327 module: module_path.to_vec(),
1328 });
1329 }
1330 }
1331 Ok(())
1332 }
1333
1334 fn validate_consistent_predicate_visibility(
1335 program: &Program,
1336 module_path: &[String],
1337 ) -> Result<(), ModuleError> {
1338 let mut visibility = HashMap::<&str, bool>::new();
1339 for declaration in &program.predicates {
1340 match visibility.get(declaration.name.as_str()) {
1341 Some(is_private) if *is_private != declaration.is_private => {
1342 return Err(ModuleError::ConflictingPredicateVisibility {
1343 name: declaration.name.clone(),
1344 module: module_path.to_vec(),
1345 });
1346 }
1347 Some(_) => {}
1348 None => {
1349 visibility.insert(declaration.name.as_str(), declaration.is_private);
1350 }
1351 }
1352 }
1353 Ok(())
1354 }
1355
1356 fn import_providers_for_module(
1357 &self,
1358 source: &Path,
1359 module_path: &[String],
1360 imported_items: Option<&HashSet<String>>,
1361 ) -> Result<ImportProviders, ModuleError> {
1362 let loaded_module = self
1363 .loaded
1364 .get(source)
1365 .ok_or_else(|| ModuleError::NotFound {
1366 path: module_path.to_vec(),
1367 searched: vec![],
1368 })?;
1369 let nested_imports = self.imports_for_source(source);
1370
1371 Self::validate_consistent_predicate_visibility(&loaded_module.program, module_path)?;
1372 Self::validate_unique_imported_functions(&loaded_module.program, module_path)?;
1373 self.validate_resolved_imports(nested_imports)?;
1374 let mut providers = self.import_providers_from_imports(nested_imports)?;
1375 for declaration in &loaded_module.program.domains {
1376 Self::record_domain_declaration(
1377 &mut providers.domain_declarations,
1378 declaration.name.clone(),
1379 ImportedDomainDeclaration {
1380 module: module_path.to_vec(),
1381 declaration: declaration.clone(),
1382 },
1383 )?;
1384 }
1385 for declaration in loaded_module
1386 .program
1387 .predicates
1388 .iter()
1389 .filter(|declaration| {
1390 !declaration.is_private
1391 && imported_items.is_none_or(|items| items.contains(&declaration.name))
1392 })
1393 {
1394 let schema =
1395 Self::predicate_declaration_schema(&providers.domain_declarations, declaration);
1396 Self::record_predicate_declaration(
1397 &mut providers.predicate_declarations,
1398 declaration.name.clone(),
1399 ImportedPredicateDeclaration {
1400 module: module_path.to_vec(),
1401 schema,
1402 },
1403 )?;
1404 }
1405 let excluded_predicates =
1406 Self::hidden_local_items(&loaded_module.program, imported_items).predicates;
1407 let local_contributions = Self::local_inferred_predicate_contributions(
1408 &loaded_module.program,
1409 &excluded_predicates,
1410 ImportProvider {
1411 module: module_path.to_vec(),
1412 source: source.to_path_buf(),
1413 },
1414 );
1415 Self::merge_inferred_predicate_contributions(
1416 &mut providers.inferred_predicate_contributions,
1417 local_contributions,
1418 );
1419 let mut local_functions = BTreeMap::new();
1420 for name in Self::visible_local_functions(&loaded_module.program, imported_items) {
1421 local_functions.insert(
1422 name,
1423 ImportProvider {
1424 module: module_path.to_vec(),
1425 source: source.to_path_buf(),
1426 },
1427 );
1428 }
1429 Self::merge_function_providers(&mut providers.functions, local_functions)?;
1433 Ok(providers)
1434 }
1435
1436 fn import_providers_from_imports(
1437 &self,
1438 imports: &[ResolvedImport],
1439 ) -> Result<ImportProviders, ModuleError> {
1440 let mut providers = ImportProviders::default();
1441 for group in Self::combined_import_selections(imports) {
1442 let incoming = self.import_providers_for_module(
1443 &group.source,
1444 &group.module_path,
1445 group.imported_items.as_ref(),
1446 )?;
1447 Self::merge_domain_declaration_map(
1448 &mut providers.domain_declarations,
1449 incoming.domain_declarations,
1450 )?;
1451 Self::merge_predicate_declaration_map(
1452 &mut providers.predicate_declarations,
1453 incoming.predicate_declarations,
1454 )?;
1455 Self::merge_inferred_predicate_contributions(
1456 &mut providers.inferred_predicate_contributions,
1457 incoming.inferred_predicate_contributions,
1458 );
1459 Self::merge_function_providers(&mut providers.functions, incoming.functions)?;
1460 }
1461 Ok(providers)
1462 }
1463
1464 fn provider_for_program(&self, program: &Program) -> ImportProvider {
1465 if let Some(entry) = &self.entry {
1466 if entry.program.imports == program.imports {
1467 return ImportProvider {
1468 module: entry.path.clone(),
1469 source: self
1470 .entry_source
1471 .clone()
1472 .unwrap_or_else(|| entry.source_file.clone()),
1473 };
1474 }
1475 }
1476 if let Some(root_source) = &self.root_module {
1477 if let Some(root) = self.loaded.get(root_source) {
1478 if root.program.imports == program.imports {
1479 return ImportProvider {
1480 module: root.path.clone(),
1481 source: root_source.clone(),
1482 };
1483 }
1484 }
1485 }
1486 ImportProvider {
1487 module: vec!["<program>".to_string()],
1488 source: PathBuf::from("<program>"),
1489 }
1490 }
1491
1492 fn validate_program_against_imports(
1493 &self,
1494 program: &Program,
1495 imports: &[ResolvedImport],
1496 ) -> Result<(), ModuleError> {
1497 let program_provider = self.provider_for_program(program);
1498 let module_path = program_provider.module.clone();
1499 let mut providers = self.import_providers_from_imports(imports)?;
1500
1501 for declaration in &program.domains {
1502 Self::record_domain_declaration(
1503 &mut providers.domain_declarations,
1504 declaration.name.clone(),
1505 ImportedDomainDeclaration {
1506 module: module_path.clone(),
1507 declaration: declaration.clone(),
1508 },
1509 )?;
1510 }
1511 for declaration in &program.predicates {
1512 let schema =
1513 Self::predicate_declaration_schema(&providers.domain_declarations, declaration);
1514 Self::record_predicate_declaration(
1515 &mut providers.predicate_declarations,
1516 declaration.name.clone(),
1517 ImportedPredicateDeclaration {
1518 module: module_path.clone(),
1519 schema,
1520 },
1521 )?;
1522 }
1523 let entry_contributions = Self::local_inferred_predicate_contributions(
1524 program,
1525 &HashSet::new(),
1526 program_provider,
1527 );
1528 Self::merge_inferred_predicate_contributions(
1529 &mut providers.inferred_predicate_contributions,
1530 entry_contributions,
1531 );
1532 for function in &program.functions {
1533 if let Some(imported) = providers.functions.get(&function.name) {
1534 return Err(ModuleError::ImportConflict {
1535 name: function.name.clone(),
1536 module1: imported.module.clone(),
1537 module2: module_path,
1538 });
1539 }
1540 }
1541
1542 Self::validate_inferred_predicate_contributions(&providers)?;
1543
1544 Ok(())
1545 }
1546
1547 fn import_scope_for_module(
1548 &self,
1549 source: &Path,
1550 module_path: &[String],
1551 imported_items: Option<&HashSet<String>>,
1552 ) -> Result<ImportScope, ModuleError> {
1553 let loaded_module = self
1554 .loaded
1555 .get(source)
1556 .ok_or_else(|| ModuleError::NotFound {
1557 path: module_path.to_vec(),
1558 searched: vec![],
1559 })?;
1560 let nested_imports = self.imports_for_source(source);
1561 let local_predicates = Self::local_predicate_names(&loaded_module.program);
1562 let local_functions = Self::local_function_names(&loaded_module.program);
1563 let local_hidden = Self::hidden_local_items(&loaded_module.program, imported_items);
1564 let mut scope = self.import_scope_from_imports(nested_imports)?;
1565 scope.visible.predicates.extend(
1566 local_predicates
1567 .difference(&local_hidden.predicates)
1568 .cloned(),
1569 );
1570 scope
1571 .visible
1572 .functions
1573 .extend(local_functions.difference(&local_hidden.functions).cloned());
1574 scope.hidden.predicates.extend(local_hidden.predicates);
1575 scope.hidden.functions.extend(local_hidden.functions);
1576 scope
1579 .hidden
1580 .predicates
1581 .retain(|name| !scope.visible.predicates.contains(name));
1582 scope
1583 .hidden
1584 .functions
1585 .retain(|name| !scope.visible.functions.contains(name));
1586 Ok(scope)
1587 }
1588
1589 fn import_scope_from_imports(
1590 &self,
1591 imports: &[ResolvedImport],
1592 ) -> Result<ImportScope, ModuleError> {
1593 let mut scope = ImportScope::default();
1594 for group in Self::combined_import_selections(imports) {
1595 let imported_scope = self.import_scope_for_module(
1596 &group.source,
1597 &group.module_path,
1598 group.imported_items.as_ref(),
1599 )?;
1600 scope
1601 .visible
1602 .predicates
1603 .extend(imported_scope.visible.predicates);
1604 scope
1605 .visible
1606 .functions
1607 .extend(imported_scope.visible.functions);
1608 scope
1609 .hidden
1610 .predicates
1611 .extend(imported_scope.hidden.predicates);
1612 scope
1613 .hidden
1614 .functions
1615 .extend(imported_scope.hidden.functions);
1616 }
1617 scope
1618 .hidden
1619 .predicates
1620 .retain(|name| !scope.visible.predicates.contains(name));
1621 scope
1622 .hidden
1623 .functions
1624 .retain(|name| !scope.visible.functions.contains(name));
1625 Ok(scope)
1626 }
1627
1628 fn imports_for_source(&self, source: &Path) -> &[ResolvedImport] {
1629 self.resolved_imports
1630 .get(source)
1631 .map(Vec::as_slice)
1632 .unwrap_or(&[])
1633 }
1634
1635 fn validate_visible_dependencies(
1636 program: &Program,
1637 imported_items: Option<&HashSet<String>>,
1638 imported_scope: &ImportScope,
1639 module_path: &[String],
1640 ) -> Result<(), ModuleError> {
1641 let local_predicates = Self::local_predicate_names(program);
1642 let local_functions = Self::local_function_names(program);
1643 let local_hidden = Self::hidden_local_items(program, imported_items);
1644 let mut hidden_predicates = local_hidden.predicates.clone();
1645 hidden_predicates.extend(
1646 imported_scope
1647 .hidden
1648 .predicates
1649 .difference(&local_predicates)
1650 .cloned(),
1651 );
1652 let mut hidden_functions = local_hidden.functions.clone();
1653 hidden_functions.extend(
1654 imported_scope
1655 .hidden
1656 .functions
1657 .difference(&local_functions)
1658 .cloned(),
1659 );
1660
1661 for rule in &program.rules {
1662 if local_hidden.predicates.contains(&rule.head.predicate) {
1663 continue;
1664 }
1665 let mut predicate_dependencies = HashSet::new();
1666 let mut function_dependencies = HashSet::new();
1667 collect_body_dependencies(
1668 &rule.body,
1669 &mut predicate_dependencies,
1670 &mut function_dependencies,
1671 );
1672 let mut hidden_dependencies = predicate_dependencies
1673 .intersection(&hidden_predicates)
1674 .chain(function_dependencies.intersection(&hidden_functions))
1675 .cloned()
1676 .collect::<Vec<_>>();
1677 hidden_dependencies.sort();
1678 if let Some(dependency) = hidden_dependencies.into_iter().next() {
1679 return Err(ModuleError::HiddenDependency {
1680 module: module_path.to_vec(),
1681 export: rule.head.predicate.clone(),
1682 dependency,
1683 });
1684 }
1685 }
1686
1687 for function in &program.functions {
1688 if local_hidden.functions.contains(&function.name) {
1689 continue;
1690 }
1691 let mut predicate_dependencies = HashSet::new();
1692 let mut function_dependencies = HashSet::new();
1693 collect_function_body_dependencies(
1694 &function.body,
1695 &mut predicate_dependencies,
1696 &mut function_dependencies,
1697 );
1698 let mut hidden_dependencies = predicate_dependencies
1699 .intersection(&hidden_predicates)
1700 .chain(function_dependencies.intersection(&hidden_functions))
1701 .cloned()
1702 .collect::<Vec<_>>();
1703 hidden_dependencies.sort();
1704 if let Some(dependency) = hidden_dependencies.into_iter().next() {
1705 return Err(ModuleError::HiddenDependency {
1706 module: module_path.to_vec(),
1707 export: function.name.clone(),
1708 dependency,
1709 });
1710 }
1711 }
1712
1713 Ok(())
1714 }
1715
1716 fn validate_supported_import_content(
1717 program: &Program,
1718 module_path: &[String],
1719 ) -> Result<(), ModuleError> {
1720 let Program {
1721 imports: _,
1722 functions: _,
1723 domains: _,
1724 predicates: _,
1725 rules: _,
1726 constraints,
1727 authored_constraint_source_bound: _,
1728 queries: _,
1729 prob_facts,
1730 annotated_disjunctions,
1731 evidence,
1732 prob_queries: _,
1733 neural_predicates,
1734 learnable_rules,
1735 directives: _,
1736 } = program;
1737
1738 let mut constructs = Vec::new();
1739 if !prob_facts.is_empty() {
1740 constructs.push("probabilistic facts".to_string());
1741 }
1742 if !annotated_disjunctions.is_empty() {
1743 constructs.push("annotated disjunctions".to_string());
1744 }
1745 if !evidence.is_empty() {
1746 constructs.push("evidence statements".to_string());
1747 }
1748 if !constraints.is_empty() {
1749 constructs.push("integrity constraints".to_string());
1750 }
1751 if !neural_predicates.is_empty() {
1752 constructs.push("neural predicate declarations".to_string());
1753 }
1754 if !learnable_rules.is_empty() {
1755 constructs.push("learnable rule templates".to_string());
1756 }
1757 constructs.sort();
1758
1759 if constructs.is_empty() {
1760 Ok(())
1761 } else {
1762 Err(ModuleError::UnsupportedImportedContent {
1763 module: module_path.to_vec(),
1764 constructs,
1765 })
1766 }
1767 }
1768
1769 fn import_merge_key(source: &Path, imported_items: Option<&HashSet<String>>) -> ImportMergeKey {
1770 let imported_items = imported_items.map(|items| {
1771 let mut sorted = items.iter().cloned().collect::<Vec<_>>();
1772 sorted.sort();
1773 sorted
1774 });
1775 ImportMergeKey {
1776 source: source.to_path_buf(),
1777 imported_items,
1778 }
1779 }
1780
1781 fn merge_import_group(
1782 &self,
1783 program: &mut Program,
1784 imports: &[ResolvedImport],
1785 merged_imports: &mut HashSet<ImportMergeKey>,
1786 ) -> Result<(), ModuleError> {
1787 for group in Self::combined_import_selections(imports) {
1788 let loaded_module =
1789 self.loaded
1790 .get(&group.source)
1791 .ok_or_else(|| ModuleError::NotFound {
1792 path: group.module_path.clone(),
1793 searched: vec![],
1794 })?;
1795 let nested_imports = self.imports_for_source(&group.source);
1796
1797 self.merge_import_group(program, nested_imports, merged_imports)?;
1798
1799 let imported_scope = self.import_scope_from_imports(nested_imports)?;
1800 Self::validate_supported_import_content(&loaded_module.program, &group.module_path)?;
1801 Self::validate_visible_dependencies(
1802 &loaded_module.program,
1803 group.imported_items.as_ref(),
1804 &imported_scope,
1805 &group.module_path,
1806 )?;
1807 let merge_key = Self::import_merge_key(&group.source, group.imported_items.as_ref());
1808 if merged_imports.insert(merge_key) {
1809 program.merge_from(&loaded_module.program, group.imported_items.as_ref());
1810 }
1811 }
1812 Ok(())
1813 }
1814
1815 pub fn merge_imports(&self, mut program: Program) -> Result<Program, ModuleError> {
1832 let imports = self.resolved_imports_for_program(&program)?;
1833 self.validate_resolved_imports(&imports)?;
1834 self.validate_program_against_imports(&program, &imports)?;
1835 let entry_rules = std::mem::take(&mut program.rules);
1836 let mut merged_imports = HashSet::new();
1837 self.merge_import_group(&mut program, &imports, &mut merged_imports)?;
1838 program.rules.extend(entry_rules);
1839
1840 Ok(program)
1841 }
1842}
1843
1844#[cfg(test)]
1845mod tests {
1846 use super::*;
1847 use std::io::Write;
1848 use tempfile::TempDir;
1849
1850 fn create_test_module(dir: &Path, name: &str, content: &str) -> PathBuf {
1851 let path = dir.join(format!("{}.xlog", name));
1852 let mut file = fs::File::create(&path).unwrap();
1853 file.write_all(content.as_bytes()).unwrap();
1854 path
1855 }
1856
1857 #[test]
1858 fn test_find_module_file() {
1859 let tmp = TempDir::new().unwrap();
1860 create_test_module(tmp.path(), "graph", "edge(1, 2).");
1861
1862 let resolver = ModuleResolver::new(vec![]);
1863 let found = resolver.find_module_file(tmp.path(), &["graph".into()]);
1864 assert!(found.is_some());
1865 }
1866
1867 #[test]
1868 fn test_load_entry_file_uses_supplied_path() {
1869 let tmp = TempDir::new().unwrap();
1870 create_test_module(tmp.path(), "helper", "helper_fact(1).");
1871 let entry = tmp.path().join("program.datalog");
1872 fs::write(&entry, "use helper.\nentry_fact(1).\n").unwrap();
1873
1874 let mut resolver = ModuleResolver::new(vec![]);
1875 let loaded = resolver.load_entry_file(&entry).unwrap();
1876
1877 assert_eq!(loaded.source_file, entry);
1878 assert_eq!(loaded.path, vec!["program"]);
1879 assert_eq!(resolver.entry().unwrap().source_file, entry);
1880 assert!(resolver.is_loaded("helper"));
1881 }
1882
1883 #[test]
1884 fn test_load_entry_file_distinguishes_same_stem_import() {
1885 let tmp = TempDir::new().unwrap();
1886 create_test_module(tmp.path(), "main", "imported_fact(1).");
1887 let entry = tmp.path().join("main.datalog");
1888 let entry_source = "use main.\nentry_fact(1).\n";
1889 fs::write(&entry, entry_source).unwrap();
1890
1891 let mut resolver = ModuleResolver::new(vec![]);
1892 let loaded = resolver.load_entry_file(&entry).unwrap();
1893
1894 assert_eq!(loaded.source_file, entry);
1895 assert_eq!(
1896 resolver
1897 .get_module(&["main".into()])
1898 .expect("same-stem import should be loaded")
1899 .source_file,
1900 tmp.path().join("main.xlog")
1901 );
1902 let merged = resolver
1903 .merge_imports(parse_program(entry_source).unwrap())
1904 .unwrap();
1905 assert!(merged
1906 .rules
1907 .iter()
1908 .any(|rule| rule.head.predicate == "imported_fact"));
1909 }
1910
1911 #[test]
1912 fn test_module_not_found() {
1913 let tmp = TempDir::new().unwrap();
1914 let mut resolver = ModuleResolver::new(vec![]);
1915
1916 let result = resolver.load_module(tmp.path(), &["nonexistent".into()]);
1917 assert!(matches!(result, Err(ModuleError::NotFound { .. })));
1918 }
1919
1920 #[test]
1921 fn merge_imports_returns_not_found_for_unloaded_module() {
1922 let resolver = ModuleResolver::new(vec![]);
1923 let program = parse_program("use nonexistent.").unwrap();
1924
1925 let result = resolver.merge_imports(program);
1926
1927 assert!(matches!(
1928 result,
1929 Err(ModuleError::NotFound { path, searched })
1930 if path == vec!["nonexistent"] && searched.is_empty()
1931 ));
1932 }
1933
1934 #[test]
1935 fn merge_imports_unions_compatible_predicates_from_separate_modules() {
1936 let tmp = TempDir::new().unwrap();
1937 create_test_module(tmp.path(), "first", "pred shared(u32). shared(1).");
1938 create_test_module(tmp.path(), "second", "pred shared(u32). shared(2).");
1939 let entry_source = "use first.\nuse second.\n";
1940 create_test_module(tmp.path(), "entry", entry_source);
1941
1942 let mut resolver = ModuleResolver::new(vec![]);
1943 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
1944
1945 let (predicate_imports, function_imports) = resolver
1946 .validate_imports(&parse_program(entry_source).unwrap())
1947 .unwrap();
1948 assert_eq!(predicate_imports.get("shared"), Some(&vec!["first".into()]));
1949 assert!(function_imports.is_empty());
1950
1951 let merged = resolver
1952 .merge_imports(parse_program(entry_source).unwrap())
1953 .unwrap();
1954 let shared_facts = merged
1955 .rules
1956 .iter()
1957 .filter(|rule| rule.head.predicate == "shared" && rule.is_fact())
1958 .collect::<Vec<_>>();
1959
1960 assert_eq!(shared_facts.len(), 2);
1961 assert!(shared_facts
1962 .iter()
1963 .any(|rule| rule.head.terms == vec![Term::Integer(1)]));
1964 assert!(shared_facts
1965 .iter()
1966 .any(|rule| rule.head.terms == vec![Term::Integer(2)]));
1967 assert_eq!(
1968 merged
1969 .predicates
1970 .iter()
1971 .filter(|declaration| declaration.name == "shared")
1972 .count(),
1973 1
1974 );
1975 }
1976
1977 #[test]
1978 fn merge_imports_unions_compatible_undeclared_predicates() {
1979 let tmp = TempDir::new().unwrap();
1980 create_test_module(tmp.path(), "first", "shared(from_first).");
1981 create_test_module(tmp.path(), "second", "shared(from_second).");
1982 let entry_source = "use first.\nuse second.\n";
1983 create_test_module(tmp.path(), "entry", entry_source);
1984
1985 let mut resolver = ModuleResolver::new(vec![]);
1986 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
1987
1988 let merged = resolver
1989 .merge_imports(parse_program(entry_source).unwrap())
1990 .unwrap();
1991 let shared_facts = merged
1992 .rules
1993 .iter()
1994 .filter(|rule| rule.head.predicate == "shared" && rule.is_fact())
1995 .collect::<Vec<_>>();
1996
1997 assert_eq!(shared_facts.len(), 2);
1998 assert!(shared_facts
1999 .iter()
2000 .any(|rule| rule.head.terms
2001 == vec![Term::Symbol(xlog_core::symbol::intern("from_first"))]));
2002 assert!(shared_facts
2003 .iter()
2004 .any(|rule| rule.head.terms
2005 == vec![Term::Symbol(xlog_core::symbol::intern("from_second"))]));
2006 assert!(!merged
2007 .predicates
2008 .iter()
2009 .any(|declaration| declaration.name == "shared"));
2010 }
2011
2012 #[test]
2013 fn merge_import_validation_keeps_undeclared_predicate_arities_distinct() {
2014 let tmp = TempDir::new().unwrap();
2015 create_test_module(tmp.path(), "unary", "shared(1).");
2016 create_test_module(tmp.path(), "binary", "shared(one, two).");
2017 let entry_source = "use unary.\nuse binary.\n";
2018 create_test_module(tmp.path(), "entry", entry_source);
2019
2020 let mut resolver = ModuleResolver::new(vec![]);
2021 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2022
2023 let merged = resolver
2024 .merge_imports(parse_program(entry_source).unwrap())
2025 .unwrap();
2026 let arities = merged
2027 .rules
2028 .iter()
2029 .filter(|rule| rule.head.predicate == "shared")
2030 .map(|rule| rule.head.arity())
2031 .collect::<Vec<_>>();
2032
2033 assert_eq!(arities, vec![1, 2]);
2034 }
2035
2036 #[test]
2037 fn merge_imports_rejects_incompatible_undeclared_predicate_schemas() {
2038 let tmp = TempDir::new().unwrap();
2039 create_test_module(tmp.path(), "first", "shared(row, 1).");
2040 create_test_module(tmp.path(), "second", "shared(row, from_second).");
2041 let entry_source = "use first.\nuse second.\n";
2042 create_test_module(tmp.path(), "entry", entry_source);
2043
2044 let mut resolver = ModuleResolver::new(vec![]);
2045 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2046
2047 let program = parse_program(entry_source).unwrap();
2048 let errors = [
2049 resolver
2050 .validate_imports(&program)
2051 .expect_err("validation must reject different inferred column types"),
2052 resolver
2053 .merge_imports(program)
2054 .expect_err("merge must reject different inferred column types"),
2055 ];
2056
2057 for error in errors {
2058 let message = error.to_string();
2059 assert!(message.contains("error[E0412]"), "{message}");
2060 assert!(message.contains("shared/2"), "{message}");
2061 assert!(message.contains("column 2"), "{message}");
2062 assert!(message.contains("first"), "{message}");
2063 assert!(message.contains("second"), "{message}");
2064 assert!(message.contains("u32"), "{message}");
2065 assert!(message.contains("symbol"), "{message}");
2066 }
2067 }
2068
2069 #[test]
2070 fn merge_imports_rejects_numeric_inference_conflicts_in_either_import_order() {
2071 let tmp = TempDir::new().unwrap();
2072 create_test_module(tmp.path(), "small", "shared(1).");
2073 create_test_module(tmp.path(), "wide", "shared(5000000000).");
2074
2075 for (entry_name, entry_source) in [
2076 ("small_first", "use small.\nuse wide.\n"),
2077 ("wide_first", "use wide.\nuse small.\n"),
2078 ] {
2079 create_test_module(tmp.path(), entry_name, entry_source);
2080 let mut resolver = ModuleResolver::new(vec![]);
2081 resolver
2082 .load_module(tmp.path(), &[entry_name.into()])
2083 .unwrap();
2084
2085 let error = resolver
2086 .merge_imports(parse_program(entry_source).unwrap())
2087 .expect_err("numeric inference must not depend on import order");
2088 let message = error.to_string();
2089
2090 assert!(message.contains("error[E0412]"), "{message}");
2091 assert!(message.contains("shared/1"), "{message}");
2092 assert!(message.contains("module `small`"), "{message}");
2093 assert!(message.contains("module `wide`"), "{message}");
2094 assert!(message.contains("u32"), "{message}");
2095 assert!(message.contains("i64"), "{message}");
2096 }
2097 }
2098
2099 #[test]
2100 fn merge_imports_uses_an_explicit_schema_for_undeclared_contributions() {
2101 let tmp = TempDir::new().unwrap();
2102 create_test_module(tmp.path(), "small", "shared(1).");
2103 create_test_module(tmp.path(), "wide", "shared(5000000000).");
2104 let entry_source = "use small.\nuse wide.\npred shared(i64).\n";
2105 create_test_module(tmp.path(), "entry", entry_source);
2106
2107 let mut resolver = ModuleResolver::new(vec![]);
2108 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2109
2110 let merged = resolver
2111 .merge_imports(parse_program(entry_source).unwrap())
2112 .expect("the explicit schema controls both imported facts");
2113
2114 assert_eq!(
2115 merged
2116 .rules
2117 .iter()
2118 .filter(|rule| rule.head.predicate == "shared")
2119 .count(),
2120 2
2121 );
2122 }
2123
2124 #[test]
2125 fn merge_imports_rejects_transitive_inferred_schema_conflicts() {
2126 let tmp = TempDir::new().unwrap();
2127 create_test_module(tmp.path(), "left_provider", "shared(1).");
2128 create_test_module(tmp.path(), "left_wrapper", "use left_provider.\n");
2129 create_test_module(tmp.path(), "right_provider", "shared(from_right).");
2130 create_test_module(tmp.path(), "right_wrapper", "use right_provider.\n");
2131 let entry_source = "use left_wrapper.\nuse right_wrapper.\n";
2132 create_test_module(tmp.path(), "entry", entry_source);
2133
2134 let mut resolver = ModuleResolver::new(vec![]);
2135 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2136
2137 let error = resolver
2138 .merge_imports(parse_program(entry_source).unwrap())
2139 .expect_err("transitive contributions must be schema-compatible");
2140 let message = error.to_string();
2141
2142 assert!(message.contains("error[E0412]"), "{message}");
2143 assert!(message.contains("shared/1"), "{message}");
2144 assert!(message.contains("left_provider"), "{message}");
2145 assert!(message.contains("right_provider"), "{message}");
2146 }
2147
2148 #[test]
2149 fn merge_imports_rejects_constant_head_rule_schema_conflicts() {
2150 let tmp = TempDir::new().unwrap();
2151 create_test_module(
2152 tmp.path(),
2153 "first",
2154 "first_source(ready).\nshared(1) :- first_source(ready).",
2155 );
2156 create_test_module(
2157 tmp.path(),
2158 "second",
2159 "second_source(ready).\nshared(from_second) :- second_source(ready).",
2160 );
2161 let entry_source = "use first.\nuse second.\n";
2162 create_test_module(tmp.path(), "entry", entry_source);
2163
2164 let mut resolver = ModuleResolver::new(vec![]);
2165 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2166
2167 let error = resolver
2168 .merge_imports(parse_program(entry_source).unwrap())
2169 .expect_err("known constant head types must agree across modules");
2170
2171 assert!(error.to_string().contains("error[E0412]"));
2172 }
2173
2174 #[test]
2175 fn merge_imports_rejects_body_inferred_rule_head_conflicts() {
2176 let tmp = TempDir::new().unwrap();
2177 create_test_module(
2178 tmp.path(),
2179 "first",
2180 "first_source(1).\nshared(X) :- first_source(X).",
2181 );
2182 create_test_module(
2183 tmp.path(),
2184 "second",
2185 "second_source(from_second).\nshared(X) :- second_source(X).",
2186 );
2187 let entry_source = "use first.\nuse second.\n";
2188 create_test_module(tmp.path(), "entry", entry_source);
2189
2190 let mut resolver = ModuleResolver::new(vec![]);
2191 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2192
2193 let error = resolver
2194 .merge_imports(parse_program(entry_source).unwrap())
2195 .expect_err("body-derived head types must agree across modules");
2196
2197 let message = error.to_string();
2198 assert!(message.contains("error[E0412]"), "{message}");
2199 assert!(message.contains("shared/1"), "{message}");
2200 assert!(message.contains("u32"), "{message}");
2201 assert!(message.contains("symbol"), "{message}");
2202 }
2203
2204 #[test]
2205 fn merge_imports_rejects_arithmetic_inferred_rule_head_conflicts() {
2206 let tmp = TempDir::new().unwrap();
2207 create_test_module(tmp.path(), "first", "shared(X) :- X is cast(1, u32).");
2208 create_test_module(tmp.path(), "second", "shared(X) :- X is cast(1, u64).");
2209 let entry_source = "use first.\nuse second.\n";
2210 create_test_module(tmp.path(), "entry", entry_source);
2211
2212 let mut resolver = ModuleResolver::new(vec![]);
2213 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2214
2215 let error = resolver
2216 .merge_imports(parse_program(entry_source).unwrap())
2217 .expect_err("arithmetic-derived head types must agree across modules");
2218 let message = error.to_string();
2219
2220 assert!(message.contains("error[E0412]"), "{message}");
2221 assert!(message.contains("shared/1"), "{message}");
2222 assert!(message.contains("module `first`"), "{message}");
2223 assert!(message.contains("module `second`"), "{message}");
2224 assert!(message.contains("u32"), "{message}");
2225 assert!(message.contains("u64"), "{message}");
2226 }
2227
2228 #[test]
2229 fn merge_imports_rejects_aggregate_inferred_rule_head_conflicts() {
2230 let tmp = TempDir::new().unwrap();
2231 create_test_module(
2232 tmp.path(),
2233 "first",
2234 "first_source(1).\nshared(min(X)) :- first_source(X).",
2235 );
2236 create_test_module(
2237 tmp.path(),
2238 "second",
2239 "second_source(1.0).\nshared(logsumexp(X)) :- second_source(X).",
2240 );
2241 let entry_source = "use first.\nuse second.\n";
2242 create_test_module(tmp.path(), "entry", entry_source);
2243
2244 let mut resolver = ModuleResolver::new(vec![]);
2245 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2246
2247 let error = resolver
2248 .merge_imports(parse_program(entry_source).unwrap())
2249 .expect_err("aggregate-derived head types must agree across modules");
2250 let message = error.to_string();
2251
2252 assert!(message.contains("error[E0412]"), "{message}");
2253 assert!(message.contains("shared/1"), "{message}");
2254 assert!(message.contains("module `first`"), "{message}");
2255 assert!(message.contains("module `second`"), "{message}");
2256 assert!(message.contains("u32"), "{message}");
2257 assert!(message.contains("f64"), "{message}");
2258 }
2259
2260 #[test]
2261 fn merge_imports_attributes_invalid_schema_inference_to_its_module() {
2262 let tmp = TempDir::new().unwrap();
2263 create_test_module(
2264 tmp.path(),
2265 "library",
2266 "shared(X) :- X is cast(1, u32) + cast(1, u64).",
2267 );
2268 let entry_source = "use library.\n";
2269 create_test_module(tmp.path(), "entry", entry_source);
2270
2271 let mut resolver = ModuleResolver::new(vec![]);
2272 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2273
2274 let error = resolver
2275 .merge_imports(parse_program(entry_source).unwrap())
2276 .expect_err("invalid arithmetic evidence must fail during schema inference");
2277 let message = error.to_string();
2278
2279 assert!(message.contains("error[E0413]"), "{message}");
2280 assert!(message.contains("shared/1"), "{message}");
2281 assert!(message.contains("module `library`"), "{message}");
2282 assert!(message.contains("Type mismatch in arithmetic"), "{message}");
2283 }
2284
2285 #[test]
2286 fn merge_imports_defers_user_function_schema_evidence_until_expansion() {
2287 let tmp = TempDir::new().unwrap();
2288 create_test_module(
2289 tmp.path(),
2290 "first",
2291 "func first_value(X) = cast(X, u32).\nshared(X) :- X is first_value(1).",
2292 );
2293 create_test_module(
2294 tmp.path(),
2295 "second",
2296 "func second_value(X) = cast(X, u64).\nshared(X) :- X is second_value(1).",
2297 );
2298 let entry_source = "use first.\nuse second.\n";
2299 create_test_module(tmp.path(), "entry", entry_source);
2300
2301 let mut resolver = ModuleResolver::new(vec![]);
2302 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2303
2304 let merged = resolver
2305 .merge_imports(parse_program(entry_source).unwrap())
2306 .expect("user-defined calls are expanded after module resolution");
2307
2308 assert_eq!(
2309 merged
2310 .rules
2311 .iter()
2312 .filter(|rule| rule.head.predicate == "shared")
2313 .count(),
2314 2
2315 );
2316 }
2317
2318 #[test]
2319 fn merge_imports_retains_independent_schema_evidence_beside_user_functions() {
2320 let tmp = TempDir::new().unwrap();
2321 create_test_module(
2322 tmp.path(),
2323 "first",
2324 "func first_value(X) = cast(X, u32).\nshared(1, X) :- X is first_value(1).",
2325 );
2326 create_test_module(
2327 tmp.path(),
2328 "second",
2329 "func second_value(X) = cast(X, u32).\nshared(from_second, X) :- X is second_value(1).",
2330 );
2331 let entry_source = "use first.\nuse second.\n";
2332 create_test_module(tmp.path(), "entry", entry_source);
2333
2334 let mut resolver = ModuleResolver::new(vec![]);
2335 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2336
2337 let error = resolver
2338 .merge_imports(parse_program(entry_source).unwrap())
2339 .expect_err("a user function must not hide independent head-column evidence");
2340 let message = error.to_string();
2341
2342 assert!(message.contains("error[E0412]"), "{message}");
2343 assert!(message.contains("shared/2"), "{message}");
2344 assert!(message.contains("module `first`"), "{message}");
2345 assert!(message.contains("module `second`"), "{message}");
2346 assert!(message.contains("u32"), "{message}");
2347 assert!(message.contains("symbol"), "{message}");
2348 }
2349
2350 #[test]
2351 fn merge_imports_reports_invalid_builtin_evidence_beside_user_functions() {
2352 let tmp = TempDir::new().unwrap();
2353 create_test_module(
2354 tmp.path(),
2355 "library",
2356 "func value(X) = cast(X, u32).\nshared(X, Y) :- X is value(1), Y is cast(1, u32) + cast(1, u64).",
2357 );
2358 let entry_source = "use library.\n";
2359 create_test_module(tmp.path(), "entry", entry_source);
2360
2361 let mut resolver = ModuleResolver::new(vec![]);
2362 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2363
2364 let error = resolver
2365 .merge_imports(parse_program(entry_source).unwrap())
2366 .expect_err("a user function must not hide invalid built-in arithmetic evidence");
2367 let message = error.to_string();
2368
2369 assert!(message.contains("error[E0413]"), "{message}");
2370 assert!(message.contains("shared/2"), "{message}");
2371 assert!(message.contains("module `library`"), "{message}");
2372 assert!(message.contains("Type mismatch in arithmetic"), "{message}");
2373 }
2374
2375 #[test]
2376 fn merge_imports_reports_invalid_builtin_subexpressions_beside_user_functions() {
2377 let tmp = TempDir::new().unwrap();
2378 create_test_module(
2379 tmp.path(),
2380 "library",
2381 "func value(X) = cast(X, u32).\nshared(Y) :- Y is value(1) + (cast(1, u32) + cast(1, u64)).",
2382 );
2383 let entry_source = "use library.\n";
2384 create_test_module(tmp.path(), "entry", entry_source);
2385
2386 let mut resolver = ModuleResolver::new(vec![]);
2387 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2388
2389 let error = resolver
2390 .merge_imports(parse_program(entry_source).unwrap())
2391 .expect_err("an unknown user-function operand must not hide an invalid sibling");
2392 let message = error.to_string();
2393
2394 assert!(message.contains("error[E0413]"), "{message}");
2395 assert!(message.contains("shared/1"), "{message}");
2396 assert!(message.contains("module `library`"), "{message}");
2397 assert!(message.contains("Type mismatch in arithmetic"), "{message}");
2398 }
2399
2400 #[test]
2401 fn merge_imports_reports_invalid_user_function_argument_evidence() {
2402 let tmp = TempDir::new().unwrap();
2403 create_test_module(
2404 tmp.path(),
2405 "library",
2406 "func value(X) = cast(X, u32).\nshared(Y) :- Y is value(cast(1, u32) + cast(1, u64)).",
2407 );
2408 let entry_source = "use library.\n";
2409 create_test_module(tmp.path(), "entry", entry_source);
2410
2411 let mut resolver = ModuleResolver::new(vec![]);
2412 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2413
2414 let error = resolver
2415 .merge_imports(parse_program(entry_source).unwrap())
2416 .expect_err("invalid built-in arithmetic inside a user-function argument must fail");
2417 let message = error.to_string();
2418
2419 assert!(message.contains("error[E0413]"), "{message}");
2420 assert!(message.contains("shared/1"), "{message}");
2421 assert!(message.contains("module `library`"), "{message}");
2422 assert!(message.contains("Type mismatch in arithmetic"), "{message}");
2423 }
2424
2425 #[test]
2426 fn merge_imports_reports_invalid_partial_arithmetic_evidence() {
2427 let cases = [
2428 (
2429 "symbols(foo).\nshared(Y) :- symbols(S), Y is value(1) + S.",
2430 "Arithmetic requires numeric type",
2431 ),
2432 (
2433 "floats(1.0).\nshared(Y) :- floats(F), Y is value(1) % F.",
2434 "Modulo (%) not supported for floating point",
2435 ),
2436 (
2437 "symbols(foo).\nshared(Y) :- symbols(S), Y is value(1) % S.",
2438 "Modulo (%) requires integer operands",
2439 ),
2440 ];
2441
2442 for (body, expected_detail) in cases {
2443 let tmp = TempDir::new().unwrap();
2444 create_test_module(
2445 tmp.path(),
2446 "library",
2447 &format!("func value(X) = cast(X, u32).\n{body}"),
2448 );
2449 let entry_source = "use library.\n";
2450 create_test_module(tmp.path(), "entry", entry_source);
2451
2452 let mut resolver = ModuleResolver::new(vec![]);
2453 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2454
2455 let error = resolver
2456 .merge_imports(parse_program(entry_source).unwrap())
2457 .expect_err("a deferred operand must not hide an invalid known operand");
2458 let message = error.to_string();
2459
2460 assert!(message.contains("error[E0413]"), "{message}");
2461 assert!(message.contains("shared/1"), "{message}");
2462 assert!(message.contains("module `library`"), "{message}");
2463 assert!(message.contains(expected_detail), "{message}");
2464 }
2465 }
2466
2467 #[test]
2468 fn merge_imports_propagates_signature_types_through_rule_chains() {
2469 let tmp = TempDir::new().unwrap();
2470 create_test_module(
2471 tmp.path(),
2472 "first",
2473 "shared(X) :- first_mid(X).\nfirst_mid(X) :- z_typed(X).\nz_typed(1).",
2474 );
2475 create_test_module(
2476 tmp.path(),
2477 "second",
2478 "shared(X) :- second_mid(X).\nsecond_mid(X) :- z_typed(prefix, X).\nz_typed(prefix, from_second).",
2479 );
2480 let entry_source = "use first.\nuse second.\n";
2481 create_test_module(tmp.path(), "entry", entry_source);
2482
2483 let mut resolver = ModuleResolver::new(vec![]);
2484 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2485
2486 let error = resolver
2487 .merge_imports(parse_program(entry_source).unwrap())
2488 .expect_err("transitive body-derived types must agree across modules");
2489 let message = error.to_string();
2490
2491 assert!(message.contains("error[E0412]"), "{message}");
2492 assert!(message.contains("shared/1"), "{message}");
2493 assert!(message.contains("u32"), "{message}");
2494 assert!(message.contains("symbol"), "{message}");
2495 }
2496
2497 #[test]
2498 fn merge_imports_leaves_unanchored_rule_head_variables_unconstrained() {
2499 let tmp = TempDir::new().unwrap();
2500 create_test_module(
2501 tmp.path(),
2502 "first",
2503 "first_cycle(X) :- first_cycle(X).\nshared(X) :- first_cycle(X).",
2504 );
2505 create_test_module(
2506 tmp.path(),
2507 "second",
2508 "second_cycle(X) :- second_cycle(X).\nshared(X) :- second_cycle(X).",
2509 );
2510 let entry_source = "use first.\nuse second.\n";
2511 create_test_module(tmp.path(), "entry", entry_source);
2512
2513 let mut resolver = ModuleResolver::new(vec![]);
2514 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2515
2516 let merged = resolver
2517 .merge_imports(parse_program(entry_source).unwrap())
2518 .expect("unanchored variables do not supply concrete schema evidence");
2519
2520 assert_eq!(
2521 merged
2522 .rules
2523 .iter()
2524 .filter(|rule| rule.head.predicate == "shared")
2525 .count(),
2526 2
2527 );
2528 }
2529
2530 #[test]
2531 fn merge_imports_uses_column_schema_arity_for_programmatic_declarations() {
2532 let tmp = TempDir::new().unwrap();
2533 create_test_module(tmp.path(), "small", "shared(1).");
2534 create_test_module(tmp.path(), "wide", "shared(5000000000).");
2535 let entry_source = "use small.\nuse wide.\n";
2536 create_test_module(tmp.path(), "entry", entry_source);
2537
2538 let mut resolver = ModuleResolver::new(vec![]);
2539 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2540 let mut program = parse_program(entry_source).unwrap();
2541 program.predicates.push(PredDecl {
2542 name: "shared".to_string(),
2543 types: Vec::new(),
2544 columns: vec![crate::ast::PredColumn {
2545 name: Some("value".to_string()),
2546 typ: TypeRef::Scalar(ScalarType::I64),
2547 }],
2548 is_private: false,
2549 });
2550
2551 let merged = resolver
2552 .merge_imports(program)
2553 .expect("the effective declaration schema must control imported facts");
2554
2555 assert_eq!(
2556 merged
2557 .rules
2558 .iter()
2559 .filter(|rule| rule.head.predicate == "shared")
2560 .count(),
2561 2
2562 );
2563 }
2564
2565 #[test]
2566 fn merge_imports_distinguishes_same_stem_entry_and_import_in_schema_errors() {
2567 let tmp = TempDir::new().unwrap();
2568 create_test_module(tmp.path(), "main", "shared(1).");
2569 let entry = tmp.path().join("main.datalog");
2570 fs::write(&entry, "use main.\nshared(from_entry).\n").unwrap();
2571
2572 let mut resolver = ModuleResolver::new(vec![]);
2573 let program = resolver.load_entry_file(&entry).unwrap().program.clone();
2574
2575 let error = resolver
2576 .merge_imports(program)
2577 .expect_err("same-stem sources with different schemas must be distinguishable");
2578 let message = error.to_string();
2579
2580 assert!(message.contains("error[E0412]"), "{message}");
2581 assert!(message.contains("main.datalog"), "{message}");
2582 assert!(message.contains("main.xlog"), "{message}");
2583 }
2584
2585 #[test]
2586 fn merge_imports_excludes_selectively_omitted_schema_conflicts() {
2587 let tmp = TempDir::new().unwrap();
2588 create_test_module(tmp.path(), "first", "shared(from_first).\nomitted(1).");
2589 create_test_module(
2590 tmp.path(),
2591 "second",
2592 "shared(from_second).\nomitted(from_second).",
2593 );
2594 let entry_source = "use first::{shared}.\nuse second::{shared}.\n";
2595 create_test_module(tmp.path(), "entry", entry_source);
2596
2597 let mut resolver = ModuleResolver::new(vec![]);
2598 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2599
2600 let merged = resolver
2601 .merge_imports(parse_program(entry_source).unwrap())
2602 .expect("omitted predicates do not participate in schema validation");
2603
2604 assert_eq!(
2605 merged
2606 .rules
2607 .iter()
2608 .filter(|rule| rule.head.predicate == "shared")
2609 .count(),
2610 2
2611 );
2612 assert!(!merged
2613 .rules
2614 .iter()
2615 .any(|rule| rule.head.predicate == "omitted"));
2616 }
2617
2618 #[test]
2619 fn merge_imports_rejects_selected_inferred_schema_conflicts() {
2620 let tmp = TempDir::new().unwrap();
2621 create_test_module(tmp.path(), "first", "shared(1).\nfirst_only(ok).");
2622 create_test_module(
2623 tmp.path(),
2624 "second",
2625 "shared(from_second).\nsecond_only(ok).",
2626 );
2627 let entry_source = "use first::{shared}.\nuse second::{shared}.\n";
2628 create_test_module(tmp.path(), "entry", entry_source);
2629
2630 let mut resolver = ModuleResolver::new(vec![]);
2631 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2632
2633 let error = resolver
2634 .merge_imports(parse_program(entry_source).unwrap())
2635 .expect_err("selected incompatible contributions must be rejected");
2636
2637 assert!(error.to_string().contains("error[E0412]"));
2638 }
2639
2640 #[test]
2641 fn merge_imports_excludes_private_schema_conflicts() {
2642 let tmp = TempDir::new().unwrap();
2643 create_test_module(
2644 tmp.path(),
2645 "first",
2646 "private pred hidden(u32).\nhidden(1).\nshared(from_first).",
2647 );
2648 create_test_module(
2649 tmp.path(),
2650 "second",
2651 "private pred hidden(symbol).\nhidden(from_second).\nshared(from_second).",
2652 );
2653 let entry_source = "use first.\nuse second.\n";
2654 create_test_module(tmp.path(), "entry", entry_source);
2655
2656 let mut resolver = ModuleResolver::new(vec![]);
2657 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2658
2659 let merged = resolver
2660 .merge_imports(parse_program(entry_source).unwrap())
2661 .expect("private predicates do not participate in import validation");
2662
2663 assert_eq!(
2664 merged
2665 .rules
2666 .iter()
2667 .filter(|rule| rule.head.predicate == "shared")
2668 .count(),
2669 2
2670 );
2671 assert!(!merged
2672 .rules
2673 .iter()
2674 .any(|rule| rule.head.predicate == "hidden"));
2675 }
2676
2677 #[test]
2678 fn merge_imports_validates_entry_clause_schema_against_imports() {
2679 let tmp = TempDir::new().unwrap();
2680 create_test_module(tmp.path(), "library", "shared(1).");
2681 let entry_source = "use library.\nshared(from_entry).\n";
2682 create_test_module(tmp.path(), "entry", entry_source);
2683
2684 let mut resolver = ModuleResolver::new(vec![]);
2685 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2686
2687 let error = resolver
2688 .merge_imports(parse_program(entry_source).unwrap())
2689 .expect_err("entry and imported contributions must agree");
2690 let message = error.to_string();
2691
2692 assert!(message.contains("error[E0412]"), "{message}");
2693 assert!(message.contains("library"), "{message}");
2694 assert!(message.contains("entry"), "{message}");
2695 }
2696
2697 #[test]
2698 fn merge_imports_unions_selectively_imported_compatible_predicates() {
2699 let tmp = TempDir::new().unwrap();
2700 create_test_module(
2701 tmp.path(),
2702 "first",
2703 "pred shared(u32). pred first_only(u32). shared(1). first_only(10).",
2704 );
2705 create_test_module(
2706 tmp.path(),
2707 "second",
2708 "pred shared(u32). pred second_only(u32). shared(2). second_only(20).",
2709 );
2710 let entry_source = "use first::{shared}.\nuse second::{shared}.\n";
2711 create_test_module(tmp.path(), "entry", entry_source);
2712
2713 let mut resolver = ModuleResolver::new(vec![]);
2714 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2715
2716 let merged = resolver
2717 .merge_imports(parse_program(entry_source).unwrap())
2718 .unwrap();
2719 let shared_facts = merged
2720 .rules
2721 .iter()
2722 .filter(|rule| rule.head.predicate == "shared" && rule.is_fact())
2723 .count();
2724
2725 assert_eq!(shared_facts, 2);
2726 assert!(!merged.rules.iter().any(|rule| {
2727 rule.head.predicate == "first_only" || rule.head.predicate == "second_only"
2728 }));
2729 assert!(!merged.predicates.iter().any(|declaration| {
2730 declaration.name == "first_only" || declaration.name == "second_only"
2731 }));
2732 }
2733
2734 #[test]
2735 fn merge_imports_unions_compatible_predicates_across_wrapper_branches() {
2736 let tmp = TempDir::new().unwrap();
2737 create_test_module(
2738 tmp.path(),
2739 "left_provider",
2740 concat!(
2741 "pred left_source(symbol).\n",
2742 "pred shared(symbol).\n",
2743 "left_source(left).\n",
2744 "shared(X) :- left_source(X).",
2745 ),
2746 );
2747 create_test_module(
2748 tmp.path(),
2749 "left_wrapper",
2750 "use left_provider.\npred left_result(symbol).\nleft_result(X) :- shared(X).",
2751 );
2752 create_test_module(
2753 tmp.path(),
2754 "right_provider",
2755 concat!(
2756 "pred right_source(symbol).\n",
2757 "pred shared(symbol).\n",
2758 "right_source(right).\n",
2759 "shared(X) :- right_source(X).",
2760 ),
2761 );
2762 create_test_module(
2763 tmp.path(),
2764 "right_wrapper",
2765 "use right_provider.\npred right_result(symbol).\nright_result(X) :- shared(X).",
2766 );
2767 let entry_source = "use left_wrapper.\nuse right_wrapper.\n";
2768 create_test_module(tmp.path(), "entry", entry_source);
2769
2770 let mut resolver = ModuleResolver::new(vec![]);
2771 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2772
2773 let merged = resolver
2774 .merge_imports(parse_program(entry_source).unwrap())
2775 .unwrap();
2776 let shared_rules = merged
2777 .rules
2778 .iter()
2779 .filter(|rule| rule.head.predicate == "shared" && !rule.is_fact())
2780 .collect::<Vec<_>>();
2781
2782 assert_eq!(shared_rules.len(), 2);
2783 assert!(merged
2784 .rules
2785 .iter()
2786 .any(|rule| rule.head.predicate == "left_result"));
2787 assert!(merged
2788 .rules
2789 .iter()
2790 .any(|rule| rule.head.predicate == "right_result"));
2791 }
2792
2793 #[test]
2794 fn merge_imports_allows_local_extension_of_one_imported_provider() {
2795 let tmp = TempDir::new().unwrap();
2796 create_test_module(
2797 tmp.path(),
2798 "provider",
2799 "pred shared(symbol). shared(provider_value).",
2800 );
2801 create_test_module(
2802 tmp.path(),
2803 "wrapper",
2804 "use provider.\npred shared(symbol).\nshared(wrapper_value).",
2805 );
2806 let entry_source = "use wrapper.\n";
2807 create_test_module(tmp.path(), "entry", entry_source);
2808
2809 let mut resolver = ModuleResolver::new(vec![]);
2810 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2811
2812 let merged = resolver
2813 .merge_imports(parse_program(entry_source).unwrap())
2814 .unwrap();
2815 let shared_facts = merged
2816 .rules
2817 .iter()
2818 .filter(|rule| rule.head.predicate == "shared" && rule.is_fact())
2819 .count();
2820
2821 assert_eq!(shared_facts, 2);
2822 }
2823
2824 #[test]
2825 fn merge_imports_rejects_local_redefinition_of_an_imported_function() {
2826 let tmp = TempDir::new().unwrap();
2827 create_test_module(tmp.path(), "base", "func shared(X) = X + 1.");
2828 create_test_module(tmp.path(), "wrapper", "use base.\nfunc shared(X) = X + 2.");
2829 let entry_source = "use wrapper.\n";
2830 create_test_module(tmp.path(), "entry", entry_source);
2831
2832 let mut resolver = ModuleResolver::new(vec![]);
2833 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2834
2835 let result = resolver.merge_imports(parse_program(entry_source).unwrap());
2836
2837 assert!(matches!(
2838 result,
2839 Err(ModuleError::ImportConflict {
2840 name,
2841 module1,
2842 module2,
2843 }) if name == "shared"
2844 && module1 == vec!["base"]
2845 && module2 == vec!["wrapper"]
2846 ));
2847 }
2848
2849 #[test]
2850 fn merge_imports_rejects_function_definitions_in_separate_branches() {
2851 let tmp = TempDir::new().unwrap();
2852 create_test_module(tmp.path(), "first", "func shared(X) = X + 1.");
2853 create_test_module(tmp.path(), "second", "func shared(X) = X + 2.");
2854 let entry_source = "use first.\nuse second.\n";
2855 create_test_module(tmp.path(), "entry", entry_source);
2856
2857 let mut resolver = ModuleResolver::new(vec![]);
2858 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2859
2860 let result = resolver.merge_imports(parse_program(entry_source).unwrap());
2861
2862 assert!(matches!(
2863 result,
2864 Err(ModuleError::ImportConflict {
2865 name,
2866 module1,
2867 module2,
2868 }) if name == "shared"
2869 && module1 == vec!["first"]
2870 && module2 == vec!["second"]
2871 ));
2872 }
2873
2874 #[test]
2875 fn merge_imports_rejects_function_definitions_across_wrapper_branches() {
2876 let tmp = TempDir::new().unwrap();
2877 create_test_module(tmp.path(), "left_provider", "func shared(X) = X + 1.");
2878 create_test_module(tmp.path(), "left_wrapper", "use left_provider.\n");
2879 create_test_module(tmp.path(), "right_provider", "func shared(X) = X + 2.");
2880 create_test_module(tmp.path(), "right_wrapper", "use right_provider.\n");
2881 let entry_source = "use left_wrapper.\nuse right_wrapper.\n";
2882 create_test_module(tmp.path(), "entry", entry_source);
2883
2884 let mut resolver = ModuleResolver::new(vec![]);
2885 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2886
2887 let result = resolver.merge_imports(parse_program(entry_source).unwrap());
2888
2889 assert!(matches!(
2890 result,
2891 Err(ModuleError::ImportConflict {
2892 name,
2893 module1,
2894 module2,
2895 }) if name == "shared"
2896 && module1 == vec!["left_provider"]
2897 && module2 == vec!["right_provider"]
2898 ));
2899 }
2900
2901 #[test]
2902 fn merge_imports_rejects_entry_redefinition_of_an_imported_function() {
2903 let tmp = TempDir::new().unwrap();
2904 create_test_module(tmp.path(), "library", "func shared(X) = X + 1.");
2905 let entry_source = "use library.\nfunc shared(X) = X + 2.\n";
2906 create_test_module(tmp.path(), "entry", entry_source);
2907
2908 let mut resolver = ModuleResolver::new(vec![]);
2909 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2910
2911 let result = resolver.merge_imports(parse_program(entry_source).unwrap());
2912
2913 assert!(matches!(
2914 result,
2915 Err(ModuleError::ImportConflict {
2916 name,
2917 module1,
2918 module2,
2919 }) if name == "shared"
2920 && module1 == vec!["library"]
2921 && module2 == vec!["entry"]
2922 ));
2923 }
2924
2925 #[test]
2926 fn merge_imports_does_not_treat_declarations_as_providers() {
2927 let tmp = TempDir::new().unwrap();
2928 create_test_module(
2929 tmp.path(),
2930 "first",
2931 "pred external(symbol).\npred first_result(symbol).\nfirst_result(X) :- external(X).",
2932 );
2933 create_test_module(
2934 tmp.path(),
2935 "second",
2936 "pred external(symbol).\npred second_result(symbol).\nsecond_result(X) :- external(X).",
2937 );
2938 let entry_source = "use first.\nuse second.\n";
2939 create_test_module(tmp.path(), "entry", entry_source);
2940
2941 let mut resolver = ModuleResolver::new(vec![]);
2942 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2943
2944 let merged = resolver
2945 .merge_imports(parse_program(entry_source).unwrap())
2946 .unwrap();
2947
2948 assert!(merged
2949 .rules
2950 .iter()
2951 .any(|rule| rule.head.predicate == "first_result"));
2952 assert!(merged
2953 .rules
2954 .iter()
2955 .any(|rule| rule.head.predicate == "second_result"));
2956 }
2957
2958 #[test]
2959 fn merge_imports_rejects_incompatible_predicate_declarations() {
2960 let tmp = TempDir::new().unwrap();
2961 create_test_module(tmp.path(), "first", "pred external(u32). external(1).");
2962 create_test_module(
2963 tmp.path(),
2964 "second",
2965 "pred external(symbol). external(value).",
2966 );
2967 let entry_source = "use first.\nuse second.\n";
2968 create_test_module(tmp.path(), "entry", entry_source);
2969
2970 let mut resolver = ModuleResolver::new(vec![]);
2971 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2972
2973 let result = resolver.merge_imports(parse_program(entry_source).unwrap());
2974
2975 assert!(matches!(
2976 result,
2977 Err(ModuleError::IncompatiblePredicateDeclaration {
2978 name,
2979 module1,
2980 module2,
2981 }) if name == "external"
2982 && module1 == vec!["first"]
2983 && module2 == vec!["second"]
2984 ));
2985 }
2986
2987 #[test]
2988 fn merge_imports_rejects_incompatible_declaration_only_modules() {
2989 let tmp = TempDir::new().unwrap();
2990 create_test_module(tmp.path(), "first", "pred external(u32).");
2991 create_test_module(tmp.path(), "second", "pred external(symbol).");
2992 let entry_source = "use first.\nuse second.\n";
2993 create_test_module(tmp.path(), "entry", entry_source);
2994
2995 let mut resolver = ModuleResolver::new(vec![]);
2996 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
2997
2998 let result = resolver.merge_imports(parse_program(entry_source).unwrap());
2999
3000 assert!(matches!(
3001 result,
3002 Err(ModuleError::IncompatiblePredicateDeclaration {
3003 name,
3004 module1,
3005 module2,
3006 }) if name == "external"
3007 && module1 == vec!["first"]
3008 && module2 == vec!["second"]
3009 ));
3010 }
3011
3012 #[test]
3013 fn merge_imports_rejects_entry_declaration_incompatible_with_import() {
3014 let tmp = TempDir::new().unwrap();
3015 create_test_module(
3016 tmp.path(),
3017 "library",
3018 "pred external(symbol). external(value).",
3019 );
3020 let entry_source = "use library.\npred external(u32).\n";
3021 create_test_module(tmp.path(), "entry", entry_source);
3022
3023 let mut resolver = ModuleResolver::new(vec![]);
3024 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
3025
3026 let result = resolver.merge_imports(parse_program(entry_source).unwrap());
3027
3028 assert!(matches!(
3029 result,
3030 Err(ModuleError::IncompatiblePredicateDeclaration {
3031 name,
3032 module1,
3033 module2,
3034 }) if name == "external"
3035 && module1 == vec!["library"]
3036 && module2 == vec!["entry"]
3037 ));
3038 }
3039
3040 #[test]
3041 fn merge_imports_rejects_incompatible_domain_declarations() {
3042 let tmp = TempDir::new().unwrap();
3043 create_test_module(
3044 tmp.path(),
3045 "first",
3046 "domain key : u32.\npred external(key).",
3047 );
3048 create_test_module(
3049 tmp.path(),
3050 "second",
3051 "domain key : symbol.\npred external(key).",
3052 );
3053 let entry_source = "use first.\nuse second.\n";
3054 create_test_module(tmp.path(), "entry", entry_source);
3055
3056 let mut resolver = ModuleResolver::new(vec![]);
3057 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
3058
3059 let result = resolver.merge_imports(parse_program(entry_source).unwrap());
3060
3061 assert!(matches!(
3062 result,
3063 Err(ModuleError::IncompatibleDomainDeclaration {
3064 name,
3065 module1,
3066 module2,
3067 }) if name == "key"
3068 && module1 == vec!["first"]
3069 && module2 == vec!["second"]
3070 ));
3071 }
3072
3073 #[test]
3074 fn merge_imports_rejects_entry_domain_incompatible_with_import() {
3075 let tmp = TempDir::new().unwrap();
3076 create_test_module(tmp.path(), "library", "domain key : symbol.");
3077 let entry_source = "use library.\ndomain key : u32.\n";
3078 create_test_module(tmp.path(), "entry", entry_source);
3079
3080 let mut resolver = ModuleResolver::new(vec![]);
3081 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
3082
3083 let result = resolver.merge_imports(parse_program(entry_source).unwrap());
3084
3085 assert!(matches!(
3086 result,
3087 Err(ModuleError::IncompatibleDomainDeclaration {
3088 name,
3089 module1,
3090 module2,
3091 }) if name == "key"
3092 && module1 == vec!["library"]
3093 && module2 == vec!["entry"]
3094 ));
3095 }
3096
3097 #[test]
3098 fn merge_imports_rejects_duplicate_functions_within_an_imported_module() {
3099 let tmp = TempDir::new().unwrap();
3100 create_test_module(
3101 tmp.path(),
3102 "library",
3103 "func shared(X) = X + 1.\nfunc shared(X) = X + 2.",
3104 );
3105 let entry_source = "use library.\n";
3106 create_test_module(tmp.path(), "entry", entry_source);
3107
3108 let mut resolver = ModuleResolver::new(vec![]);
3109 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
3110
3111 let result = resolver.merge_imports(parse_program(entry_source).unwrap());
3112
3113 assert!(matches!(
3114 result,
3115 Err(ModuleError::DuplicateImportedFunction { name, module })
3116 if name == "shared" && module == vec!["library"]
3117 ));
3118 }
3119
3120 #[test]
3121 fn merge_imports_rejects_unselected_duplicate_public_functions() {
3122 let tmp = TempDir::new().unwrap();
3123 create_test_module(
3124 tmp.path(),
3125 "library",
3126 "func shared(X) = X + 1.\nfunc shared(X) = X + 2.\nfunc other(X) = X.",
3127 );
3128 let entry_source = "use library::{other}.\n";
3129 create_test_module(tmp.path(), "entry", entry_source);
3130
3131 let mut resolver = ModuleResolver::new(vec![]);
3132 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
3133
3134 let result = resolver.merge_imports(parse_program(entry_source).unwrap());
3135
3136 assert!(matches!(
3137 result,
3138 Err(ModuleError::DuplicateImportedFunction { name, module })
3139 if name == "shared" && module == vec!["library"]
3140 ));
3141 }
3142
3143 #[test]
3144 fn merge_imports_rejects_private_and_public_definitions_of_an_exported_function() {
3145 let tmp = TempDir::new().unwrap();
3146 create_test_module(
3147 tmp.path(),
3148 "library",
3149 "private func shared(X) = X + 1.\nfunc shared(X) = X + 2.",
3150 );
3151 let entry_source = "use library.\n";
3152 create_test_module(tmp.path(), "entry", entry_source);
3153
3154 let mut resolver = ModuleResolver::new(vec![]);
3155 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
3156
3157 let result = resolver.merge_imports(parse_program(entry_source).unwrap());
3158
3159 assert!(matches!(
3160 result,
3161 Err(ModuleError::DuplicateImportedFunction { name, module })
3162 if name == "shared" && module == vec!["library"]
3163 ));
3164 }
3165
3166 #[test]
3167 fn validation_rejects_mixed_visibility_for_one_imported_predicate() {
3168 let tmp = TempDir::new().unwrap();
3169 create_test_module(
3170 tmp.path(),
3171 "library",
3172 "private pred shared(u32).\npred shared(u32).\nshared(1).",
3173 );
3174 let entry_source = "use library.\n";
3175 create_test_module(tmp.path(), "entry", entry_source);
3176
3177 let mut resolver = ModuleResolver::new(vec![]);
3178 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
3179 let program = parse_program(entry_source).unwrap();
3180
3181 assert!(matches!(
3182 resolver.check_import(&["library".to_string()], "shared"),
3183 Err(ModuleError::ConflictingPredicateVisibility { name, module })
3184 if name == "shared" && module == vec!["library"]
3185 ));
3186 assert!(matches!(
3187 resolver.validate_imports(&program),
3188 Err(ModuleError::ConflictingPredicateVisibility { name, module })
3189 if name == "shared" && module == vec!["library"]
3190 ));
3191 assert!(matches!(
3192 resolver.merge_imports(program),
3193 Err(ModuleError::ConflictingPredicateVisibility { name, module })
3194 if name == "shared" && module == vec!["library"]
3195 ));
3196 }
3197
3198 #[test]
3199 fn merge_imports_accepts_equivalent_predicate_schemas_using_distinct_domains() {
3200 let tmp = TempDir::new().unwrap();
3201 create_test_module(
3202 tmp.path(),
3203 "first",
3204 "domain first_key : u32.\npred external(first_key).",
3205 );
3206 create_test_module(
3207 tmp.path(),
3208 "second",
3209 "domain second_key : u32.\npred external(second_key).",
3210 );
3211 let entry_source = "use first.\nuse second.\n";
3212 create_test_module(tmp.path(), "entry", entry_source);
3213
3214 let mut resolver = ModuleResolver::new(vec![]);
3215 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
3216
3217 let merged = resolver
3218 .merge_imports(parse_program(entry_source).unwrap())
3219 .unwrap();
3220
3221 assert!(merged
3222 .predicates
3223 .iter()
3224 .any(|declaration| declaration.name == "external"));
3225 }
3226
3227 #[test]
3228 fn merge_imports_normalizes_a_predicate_schema_with_an_imported_domain() {
3229 let tmp = TempDir::new().unwrap();
3230 create_test_module(tmp.path(), "types", "domain key : u32.");
3231 create_test_module(tmp.path(), "wrapper", "use types.\npred external(key).");
3232 create_test_module(tmp.path(), "second", "pred external(u32).");
3233 let entry_source = "use wrapper.\nuse second.\n";
3234 create_test_module(tmp.path(), "entry", entry_source);
3235
3236 let mut resolver = ModuleResolver::new(vec![]);
3237 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
3238
3239 let merged = resolver
3240 .merge_imports(parse_program(entry_source).unwrap())
3241 .unwrap();
3242
3243 assert!(merged
3244 .predicates
3245 .iter()
3246 .any(|declaration| declaration.name == "external"));
3247 }
3248
3249 #[test]
3250 fn merge_imports_normalizes_an_entry_schema_with_an_imported_domain() {
3251 let tmp = TempDir::new().unwrap();
3252 create_test_module(tmp.path(), "types", "domain key : u32.");
3253 create_test_module(tmp.path(), "provider", "pred external(u32).");
3254 let entry_source = "use types.\nuse provider.\npred external(key).\n";
3255 create_test_module(tmp.path(), "entry", entry_source);
3256
3257 let mut resolver = ModuleResolver::new(vec![]);
3258 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
3259
3260 let merged = resolver
3261 .merge_imports(parse_program(entry_source).unwrap())
3262 .unwrap();
3263
3264 assert!(merged
3265 .predicates
3266 .iter()
3267 .any(|declaration| declaration.name == "external"));
3268 }
3269
3270 #[test]
3271 fn merge_imports_allows_entry_to_extend_an_imported_predicate() {
3272 let tmp = TempDir::new().unwrap();
3273 create_test_module(tmp.path(), "library", "pred shared(u32). shared(1).");
3274 let entry_source = "use library.\npred shared(u32).\nshared(2).\n";
3275 create_test_module(tmp.path(), "entry", entry_source);
3276
3277 let mut resolver = ModuleResolver::new(vec![]);
3278 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
3279
3280 let merged = resolver
3281 .merge_imports(parse_program(entry_source).unwrap())
3282 .unwrap();
3283 let shared_facts = merged
3284 .rules
3285 .iter()
3286 .filter(|rule| rule.head.predicate == "shared" && rule.is_fact())
3287 .count();
3288
3289 assert_eq!(shared_facts, 2);
3290 }
3291
3292 #[test]
3293 fn merge_imports_resolves_same_relative_name_per_importer() {
3294 let tmp = TempDir::new().unwrap();
3295 let left = tmp.path().join("left");
3296 let right = tmp.path().join("right");
3297 fs::create_dir_all(&left).unwrap();
3298 fs::create_dir_all(&right).unwrap();
3299 create_test_module(
3300 &left,
3301 "support",
3302 "pred left_local(symbol). left_local(left).",
3303 );
3304 create_test_module(
3305 &left,
3306 "wrapper",
3307 "use support.\npred left_result(symbol).\nleft_result(X) :- left_local(X).",
3308 );
3309 create_test_module(
3310 &right,
3311 "support",
3312 "pred right_local(symbol). right_local(right).",
3313 );
3314 create_test_module(
3315 &right,
3316 "wrapper",
3317 "use support.\npred right_result(symbol).\nright_result(X) :- right_local(X).",
3318 );
3319 let entry_source = "use left/wrapper.\nuse right/wrapper.\n";
3320 let entry = create_test_module(tmp.path(), "entry", entry_source);
3321
3322 let mut resolver = ModuleResolver::new(vec![]);
3323 resolver.load_entry_file(&entry).unwrap();
3324
3325 let merged = resolver
3326 .merge_imports(parse_program(entry_source).unwrap())
3327 .unwrap();
3328
3329 assert!(merged
3330 .rules
3331 .iter()
3332 .any(|rule| rule.head.predicate == "left_local"));
3333 assert!(merged
3334 .rules
3335 .iter()
3336 .any(|rule| rule.head.predicate == "right_local"));
3337 }
3338
3339 #[test]
3340 fn merge_imports_deduplicates_aliases_of_one_source_file() {
3341 let tmp = TempDir::new().unwrap();
3342 let util = tmp.path().join("util");
3343 fs::create_dir_all(&util).unwrap();
3344 create_test_module(
3345 &util,
3346 "helpers",
3347 "pred shared(symbol). shared(helper_value).",
3348 );
3349 create_test_module(
3350 &util,
3351 "wrapper",
3352 "use helpers.\npred wrapped(symbol).\nwrapped(X) :- shared(X).",
3353 );
3354 let entry_source = "use util/wrapper.\nuse util/helpers.\n";
3355 let entry = create_test_module(tmp.path(), "entry", entry_source);
3356
3357 let mut resolver = ModuleResolver::new(vec![]);
3358 resolver.load_entry_file(&entry).unwrap();
3359
3360 let merged = resolver
3361 .merge_imports(parse_program(entry_source).unwrap())
3362 .unwrap();
3363 let shared_facts = merged
3364 .rules
3365 .iter()
3366 .filter(|rule| rule.head.predicate == "shared" && rule.is_fact())
3367 .count();
3368
3369 assert_eq!(shared_facts, 1);
3370 assert!(merged
3371 .rules
3372 .iter()
3373 .any(|rule| rule.head.predicate == "wrapped"));
3374 }
3375
3376 #[cfg(unix)]
3377 #[test]
3378 fn merge_imports_resolves_symlinked_module_dependencies_from_canonical_source() {
3379 let tmp = TempDir::new().unwrap();
3380 let real = tmp.path().join("real");
3381 let alias = tmp.path().join("alias");
3382 fs::create_dir_all(&real).unwrap();
3383 fs::create_dir_all(&alias).unwrap();
3384 create_test_module(
3385 &real,
3386 "support",
3387 "pred support_value(u32). support_value(1).",
3388 );
3389 create_test_module(
3390 &alias,
3391 "support",
3392 "pred support_value(u32). support_value(2).",
3393 );
3394 let shared = create_test_module(
3395 &real,
3396 "shared",
3397 "use support.\npred shared_value(symbol).\nshared_value(X) :- support_value(X).",
3398 );
3399 std::os::unix::fs::symlink(&shared, alias.join("shared.xlog")).unwrap();
3400 let entry_source = "use alias/shared.\n";
3401 let entry = create_test_module(tmp.path(), "entry", entry_source);
3402
3403 let mut resolver = ModuleResolver::new(vec![]);
3404 resolver.load_entry_file(&entry).unwrap();
3405
3406 let merged = resolver
3407 .merge_imports(parse_program(entry_source).unwrap())
3408 .unwrap();
3409 let support_values = merged
3410 .rules
3411 .iter()
3412 .filter(|rule| rule.head.predicate == "support_value" && rule.is_fact())
3413 .map(|rule| rule.head.terms.clone())
3414 .collect::<Vec<_>>();
3415
3416 assert_eq!(support_values, vec![vec![Term::Integer(1)]]);
3417 }
3418
3419 #[test]
3420 fn merge_imports_keeps_importer_local_resolution_across_search_paths() {
3421 let tmp = TempDir::new().unwrap();
3422 let entry_dir = tmp.path().join("entry");
3423 let module_dir = tmp.path().join("modules");
3424 fs::create_dir_all(&entry_dir).unwrap();
3425 fs::create_dir_all(&module_dir).unwrap();
3426 create_test_module(
3427 &entry_dir,
3428 "support",
3429 "pred entry_support(symbol). entry_support(local).",
3430 );
3431 create_test_module(
3432 &module_dir,
3433 "support",
3434 "pred wrapper_support(symbol). wrapper_support(search_path).",
3435 );
3436 create_test_module(
3437 &module_dir,
3438 "wrapper",
3439 "use support.\npred wrapped(symbol).\nwrapped(X) :- wrapper_support(X).",
3440 );
3441 let entry_source = "use support.\nuse wrapper.\n";
3442 let entry = create_test_module(&entry_dir, "main", entry_source);
3443
3444 let mut resolver = ModuleResolver::new(vec![module_dir]);
3445 resolver.load_entry_file(&entry).unwrap();
3446
3447 let merged = resolver
3448 .merge_imports(parse_program(entry_source).unwrap())
3449 .unwrap();
3450
3451 for predicate in ["entry_support", "wrapper_support", "wrapped"] {
3452 assert!(
3453 merged
3454 .rules
3455 .iter()
3456 .any(|rule| rule.head.predicate == predicate),
3457 "missing rules for {predicate}"
3458 );
3459 }
3460 }
3461
3462 #[test]
3463 fn validate_imports_rejects_an_unanchored_ambiguous_module_path() {
3464 let tmp = TempDir::new().unwrap();
3465 let left = tmp.path().join("left");
3466 let right = tmp.path().join("right");
3467 fs::create_dir_all(&left).unwrap();
3468 fs::create_dir_all(&right).unwrap();
3469 create_test_module(&left, "support", "pred left(symbol). left(value).");
3470 create_test_module(&right, "support", "pred right(symbol). right(value).");
3471
3472 let mut resolver = ModuleResolver::new(vec![]);
3473 resolver.load_module(&left, &["support".into()]).unwrap();
3474 resolver.load_module(&right, &["support".into()]).unwrap();
3475
3476 let first = resolver
3477 .get_module(&["support".into()])
3478 .expect("first source remains available through the compatibility API");
3479 assert_eq!(first.source_file, left.join("support.xlog"));
3480 assert!(resolver.check_import(&["support".into()], "left").is_ok());
3481 assert!(resolver.check_import(&["support".into()], "right").is_err());
3482 assert_eq!(
3483 resolver
3484 .loaded_modules()
3485 .into_iter()
3486 .filter(|alias| *alias == "support")
3487 .count(),
3488 1
3489 );
3490
3491 let result = resolver.validate_imports(&parse_program("use support.").unwrap());
3492
3493 assert!(matches!(
3494 result,
3495 Err(ModuleError::AmbiguousModulePath { path, candidates })
3496 if path == vec!["support"] && candidates.len() == 2
3497 ));
3498 }
3499
3500 #[test]
3501 fn load_entry_file_detects_an_import_of_itself() {
3502 let tmp = TempDir::new().unwrap();
3503 let entry = create_test_module(tmp.path(), "entry", "use entry.");
3504 let mut resolver = ModuleResolver::new(vec![]);
3505
3506 let result = resolver.load_entry_file(&entry);
3507
3508 assert!(matches!(result, Err(ModuleError::CircularImport { .. })));
3509 }
3510
3511 #[cfg(unix)]
3512 #[test]
3513 fn load_entry_file_detects_a_symlink_import_of_itself() {
3514 let tmp = TempDir::new().unwrap();
3515 let entry = create_test_module(tmp.path(), "entry", "use alias.");
3516 std::os::unix::fs::symlink(&entry, tmp.path().join("alias.xlog")).unwrap();
3517 let mut resolver = ModuleResolver::new(vec![]);
3518
3519 let result = resolver.load_entry_file(&entry);
3520
3521 assert!(matches!(result, Err(ModuleError::CircularImport { .. })));
3522 }
3523
3524 #[test]
3525 fn merge_imports_deduplicates_function_aliases_of_one_source_file() {
3526 let tmp = TempDir::new().unwrap();
3527 let util = tmp.path().join("util");
3528 fs::create_dir_all(&util).unwrap();
3529 create_test_module(&util, "helpers", "func normalize(X) = X + 1.");
3530 create_test_module(&util, "wrapper", "use helpers.\n");
3531 let entry_source = "use util/wrapper.\nuse util/helpers.\n";
3532 let entry = create_test_module(tmp.path(), "entry", entry_source);
3533
3534 let mut resolver = ModuleResolver::new(vec![]);
3535 resolver.load_entry_file(&entry).unwrap();
3536
3537 let merged = resolver
3538 .merge_imports(parse_program(entry_source).unwrap())
3539 .unwrap();
3540
3541 assert_eq!(
3542 merged
3543 .functions
3544 .iter()
3545 .filter(|function| function.name == "normalize")
3546 .count(),
3547 1
3548 );
3549 }
3550
3551 #[test]
3552 fn test_circular_import() {
3553 let tmp = TempDir::new().unwrap();
3554 create_test_module(tmp.path(), "a", "use b.");
3555 create_test_module(tmp.path(), "b", "use a.");
3556
3557 let mut resolver = ModuleResolver::new(vec![]);
3558 let result = resolver.load_module(tmp.path(), &["a".into()]);
3559 assert!(matches!(result, Err(ModuleError::CircularImport { .. })));
3560 }
3561
3562 #[test]
3563 fn test_load_simple_module() {
3564 let tmp = TempDir::new().unwrap();
3565 create_test_module(
3566 tmp.path(),
3567 "math",
3568 r#"
3569 pred add(u32, u32, u32).
3570 add(1, 2, 3).
3571 "#,
3572 );
3573
3574 let mut resolver = ModuleResolver::new(vec![]);
3575 let result = resolver.load_module(tmp.path(), &["math".into()]);
3576 assert!(result.is_ok());
3577 let module = result.unwrap();
3578 assert!(module.exports.contains("add"));
3579 }
3580
3581 #[test]
3582 fn test_private_not_exported() {
3583 let tmp = TempDir::new().unwrap();
3584 create_test_module(
3585 tmp.path(),
3586 "graph",
3587 r#"
3588 pred edge(u32, u32).
3589 private pred helper(u32).
3590 edge(1, 2).
3591 helper(1).
3592 "#,
3593 );
3594
3595 let mut resolver = ModuleResolver::new(vec![]);
3596 let result = resolver.load_module(tmp.path(), &["graph".into()]);
3597 assert!(result.is_ok());
3598 let module = result.unwrap();
3599 assert!(module.exports.contains("edge"));
3600 assert!(!module.exports.contains("helper"));
3601 }
3602
3603 #[test]
3604 fn test_merge_rejects_export_with_private_support() {
3605 let tmp = TempDir::new().unwrap();
3606 create_test_module(
3607 tmp.path(),
3608 "library",
3609 r#"
3610 private pred hidden(u32).
3611 pred visible(u32).
3612 hidden(1).
3613 visible(X) :- hidden(X).
3614 "#,
3615 );
3616 let entry_source = "use library.\nquery(visible(1)).\n";
3617 create_test_module(tmp.path(), "entry", entry_source);
3618
3619 let mut resolver = ModuleResolver::new(vec![]);
3620 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
3621 let error = resolver
3622 .merge_imports(parse_program(entry_source).unwrap())
3623 .unwrap_err()
3624 .to_string();
3625
3626 assert!(error.contains("error[E0406]"), "{error}");
3627 assert!(error.contains("`visible`"), "{error}");
3628 assert!(error.contains("`hidden`"), "{error}");
3629 assert!(error.contains("`library`"), "{error}");
3630 }
3631
3632 #[test]
3633 fn test_merge_rejects_export_with_selectively_omitted_support() {
3634 let tmp = TempDir::new().unwrap();
3635 create_test_module(
3636 tmp.path(),
3637 "library",
3638 r#"
3639 pred support(u32).
3640 pred visible(u32).
3641 support(1).
3642 visible(X) :- support(X).
3643 "#,
3644 );
3645 let entry_source = "use library::{visible}.\nquery(visible(1)).\n";
3646 create_test_module(tmp.path(), "entry", entry_source);
3647
3648 let mut resolver = ModuleResolver::new(vec![]);
3649 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
3650 let error = resolver
3651 .merge_imports(parse_program(entry_source).unwrap())
3652 .unwrap_err()
3653 .to_string();
3654
3655 assert!(error.contains("error[E0406]"), "{error}");
3656 assert!(error.contains("`visible`"), "{error}");
3657 assert!(error.contains("`support`"), "{error}");
3658 assert!(error.contains("`library`"), "{error}");
3659 }
3660
3661 #[test]
3662 fn test_merge_combines_separate_selective_imports() {
3663 let tmp = TempDir::new().unwrap();
3664 create_test_module(
3665 tmp.path(),
3666 "library",
3667 r#"
3668 pred support(u32).
3669 pred visible(u32).
3670 support(1).
3671 visible(X) :- support(X).
3672 "#,
3673 );
3674 let entry_source = "use library::{visible}.\nuse library::{support}.\n";
3675 create_test_module(tmp.path(), "entry", entry_source);
3676
3677 let mut resolver = ModuleResolver::new(vec![]);
3678 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
3679 let merged = resolver
3680 .merge_imports(parse_program(entry_source).unwrap())
3681 .unwrap();
3682
3683 assert!(merged
3684 .rules
3685 .iter()
3686 .any(|rule| rule.head.predicate == "visible"));
3687 assert!(merged
3688 .rules
3689 .iter()
3690 .any(|rule| rule.head.predicate == "support"));
3691 }
3692
3693 #[test]
3694 fn test_merge_accepts_visible_provider_for_same_named_private_item() {
3695 let tmp = TempDir::new().unwrap();
3696 create_test_module(
3697 tmp.path(),
3698 "private_provider",
3699 "private pred hidden(u32).\nhidden(1).\n",
3700 );
3701 create_test_module(
3702 tmp.path(),
3703 "public_provider",
3704 "pred hidden(u32).\nhidden(2).\n",
3705 );
3706 create_test_module(
3707 tmp.path(),
3708 "wrapper",
3709 "use private_provider.\nuse public_provider.\npred visible(u32).\nvisible(X) :- hidden(X).\n",
3710 );
3711 let entry_source = "use wrapper.\n";
3712 create_test_module(tmp.path(), "entry", entry_source);
3713
3714 let mut resolver = ModuleResolver::new(vec![]);
3715 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
3716 let merged = resolver
3717 .merge_imports(parse_program(entry_source).unwrap())
3718 .unwrap();
3719
3720 assert!(merged
3721 .rules
3722 .iter()
3723 .any(|rule| rule.head.predicate == "visible"));
3724 assert!(merged.rules.iter().any(|rule| {
3725 rule.head.predicate == "hidden" && rule.head.terms == vec![Term::Integer(2)]
3726 }));
3727 }
3728
3729 #[test]
3730 fn test_unrelated_import_does_not_satisfy_selective_dependency() {
3731 let tmp = TempDir::new().unwrap();
3732 create_test_module(
3733 tmp.path(),
3734 "catalog",
3735 "pred support(u32).\npred marker(u32).\nsupport(1).\nmarker(1).\n",
3736 );
3737 create_test_module(
3738 tmp.path(),
3739 "wrapper_a",
3740 "use catalog::{marker}.\npred visible(u32).\nvisible(X) :- support(X).\n",
3741 );
3742 create_test_module(tmp.path(), "wrapper_b", "use catalog::{support}.\n");
3743 let entry_source = "use wrapper_a.\nuse wrapper_b.\n";
3744 create_test_module(tmp.path(), "entry", entry_source);
3745
3746 let mut resolver = ModuleResolver::new(vec![]);
3747 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
3748 let error = resolver
3749 .merge_imports(parse_program(entry_source).unwrap())
3750 .unwrap_err()
3751 .to_string();
3752
3753 assert!(error.contains("error[E0406]"), "{error}");
3754 assert!(error.contains("`visible`"), "{error}");
3755 assert!(error.contains("`support`"), "{error}");
3756 assert!(error.contains("`wrapper_a`"), "{error}");
3757 }
3758
3759 #[test]
3760 fn test_merge_rejects_unknown_selected_item() {
3761 let tmp = TempDir::new().unwrap();
3762 create_test_module(tmp.path(), "library", "known(1).\n");
3763 let entry_source = "use library::{missing}.\n";
3764 create_test_module(tmp.path(), "entry", entry_source);
3765
3766 let mut resolver = ModuleResolver::new(vec![]);
3767 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
3768 let error = resolver
3769 .merge_imports(parse_program(entry_source).unwrap())
3770 .unwrap_err()
3771 .to_string();
3772
3773 assert!(error.contains("error[E0404]"), "{error}");
3774 assert!(error.contains("`missing`"), "{error}");
3775 assert!(error.contains("module library"), "{error}");
3776 }
3777
3778 #[test]
3779 fn test_merge_rejects_transitive_private_support() {
3780 let tmp = TempDir::new().unwrap();
3781 create_test_module(
3782 tmp.path(),
3783 "base",
3784 "private pred hidden(u32).\nhidden(1).\n",
3785 );
3786 create_test_module(
3787 tmp.path(),
3788 "wrapper",
3789 "use base.\npred visible(u32).\nvisible(X) :- hidden(X).\n",
3790 );
3791 let entry_source = "use wrapper.\n";
3792 create_test_module(tmp.path(), "entry", entry_source);
3793
3794 let mut resolver = ModuleResolver::new(vec![]);
3795 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
3796 let error = resolver
3797 .merge_imports(parse_program(entry_source).unwrap())
3798 .unwrap_err()
3799 .to_string();
3800
3801 assert!(error.contains("error[E0406]"), "{error}");
3802 assert!(error.contains("`visible`"), "{error}");
3803 assert!(error.contains("`hidden`"), "{error}");
3804 assert!(error.contains("`wrapper`"), "{error}");
3805 }
3806
3807 #[test]
3808 fn test_merge_rejects_exported_function_with_private_support() {
3809 let tmp = TempDir::new().unwrap();
3810 create_test_module(
3811 tmp.path(),
3812 "library",
3813 r#"
3814 private func hidden(X) = X + 1.
3815 func visible(X) = hidden(X) * 2.
3816 "#,
3817 );
3818 let entry_source = "use library.\n";
3819 create_test_module(tmp.path(), "entry", entry_source);
3820
3821 let mut resolver = ModuleResolver::new(vec![]);
3822 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
3823 let error = resolver
3824 .merge_imports(parse_program(entry_source).unwrap())
3825 .unwrap_err()
3826 .to_string();
3827
3828 assert!(error.contains("error[E0406]"), "{error}");
3829 assert!(error.contains("`visible`"), "{error}");
3830 assert!(error.contains("`hidden`"), "{error}");
3831 assert!(error.contains("`library`"), "{error}");
3832 }
3833
3834 #[test]
3835 fn test_merge_rejects_private_safe_meta_dependencies() {
3836 let tmp = TempDir::new().unwrap();
3837 for (export, rule) in [
3838 ("all_hidden", "all_hidden() :- maplist(hidden, [1])."),
3839 (
3840 "collect_hidden",
3841 "collect_hidden(Values) :- findall(X, hidden(X), Values).",
3842 ),
3843 ] {
3844 create_test_module(
3845 tmp.path(),
3846 "library",
3847 &format!("private pred hidden(u32).\nhidden(1).\n{rule}\n"),
3848 );
3849 let entry_source = "use library.\n";
3850 create_test_module(tmp.path(), "entry", entry_source);
3851
3852 let mut resolver = ModuleResolver::new(vec![]);
3853 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
3854 let error = resolver
3855 .merge_imports(parse_program(entry_source).unwrap())
3856 .unwrap_err()
3857 .to_string();
3858
3859 assert!(error.contains("error[E0406]"), "{error}");
3860 assert!(error.contains(&format!("`{export}`")), "{error}");
3861 assert!(error.contains("`hidden`"), "{error}");
3862 }
3863 }
3864
3865 #[test]
3866 fn test_merge_rejects_imported_program_level_constructs() {
3867 let tmp = TempDir::new().unwrap();
3868 create_test_module(
3869 tmp.path(),
3870 "library",
3871 r#"
3872 pred coin(symbol).
3873 pred left(symbol).
3874 pred right(symbol).
3875 0.5::coin(heads).
3876 0.4::left(choice); 0.6::right(choice).
3877 evidence(coin(heads), true).
3878 :- coin(heads), not left(choice).
3879 nn(classifier, [X], Y, [yes, no]) :: neural_label(X, Y).
3880 learnable(W) :: learned(X) :- source(X).
3881 "#,
3882 );
3883 let entry_source = "use library.\n";
3884 create_test_module(tmp.path(), "entry", entry_source);
3885
3886 let mut resolver = ModuleResolver::new(vec![]);
3887 resolver.load_module(tmp.path(), &["entry".into()]).unwrap();
3888 let error = resolver
3889 .merge_imports(parse_program(entry_source).unwrap())
3890 .unwrap_err()
3891 .to_string();
3892
3893 assert!(error.contains("error[E0405]"), "{error}");
3894 assert!(error.contains("`library`"), "{error}");
3895 assert!(
3896 error.contains(
3897 "annotated disjunctions, evidence statements, integrity constraints, learnable rule templates, neural predicate declarations, probabilistic facts"
3898 ),
3899 "{error}"
3900 );
3901 }
3902
3903 #[test]
3904 fn test_search_paths() {
3905 let tmp = TempDir::new().unwrap();
3906 let lib_dir = tmp.path().join("lib");
3907 fs::create_dir(&lib_dir).unwrap();
3908 create_test_module(&lib_dir, "stdlib", "helper(1).");
3909
3910 let resolver = ModuleResolver::new(vec![lib_dir.clone()]);
3911 let found = resolver.find_module_file(tmp.path(), &["stdlib".into()]);
3912 assert!(found.is_some());
3913 assert!(found.unwrap().starts_with(&lib_dir));
3914 }
3915
3916 #[test]
3917 fn test_function_exports() {
3918 let tmp = TempDir::new().unwrap();
3919 create_test_module(
3920 tmp.path(),
3921 "mathfuncs",
3922 r#"
3923 func square(X) = X * X.
3924 func cube(X) = X * X * X.
3925 private func helper(X) = X.
3926 "#,
3927 );
3928
3929 let mut resolver = ModuleResolver::new(vec![]);
3930 let result = resolver.load_module(tmp.path(), &["mathfuncs".into()]);
3931 assert!(result.is_ok());
3932 let module = result.unwrap();
3933
3934 assert!(module.function_exports.contains("square"));
3936 assert!(module.function_exports.contains("cube"));
3937
3938 assert!(!module.function_exports.contains("helper"));
3940 }
3941
3942 #[test]
3943 fn test_mixed_exports() {
3944 let tmp = TempDir::new().unwrap();
3945 create_test_module(
3946 tmp.path(),
3947 "mixed",
3948 r#"
3949 pred value(i64).
3950 value(42).
3951 func double(X) = X * 2.
3952 "#,
3953 );
3954
3955 let mut resolver = ModuleResolver::new(vec![]);
3956 let result = resolver.load_module(tmp.path(), &["mixed".into()]);
3957 assert!(result.is_ok());
3958 let module = result.unwrap();
3959
3960 assert!(module.exports.contains("value"));
3962 assert!(module.function_exports.contains("double"));
3963 }
3964
3965 #[test]
3966 fn test_ignored_import_pragmas_lists_imported_module_pragmas_only() {
3967 let tmp = TempDir::new().unwrap();
3968 create_test_module(
3969 tmp.path(),
3970 "entry",
3971 r#"
3972 #pragma magic_sets = auto
3973 use lib.
3974 result(1).
3975 "#,
3976 );
3977 create_test_module(
3978 tmp.path(),
3979 "lib",
3980 r#"
3981 #pragma magic_sets = auto
3982 #pragma prob_seed = 7
3983 helper(1).
3984 "#,
3985 );
3986
3987 let mut resolver = ModuleResolver::new(vec![]);
3988 resolver
3989 .load_module(tmp.path(), &["entry".into()])
3990 .expect("load entry");
3991 resolver.mark_entry_module("entry");
3992
3993 let ignored = resolver.ignored_import_pragmas();
3996 assert_eq!(
3997 ignored,
3998 vec![
3999 IgnoredImportPragma {
4000 module: "lib".to_string(),
4001 pragma: "magic_sets",
4002 },
4003 IgnoredImportPragma {
4004 module: "lib".to_string(),
4005 pragma: "prob_seed",
4006 },
4007 ]
4008 );
4009
4010 assert_eq!(
4012 ignored[0].to_string(),
4013 "warning[W0510]: `#pragma magic_sets` in imported module `lib` is ignored\n \
4014 = note: pragmas apply only when declared in the entry file"
4015 );
4016 }
4017
4018 #[test]
4019 fn test_ignored_import_pragmas_empty_without_module_pragmas() {
4020 let tmp = TempDir::new().unwrap();
4021 create_test_module(
4022 tmp.path(),
4023 "entry",
4024 r#"
4025 #pragma magic_sets = on
4026 use quiet.
4027 result(1).
4028 "#,
4029 );
4030 create_test_module(tmp.path(), "quiet", "helper(1).");
4031
4032 let mut resolver = ModuleResolver::new(vec![]);
4033 resolver
4034 .load_module(tmp.path(), &["entry".into()])
4035 .expect("load entry");
4036 resolver.mark_entry_module("entry");
4037
4038 assert!(resolver.ignored_import_pragmas().is_empty());
4039 }
4040
4041 #[test]
4042 fn test_ignored_import_pragmas_sorted_across_modules() {
4043 let tmp = TempDir::new().unwrap();
4044 create_test_module(
4045 tmp.path(),
4046 "entry",
4047 r#"
4048 use zeta.
4049 use alpha.
4050 result(1).
4051 "#,
4052 );
4053 create_test_module(
4054 tmp.path(),
4055 "zeta",
4056 r#"
4057 #pragma prob_seed = 3
4058 z(1).
4059 "#,
4060 );
4061 create_test_module(
4062 tmp.path(),
4063 "alpha",
4064 r#"
4065 #pragma magic_sets = off
4066 a(1).
4067 "#,
4068 );
4069
4070 let mut resolver = ModuleResolver::new(vec![]);
4071 resolver
4072 .load_module(tmp.path(), &["entry".into()])
4073 .expect("load entry");
4074 resolver.mark_entry_module("entry");
4075
4076 assert_eq!(
4078 resolver.ignored_import_pragmas(),
4079 vec![
4080 IgnoredImportPragma {
4081 module: "alpha".to_string(),
4082 pragma: "magic_sets",
4083 },
4084 IgnoredImportPragma {
4085 module: "zeta".to_string(),
4086 pragma: "prob_seed",
4087 },
4088 ]
4089 );
4090 }
4091
4092 #[test]
4093 fn test_ignored_import_pragmas_dedups_two_spellings_of_one_file() {
4094 let tmp = TempDir::new().unwrap();
4095 let util = tmp.path().join("util");
4096 fs::create_dir_all(&util).unwrap();
4097 create_test_module(
4098 tmp.path(),
4099 "entry",
4100 r#"
4101 use util/b.
4102 use util/helpers.
4103 result(1).
4104 "#,
4105 );
4106 create_test_module(
4107 tmp.path(),
4108 "util/b",
4109 r#"
4110 use helpers.
4111 b_pred(1).
4112 "#,
4113 );
4114 create_test_module(
4115 tmp.path(),
4116 "util/helpers",
4117 r#"
4118 #pragma magic_sets = auto
4119 helper(1).
4120 "#,
4121 );
4122
4123 let mut resolver = ModuleResolver::new(vec![]);
4124 resolver
4125 .load_module(tmp.path(), &["entry".into()])
4126 .expect("load entry");
4127 resolver.mark_entry_module("entry");
4128
4129 assert_eq!(
4133 resolver.ignored_import_pragmas(),
4134 vec![IgnoredImportPragma {
4135 module: "helpers".to_string(),
4136 pragma: "magic_sets",
4137 }]
4138 );
4139 }
4140}