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