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
953#[derive(Debug, Clone, Default, PartialEq, Eq)]
954pub(crate) struct ProgramMergeReport {
955 pub domains: Vec<usize>,
956 pub predicates: Vec<usize>,
957 pub functions: Vec<usize>,
958 pub rules: Vec<usize>,
959}
960
961impl Program {
962 pub fn new() -> Self {
964 Self::default()
965 }
966
967 pub fn prepare_authored_constraint_identity(
969 &mut self,
970 authored_source_constraint_count: usize,
971 ) -> Result<()> {
972 if let Some(existing_bound) = self.authored_constraint_source_bound {
973 if existing_bound != authored_source_constraint_count {
974 return Err(XlogError::Compilation(format!(
975 "authored constraint source bound {existing_bound} does not match requested bound {authored_source_constraint_count}"
976 )));
977 }
978 }
979
980 let assigned = self
981 .constraints
982 .iter()
983 .filter(|constraint| constraint.authored_index.is_some())
984 .count();
985
986 if assigned == 0 {
987 if self.constraints.len() != authored_source_constraint_count {
988 return Err(XlogError::Compilation(format!(
989 "unassigned constraint count {} does not match authored source bound {}",
990 self.constraints.len(),
991 authored_source_constraint_count
992 )));
993 }
994 for (authored_index, constraint) in self.constraints.iter_mut().enumerate() {
995 constraint.authored_index = Some(authored_index);
996 }
997 self.authored_constraint_source_bound = Some(authored_source_constraint_count);
998 return Ok(());
999 }
1000
1001 if assigned != self.constraints.len() {
1002 return Err(XlogError::Compilation(
1003 "mixed assigned and unassigned authored constraint identities".to_string(),
1004 ));
1005 }
1006
1007 let mut seen = std::collections::HashSet::with_capacity(self.constraints.len());
1008 for constraint in &self.constraints {
1009 let authored_index = constraint
1010 .authored_index
1011 .expect("all constraint identities were checked as assigned");
1012 if authored_index >= authored_source_constraint_count {
1013 return Err(XlogError::Compilation(format!(
1014 "authored constraint index {authored_index} is outside source bound {authored_source_constraint_count}"
1015 )));
1016 }
1017 if !seen.insert(authored_index) {
1018 return Err(XlogError::Compilation(format!(
1019 "duplicate authored constraint index {authored_index}"
1020 )));
1021 }
1022 }
1023 self.authored_constraint_source_bound = Some(authored_source_constraint_count);
1024 Ok(())
1025 }
1026
1027 pub fn prepare_authored_constraint_identity_at_root(&mut self) -> Result<()> {
1029 let authored_source_constraint_count = self.constraints.len();
1030 self.prepare_authored_constraint_identity(authored_source_constraint_count)
1031 }
1032
1033 pub fn validate_prepared_authored_constraint_identity(&self) -> Result<()> {
1035 if self.constraints.is_empty() && self.authored_constraint_source_bound.is_none() {
1036 return Ok(());
1037 }
1038 let authored_source_constraint_count =
1039 self.authored_constraint_source_bound.ok_or_else(|| {
1040 XlogError::Compilation(
1041 "prepared constraint compilation requires authored identities and a source bound"
1042 .to_string(),
1043 )
1044 })?;
1045 if self
1046 .constraints
1047 .iter()
1048 .any(|constraint| constraint.authored_index.is_none())
1049 {
1050 return Err(XlogError::Compilation(
1051 "prepared constraint compilation requires authored identities".to_string(),
1052 ));
1053 }
1054
1055 let mut seen = std::collections::HashSet::with_capacity(self.constraints.len());
1056 for constraint in &self.constraints {
1057 let authored_index = constraint
1058 .authored_index
1059 .expect("all prepared constraint identities were checked as assigned");
1060 if authored_index >= authored_source_constraint_count {
1061 return Err(XlogError::Compilation(format!(
1062 "authored constraint index {authored_index} is outside source bound {authored_source_constraint_count}"
1063 )));
1064 }
1065 if !seen.insert(authored_index) {
1066 return Err(XlogError::Compilation(format!(
1067 "duplicate authored constraint index {authored_index}"
1068 )));
1069 }
1070 }
1071 Ok(())
1072 }
1073
1074 pub fn facts(&self) -> impl Iterator<Item = &Rule> {
1076 self.rules.iter().filter(|r| r.is_fact())
1077 }
1078
1079 pub fn proper_rules(&self) -> impl Iterator<Item = &Rule> {
1081 self.rules.iter().filter(|r| !r.is_fact())
1082 }
1083
1084 pub fn defined_predicates(&self) -> Vec<&str> {
1086 self.rules
1087 .iter()
1088 .map(|r| r.head.predicate.as_str())
1089 .collect::<std::collections::HashSet<_>>()
1090 .into_iter()
1091 .collect()
1092 }
1093
1094 pub fn is_probabilistic_profile(&self) -> bool {
1096 !self.prob_facts.is_empty()
1097 || !self.annotated_disjunctions.is_empty()
1098 || !self.evidence.is_empty()
1099 || !self.prob_queries.is_empty()
1100 || self.directives.prob_engine.is_some()
1101 || self.directives.prob_cache.is_some()
1102 || self.directives.prob_samples.is_some()
1103 || self.directives.prob_seed.is_some()
1104 || self.directives.prob_confidence.is_some()
1105 || self.directives.prob_method.is_some()
1106 || self.directives.prob_max_nonmonotone_iterations.is_some()
1107 }
1108
1109 pub fn prob_engine(&self) -> ProbEngine {
1111 self.directives.prob_engine_or_default()
1112 }
1113
1114 pub fn merge_from(
1122 &mut self,
1123 other: &Program,
1124 imported_items: Option<&std::collections::HashSet<String>>,
1125 ) {
1126 self.merge_from_with_report(other, imported_items);
1127 }
1128
1129 pub(crate) fn merge_from_with_report(
1130 &mut self,
1131 other: &Program,
1132 imported_items: Option<&std::collections::HashSet<String>>,
1133 ) -> ProgramMergeReport {
1134 use std::collections::HashSet;
1135
1136 let mut report = ProgramMergeReport::default();
1137
1138 let private_preds: HashSet<&str> = other
1140 .predicates
1141 .iter()
1142 .filter(|p| p.is_private)
1143 .map(|p| p.name.as_str())
1144 .collect();
1145
1146 let _private_funcs: HashSet<&str> = other
1147 .functions
1148 .iter()
1149 .filter(|f| f.is_private)
1150 .map(|f| f.name.as_str())
1151 .collect();
1152
1153 for (source_index, pred) in other.predicates.iter().enumerate() {
1155 if pred.is_private {
1156 continue;
1157 }
1158 if let Some(items) = imported_items {
1160 if !items.contains(&pred.name) {
1161 continue;
1162 }
1163 }
1164 if !self.predicates.iter().any(|p| p.name == pred.name) {
1166 self.predicates.push(pred.clone());
1167 report.predicates.push(source_index);
1168 }
1169 }
1170
1171 for (source_index, func) in other.functions.iter().enumerate() {
1173 if func.is_private {
1174 continue;
1175 }
1176 if let Some(items) = imported_items {
1177 if !items.contains(&func.name) {
1178 continue;
1179 }
1180 }
1181 if !self.functions.iter().any(|f| f.name == func.name) {
1183 self.functions.push(func.clone());
1184 report.functions.push(source_index);
1185 }
1186 }
1187
1188 for (source_index, rule) in other.rules.iter().enumerate() {
1190 if private_preds.contains(rule.head.predicate.as_str()) {
1192 continue;
1193 }
1194 if let Some(items) = imported_items {
1196 if !items.contains(&rule.head.predicate) {
1197 continue;
1198 }
1199 }
1200 if !self.rules.iter().any(|existing| existing == rule) {
1201 self.rules.push(rule.clone());
1202 report.rules.push(source_index);
1203 }
1204 }
1205
1206 for (source_index, domain) in other.domains.iter().enumerate() {
1208 if !self.domains.iter().any(|d| d.name == domain.name) {
1209 self.domains.push(domain.clone());
1210 report.domains.push(source_index);
1211 }
1212 }
1213
1214 report
1215 }
1216}
1217
1218#[cfg(test)]
1219mod tests {
1220 use super::*;
1221
1222 #[test]
1223 fn test_directives_set_pragma_names() {
1224 let mut directives = Directives::default();
1225 assert!(directives.set_pragma_names().is_empty());
1226
1227 directives.prob_seed = Some(7);
1228 directives.magic_sets = Some(MagicSetsMode::Auto);
1229 assert_eq!(
1230 directives.set_pragma_names(),
1231 vec!["prob_seed", "magic_sets"]
1232 );
1233 }
1234
1235 #[test]
1236 fn test_directives_set_pragma_names_covers_all_ten_pragmas() {
1237 let directives = Directives {
1238 prob_engine: Some(ProbEngine::Mc),
1239 prob_cache: Some(ProbCache::On),
1240 prob_samples: Some(20000),
1241 prob_seed: Some(7),
1242 prob_confidence: Some(0.9),
1243 prob_method: Some(ProbMethod::Rejection),
1244 prob_max_nonmonotone_iterations: Some(64),
1245 max_recursion_depth: Some(100),
1246 epistemic_mode: Some(EpistemicMode::G91),
1247 magic_sets: Some(MagicSetsMode::Auto),
1248 };
1249 assert_eq!(
1250 directives.set_pragma_names(),
1251 vec![
1252 "prob_engine",
1253 "prob_cache",
1254 "prob_samples",
1255 "prob_seed",
1256 "prob_confidence",
1257 "prob_method",
1258 "prob_max_nonmonotone_iterations",
1259 "max_recursion_depth",
1260 "epistemic_mode",
1261 "magic_sets",
1262 ]
1263 );
1264 }
1265
1266 #[test]
1267 fn test_term_variable() {
1268 let term = Term::Variable("X".to_string());
1269 assert!(term.is_variable());
1270 assert!(!term.is_constant());
1271 }
1272
1273 #[test]
1274 fn test_term_constant() {
1275 let term = Term::Integer(42);
1276 assert!(!term.is_variable());
1277 assert!(term.is_constant());
1278 }
1279
1280 #[test]
1281 fn test_atom_arity() {
1282 let atom = Atom {
1283 predicate: "edge".to_string(),
1284 terms: vec![Term::Integer(1), Term::Integer(2)],
1285 };
1286 assert_eq!(atom.arity(), 2);
1287 }
1288
1289 #[test]
1290 fn test_atom_variables() {
1291 let atom = Atom {
1292 predicate: "edge".to_string(),
1293 terms: vec![Term::Variable("X".to_string()), Term::Integer(2)],
1294 };
1295 let vars = atom.variables();
1296 assert_eq!(vars, vec!["X"]);
1297 }
1298
1299 #[test]
1300 fn predicate_declaration_uses_its_effective_schema_representation() {
1301 let types_only = PredDecl {
1302 name: "types_only".to_string(),
1303 types: vec![TypeRef::Scalar(ScalarType::U64)],
1304 columns: vec![],
1305 is_private: false,
1306 };
1307 assert_eq!(types_only.arity(), 1);
1308 assert_eq!(
1309 types_only.schema_columns(),
1310 vec![PredColumn {
1311 name: None,
1312 typ: TypeRef::Scalar(ScalarType::U64),
1313 }]
1314 );
1315
1316 let columns_only = PredDecl {
1317 name: "columns_only".to_string(),
1318 types: vec![],
1319 columns: vec![PredColumn {
1320 name: Some("value".to_string()),
1321 typ: TypeRef::Scalar(ScalarType::Symbol),
1322 }],
1323 is_private: false,
1324 };
1325 assert_eq!(columns_only.arity(), 1);
1326 assert_eq!(
1327 columns_only.schema_columns(),
1328 vec![PredColumn {
1329 name: Some("value".to_string()),
1330 typ: TypeRef::Scalar(ScalarType::Symbol),
1331 }]
1332 );
1333 }
1334
1335 #[test]
1336 fn test_rule_is_fact() {
1337 let fact = Rule {
1338 head: Atom {
1339 predicate: "edge".to_string(),
1340 terms: vec![Term::Integer(1), Term::Integer(2)],
1341 },
1342 body: vec![],
1343 };
1344 assert!(fact.is_fact());
1345 }
1346
1347 #[test]
1348 fn test_rule_has_negation() {
1349 let rule = Rule {
1350 head: Atom {
1351 predicate: "isolated".to_string(),
1352 terms: vec![Term::Variable("X".to_string())],
1353 },
1354 body: vec![
1355 BodyLiteral::Positive(Atom {
1356 predicate: "node".to_string(),
1357 terms: vec![Term::Variable("X".to_string())],
1358 }),
1359 BodyLiteral::Negated(Atom {
1360 predicate: "edge".to_string(),
1361 terms: vec![
1362 Term::Variable("X".to_string()),
1363 Term::Variable("Y".to_string()),
1364 ],
1365 }),
1366 ],
1367 };
1368 assert!(rule.has_negation());
1369 }
1370
1371 #[test]
1372 fn test_program_facts() {
1373 let mut program = Program::new();
1374 program.rules.push(Rule {
1375 head: Atom {
1376 predicate: "edge".to_string(),
1377 terms: vec![Term::Integer(1), Term::Integer(2)],
1378 },
1379 body: vec![],
1380 });
1381 program.rules.push(Rule {
1382 head: Atom {
1383 predicate: "reach".to_string(),
1384 terms: vec![
1385 Term::Variable("X".to_string()),
1386 Term::Variable("Y".to_string()),
1387 ],
1388 },
1389 body: vec![BodyLiteral::Positive(Atom {
1390 predicate: "edge".to_string(),
1391 terms: vec![
1392 Term::Variable("X".to_string()),
1393 Term::Variable("Y".to_string()),
1394 ],
1395 })],
1396 });
1397 assert_eq!(program.facts().count(), 1);
1398 assert_eq!(program.proper_rules().count(), 1);
1399 }
1400
1401 #[test]
1402 fn test_arith_expr_structure() {
1403 let expr = ArithExpr::Add(
1404 Box::new(ArithExpr::Variable("X".to_string())),
1405 Box::new(ArithExpr::Integer(1)),
1406 );
1407 assert!(matches!(expr, ArithExpr::Add(_, _)));
1408 }
1409
1410 #[test]
1411 fn test_is_expr_structure() {
1412 let is_expr = IsExpr {
1413 target: "Z".to_string(),
1414 expr: ArithExpr::Variable("Y".to_string()),
1415 };
1416 assert_eq!(is_expr.target, "Z");
1417 }
1418}