1use xlog_core::{Result, ScalarType, XlogError};
4
5#[derive(Debug, Clone, PartialEq)]
7pub enum Term {
8 Variable(String),
10 Anonymous,
12 Integer(i64),
14 Float(f64),
16 String(String),
18 Symbol(u32),
20 List(Vec<Term>),
22 Cons {
24 head: Box<Term>,
26 tail: Box<Term>,
28 },
29 Compound {
31 functor: String,
33 args: Vec<Term>,
35 },
36 PredRef(String),
38 Aggregate(AggExpr),
40}
41
42impl Term {
43 pub fn is_variable(&self) -> bool {
45 matches!(self, Term::Variable(_))
46 }
47
48 pub fn is_anonymous(&self) -> bool {
50 matches!(self, Term::Anonymous)
51 }
52
53 pub fn is_any_variable(&self) -> bool {
55 matches!(self, Term::Variable(_) | Term::Anonymous)
56 }
57
58 pub fn is_constant(&self) -> bool {
60 !self.is_any_variable()
61 && !matches!(
62 self,
63 Term::Aggregate(_)
64 | Term::List(_)
65 | Term::Cons { .. }
66 | Term::Compound { .. }
67 | Term::PredRef(_)
68 )
69 }
70
71 pub fn variable_name(&self) -> Option<&str> {
73 match self {
74 Term::Variable(name) => Some(name),
75 _ => None,
76 }
77 }
78
79 pub fn inferred_scalar_type(&self) -> ScalarType {
82 match self {
83 Term::Variable(_) | Term::Anonymous => ScalarType::U64,
84 Term::Integer(value) => {
85 if *value >= 0 && *value <= u32::MAX as i64 {
86 ScalarType::U32
87 } else {
88 ScalarType::I64
89 }
90 }
91 Term::Float(_) => ScalarType::F64,
92 Term::String(_) | Term::Symbol(_) => ScalarType::Symbol,
93 Term::List(_) | Term::Cons { .. } | Term::Compound { .. } | Term::PredRef(_) => {
94 ScalarType::U64
95 }
96 Term::Aggregate(aggregate) => aggregate.default_result_type(),
97 }
98 }
99
100 pub fn variables(&self) -> Vec<&str> {
102 match self {
103 Term::Variable(name) => vec![name.as_str()],
104 Term::List(items) => items.iter().flat_map(Term::variables).collect(),
105 Term::Cons { head, tail } => {
106 let mut vars = head.variables();
107 vars.extend(tail.variables());
108 vars
109 }
110 Term::Compound { args, .. } => args.iter().flat_map(Term::variables).collect(),
111 Term::Anonymous
112 | Term::Integer(_)
113 | Term::Float(_)
114 | Term::String(_)
115 | Term::Symbol(_)
116 | Term::PredRef(_)
117 | Term::Aggregate(_) => vec![],
118 }
119 }
120}
121
122#[derive(Debug, Clone, PartialEq)]
124pub struct AggExpr {
125 pub op: AggOp,
127 pub variable: String,
129}
130
131impl AggExpr {
132 pub(crate) fn result_type_for_input(&self, input: ScalarType) -> Option<ScalarType> {
137 match self.op {
138 AggOp::Count => Some(ScalarType::U64),
139 AggOp::Sum if matches!(input, ScalarType::U32 | ScalarType::U64) => {
140 Some(ScalarType::U64)
141 }
142 AggOp::Min | AggOp::Max if matches!(input, ScalarType::U32 | ScalarType::U64) => {
143 Some(input)
144 }
145 AggOp::LogSumExp if input == ScalarType::F64 => Some(ScalarType::F64),
146 AggOp::Sum | AggOp::Min | AggOp::Max | AggOp::LogSumExp => None,
147 }
148 }
149
150 pub(crate) fn input_independent_result_type(&self) -> Option<ScalarType> {
152 match self.op {
153 AggOp::Count | AggOp::Sum => Some(ScalarType::U64),
154 AggOp::LogSumExp => Some(ScalarType::F64),
155 AggOp::Min | AggOp::Max => None,
156 }
157 }
158
159 fn default_result_type(&self) -> ScalarType {
160 self.input_independent_result_type()
161 .unwrap_or(ScalarType::U64)
162 }
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
167pub enum AggOp {
168 Count,
170 Sum,
172 Min,
174 Max,
176 LogSumExp,
178}
179
180#[derive(Debug, Clone, PartialEq)]
182pub enum ArithExpr {
183 Variable(String),
185 Integer(i64),
187 Float(f64),
189
190 Add(Box<ArithExpr>, Box<ArithExpr>),
192 Sub(Box<ArithExpr>, Box<ArithExpr>),
194 Mul(Box<ArithExpr>, Box<ArithExpr>),
196 Div(Box<ArithExpr>, Box<ArithExpr>),
198 Mod(Box<ArithExpr>, Box<ArithExpr>),
200
201 Abs(Box<ArithExpr>),
203 Min(Box<ArithExpr>, Box<ArithExpr>),
205 Max(Box<ArithExpr>, Box<ArithExpr>),
207 Pow(Box<ArithExpr>, Box<ArithExpr>),
209
210 Cast(Box<ArithExpr>, ScalarType),
212
213 FuncCall {
215 name: String,
217 args: Vec<ArithExpr>,
219 },
220
221 Conditional {
223 cond_left: Box<ArithExpr>,
225 cond_op: CompOp,
227 cond_right: Box<ArithExpr>,
229 then_expr: Box<ArithExpr>,
231 else_expr: Box<ArithExpr>,
233 },
234}
235
236impl ArithExpr {
237 pub fn variables(&self) -> Vec<&str> {
239 match self {
240 ArithExpr::Variable(name) => vec![name.as_str()],
241 ArithExpr::Integer(_) | ArithExpr::Float(_) => vec![],
242 ArithExpr::Add(l, r)
243 | ArithExpr::Sub(l, r)
244 | ArithExpr::Mul(l, r)
245 | ArithExpr::Div(l, r)
246 | ArithExpr::Mod(l, r)
247 | ArithExpr::Min(l, r)
248 | ArithExpr::Max(l, r)
249 | ArithExpr::Pow(l, r) => {
250 let mut vars = l.variables();
251 vars.extend(r.variables());
252 vars
253 }
254 ArithExpr::Abs(e) | ArithExpr::Cast(e, _) => e.variables(),
255 ArithExpr::FuncCall { args, .. } => args.iter().flat_map(|a| a.variables()).collect(),
256 ArithExpr::Conditional {
257 cond_left,
258 cond_right,
259 then_expr,
260 else_expr,
261 ..
262 } => {
263 let mut vars = cond_left.variables();
264 vars.extend(cond_right.variables());
265 vars.extend(then_expr.variables());
266 vars.extend(else_expr.variables());
267 vars
268 }
269 }
270 }
271}
272
273#[derive(Debug, Clone, PartialEq)]
275pub struct IsExpr {
276 pub target: String,
278 pub expr: ArithExpr,
280}
281
282#[derive(Debug, Clone, PartialEq)]
284pub struct Atom {
285 pub predicate: String,
287 pub terms: Vec<Term>,
289}
290
291impl Atom {
292 pub fn arity(&self) -> usize {
294 self.terms.len()
295 }
296
297 pub fn variables(&self) -> Vec<&str> {
299 self.terms.iter().flat_map(Term::variables).collect()
300 }
301}
302
303#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
305pub enum EpistemicOp {
306 Know,
308 Possible,
310}
311
312#[derive(Debug, Clone, PartialEq)]
314pub struct EpistemicLiteral {
315 pub op: EpistemicOp,
317 pub negated: bool,
319 pub atom: Atom,
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq)]
325pub enum CompOp {
326 Eq,
328 Ne,
330 Lt,
332 Le,
334 Gt,
336 Ge,
338}
339
340#[derive(Debug, Clone, PartialEq)]
342pub struct Comparison {
343 pub left: Term,
345 pub op: CompOp,
347 pub right: Term,
349}
350
351#[derive(Debug, Clone, PartialEq)]
353pub struct Univ {
354 pub term: Term,
356 pub parts: Term,
358}
359
360#[derive(Debug, Clone, PartialEq)]
362pub enum BodyLiteral {
363 Positive(Atom),
365 Negated(Atom),
367 Epistemic(EpistemicLiteral),
369 Comparison(Comparison),
371 IsExpr(IsExpr),
373 Univ(Univ),
375}
376
377impl BodyLiteral {
378 pub fn is_positive(&self) -> bool {
380 matches!(self, BodyLiteral::Positive(_))
381 }
382
383 pub fn is_negated(&self) -> bool {
385 matches!(self, BodyLiteral::Negated(_))
386 }
387
388 pub fn atom(&self) -> Option<&Atom> {
390 match self {
391 BodyLiteral::Positive(a) | BodyLiteral::Negated(a) => Some(a),
392 BodyLiteral::Epistemic(lit) => Some(&lit.atom),
393 BodyLiteral::Comparison(_) | BodyLiteral::IsExpr(_) | BodyLiteral::Univ(_) => None,
394 }
395 }
396
397 pub fn variables(&self) -> Vec<&str> {
399 match self {
400 BodyLiteral::Positive(a) | BodyLiteral::Negated(a) => a.variables(),
401 BodyLiteral::Epistemic(lit) => lit.atom.variables(),
402 BodyLiteral::Comparison(c) => {
403 let mut vars = vec![];
404 vars.extend(c.left.variables());
405 vars.extend(c.right.variables());
406 vars
407 }
408 BodyLiteral::IsExpr(is_expr) => {
409 let mut vars = is_expr.expr.variables();
410 vars.push(is_expr.target.as_str());
411 vars
412 }
413 BodyLiteral::Univ(univ) => {
414 let mut vars = univ.term.variables();
415 vars.extend(univ.parts.variables());
416 vars
417 }
418 }
419 }
420}
421
422#[derive(Debug, Clone, PartialEq)]
424pub struct Rule {
425 pub head: Atom,
427 pub body: Vec<BodyLiteral>,
429}
430
431impl Rule {
432 pub fn is_fact(&self) -> bool {
434 self.body.is_empty()
435 }
436
437 pub fn has_negation(&self) -> bool {
439 self.body.iter().any(|l| l.is_negated())
440 }
441
442 pub fn has_aggregation(&self) -> bool {
444 self.head
445 .terms
446 .iter()
447 .any(|t| matches!(t, Term::Aggregate(_)))
448 }
449
450 pub fn body_predicates(&self) -> Vec<&str> {
452 self.body
453 .iter()
454 .filter_map(|l| l.atom().map(|a| a.predicate.as_str()))
455 .collect()
456 }
457
458 pub fn head_variables(&self) -> Vec<&str> {
460 self.head.variables()
461 }
462
463 pub fn body_variables(&self) -> Vec<&str> {
465 self.body.iter().flat_map(|l| l.variables()).collect()
466 }
467
468 pub(crate) fn inferred_head_variable_type<F>(
475 &self,
476 variable: &str,
477 mut column_type: F,
478 ) -> Option<ScalarType>
479 where
480 F: FnMut(&Atom, usize, bool) -> Option<ScalarType>,
481 {
482 for literal in &self.body {
483 let (atom, negated) = match literal {
484 BodyLiteral::Positive(atom) => (atom, false),
485 BodyLiteral::Negated(atom) => (atom, true),
486 BodyLiteral::Epistemic(_)
487 | BodyLiteral::Comparison(_)
488 | BodyLiteral::IsExpr(_)
489 | BodyLiteral::Univ(_) => continue,
490 };
491 for (index, term) in atom.terms.iter().enumerate() {
492 if matches!(term, Term::Variable(name) if name == variable) {
493 if let Some(typ) = column_type(atom, index, negated) {
494 return Some(typ);
495 }
496 }
497 }
498 }
499 None
500 }
501}
502
503#[derive(Debug, Clone, PartialEq)]
505pub struct Constraint {
506 pub authored_index: Option<usize>,
508 pub body: Vec<BodyLiteral>,
510}
511
512impl Constraint {
513 pub fn require_authored_index(&self) -> Result<usize> {
515 self.authored_index.ok_or_else(|| {
516 XlogError::Compilation(
517 "prepared constraint compilation requires authored identities".to_string(),
518 )
519 })
520 }
521}
522
523#[derive(Debug, Clone, PartialEq)]
525pub struct Query {
526 pub atom: Atom,
528}
529
530#[derive(Debug, Clone, Copy, PartialEq, Eq)]
532pub enum ProbEngine {
533 ExactDdnnf,
535 Mc,
537}
538
539#[derive(Debug, Clone, Copy, PartialEq, Eq)]
541pub enum ProbCache {
542 On,
544 Off,
546}
547
548#[derive(Debug, Clone, Copy, PartialEq, Eq)]
550pub enum EpistemicMode {
551 G91,
553 Faeel,
555}
556
557#[derive(Debug, Clone, Copy, PartialEq, Eq)]
559pub enum ProbMethod {
560 Rejection,
562 EvidenceClamping,
564}
565
566#[derive(Debug, Clone, Copy, PartialEq, Eq)]
568pub enum MagicSetsMode {
569 Auto,
571 On,
573 Off,
575}
576
577#[derive(Debug, Clone, Default, PartialEq)]
579pub struct Directives {
580 pub prob_engine: Option<ProbEngine>,
582 pub prob_cache: Option<ProbCache>,
584 pub prob_samples: Option<usize>,
586 pub prob_seed: Option<u64>,
588 pub prob_confidence: Option<f64>,
590 pub prob_method: Option<ProbMethod>,
592 pub prob_max_nonmonotone_iterations: Option<usize>,
594 pub max_recursion_depth: Option<u32>,
596 pub epistemic_mode: Option<EpistemicMode>,
598 pub magic_sets: Option<MagicSetsMode>,
600}
601
602impl Directives {
603 pub fn set_pragma_names(&self) -> Vec<&'static str> {
607 let Directives {
611 prob_engine,
612 prob_cache,
613 prob_samples,
614 prob_seed,
615 prob_confidence,
616 prob_method,
617 prob_max_nonmonotone_iterations,
618 max_recursion_depth,
619 epistemic_mode,
620 magic_sets,
621 } = self;
622 let mut names = Vec::new();
623 if prob_engine.is_some() {
624 names.push("prob_engine");
625 }
626 if prob_cache.is_some() {
627 names.push("prob_cache");
628 }
629 if prob_samples.is_some() {
630 names.push("prob_samples");
631 }
632 if prob_seed.is_some() {
633 names.push("prob_seed");
634 }
635 if prob_confidence.is_some() {
636 names.push("prob_confidence");
637 }
638 if prob_method.is_some() {
639 names.push("prob_method");
640 }
641 if prob_max_nonmonotone_iterations.is_some() {
642 names.push("prob_max_nonmonotone_iterations");
643 }
644 if max_recursion_depth.is_some() {
645 names.push("max_recursion_depth");
646 }
647 if epistemic_mode.is_some() {
648 names.push("epistemic_mode");
649 }
650 if magic_sets.is_some() {
651 names.push("magic_sets");
652 }
653 names
654 }
655
656 pub fn prob_engine_or_default(&self) -> ProbEngine {
658 self.prob_engine.unwrap_or(ProbEngine::ExactDdnnf)
659 }
660
661 pub fn max_recursion_depth_or_default(&self) -> u32 {
663 self.max_recursion_depth.unwrap_or(1000)
664 }
665
666 pub fn epistemic_mode_or_default(&self) -> EpistemicMode {
668 self.epistemic_mode.unwrap_or(EpistemicMode::Faeel)
669 }
670
671 pub fn prob_samples_or_default(&self) -> usize {
673 self.prob_samples.unwrap_or(10000)
674 }
675
676 pub fn prob_seed_or_default(&self) -> u64 {
678 self.prob_seed.unwrap_or(0)
679 }
680
681 pub fn prob_confidence_or_default(&self) -> f64 {
683 self.prob_confidence.unwrap_or(0.95)
684 }
685
686 pub fn prob_max_nonmonotone_iterations_or_default(&self) -> usize {
688 self.prob_max_nonmonotone_iterations.unwrap_or(1024)
689 }
690}
691
692#[derive(Debug, Clone, PartialEq)]
694pub struct ProbFact {
695 pub prob: f64,
697 pub atom: Atom,
699}
700
701#[derive(Debug, Clone, PartialEq)]
715pub struct NeuralPredDecl {
716 pub network: String,
718 pub inputs: Vec<String>,
720 pub output: String,
722 pub labels: Option<Vec<NeuralLabel>>,
725 pub predicate: Atom,
727}
728
729#[derive(Debug, Clone, PartialEq)]
733pub enum NeuralLabel {
734 Integer(i64),
736 Symbol(String),
738}
739
740#[derive(Debug, Clone)]
744pub struct LearnableRule {
745 pub mask_name: String,
747 pub head: Atom,
749 pub body: Vec<BodyLiteral>,
751}
752
753#[derive(Debug, Clone, PartialEq)]
755pub struct AnnotatedDisjunction {
756 pub choices: Vec<ProbFact>,
758}
759
760#[derive(Debug, Clone, PartialEq)]
762pub struct Evidence {
763 pub atom: Atom,
765 pub value: bool,
767}
768
769#[derive(Debug, Clone, PartialEq)]
771pub struct ProbQuery {
772 pub atom: Atom,
774}
775
776#[derive(Debug, Clone, PartialEq)]
778pub struct UseDecl {
779 pub module_path: Vec<String>,
781 pub imports: Option<Vec<String>>,
783}
784
785#[derive(Debug, Clone, PartialEq)]
787pub struct DomainDecl {
788 pub name: String,
790 pub typ: ScalarType,
792}
793
794#[derive(Debug, Clone, PartialEq, Eq)]
796pub enum TypeRef {
797 Scalar(ScalarType),
799 Domain(String),
801 List(Box<TypeRef>),
803 Term,
805 Compound,
807 PredRef,
809}
810
811#[derive(Debug, Clone, PartialEq, Eq)]
813pub struct PredColumn {
814 pub name: Option<String>,
816 pub typ: TypeRef,
818}
819
820#[derive(Debug, Clone, PartialEq)]
822pub struct PredDecl {
823 pub name: String,
825 pub types: Vec<TypeRef>,
827 pub columns: Vec<PredColumn>,
829 pub is_private: bool,
831}
832
833impl PredDecl {
834 pub fn schema_columns(&self) -> Vec<PredColumn> {
839 if self.columns.is_empty() {
840 self.types
841 .iter()
842 .cloned()
843 .map(|typ| PredColumn { name: None, typ })
844 .collect()
845 } else {
846 self.columns.clone()
847 }
848 }
849
850 pub fn arity(&self) -> usize {
852 if self.columns.is_empty() {
853 self.types.len()
854 } else {
855 self.columns.len()
856 }
857 }
858}
859
860#[derive(Debug, Clone, PartialEq)]
862pub struct FuncParam {
863 pub name: String,
865 pub typ: Option<ScalarType>,
867}
868
869#[derive(Debug, Clone, PartialEq)]
871pub struct CondExpr {
872 pub cond_left: ArithExpr,
874 pub cond_op: CompOp,
876 pub cond_right: ArithExpr,
878 pub then_branch: Box<FuncBody>,
880 pub else_branch: Box<FuncBody>,
882}
883
884#[derive(Debug, Clone, PartialEq)]
886pub enum FuncBody {
887 Arithmetic(ArithExpr),
889 Conditional(CondExpr),
891 Predicate {
893 result: String,
895 body: Vec<BodyLiteral>,
897 },
898}
899
900#[derive(Debug, Clone, PartialEq)]
902pub struct FuncDef {
903 pub name: String,
905 pub params: Vec<FuncParam>,
907 pub return_type: Option<ScalarType>,
909 pub body: FuncBody,
911 pub is_private: bool,
913}
914
915#[derive(Debug, Clone, Default)]
917pub struct Program {
918 pub imports: Vec<UseDecl>,
920 pub functions: Vec<FuncDef>,
922 pub domains: Vec<DomainDecl>,
924 pub predicates: Vec<PredDecl>,
926 pub rules: Vec<Rule>,
928 pub constraints: Vec<Constraint>,
930 pub authored_constraint_source_bound: Option<usize>,
935 pub queries: Vec<Query>,
937 pub prob_facts: Vec<ProbFact>,
939 pub annotated_disjunctions: Vec<AnnotatedDisjunction>,
941 pub evidence: Vec<Evidence>,
943 pub prob_queries: Vec<ProbQuery>,
945 pub neural_predicates: Vec<NeuralPredDecl>,
947 pub learnable_rules: Vec<LearnableRule>,
949 pub directives: Directives,
951}
952
953impl Program {
954 pub fn new() -> Self {
956 Self::default()
957 }
958
959 pub fn prepare_authored_constraint_identity(
961 &mut self,
962 authored_source_constraint_count: usize,
963 ) -> Result<()> {
964 if let Some(existing_bound) = self.authored_constraint_source_bound {
965 if existing_bound != authored_source_constraint_count {
966 return Err(XlogError::Compilation(format!(
967 "authored constraint source bound {existing_bound} does not match requested bound {authored_source_constraint_count}"
968 )));
969 }
970 }
971
972 let assigned = self
973 .constraints
974 .iter()
975 .filter(|constraint| constraint.authored_index.is_some())
976 .count();
977
978 if assigned == 0 {
979 if self.constraints.len() != authored_source_constraint_count {
980 return Err(XlogError::Compilation(format!(
981 "unassigned constraint count {} does not match authored source bound {}",
982 self.constraints.len(),
983 authored_source_constraint_count
984 )));
985 }
986 for (authored_index, constraint) in self.constraints.iter_mut().enumerate() {
987 constraint.authored_index = Some(authored_index);
988 }
989 self.authored_constraint_source_bound = Some(authored_source_constraint_count);
990 return Ok(());
991 }
992
993 if assigned != self.constraints.len() {
994 return Err(XlogError::Compilation(
995 "mixed assigned and unassigned authored constraint identities".to_string(),
996 ));
997 }
998
999 let mut seen = std::collections::HashSet::with_capacity(self.constraints.len());
1000 for constraint in &self.constraints {
1001 let authored_index = constraint
1002 .authored_index
1003 .expect("all constraint identities were checked as assigned");
1004 if authored_index >= authored_source_constraint_count {
1005 return Err(XlogError::Compilation(format!(
1006 "authored constraint index {authored_index} is outside source bound {authored_source_constraint_count}"
1007 )));
1008 }
1009 if !seen.insert(authored_index) {
1010 return Err(XlogError::Compilation(format!(
1011 "duplicate authored constraint index {authored_index}"
1012 )));
1013 }
1014 }
1015 self.authored_constraint_source_bound = Some(authored_source_constraint_count);
1016 Ok(())
1017 }
1018
1019 pub fn prepare_authored_constraint_identity_at_root(&mut self) -> Result<()> {
1021 let authored_source_constraint_count = self.constraints.len();
1022 self.prepare_authored_constraint_identity(authored_source_constraint_count)
1023 }
1024
1025 pub fn validate_prepared_authored_constraint_identity(&self) -> Result<()> {
1027 if self.constraints.is_empty() && self.authored_constraint_source_bound.is_none() {
1028 return Ok(());
1029 }
1030 let authored_source_constraint_count =
1031 self.authored_constraint_source_bound.ok_or_else(|| {
1032 XlogError::Compilation(
1033 "prepared constraint compilation requires authored identities and a source bound"
1034 .to_string(),
1035 )
1036 })?;
1037 if self
1038 .constraints
1039 .iter()
1040 .any(|constraint| constraint.authored_index.is_none())
1041 {
1042 return Err(XlogError::Compilation(
1043 "prepared constraint compilation requires authored identities".to_string(),
1044 ));
1045 }
1046
1047 let mut seen = std::collections::HashSet::with_capacity(self.constraints.len());
1048 for constraint in &self.constraints {
1049 let authored_index = constraint
1050 .authored_index
1051 .expect("all prepared constraint identities were checked as assigned");
1052 if authored_index >= authored_source_constraint_count {
1053 return Err(XlogError::Compilation(format!(
1054 "authored constraint index {authored_index} is outside source bound {authored_source_constraint_count}"
1055 )));
1056 }
1057 if !seen.insert(authored_index) {
1058 return Err(XlogError::Compilation(format!(
1059 "duplicate authored constraint index {authored_index}"
1060 )));
1061 }
1062 }
1063 Ok(())
1064 }
1065
1066 pub fn facts(&self) -> impl Iterator<Item = &Rule> {
1068 self.rules.iter().filter(|r| r.is_fact())
1069 }
1070
1071 pub fn proper_rules(&self) -> impl Iterator<Item = &Rule> {
1073 self.rules.iter().filter(|r| !r.is_fact())
1074 }
1075
1076 pub fn defined_predicates(&self) -> Vec<&str> {
1078 self.rules
1079 .iter()
1080 .map(|r| r.head.predicate.as_str())
1081 .collect::<std::collections::HashSet<_>>()
1082 .into_iter()
1083 .collect()
1084 }
1085
1086 pub fn is_probabilistic_profile(&self) -> bool {
1088 !self.prob_facts.is_empty()
1089 || !self.annotated_disjunctions.is_empty()
1090 || !self.evidence.is_empty()
1091 || !self.prob_queries.is_empty()
1092 || self.directives.prob_engine.is_some()
1093 || self.directives.prob_cache.is_some()
1094 || self.directives.prob_samples.is_some()
1095 || self.directives.prob_seed.is_some()
1096 || self.directives.prob_confidence.is_some()
1097 || self.directives.prob_method.is_some()
1098 || self.directives.prob_max_nonmonotone_iterations.is_some()
1099 }
1100
1101 pub fn prob_engine(&self) -> ProbEngine {
1103 self.directives.prob_engine_or_default()
1104 }
1105
1106 pub fn merge_from(
1114 &mut self,
1115 other: &Program,
1116 imported_items: Option<&std::collections::HashSet<String>>,
1117 ) {
1118 use std::collections::HashSet;
1119
1120 let private_preds: HashSet<&str> = other
1122 .predicates
1123 .iter()
1124 .filter(|p| p.is_private)
1125 .map(|p| p.name.as_str())
1126 .collect();
1127
1128 let _private_funcs: HashSet<&str> = other
1129 .functions
1130 .iter()
1131 .filter(|f| f.is_private)
1132 .map(|f| f.name.as_str())
1133 .collect();
1134
1135 for pred in &other.predicates {
1137 if pred.is_private {
1138 continue;
1139 }
1140 if let Some(items) = imported_items {
1142 if !items.contains(&pred.name) {
1143 continue;
1144 }
1145 }
1146 if !self.predicates.iter().any(|p| p.name == pred.name) {
1148 self.predicates.push(pred.clone());
1149 }
1150 }
1151
1152 for func in &other.functions {
1154 if func.is_private {
1155 continue;
1156 }
1157 if let Some(items) = imported_items {
1158 if !items.contains(&func.name) {
1159 continue;
1160 }
1161 }
1162 if !self.functions.iter().any(|f| f.name == func.name) {
1164 self.functions.push(func.clone());
1165 }
1166 }
1167
1168 for rule in &other.rules {
1170 if private_preds.contains(rule.head.predicate.as_str()) {
1172 continue;
1173 }
1174 if let Some(items) = imported_items {
1176 if !items.contains(&rule.head.predicate) {
1177 continue;
1178 }
1179 }
1180 if !self.rules.iter().any(|existing| existing == rule) {
1181 self.rules.push(rule.clone());
1182 }
1183 }
1184
1185 for domain in &other.domains {
1187 if !self.domains.iter().any(|d| d.name == domain.name) {
1188 self.domains.push(domain.clone());
1189 }
1190 }
1191 }
1192}
1193
1194#[cfg(test)]
1195mod tests {
1196 use super::*;
1197
1198 #[test]
1199 fn test_directives_set_pragma_names() {
1200 let mut directives = Directives::default();
1201 assert!(directives.set_pragma_names().is_empty());
1202
1203 directives.prob_seed = Some(7);
1204 directives.magic_sets = Some(MagicSetsMode::Auto);
1205 assert_eq!(
1206 directives.set_pragma_names(),
1207 vec!["prob_seed", "magic_sets"]
1208 );
1209 }
1210
1211 #[test]
1212 fn test_directives_set_pragma_names_covers_all_ten_pragmas() {
1213 let directives = Directives {
1214 prob_engine: Some(ProbEngine::Mc),
1215 prob_cache: Some(ProbCache::On),
1216 prob_samples: Some(20000),
1217 prob_seed: Some(7),
1218 prob_confidence: Some(0.9),
1219 prob_method: Some(ProbMethod::Rejection),
1220 prob_max_nonmonotone_iterations: Some(64),
1221 max_recursion_depth: Some(100),
1222 epistemic_mode: Some(EpistemicMode::G91),
1223 magic_sets: Some(MagicSetsMode::Auto),
1224 };
1225 assert_eq!(
1226 directives.set_pragma_names(),
1227 vec![
1228 "prob_engine",
1229 "prob_cache",
1230 "prob_samples",
1231 "prob_seed",
1232 "prob_confidence",
1233 "prob_method",
1234 "prob_max_nonmonotone_iterations",
1235 "max_recursion_depth",
1236 "epistemic_mode",
1237 "magic_sets",
1238 ]
1239 );
1240 }
1241
1242 #[test]
1243 fn test_term_variable() {
1244 let term = Term::Variable("X".to_string());
1245 assert!(term.is_variable());
1246 assert!(!term.is_constant());
1247 }
1248
1249 #[test]
1250 fn test_term_constant() {
1251 let term = Term::Integer(42);
1252 assert!(!term.is_variable());
1253 assert!(term.is_constant());
1254 }
1255
1256 #[test]
1257 fn test_atom_arity() {
1258 let atom = Atom {
1259 predicate: "edge".to_string(),
1260 terms: vec![Term::Integer(1), Term::Integer(2)],
1261 };
1262 assert_eq!(atom.arity(), 2);
1263 }
1264
1265 #[test]
1266 fn test_atom_variables() {
1267 let atom = Atom {
1268 predicate: "edge".to_string(),
1269 terms: vec![Term::Variable("X".to_string()), Term::Integer(2)],
1270 };
1271 let vars = atom.variables();
1272 assert_eq!(vars, vec!["X"]);
1273 }
1274
1275 #[test]
1276 fn predicate_declaration_uses_its_effective_schema_representation() {
1277 let types_only = PredDecl {
1278 name: "types_only".to_string(),
1279 types: vec![TypeRef::Scalar(ScalarType::U64)],
1280 columns: vec![],
1281 is_private: false,
1282 };
1283 assert_eq!(types_only.arity(), 1);
1284 assert_eq!(
1285 types_only.schema_columns(),
1286 vec![PredColumn {
1287 name: None,
1288 typ: TypeRef::Scalar(ScalarType::U64),
1289 }]
1290 );
1291
1292 let columns_only = PredDecl {
1293 name: "columns_only".to_string(),
1294 types: vec![],
1295 columns: vec![PredColumn {
1296 name: Some("value".to_string()),
1297 typ: TypeRef::Scalar(ScalarType::Symbol),
1298 }],
1299 is_private: false,
1300 };
1301 assert_eq!(columns_only.arity(), 1);
1302 assert_eq!(
1303 columns_only.schema_columns(),
1304 vec![PredColumn {
1305 name: Some("value".to_string()),
1306 typ: TypeRef::Scalar(ScalarType::Symbol),
1307 }]
1308 );
1309 }
1310
1311 #[test]
1312 fn test_rule_is_fact() {
1313 let fact = Rule {
1314 head: Atom {
1315 predicate: "edge".to_string(),
1316 terms: vec![Term::Integer(1), Term::Integer(2)],
1317 },
1318 body: vec![],
1319 };
1320 assert!(fact.is_fact());
1321 }
1322
1323 #[test]
1324 fn test_rule_has_negation() {
1325 let rule = Rule {
1326 head: Atom {
1327 predicate: "isolated".to_string(),
1328 terms: vec![Term::Variable("X".to_string())],
1329 },
1330 body: vec![
1331 BodyLiteral::Positive(Atom {
1332 predicate: "node".to_string(),
1333 terms: vec![Term::Variable("X".to_string())],
1334 }),
1335 BodyLiteral::Negated(Atom {
1336 predicate: "edge".to_string(),
1337 terms: vec![
1338 Term::Variable("X".to_string()),
1339 Term::Variable("Y".to_string()),
1340 ],
1341 }),
1342 ],
1343 };
1344 assert!(rule.has_negation());
1345 }
1346
1347 #[test]
1348 fn test_program_facts() {
1349 let mut program = Program::new();
1350 program.rules.push(Rule {
1351 head: Atom {
1352 predicate: "edge".to_string(),
1353 terms: vec![Term::Integer(1), Term::Integer(2)],
1354 },
1355 body: vec![],
1356 });
1357 program.rules.push(Rule {
1358 head: Atom {
1359 predicate: "reach".to_string(),
1360 terms: vec![
1361 Term::Variable("X".to_string()),
1362 Term::Variable("Y".to_string()),
1363 ],
1364 },
1365 body: vec![BodyLiteral::Positive(Atom {
1366 predicate: "edge".to_string(),
1367 terms: vec![
1368 Term::Variable("X".to_string()),
1369 Term::Variable("Y".to_string()),
1370 ],
1371 })],
1372 });
1373 assert_eq!(program.facts().count(), 1);
1374 assert_eq!(program.proper_rules().count(), 1);
1375 }
1376
1377 #[test]
1378 fn test_arith_expr_structure() {
1379 let expr = ArithExpr::Add(
1380 Box::new(ArithExpr::Variable("X".to_string())),
1381 Box::new(ArithExpr::Integer(1)),
1382 );
1383 assert!(matches!(expr, ArithExpr::Add(_, _)));
1384 }
1385
1386 #[test]
1387 fn test_is_expr_structure() {
1388 let is_expr = IsExpr {
1389 target: "Z".to_string(),
1390 expr: ArithExpr::Variable("Y".to_string()),
1391 };
1392 assert_eq!(is_expr.target, "Z");
1393 }
1394}