1use std::collections::{BTreeMap, BTreeSet};
4
5use xlog_core::{Result, XlogError};
6use xlog_ir::{
7 EirBodyLiteral, EirEpistemicLiteral, EirEpistemicMode, EirEpistemicOp, EirProgram, EirTerm,
8 EpistemicConstraintPlan, EpistemicExecutablePlan, EpistemicGpuPlan, EpistemicReductionPlan,
9 EpistemicSolverAssumptionBinding, EpistemicSolverServiceContract,
10 EpistemicTupleMembershipBinding, EpistemicWcojReductionStatus,
11};
12use xlog_stats::StatsSnapshot;
13
14use crate::ast::{
15 Atom, BodyLiteral, CompOp, Comparison, Constraint, EpistemicLiteral, EpistemicMode,
16 EpistemicOp, Program, Term,
17};
18use crate::build_eir;
19use crate::compile::Compiler;
20use crate::eir::convert_term;
21use crate::lower::Lowerer;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum TruthValue {
26 True,
28 False,
30}
31
32impl TruthValue {
33 fn from_bool(value: bool) -> Self {
34 if value {
35 TruthValue::True
36 } else {
37 TruthValue::False
38 }
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
43enum EpistemicTermKey {
44 Integer(i64),
45 FloatBits(u64),
46 String(String),
47 Symbol(u32),
48 List(Vec<EpistemicTermKey>),
49 Cons {
50 head: Box<EpistemicTermKey>,
51 tail: Box<EpistemicTermKey>,
52 },
53 Compound {
54 functor: String,
55 args: Vec<EpistemicTermKey>,
56 },
57 PredRef(String),
58}
59
60impl EpistemicTermKey {
61 fn from_term(term: &Term) -> Result<Self> {
62 Ok(match term {
63 Term::Integer(value) => Self::Integer(*value),
64 Term::Float(value) => Self::FloatBits(value.to_bits()),
65 Term::String(value) => Self::String(value.clone()),
66 Term::Symbol(value) => Self::Symbol(*value),
67 Term::List(items) => Self::List(
68 items
69 .iter()
70 .map(Self::from_term)
71 .collect::<Result<Vec<_>>>()?,
72 ),
73 Term::Cons { head, tail } => Self::Cons {
74 head: Box::new(Self::from_term(head)?),
75 tail: Box::new(Self::from_term(tail)?),
76 },
77 Term::Compound { functor, args } => Self::Compound {
78 functor: functor.clone(),
79 args: args
80 .iter()
81 .map(Self::from_term)
82 .collect::<Result<Vec<_>>>()?,
83 },
84 Term::PredRef(value) => Self::PredRef(value.clone()),
85 Term::Variable(_) | Term::Anonymous | Term::Aggregate(_) => {
86 return Err(XlogError::UnsupportedEpistemicConstruct {
87 construct: "epistemic tuple key".to_string(),
88 context: "tuple-key epistemic facts require ground terms".to_string(),
89 });
90 }
91 })
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
96enum EpistemicAtomKey {
97 Arity {
98 predicate: String,
99 arity: usize,
100 },
101 Ground {
102 predicate: String,
103 terms: Vec<EpistemicTermKey>,
104 },
105}
106
107impl EpistemicAtomKey {
108 fn from_arity(predicate: impl Into<String>, arity: usize) -> Self {
109 Self::Arity {
110 predicate: predicate.into(),
111 arity,
112 }
113 }
114
115 fn from_terms(predicate: impl Into<String>, terms: &[Term]) -> Result<Self> {
116 Ok(Self::Ground {
117 predicate: predicate.into(),
118 terms: terms
119 .iter()
120 .map(EpistemicTermKey::from_term)
121 .collect::<Result<Vec<_>>>()?,
122 })
123 }
124
125 fn predicate(&self) -> &str {
126 match self {
127 Self::Arity { predicate, .. } | Self::Ground { predicate, .. } => predicate,
128 }
129 }
130
131 fn arity(&self) -> usize {
132 match self {
133 Self::Arity { arity, .. } => *arity,
134 Self::Ground { terms, .. } => terms.len(),
135 }
136 }
137
138 fn matches_atom(&self, atom: &Atom) -> bool {
139 if self.predicate() != atom.predicate || self.arity() != atom.arity() {
140 return false;
141 }
142 match self {
143 Self::Arity { .. } => true,
144 Self::Ground { terms, .. } => atom
145 .terms
146 .iter()
147 .map(EpistemicTermKey::from_term)
148 .collect::<Result<Vec<_>>>()
149 .is_ok_and(|atom_terms| atom_terms == *terms),
150 }
151 }
152
153 fn overlaps(&self, other: &Self) -> bool {
154 if self.predicate() != other.predicate() || self.arity() != other.arity() {
155 return false;
156 }
157 matches!(self, Self::Arity { .. }) || matches!(other, Self::Arity { .. }) || self == other
158 }
159}
160
161#[derive(Debug, Clone, Default, PartialEq, Eq)]
163pub struct EpistemicInterpretation {
164 known: BTreeSet<EpistemicAtomKey>,
165 possible: BTreeSet<EpistemicAtomKey>,
166 rejected: BTreeSet<EpistemicAtomKey>,
167}
168
169impl EpistemicInterpretation {
170 pub fn new() -> Self {
172 Self::default()
173 }
174
175 pub fn with_known(mut self, predicate: impl Into<String>, arity: usize) -> Self {
177 self.known
178 .insert(EpistemicAtomKey::from_arity(predicate, arity));
179 self
180 }
181
182 pub fn with_known_terms(
184 mut self,
185 predicate: impl Into<String>,
186 terms: Vec<Term>,
187 ) -> Result<Self> {
188 self.known
189 .insert(EpistemicAtomKey::from_terms(predicate, &terms)?);
190 Ok(self)
191 }
192
193 pub fn with_possible(mut self, predicate: impl Into<String>, arity: usize) -> Self {
195 self.possible
196 .insert(EpistemicAtomKey::from_arity(predicate, arity));
197 self
198 }
199
200 pub fn with_possible_terms(
202 mut self,
203 predicate: impl Into<String>,
204 terms: Vec<Term>,
205 ) -> Result<Self> {
206 self.possible
207 .insert(EpistemicAtomKey::from_terms(predicate, &terms)?);
208 Ok(self)
209 }
210
211 pub fn with_rejected(mut self, predicate: impl Into<String>, arity: usize) -> Self {
213 self.rejected
214 .insert(EpistemicAtomKey::from_arity(predicate, arity));
215 self
216 }
217
218 pub fn with_rejected_terms(
220 mut self,
221 predicate: impl Into<String>,
222 terms: Vec<Term>,
223 ) -> Result<Self> {
224 self.rejected
225 .insert(EpistemicAtomKey::from_terms(predicate, &terms)?);
226 Ok(self)
227 }
228
229 fn first_contradiction(&self) -> Option<(String, usize)> {
230 self.known
231 .iter()
232 .find(|key| self.rejected.iter().any(|rejected| key.overlaps(rejected)))
233 .map(|key| (key.predicate().to_string(), key.arity()))
234 }
235
236 fn contains_known(&self, atom: &Atom) -> bool {
237 self.known.iter().any(|key| key.matches_atom(atom))
238 }
239
240 fn contains_possible(&self, atom: &Atom) -> bool {
241 self.possible.iter().any(|key| key.matches_atom(atom))
242 }
243
244 fn contains_rejected(&self, atom: &Atom) -> bool {
245 self.rejected.iter().any(|key| key.matches_atom(atom))
246 }
247
248 fn epistemic_guess_count(&self) -> usize {
249 self.known.len() + self.possible.len() + self.rejected.len()
250 }
251}
252
253#[derive(Debug, Clone, Default, PartialEq, Eq)]
255pub struct EpistemicWorld {
256 facts: BTreeSet<EpistemicAtomKey>,
257}
258
259impl EpistemicWorld {
260 pub fn new() -> Self {
262 Self::default()
263 }
264
265 pub fn with_fact(mut self, predicate: impl Into<String>, arity: usize) -> Self {
267 self.facts
268 .insert(EpistemicAtomKey::from_arity(predicate, arity));
269 self
270 }
271
272 pub fn with_fact_terms(
274 mut self,
275 predicate: impl Into<String>,
276 terms: Vec<Term>,
277 ) -> Result<Self> {
278 self.facts
279 .insert(EpistemicAtomKey::from_terms(predicate, &terms)?);
280 Ok(self)
281 }
282
283 fn contains(&self, atom: &Atom) -> bool {
284 self.facts.iter().any(|fact| fact.matches_atom(atom))
285 }
286}
287
288#[derive(Debug, Clone, PartialEq, Eq)]
290pub struct EpistemicWorldView {
291 worlds: Vec<EpistemicWorld>,
292}
293
294impl EpistemicWorldView {
295 pub fn from_worlds(worlds: Vec<EpistemicWorld>) -> Result<Self> {
297 if worlds.is_empty() {
298 return Err(xlog_core::XlogError::UnsupportedEpistemicConstruct {
299 construct: "world view boundary".to_string(),
300 context: "world view requires at least one stable model".to_string(),
301 });
302 }
303 Ok(Self { worlds })
304 }
305
306 pub fn world_count(&self) -> usize {
308 self.worlds.len()
309 }
310
311 pub fn evaluate(&self, lit: &EpistemicLiteral) -> TruthValue {
313 let value = match lit.op {
314 EpistemicOp::Know => self.worlds.iter().all(|world| world.contains(&lit.atom)),
315 EpistemicOp::Possible => self.worlds.iter().any(|world| world.contains(&lit.atom)),
316 };
317
318 TruthValue::from_bool(if lit.negated { !value } else { value })
319 }
320}
321
322pub fn plan_epistemic_gpu_execution(program: &Program) -> Result<EpistemicGpuPlan> {
329 let mut prepared = program.clone();
330 if prepared.authored_constraint_source_bound.is_some() {
331 prepared.validate_prepared_authored_constraint_identity()?;
332 } else {
333 prepared.prepare_authored_constraint_identity_at_root()?;
334 }
335 plan_prepared_epistemic_gpu_execution(&prepared)
336}
337
338fn plan_prepared_epistemic_gpu_execution(program: &Program) -> Result<EpistemicGpuPlan> {
339 program.validate_prepared_authored_constraint_identity()?;
340 reject_recursive_epistemic_program(program)?;
341 validate_epistemic_relation_shapes(program, &BTreeSet::new())?;
342 let eir = build_eir(program)?;
343 let mut epistemic_literals = Vec::new();
347 let mut reductions = Vec::new();
348 let mut tuple_membership_bindings = Vec::new();
349 let mut solver_assumption_bindings = Vec::new();
350
351 for (rule_index, rule) in eir.rules.iter().enumerate() {
352 let mut rule_epistemic_literals = Vec::new();
353 let mut positive_relational_atoms = Vec::new();
354 let mut has_negated_relational_atom = false;
355
356 for lit in &rule.body {
357 match lit {
358 EirBodyLiteral::Relational { negated, atom } => {
359 if *negated {
360 has_negated_relational_atom = true;
361 } else {
362 positive_relational_atoms.push(atom.clone());
363 }
364 }
365 EirBodyLiteral::Epistemic(lit) => {
366 rule_epistemic_literals.push(lit.clone());
367 }
368 EirBodyLiteral::Constraint | EirBodyLiteral::Binding => {}
369 }
370 }
371
372 if rule_epistemic_literals.is_empty() {
373 continue;
374 }
375
376 let reduction_index = reductions.len();
377 for lit in rule_epistemic_literals {
378 let lit = flatten_epistemic_literal(&lit)?;
386 let literal_index = epistemic_literals.len();
387 let augmented_head_terms = augmented_eir_head_terms(rule);
388 tuple_membership_bindings.push(EpistemicTupleMembershipBinding {
389 literal_index,
390 reduction_index,
391 predicate: lit.atom.predicate.clone(),
392 arity: lit.atom.arity,
393 key_columns: (0..lit.atom.arity).collect(),
394 bound_output_columns: bound_output_columns_for_terms(
395 &lit.atom.terms,
396 &augmented_head_terms,
397 ),
398 key_terms: lit.atom.terms.clone(),
399 op: lit.op,
400 negated: lit.negated,
401 });
402 solver_assumption_bindings.push(EpistemicSolverAssumptionBinding {
403 literal_index,
404 reduction_index,
405 predicate: lit.atom.predicate.clone(),
406 arity: lit.atom.arity,
407 terms: lit.atom.terms.clone(),
408 op: lit.op,
409 negated: lit.negated,
410 });
411 epistemic_literals.push(lit);
412 }
413 reductions.push(EpistemicReductionPlan {
414 rule_index,
415 head_predicate: rule.head.predicate.clone(),
416 public_head_arity: rule.head.terms.len(),
417 relational_body_atoms: positive_relational_atoms.len(),
418 wcoj_status: wcoj_status_for_reduction(
419 &positive_relational_atoms,
420 has_negated_relational_atom,
421 ),
422 });
423 }
424
425 if epistemic_literals.is_empty() {
426 return Err(xlog_core::XlogError::UnsupportedEpistemicConstruct {
427 construct: "epistemic GPU execution plan".to_string(),
428 context: "requires at least one epistemic literal".to_string(),
429 });
430 }
431
432 let constraints = lower_epistemic_constraints(
438 &eir,
439 &mut epistemic_literals,
440 &reductions,
441 &mut tuple_membership_bindings,
442 &mut solver_assumption_bindings,
443 )?;
444
445 let final_output_columns = final_output_columns_for_eir(&eir);
446 let gpu_plan = EpistemicGpuPlan::new(eir.mode, epistemic_literals, reductions)
447 .with_tuple_membership_bindings(tuple_membership_bindings)
448 .with_constraints(constraints)
449 .with_final_output_columns(final_output_columns)
450 .with_solver_contract(EpistemicSolverServiceContract::production_default(
451 solver_assumption_bindings,
452 ));
453 gpu_plan.validate_tuple_membership_bindings()?;
454 gpu_plan.validate_solver_contract()?;
455 gpu_plan.validate_constraints()?;
456 Ok(gpu_plan)
457}
458
459fn lower_epistemic_constraints(
475 eir: &EirProgram,
476 epistemic_literals: &mut Vec<EirEpistemicLiteral>,
477 reductions: &[EpistemicReductionPlan],
478 tuple_membership_bindings: &mut Vec<EpistemicTupleMembershipBinding>,
479 solver_assumption_bindings: &mut Vec<EpistemicSolverAssumptionBinding>,
480) -> Result<Vec<EpistemicConstraintPlan>> {
481 let mut constraint_plans = Vec::new();
482 for constraint in &eir.constraints {
483 let constraint_index = constraint.authored_index.ok_or_else(|| {
484 XlogError::Compilation(
485 "prepared constraint compilation requires authored identities".to_string(),
486 )
487 })?;
488 let has_epistemic = constraint
489 .body
490 .iter()
491 .any(|lit| matches!(lit, EirBodyLiteral::Epistemic(_)));
492 if !has_epistemic {
493 continue;
496 }
497
498 if reductions.is_empty() {
499 return Err(XlogError::UnsupportedEpistemicConstruct {
500 construct: "epistemic GPU world-view constraint".to_string(),
501 context: format!(
502 "constraint[{constraint_index}] is an epistemic integrity constraint but the \
503 program has no epistemic rule to host its world-view evaluation; add an \
504 epistemic rule whose reduced model provides the accepted world view, or \
505 express the constraint over an existing epistemic rule"
506 ),
507 });
508 }
509 let reduction_index = reductions.len() - 1;
513
514 let mut flattened_literals = Vec::new();
520 for lit in &constraint.body {
521 match lit {
522 EirBodyLiteral::Epistemic(lit) => {
523 flattened_literals.push(flatten_epistemic_literal(lit)?);
524 }
525 EirBodyLiteral::Relational { .. }
526 | EirBodyLiteral::Constraint
527 | EirBodyLiteral::Binding => {
528 return Err(XlogError::UnsupportedEpistemicConstruct {
529 construct: "epistemic GPU world-view constraint".to_string(),
530 context: format!(
531 "constraint[{constraint_index}] mixes non-epistemic body literals with \
532 modal literals; world-view integrity constraints currently support \
533 pure know/possible conjunctions so the constraint can be evaluated \
534 against accepted world views without an ordinary-RIR rewrite"
535 ),
536 });
537 }
538 }
539 }
540
541 let mut variable_occurrences: std::collections::BTreeMap<String, usize> =
558 std::collections::BTreeMap::new();
559 for lit in &flattened_literals {
560 for term in &lit.atom.terms {
561 if let EirTerm::Variable(name) = term {
562 *variable_occurrences.entry(name.clone()).or_insert(0) += 1;
563 }
564 }
565 }
566
567 let mut literal_indices = Vec::new();
568 for lit in flattened_literals {
569 let mut anonymized_terms = Vec::with_capacity(lit.atom.terms.len());
572 for term in &lit.atom.terms {
573 match term {
574 EirTerm::Integer(_) | EirTerm::Symbol(_) | EirTerm::Anonymous => {
575 anonymized_terms.push(term.clone());
576 }
577 EirTerm::Variable(name) => {
578 if variable_occurrences.get(name).copied().unwrap_or(0) > 1 {
579 return Err(XlogError::UnsupportedEpistemicConstruct {
580 construct: "epistemic GPU world-view constraint".to_string(),
581 context: format!(
582 "constraint[{constraint_index}] reuses tuple-key variable \
583 {name} across literals/positions; shared-variable epistemic \
584 constraint joins (`:- know p(X), q(X).` / diagonal \
585 `:- know p(X, X).`) are not yet implemented for GPU world-view \
586 pruning. Single-occurrence variable keys (`:- know p(X).`) are \
587 supported and range existentially over the modal relation"
588 ),
589 });
590 }
591 if lit.negated {
611 return Err(XlogError::Compilation(format!(
612 "v0.8.5 naf error: unbound variable {name} in negated modal atom \
613 {}/{} in constraint[{constraint_index}]; bind it before not with \
614 a positive atom, or use '_' for existential positions",
615 lit.atom.predicate, lit.atom.arity
616 )));
617 }
618 anonymized_terms.push(EirTerm::Anonymous);
623 }
624 other => {
625 return Err(XlogError::UnsupportedEpistemicConstruct {
626 construct: "epistemic GPU world-view constraint".to_string(),
627 context: format!(
628 "constraint[{constraint_index}] uses {} {}/{} with an unsupported \
629 tuple-key term {other:?}; headless world-view constraints support \
630 ground (integer/symbol) and single-occurrence variable/anonymous \
631 modal atoms",
632 eir_epistemic_literal_label(&lit),
633 lit.atom.predicate,
634 lit.atom.arity
635 ),
636 });
637 }
638 }
639 }
640 let mut lit = lit;
645 lit.atom.terms = anonymized_terms;
646
647 let literal_index = epistemic_literals.len();
648 let bound_output_columns = vec![None; lit.atom.arity];
649 tuple_membership_bindings.push(EpistemicTupleMembershipBinding {
650 literal_index,
651 reduction_index,
652 predicate: lit.atom.predicate.clone(),
653 arity: lit.atom.arity,
654 key_columns: (0..lit.atom.arity).collect(),
655 key_terms: lit.atom.terms.clone(),
656 bound_output_columns,
657 op: lit.op,
658 negated: lit.negated,
659 });
660 solver_assumption_bindings.push(EpistemicSolverAssumptionBinding {
661 literal_index,
662 reduction_index,
663 predicate: lit.atom.predicate.clone(),
664 arity: lit.atom.arity,
665 terms: lit.atom.terms.clone(),
666 op: lit.op,
667 negated: lit.negated,
668 });
669 epistemic_literals.push(lit);
670 literal_indices.push(literal_index);
671 }
672
673 constraint_plans.push(EpistemicConstraintPlan {
674 constraint_index,
675 literal_indices,
676 });
677 }
678 Ok(constraint_plans)
679}
680
681#[derive(Debug, Clone, PartialEq, Eq)]
694pub enum RecursiveEpistemicClass {
695 NonRecursive,
698 CaseA,
702 CaseB,
723 ModalCycle,
730}
731
732fn reject_recursive_epistemic_program(program: &Program) -> Result<()> {
742 match classify_recursive_epistemic_program(program) {
743 Ok(RecursiveEpistemicClass::NonRecursive) => Ok(()),
744 Ok(
745 RecursiveEpistemicClass::CaseA
746 | RecursiveEpistemicClass::CaseB
747 | RecursiveEpistemicClass::ModalCycle,
748 ) => Err(recursive_epistemic_rejection(
749 "an epistemic program contains an ordinary or modal dependency cycle; the \
750 single-pass epistemic GPU planner cannot iterate a fixpoint. Admissible \
751 recursive epistemic programs require recursive source preparation and an \
752 iterative execution plan, not this planner.",
753 )),
754 Err(err) => Err(err),
757 }
758}
759
760pub fn classify_recursive_epistemic_program(program: &Program) -> Result<RecursiveEpistemicClass> {
765 let has_epistemic = program.rules.iter().any(|rule| {
766 rule.body
767 .iter()
768 .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
769 });
770 if !has_epistemic {
771 return Ok(RecursiveEpistemicClass::NonRecursive);
773 }
774
775 let (ordinary_deps, deps) = epistemic_dependency_graphs(program);
781
782 let ordinary_recursive_predicates: BTreeSet<&str> = ordinary_deps
783 .keys()
784 .copied()
785 .filter(|pred| {
786 predicate_dependency_reaches(pred, pred, &ordinary_deps, &mut BTreeSet::new())
787 })
788 .collect();
789
790 let recursive_predicates: BTreeSet<&str> = deps
792 .keys()
793 .copied()
794 .filter(|pred| predicate_dependency_reaches(pred, pred, &deps, &mut BTreeSet::new()))
795 .collect();
796
797 if recursive_predicates.is_empty() {
798 return Ok(RecursiveEpistemicClass::NonRecursive);
799 }
800 let modal_only_recursion = ordinary_recursive_predicates.is_empty();
801
802 let invariant = InvariantRelations::analyze(program);
836 let mut saw_case_b = false;
837 let mut saw_negated_non_invariant_modal = false;
841 for rule in &program.rules {
842 for lit in &rule.body {
843 let BodyLiteral::Epistemic(modal) = lit else {
844 continue;
845 };
846 if invariant.is_invariant(&modal.atom.predicate) {
847 continue;
853 }
854
855 if modal.negated {
858 saw_negated_non_invariant_modal = true;
877 saw_case_b = true;
878 continue;
879 }
880
881 saw_case_b = true;
887 }
888 }
889
890 if saw_negated_non_invariant_modal {
895 let _reduced = reduce_case_a_epistemic_program_to_ordinary(program);
899 }
900
901 let has_epistemic_constraint = program.constraints.iter().any(|constraint| {
913 constraint
914 .body
915 .iter()
916 .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
917 });
918 if has_epistemic_constraint {
919 return Err(recursive_epistemic_rejection(
920 "a recursive epistemic program carries an epistemic integrity constraint \
921 (`:- know ...` / `:- not know ...`). Recursive reductions do not run the \
922 single-pass world-view constraint kernel and would otherwise drop the \
923 modal constraint, yielding a result that ignores it. To keep results sound \
924 this fails closed rather than silently dropping the constraint. \
925 Remove the recursion or express the integrity constraint over a \
926 non-recursive (single-pass) epistemic relation.",
927 ));
928 }
929
930 if modal_only_recursion {
931 debug_assert!(
932 saw_case_b,
933 "a modal-only cycle must have a co-evolving target"
934 );
935 Ok(RecursiveEpistemicClass::ModalCycle)
936 } else if saw_case_b {
937 Ok(RecursiveEpistemicClass::CaseB)
938 } else {
939 Ok(RecursiveEpistemicClass::CaseA)
940 }
941}
942
943type PredicateDependencyMap<'a> = BTreeMap<&'a str, BTreeSet<&'a str>>;
944
945fn epistemic_dependency_graphs(
946 program: &Program,
947) -> (PredicateDependencyMap<'_>, PredicateDependencyMap<'_>) {
948 let mut ordinary_dependencies = BTreeMap::new();
949 let mut all_dependencies = BTreeMap::new();
950 for rule in &program.rules {
951 let head = rule.head.predicate.as_str();
952 let all = all_dependencies.entry(head).or_insert_with(BTreeSet::new);
953 let ordinary = ordinary_dependencies
954 .entry(head)
955 .or_insert_with(BTreeSet::new);
956 for literal in &rule.body {
957 match literal {
958 BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) => {
959 all.insert(atom.predicate.as_str());
960 ordinary.insert(atom.predicate.as_str());
961 }
962 BodyLiteral::Epistemic(modal) => {
963 all.insert(modal.atom.predicate.as_str());
964 }
965 BodyLiteral::Comparison(_) | BodyLiteral::IsExpr(_) | BodyLiteral::Univ(_) => {}
966 }
967 }
968 }
969 (ordinary_dependencies, all_dependencies)
970}
971
972fn predicate_dependency_reaches<'a>(
973 start: &'a str,
974 target: &str,
975 dependencies: &BTreeMap<&'a str, BTreeSet<&'a str>>,
976 seen: &mut BTreeSet<&'a str>,
977) -> bool {
978 let Some(next) = dependencies.get(start) else {
979 return false;
980 };
981 for &predicate in next {
982 if predicate == target {
983 return true;
984 }
985 if seen.insert(predicate)
986 && predicate_dependency_reaches(predicate, target, dependencies, seen)
987 {
988 return true;
989 }
990 }
991 false
992}
993
994fn recursive_modal_dependency_edges(program: &Program) -> BTreeSet<(String, String)> {
998 let (_, dependencies) = epistemic_dependency_graphs(program);
999 let mut edges = BTreeSet::new();
1000 for rule in &program.rules {
1001 for literal in &rule.body {
1002 let BodyLiteral::Epistemic(modal) = literal else {
1003 continue;
1004 };
1005 if modal.atom.predicate == rule.head.predicate
1006 || predicate_dependency_reaches(
1007 modal.atom.predicate.as_str(),
1008 rule.head.predicate.as_str(),
1009 &dependencies,
1010 &mut BTreeSet::new(),
1011 )
1012 {
1013 edges.insert((rule.head.predicate.clone(), modal.atom.predicate.clone()));
1014 }
1015 }
1016 }
1017 edges
1018}
1019
1020fn recursive_epistemic_rejection(context: &str) -> XlogError {
1021 XlogError::UnsupportedEpistemicConstruct {
1022 construct: "recursive epistemic program".to_string(),
1023 context: context.to_string(),
1024 }
1025}
1026
1027struct InvariantRelations<'a> {
1036 ordinary_deps: BTreeMap<&'a str, BTreeSet<&'a str>>,
1038 epistemic_heads: BTreeSet<&'a str>,
1041 derived_heads: BTreeSet<&'a str>,
1043}
1044
1045impl<'a> InvariantRelations<'a> {
1046 fn analyze(program: &'a Program) -> Self {
1047 let mut ordinary_deps: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
1048 let mut epistemic_heads: BTreeSet<&str> = BTreeSet::new();
1049 let mut derived_heads: BTreeSet<&str> = BTreeSet::new();
1050 for rule in &program.rules {
1051 if rule.body.is_empty() {
1052 continue;
1053 }
1054 let head = rule.head.predicate.as_str();
1055 derived_heads.insert(head);
1056 let entry = ordinary_deps.entry(head).or_default();
1057 for lit in &rule.body {
1058 match lit {
1059 BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) => {
1060 entry.insert(atom.predicate.as_str());
1061 }
1062 BodyLiteral::Epistemic(_) => {
1063 epistemic_heads.insert(head);
1064 }
1065 BodyLiteral::Comparison(_) | BodyLiteral::IsExpr(_) | BodyLiteral::Univ(_) => {}
1066 }
1067 }
1068 }
1069 Self {
1070 ordinary_deps,
1071 epistemic_heads,
1072 derived_heads,
1073 }
1074 }
1075
1076 fn is_invariant(&self, predicate: &str) -> bool {
1078 let mut seen = BTreeSet::new();
1079 self.is_invariant_inner(predicate, &mut seen)
1080 }
1081
1082 fn is_invariant_inner<'b>(&'b self, predicate: &'b str, seen: &mut BTreeSet<&'b str>) -> bool {
1083 if !seen.insert(predicate) {
1084 return false;
1086 }
1087 let invariant = if !self.derived_heads.contains(predicate) {
1088 true
1090 } else if self.epistemic_heads.contains(predicate) {
1091 false
1093 } else {
1094 match self.ordinary_deps.get(predicate) {
1095 None => true,
1096 Some(deps) => deps.iter().all(|dep| self.is_invariant_inner(dep, seen)),
1097 }
1098 };
1099 seen.remove(predicate);
1103 invariant
1104 }
1105}
1106
1107fn eir_epistemic_literal_label(lit: &xlog_ir::EirEpistemicLiteral) -> &'static str {
1108 match (lit.negated, lit.op) {
1109 (false, EirEpistemicOp::Know) => "know",
1110 (false, EirEpistemicOp::Possible) => "possible",
1111 (true, EirEpistemicOp::Know) => "not know",
1112 (true, EirEpistemicOp::Possible) => "not possible",
1113 }
1114}
1115
1116fn has_independent_founded_support(eir: &EirProgram, atom: &xlog_ir::EirAtom) -> bool {
1117 if atom.arity > 0 && !atom.terms.iter().all(eir_term_is_ground) {
1118 return false;
1119 }
1120
1121 let mut support_stack = Vec::new();
1122 has_independent_founded_support_inner(eir, atom, &mut support_stack)
1123}
1124
1125fn has_unconditional_ground_founded_support(eir: &EirProgram, atom: &xlog_ir::EirAtom) -> bool {
1134 if !atom.terms.iter().all(eir_term_is_ground) {
1135 return false;
1136 }
1137
1138 let mut support_stack = Vec::new();
1139 has_unconditional_ground_founded_support_inner(eir, atom, &mut support_stack)
1140}
1141
1142fn has_unconditional_ground_founded_support_inner(
1143 eir: &EirProgram,
1144 atom: &xlog_ir::EirAtom,
1145 support_stack: &mut Vec<(String, Vec<EirTerm>)>,
1146) -> bool {
1147 let key = (atom.predicate.clone(), atom.terms.clone());
1148 if support_stack.iter().any(|ancestor| ancestor == &key) {
1149 return false;
1150 }
1151 support_stack.push(key);
1152
1153 let supported = eir.rules.iter().any(|rule| {
1154 let Some(substitution) = head_substitution_to_atom(&rule.head, atom) else {
1155 return false;
1156 };
1157 rule.body.iter().all(|literal| match literal {
1158 EirBodyLiteral::Relational {
1159 negated: false,
1160 atom,
1161 } => substitute_eir_atom(atom, &substitution).is_some_and(|atom| {
1162 atom.terms.iter().all(eir_term_is_ground)
1163 && has_unconditional_ground_founded_support_inner(eir, &atom, support_stack)
1164 }),
1165 EirBodyLiteral::Epistemic(_)
1166 | EirBodyLiteral::Relational { negated: true, .. }
1167 | EirBodyLiteral::Constraint
1168 | EirBodyLiteral::Binding => false,
1169 })
1170 });
1171
1172 support_stack.pop();
1173 supported
1174}
1175
1176fn has_tuple_level_independent_founded_support(
1177 eir: &EirProgram,
1178 modal_rule: &xlog_ir::EirRule,
1179 atom: &xlog_ir::EirAtom,
1180) -> bool {
1181 if atom.arity == 0 {
1182 return false;
1183 }
1184
1185 let modal_domain = positive_relational_body_atoms(modal_rule);
1186 eir.rules.iter().any(|support_rule| {
1187 if !support_rule_head_matches_modal_atom(support_rule, atom) {
1188 return false;
1189 }
1190 let mut support_stack = vec![(atom.predicate.clone(), atom.arity)];
1191 if !eir_rule_has_independent_founded_body(eir, support_rule, &mut support_stack) {
1192 return false;
1193 }
1194 let Some(substitution) = head_substitution_to_atom(&support_rule.head, atom) else {
1195 return false;
1196 };
1197 let support_domain = positive_relational_body_atoms(support_rule);
1198 if support_domain.is_empty() {
1199 return false;
1200 }
1201 let Some(substituted_support_domain) = support_domain
1202 .iter()
1203 .map(|atom| substitute_eir_atom(atom, &substitution))
1204 .collect::<Option<Vec<_>>>()
1205 else {
1206 return false;
1207 };
1208 substituted_support_domain.iter().all(|support_atom| {
1209 modal_domain
1210 .iter()
1211 .any(|modal_atom| modal_atom == support_atom)
1212 })
1213 })
1214}
1215
1216fn positive_relational_body_atoms(rule: &xlog_ir::EirRule) -> Vec<xlog_ir::EirAtom> {
1217 rule.body
1218 .iter()
1219 .filter_map(|lit| match lit {
1220 EirBodyLiteral::Relational {
1221 negated: false,
1222 atom,
1223 } => Some(atom.clone()),
1224 _ => None,
1225 })
1226 .collect()
1227}
1228
1229fn support_rule_head_matches_modal_atom(rule: &xlog_ir::EirRule, atom: &xlog_ir::EirAtom) -> bool {
1230 rule.head.predicate == atom.predicate
1231 && rule.head.arity == atom.arity
1232 && head_substitution_to_atom(&rule.head, atom).is_some()
1233}
1234
1235fn head_substitution_to_atom(
1236 head: &xlog_ir::EirAtom,
1237 atom: &xlog_ir::EirAtom,
1238) -> Option<BTreeMap<String, EirTerm>> {
1239 if head.predicate != atom.predicate || head.arity != atom.arity {
1240 return None;
1241 }
1242 let mut substitution = BTreeMap::new();
1243 for (head_term, atom_term) in head.terms.iter().zip(&atom.terms) {
1244 match head_term {
1245 EirTerm::Variable(name) => match substitution.get(name) {
1246 Some(existing) if existing != atom_term => return None,
1247 Some(_) => {}
1248 None => {
1249 substitution.insert(name.clone(), atom_term.clone());
1250 }
1251 },
1252 EirTerm::Anonymous => return None,
1253 other if other == atom_term => {}
1254 _ => return None,
1255 }
1256 }
1257 Some(substitution)
1258}
1259
1260fn substitute_eir_atom(
1261 atom: &xlog_ir::EirAtom,
1262 substitution: &BTreeMap<String, EirTerm>,
1263) -> Option<xlog_ir::EirAtom> {
1264 let terms = atom
1265 .terms
1266 .iter()
1267 .map(|term| substitute_eir_term(term, substitution))
1268 .collect::<Option<Vec<_>>>()?;
1269 Some(xlog_ir::EirAtom {
1270 predicate: atom.predicate.clone(),
1271 arity: atom.arity,
1272 terms,
1273 })
1274}
1275
1276fn substitute_eir_term(
1277 term: &EirTerm,
1278 substitution: &BTreeMap<String, EirTerm>,
1279) -> Option<EirTerm> {
1280 match term {
1281 EirTerm::Variable(name) => Some(
1282 substitution
1283 .get(name)
1284 .cloned()
1285 .unwrap_or_else(|| term.clone()),
1286 ),
1287 EirTerm::Anonymous => None,
1288 EirTerm::List(items) => items
1289 .iter()
1290 .map(|item| substitute_eir_term(item, substitution))
1291 .collect::<Option<Vec<_>>>()
1292 .map(EirTerm::List),
1293 EirTerm::Cons { head, tail } => Some(EirTerm::Cons {
1294 head: Box::new(substitute_eir_term(head, substitution)?),
1295 tail: Box::new(substitute_eir_term(tail, substitution)?),
1296 }),
1297 EirTerm::Compound { functor, args } => Some(EirTerm::Compound {
1298 functor: functor.clone(),
1299 args: args
1300 .iter()
1301 .map(|arg| substitute_eir_term(arg, substitution))
1302 .collect::<Option<Vec<_>>>()?,
1303 }),
1304 EirTerm::Aggregate { .. } => None,
1305 EirTerm::Integer(_)
1306 | EirTerm::FloatBits(_)
1307 | EirTerm::String(_)
1308 | EirTerm::Symbol(_)
1309 | EirTerm::PredRef(_) => Some(term.clone()),
1310 }
1311}
1312
1313fn has_independent_founded_support_inner(
1314 eir: &EirProgram,
1315 atom: &xlog_ir::EirAtom,
1316 support_stack: &mut Vec<(String, usize)>,
1317) -> bool {
1318 if atom.arity > 0 && !atom.terms.iter().all(eir_term_is_ground) {
1319 return false;
1320 }
1321
1322 let key = (atom.predicate.clone(), atom.arity);
1323 if support_stack.iter().any(|ancestor| ancestor == &key) {
1324 return false;
1325 }
1326 support_stack.push(key);
1327
1328 let supported = eir.rules.iter().any(|rule| {
1329 let Some(substitution) = head_substitution_to_atom(&rule.head, atom) else {
1330 return false;
1331 };
1332 eir_rule_has_independent_founded_body_with_substitution(
1333 eir,
1334 rule,
1335 &substitution,
1336 support_stack,
1337 )
1338 });
1339
1340 support_stack.pop();
1341 supported
1342}
1343
1344fn eir_rule_has_independent_founded_body(
1345 eir: &EirProgram,
1346 rule: &xlog_ir::EirRule,
1347 support_stack: &mut Vec<(String, usize)>,
1348) -> bool {
1349 eir_rule_has_independent_founded_body_with_substitution(
1350 eir,
1351 rule,
1352 &BTreeMap::new(),
1353 support_stack,
1354 )
1355}
1356
1357fn eir_rule_has_independent_founded_body_with_substitution(
1358 eir: &EirProgram,
1359 rule: &xlog_ir::EirRule,
1360 substitution: &BTreeMap<String, EirTerm>,
1361 support_stack: &mut Vec<(String, usize)>,
1362) -> bool {
1363 rule.body.iter().all(|lit| match lit {
1364 EirBodyLiteral::Epistemic(_) => false,
1365 EirBodyLiteral::Relational { negated: true, .. } => false,
1366 EirBodyLiteral::Relational {
1367 negated: false,
1368 atom,
1369 } => {
1370 let Some(atom) = substitute_eir_atom(atom, substitution) else {
1371 return false;
1372 };
1373 let dependency_key = (atom.predicate.clone(), atom.arity);
1374 if support_stack
1375 .iter()
1376 .any(|ancestor| ancestor == &dependency_key)
1377 {
1378 return false;
1379 }
1380 if !eir
1381 .rules
1382 .iter()
1383 .any(|rule| head_substitution_to_atom(&rule.head, &atom).is_some())
1384 {
1385 return true;
1386 }
1387 has_independent_founded_support_inner(eir, &atom, support_stack)
1388 }
1389 EirBodyLiteral::Constraint | EirBodyLiteral::Binding => false,
1396 })
1397}
1398
1399fn eir_term_is_ground(term: &EirTerm) -> bool {
1400 match term {
1401 EirTerm::Variable(_) | EirTerm::Anonymous | EirTerm::Aggregate { .. } => false,
1402 EirTerm::Integer(_) | EirTerm::FloatBits(_) | EirTerm::String(_) | EirTerm::Symbol(_) => {
1403 true
1404 }
1405 EirTerm::List(items) => items.iter().all(eir_term_is_ground),
1406 EirTerm::Cons { head, tail } => eir_term_is_ground(head) && eir_term_is_ground(tail),
1407 EirTerm::Compound { args, .. } => args.iter().all(eir_term_is_ground),
1408 EirTerm::PredRef(_) => true,
1409 }
1410}
1411
1412pub fn compile_epistemic_gpu_execution(program: &Program) -> Result<EpistemicExecutablePlan> {
1420 compile_epistemic_gpu_execution_with_stats_snapshot(program, None)
1421}
1422
1423pub fn compile_epistemic_gpu_execution_with_stats_snapshot(
1430 program: &Program,
1431 stats_snapshot: Option<&StatsSnapshot>,
1432) -> Result<EpistemicExecutablePlan> {
1433 let mut prepared = program.clone();
1434 if prepared.authored_constraint_source_bound.is_some() {
1435 prepared.validate_prepared_authored_constraint_identity()?;
1436 } else {
1437 prepared.prepare_authored_constraint_identity_at_root()?;
1438 }
1439 compile_epistemic_gpu_execution_inner(&prepared, stats_snapshot, false)
1440}
1441
1442fn compile_epistemic_gpu_execution_inner(
1452 program: &Program,
1453 stats_snapshot: Option<&StatsSnapshot>,
1454 allow_multiple_output_heads: bool,
1455) -> Result<EpistemicExecutablePlan> {
1456 program.validate_prepared_authored_constraint_identity()?;
1457 let gpu_plan = plan_prepared_epistemic_gpu_execution(program)?;
1458 if !allow_multiple_output_heads {
1459 require_single_epistemic_output_relation(&gpu_plan)?;
1460 }
1461 let reduced_program = reduce_epistemic_program_to_ordinary(program)?;
1470 let mut compiler = Compiler::new();
1471 let reduced_runtime_plan =
1472 compiler.compile_prepared_program_with_stats_snapshot(&reduced_program, stats_snapshot)?;
1473 let relation_ids = compiler
1474 .rel_ids()
1475 .iter()
1476 .map(|(name, rel)| (name.clone(), *rel))
1477 .collect();
1478
1479 Ok(EpistemicExecutablePlan {
1480 gpu_plan,
1481 relation_ids,
1482 reduced_runtime_plan,
1483 })
1484}
1485
1486#[derive(Debug, Clone)]
1489pub struct PreparedEpistemicProgram {
1490 active_program: Program,
1491 removed_unfounded_rule_count: usize,
1492}
1493
1494#[derive(Debug, Clone)]
1504pub struct G91CompatibilityReduction {
1505 upper_bound_program: Program,
1506 refinement_program: Program,
1507 snapshot_relations: BTreeMap<String, String>,
1508 convergence_predicates: Vec<String>,
1509}
1510
1511impl G91CompatibilityReduction {
1512 pub fn upper_bound_program(&self) -> &Program {
1515 &self.upper_bound_program
1516 }
1517
1518 pub fn refinement_program(&self) -> &Program {
1521 &self.refinement_program
1522 }
1523
1524 pub fn snapshot_relations(&self) -> &BTreeMap<String, String> {
1526 &self.snapshot_relations
1527 }
1528
1529 pub fn convergence_predicates(&self) -> &[String] {
1531 &self.convergence_predicates
1532 }
1533}
1534
1535impl PreparedEpistemicProgram {
1536 pub fn active_program(&self) -> &Program {
1538 &self.active_program
1539 }
1540
1541 pub fn removed_unfounded_rules(&self) -> bool {
1543 self.removed_unfounded_rule_count != 0
1544 }
1545}
1546
1547pub fn prepare_epistemic_program(program: &Program) -> Result<PreparedEpistemicProgram> {
1550 let prepared = prepare_root_authored_constraint_identity(program)?;
1551 validate_prepared_epistemic_source_program(&prepared)?;
1552 let removed_rule_indices = faeel_unfounded_exact_tuple_self_support_rule_indices(&prepared);
1553 Ok(PreparedEpistemicProgram {
1554 active_program: program_without_rule_indices(&prepared, &removed_rule_indices),
1555 removed_unfounded_rule_count: removed_rule_indices.len(),
1556 })
1557}
1558
1559#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1560struct G91CompatibilityLiteralLocation {
1561 rule_index: usize,
1562 literal_index: usize,
1563}
1564
1565pub fn try_prepare_g91_compatibility_reduction(
1568 prepared: &PreparedEpistemicProgram,
1569) -> Result<Option<G91CompatibilityReduction>> {
1570 let program = prepared.active_program();
1571 if program.directives.epistemic_mode_or_default() != EpistemicMode::G91 {
1572 return Ok(None);
1573 }
1574 if classify_recursive_epistemic_program(program)? == RecursiveEpistemicClass::NonRecursive {
1575 return Ok(None);
1576 }
1577
1578 validate_epistemic_derived_relation_identity(program, &BTreeSet::new())?;
1579 let recursive_modal_edges = recursive_modal_dependency_edges(program);
1580 let invariant = InvariantRelations::analyze(program);
1581 let mut locations = BTreeSet::new();
1582 let mut target_arities = BTreeMap::new();
1583 for (rule_index, rule) in program.rules.iter().enumerate() {
1584 for (literal_index, literal) in rule.body.iter().enumerate() {
1585 let BodyLiteral::Epistemic(modal) = literal else {
1586 continue;
1587 };
1588 if !is_g91_compatibility_literal(rule, modal, &invariant, &recursive_modal_edges) {
1589 continue;
1590 }
1591 locations.insert(G91CompatibilityLiteralLocation {
1592 rule_index,
1593 literal_index,
1594 });
1595 target_arities
1596 .entry(modal.atom.predicate.clone())
1597 .and_modify(|arity| {
1598 debug_assert_eq!(*arity, modal.atom.arity());
1599 })
1600 .or_insert(modal.atom.arity());
1601 }
1602 }
1603 if locations.is_empty() {
1604 return Ok(None);
1605 }
1606
1607 reject_nonmonotone_g91_compatibility_components(program, &locations)?;
1608 let snapshot_relations = g91_snapshot_relation_names(program, target_arities.keys());
1609 let upper_bound_program = transform_g91_compatibility_program(
1610 program,
1611 &locations,
1612 G91CompatibilityTransform::UpperBound,
1613 );
1614 let mut refinement_program = transform_g91_compatibility_program(
1615 program,
1616 &locations,
1617 G91CompatibilityTransform::Snapshot(&snapshot_relations),
1618 );
1619 add_declared_g91_snapshot_relations(
1620 &mut refinement_program,
1621 program,
1622 &snapshot_relations,
1623 &target_arities,
1624 );
1625
1626 let convergence_predicates = program
1627 .proper_rules()
1628 .map(|rule| rule.head.predicate.clone())
1629 .collect::<BTreeSet<_>>()
1630 .into_iter()
1631 .collect();
1632 Ok(Some(G91CompatibilityReduction {
1633 upper_bound_program,
1634 refinement_program,
1635 snapshot_relations,
1636 convergence_predicates,
1637 }))
1638}
1639
1640fn is_g91_compatibility_literal(
1641 rule: &crate::ast::Rule,
1642 modal: &EpistemicLiteral,
1643 invariant: &InvariantRelations<'_>,
1644 recursive_modal_edges: &BTreeSet<(String, String)>,
1645) -> bool {
1646 modal.op == EpistemicOp::Possible
1647 && !modal.negated
1648 && !invariant.is_invariant(&modal.atom.predicate)
1649 && recursive_modal_edges
1650 .contains(&(rule.head.predicate.clone(), modal.atom.predicate.clone()))
1651 && modal.atom.terms == rule.head.terms
1652}
1653
1654enum G91CompatibilityTransform<'a> {
1655 UpperBound,
1656 Snapshot(&'a BTreeMap<String, String>),
1657}
1658
1659fn transform_g91_compatibility_program(
1660 program: &Program,
1661 locations: &BTreeSet<G91CompatibilityLiteralLocation>,
1662 transform: G91CompatibilityTransform<'_>,
1663) -> Program {
1664 let mut reduced = program.clone();
1665 for (rule_index, rule) in reduced.rules.iter_mut().enumerate() {
1666 for (literal_index, literal) in rule.body.iter_mut().enumerate() {
1667 let BodyLiteral::Epistemic(modal) = literal else {
1668 continue;
1669 };
1670 if locations.contains(&G91CompatibilityLiteralLocation {
1671 rule_index,
1672 literal_index,
1673 }) {
1674 *literal = match &transform {
1675 G91CompatibilityTransform::UpperBound => BodyLiteral::Comparison(Comparison {
1676 left: Term::Integer(1),
1677 op: CompOp::Eq,
1678 right: Term::Integer(1),
1679 }),
1680 G91CompatibilityTransform::Snapshot(snapshot_relations) => {
1681 let mut atom = modal.atom.clone();
1682 atom.predicate = snapshot_relations
1683 .get(&atom.predicate)
1684 .expect("selected compatibility target has a snapshot name")
1685 .clone();
1686 BodyLiteral::Positive(atom)
1687 }
1688 };
1689 } else {
1690 *literal = if modal.negated {
1691 BodyLiteral::Negated(modal.atom.clone())
1692 } else {
1693 BodyLiteral::Positive(modal.atom.clone())
1694 };
1695 }
1696 }
1697 }
1698 reduced.constraints.retain(|constraint| {
1699 !constraint
1700 .body
1701 .iter()
1702 .any(|literal| matches!(literal, BodyLiteral::Epistemic(_)))
1703 });
1704 qualify_extensional_multi_arity_predicates(&mut reduced, program, &BTreeSet::new());
1705 reduced
1706}
1707
1708fn g91_snapshot_relation_names<'a>(
1709 program: &Program,
1710 targets: impl Iterator<Item = &'a String>,
1711) -> BTreeMap<String, String> {
1712 let mut reserved = collect_epistemic_relation_identities(program, &BTreeSet::new())
1713 .0
1714 .into_keys()
1715 .collect::<BTreeSet<_>>();
1716 let mut names = BTreeMap::new();
1717 for target in targets {
1718 let stem = target
1719 .chars()
1720 .map(|character| {
1721 if character.is_ascii_alphanumeric() || character == '_' {
1722 character
1723 } else {
1724 '_'
1725 }
1726 })
1727 .collect::<String>();
1728 let base = format!("__xlog_g91_snapshot_{stem}");
1729 let mut candidate = base.clone();
1730 let mut suffix = 0usize;
1731 while reserved.contains(&candidate) {
1732 candidate = format!("{base}_{suffix}");
1733 suffix += 1;
1734 }
1735 reserved.insert(candidate.clone());
1736 names.insert(target.clone(), candidate);
1737 }
1738 names
1739}
1740
1741fn add_declared_g91_snapshot_relations(
1742 refinement: &mut Program,
1743 source: &Program,
1744 snapshots: &BTreeMap<String, String>,
1745 target_arities: &BTreeMap<String, usize>,
1746) {
1747 for (target, snapshot) in snapshots {
1748 let expected_arity = target_arities
1749 .get(target)
1750 .expect("snapshot target has an authored arity");
1751 if let Some(declaration) = source.predicates.iter().find(|declaration| {
1752 declaration.name == *target && declaration.arity() == *expected_arity
1753 }) {
1754 let mut declaration = declaration.clone();
1755 declaration.name = snapshot.clone();
1756 declaration.is_private = false;
1757 refinement.predicates.push(declaration);
1758 }
1759 }
1760}
1761
1762fn reject_nonmonotone_g91_compatibility_components(
1763 program: &Program,
1764 locations: &BTreeSet<G91CompatibilityLiteralLocation>,
1765) -> Result<()> {
1766 let (_, dependencies) = epistemic_dependency_graphs(program);
1767 let selected_heads = locations
1768 .iter()
1769 .map(|location| program.rules[location.rule_index].head.predicate.as_str())
1770 .collect::<BTreeSet<_>>();
1771 for rule in &program.rules {
1772 let in_selected_component = selected_heads.iter().any(|selected| {
1773 rule.head.predicate == **selected
1774 || (predicate_dependency_reaches(
1775 selected,
1776 &rule.head.predicate,
1777 &dependencies,
1778 &mut BTreeSet::new(),
1779 ) && predicate_dependency_reaches(
1780 &rule.head.predicate,
1781 selected,
1782 &dependencies,
1783 &mut BTreeSet::new(),
1784 ))
1785 });
1786 if !in_selected_component {
1787 continue;
1788 }
1789 if rule.has_aggregation() {
1790 return Err(XlogError::UnsupportedEpistemicConstruct {
1791 construct: "Gelfond-1991 compatibility cycle through aggregation".to_string(),
1792 context: format!(
1793 "aggregate predicate `{}` belongs to a positive `possible` compatibility \
1794 component; the tuple-level greatest fixpoint requires every dependency in \
1795 that component to be monotone",
1796 rule.head.predicate
1797 ),
1798 });
1799 }
1800 if rule
1801 .body
1802 .iter()
1803 .filter_map(|literal| match literal {
1804 BodyLiteral::Negated(atom) => Some(atom),
1805 BodyLiteral::Epistemic(modal) if modal.negated => Some(&modal.atom),
1806 BodyLiteral::Positive(_)
1807 | BodyLiteral::Epistemic(_)
1808 | BodyLiteral::Comparison(_)
1809 | BodyLiteral::IsExpr(_)
1810 | BodyLiteral::Univ(_) => None,
1811 })
1812 .any(|atom| {
1813 predicate_dependency_reaches(
1814 &atom.predicate,
1815 &rule.head.predicate,
1816 &dependencies,
1817 &mut BTreeSet::new(),
1818 )
1819 })
1820 {
1821 return Err(XlogError::UnsupportedEpistemicConstruct {
1822 construct: "Gelfond-1991 compatibility cycle through negation".to_string(),
1823 context: format!(
1824 "predicate `{}` belongs to a positive `possible` compatibility component \
1825 that also has a recursive negated dependency; the tuple-level greatest \
1826 fixpoint requires a monotone component",
1827 rule.head.predicate
1828 ),
1829 });
1830 }
1831 }
1832 Ok(())
1833}
1834
1835pub fn try_reduce_prepared_recursive_epistemic_program(
1837 prepared: &PreparedEpistemicProgram,
1838) -> Result<Option<Program>> {
1839 if try_prepare_g91_compatibility_reduction(prepared)?.is_some() {
1840 return Err(XlogError::UnsupportedEpistemicConstruct {
1841 construct: "Gelfond-1991 tuple compatibility ordinary reduction".to_string(),
1842 context: "positive `possible` compatibility cycles require the explicit upper-bound \
1843 and frozen-snapshot greatest-fixpoint plan returned by \
1844 `try_prepare_g91_compatibility_reduction`; they cannot be represented by \
1845 one ordinary least-fixpoint program"
1846 .to_string(),
1847 });
1848 }
1849 let active_program = prepared.active_program();
1850 let recursive_class = classify_recursive_epistemic_program(active_program)?;
1851 if recursive_class == RecursiveEpistemicClass::NonRecursive
1852 && !prepared.removed_unfounded_rules()
1853 {
1854 return Ok(None);
1855 }
1856
1857 validate_epistemic_derived_relation_identity(active_program, &BTreeSet::new())?;
1858 match recursive_class {
1859 RecursiveEpistemicClass::NonRecursive => Ok(Some(
1860 reduce_founded_epistemic_program_to_ordinary(active_program),
1861 )),
1862 RecursiveEpistemicClass::CaseA
1868 | RecursiveEpistemicClass::CaseB
1869 | RecursiveEpistemicClass::ModalCycle => Ok(Some(
1870 reduce_case_a_epistemic_program_to_ordinary(active_program),
1871 )),
1872 }
1873}
1874
1875pub fn try_reduce_case_a_recursive_epistemic_program(program: &Program) -> Result<Option<Program>> {
1891 let prepared = prepare_epistemic_program(program)?;
1892 try_reduce_prepared_recursive_epistemic_program(&prepared)
1893}
1894
1895fn require_single_epistemic_output_relation(gpu_plan: &EpistemicGpuPlan) -> Result<()> {
1896 let output_relations: BTreeSet<&str> = gpu_plan
1897 .reductions
1898 .iter()
1899 .map(|reduction| reduction.head_predicate.as_str())
1900 .collect();
1901 if output_relations.len() > 1 {
1902 return Err(XlogError::UnsupportedEpistemicConstruct {
1903 construct: "epistemic GPU final output relation".to_string(),
1904 context: format!(
1905 "single-plan GPU execution materializes one final output buffer, but reductions \
1906 target multiple head predicates {:?}; use split GPU execution for independent \
1907 epistemic outputs",
1908 output_relations
1909 ),
1910 });
1911 }
1912 Ok(())
1913}
1914
1915fn reject_epistemic_constraints(program: &Program) -> Result<()> {
1916 reject_epistemic_constraints_for_boundary(program, "epistemic GPU constraint", "GPU lowering")
1917}
1918
1919fn reject_gpt_epistemic_constraints(program: &Program) -> Result<()> {
1920 reject_epistemic_constraints_for_boundary(
1921 program,
1922 "epistemic GPT constraint",
1923 "GPT candidate testing",
1924 )
1925}
1926
1927fn reject_epistemic_constraints_for_boundary(
1928 program: &Program,
1929 construct: &str,
1930 boundary: &str,
1931) -> Result<()> {
1932 for constraint in &program.constraints {
1933 let constraint_index = constraint.require_authored_index()?;
1934 for lit in &constraint.body {
1935 let BodyLiteral::Epistemic(lit) = lit else {
1936 continue;
1937 };
1938 return Err(XlogError::UnsupportedEpistemicConstruct {
1939 construct: construct.to_string(),
1940 context: format!(
1941 "constraint[{constraint_index}] contains unsupported {} {}/{}; epistemic integrity constraints must be represented explicitly before {boundary}",
1942 epistemic_literal_label(lit),
1943 lit.atom.predicate,
1944 lit.atom.arity()
1945 ),
1946 });
1947 }
1948 }
1949 Ok(())
1950}
1951
1952fn epistemic_literal_label(lit: &EpistemicLiteral) -> &'static str {
1953 match (lit.negated, lit.op) {
1954 (false, EpistemicOp::Know) => "know",
1955 (false, EpistemicOp::Possible) => "possible",
1956 (true, EpistemicOp::Know) => "not know",
1957 (true, EpistemicOp::Possible) => "not possible",
1958 }
1959}
1960
1961fn flatten_epistemic_literal(lit: &EirEpistemicLiteral) -> Result<EirEpistemicLiteral> {
1971 let (arity, terms, _key_columns) =
1972 flatten_structured_key_terms(&lit.atom.predicate, &lit.atom.terms)?;
1973 Ok(EirEpistemicLiteral {
1974 op: lit.op,
1975 negated: lit.negated,
1976 atom: xlog_ir::EirAtom {
1977 predicate: lit.atom.predicate.clone(),
1978 arity,
1979 terms,
1980 },
1981 })
1982}
1983
1984fn eir_term_is_scalar_key_element(term: &EirTerm) -> bool {
1990 matches!(
1991 term,
1992 EirTerm::Variable(_)
1993 | EirTerm::Anonymous
1994 | EirTerm::Integer(_)
1995 | EirTerm::FloatBits(_)
1996 | EirTerm::String(_)
1997 | EirTerm::Symbol(_)
1998 )
1999}
2000
2001fn flatten_structured_key_terms(
2015 predicate: &str,
2016 terms: &[EirTerm],
2017) -> Result<(usize, Vec<EirTerm>, Vec<usize>)> {
2018 let mut flattened: Vec<EirTerm> = Vec::with_capacity(terms.len());
2019 for term in terms {
2020 match term {
2021 EirTerm::List(items) => {
2022 flatten_structured_elements(predicate, "list", items, &mut flattened)?;
2023 }
2024 EirTerm::Compound { functor, args } => {
2025 flatten_structured_elements(
2026 predicate,
2027 &format!("compound {functor}/{}", args.len()),
2028 args,
2029 &mut flattened,
2030 )?;
2031 }
2032 EirTerm::Cons { .. } => {
2033 return Err(XlogError::ResourceExhausted {
2034 context: format!(
2035 "modal tuple-key for {predicate} uses a `cons` pattern whose tail length \
2036 is not statically fixed, so it has no finite, typed GPU key-column set; \
2037 bind it to a fixed-arity list literal `[a, b, ...]` instead"
2038 ),
2039 estimated_bytes: 0,
2040 budget_bytes: 0,
2041 });
2042 }
2043 EirTerm::PredRef(name) => {
2044 return Err(XlogError::ResourceExhausted {
2045 context: format!(
2046 "modal tuple-key for {predicate} uses predref `{name}`, which has no \
2047 finite, typed GPU key-column encoding"
2048 ),
2049 estimated_bytes: 0,
2050 budget_bytes: 0,
2051 });
2052 }
2053 EirTerm::Aggregate { op, variable } => {
2054 return Err(XlogError::ResourceExhausted {
2055 context: format!(
2056 "modal tuple-key for {predicate} uses aggregate `{op}({variable})`, whose \
2057 value is not a finite, typed GPU key-column tuple"
2058 ),
2059 estimated_bytes: 0,
2060 budget_bytes: 0,
2061 });
2062 }
2063 scalar => flattened.push(scalar.clone()),
2064 }
2065 }
2066
2067 let arity = flattened.len();
2068 let key_columns = (0..arity).collect();
2069 Ok((arity, flattened, key_columns))
2070}
2071
2072fn flatten_structured_elements(
2078 predicate: &str,
2079 shape: &str,
2080 elements: &[EirTerm],
2081 flattened: &mut Vec<EirTerm>,
2082) -> Result<()> {
2083 for element in elements {
2084 if eir_term_is_scalar_key_element(element) {
2085 flattened.push(element.clone());
2086 } else {
2087 return Err(XlogError::ResourceExhausted {
2088 context: format!(
2089 "modal tuple-key for {predicate} nests a non-scalar element {element:?} inside \
2090 a {shape}; only fixed-arity structures of scalar/Symbol-typed elements have a \
2091 finite, typed GPU key-column encoding"
2092 ),
2093 estimated_bytes: 0,
2094 budget_bytes: 0,
2095 });
2096 }
2097 }
2098 Ok(())
2099}
2100
2101fn bound_output_columns_for_terms(
2102 key_terms: &[EirTerm],
2103 output_terms: &[EirTerm],
2104) -> Vec<Option<usize>> {
2105 key_terms
2106 .iter()
2107 .map(|term| match term {
2108 EirTerm::Variable(variable) => output_terms.iter().position(
2109 |head_term| matches!(head_term, EirTerm::Variable(name) if name == variable),
2110 ),
2111 _ => None,
2112 })
2113 .collect()
2114}
2115
2116fn augmented_eir_head_terms(rule: &xlog_ir::EirRule) -> Vec<EirTerm> {
2117 let mut output_terms = rule.head.terms.clone();
2118 for lit in &rule.body {
2119 let EirBodyLiteral::Epistemic(lit) = lit else {
2120 continue;
2121 };
2122 let key_terms = flatten_structured_key_terms(&lit.atom.predicate, &lit.atom.terms)
2128 .map(|(_, terms, _)| terms)
2129 .unwrap_or_else(|_| lit.atom.terms.clone());
2130 for term in &key_terms {
2131 let EirTerm::Variable(variable) = term else {
2132 continue;
2133 };
2134 if !output_terms
2135 .iter()
2136 .any(|head_term| matches!(head_term, EirTerm::Variable(name) if name == variable))
2137 {
2138 output_terms.push(EirTerm::Variable(variable.clone()));
2139 }
2140 }
2141 }
2142 output_terms
2143}
2144
2145fn final_output_columns_for_eir(eir: &EirProgram) -> Option<Vec<usize>> {
2146 let mut final_columns = Vec::new();
2147 let mut needs_projection = false;
2148 for rule in &eir.rules {
2149 if !rule
2150 .body
2151 .iter()
2152 .any(|lit| matches!(lit, EirBodyLiteral::Epistemic(_)))
2153 {
2154 continue;
2155 }
2156 let augmented_len = augmented_eir_head_terms(rule).len();
2157 if augmented_len > rule.head.terms.len() {
2158 needs_projection = true;
2159 }
2160 if final_columns.is_empty() {
2161 final_columns = (0..rule.head.terms.len()).collect();
2162 }
2163 }
2164 if needs_projection {
2165 Some(final_columns)
2166 } else {
2167 None
2168 }
2169}
2170
2171fn faeel_unfounded_exact_tuple_self_support_rule_indices(program: &Program) -> Vec<usize> {
2189 let Ok(eir) = build_eir(program) else {
2190 return Vec::new();
2191 };
2192 if eir.mode != EirEpistemicMode::Faeel {
2193 return Vec::new();
2194 }
2195 let mut indices = Vec::new();
2196 for (index, (rule, eir_rule)) in program.rules.iter().zip(&eir.rules).enumerate() {
2197 let modal_only_output_variables = modal_only_bound_output_variables(rule);
2198 let drop = eir_rule.body.iter().any(|lit| {
2199 let EirBodyLiteral::Epistemic(modal) = lit else {
2200 return false;
2201 };
2202 if modal.negated
2203 || modal.atom.predicate != eir_rule.head.predicate
2204 || modal.atom.arity != eir_rule.head.arity
2205 || modal.atom.terms != eir_rule.head.terms
2206 {
2207 return false;
2208 }
2209 if has_independent_founded_support(&eir, &modal.atom)
2212 || has_tuple_level_independent_founded_support(&eir, eir_rule, &modal.atom)
2213 {
2214 return false;
2215 }
2216 if modal
2220 .atom
2221 .terms
2222 .iter()
2223 .any(|term| matches!(term, EirTerm::Variable(name) if modal_only_output_variables.contains(name)))
2224 {
2225 return false;
2226 }
2227 true
2228 });
2229 if drop {
2230 indices.push(index);
2231 }
2232 }
2233 indices
2234}
2235
2236fn program_without_rule_indices(program: &Program, removed_rule_indices: &[usize]) -> Program {
2237 if removed_rule_indices.is_empty() {
2238 return program.clone();
2239 }
2240
2241 let removed_rule_indices = removed_rule_indices
2242 .iter()
2243 .copied()
2244 .collect::<BTreeSet<_>>();
2245 let mut filtered = program.clone();
2246 filtered.rules = program
2247 .rules
2248 .iter()
2249 .enumerate()
2250 .filter(|(index, _)| !removed_rule_indices.contains(index))
2251 .map(|(_, rule)| rule.clone())
2252 .collect();
2253 filtered
2254}
2255
2256pub fn validate_epistemic_source_program(program: &Program) -> Result<()> {
2266 let prepared = prepare_root_authored_constraint_identity(program)?;
2267 validate_prepared_epistemic_source_program(&prepared)
2268}
2269
2270fn validate_prepared_epistemic_source_program(program: &Program) -> Result<()> {
2271 validate_authored_modal_key_shapes(program)?;
2272 let invariant = InvariantRelations::analyze(program);
2273 let determined = EpistemicallyDeterminedPredicates::analyze(program);
2274 for rule in &program.rules {
2275 validate_modal_variable_bindings(&rule.body, &invariant, &determined)?;
2276 }
2277 for constraint in &program.constraints {
2278 validate_modal_variable_bindings(&constraint.body, &invariant, &determined)?;
2279 }
2280
2281 let mut validation = program.clone();
2282 for rule in &mut validation.rules {
2283 rewrite_modal_literals_for_source_validation(&mut rule.body, &invariant, &determined);
2284 }
2285 for constraint in &mut validation.constraints {
2286 rewrite_modal_literals_for_source_validation(&mut constraint.body, &invariant, &determined);
2287 }
2288
2289 let multi_arity_predicates = all_multi_arity_predicates(program);
2290 qualify_predicate_signatures(&mut validation, &multi_arity_predicates);
2291 Compiler::new().validate_program_without_stratification(&validation)
2292}
2293
2294fn validate_authored_modal_key_shapes(program: &Program) -> Result<()> {
2304 let target_arities = non_modal_relation_arities(program);
2305 let eir = build_eir(program)?;
2306 for modal in eir
2307 .rules
2308 .iter()
2309 .flat_map(|rule| &rule.body)
2310 .chain(
2311 eir.constraints
2312 .iter()
2313 .flat_map(|constraint| &constraint.body),
2314 )
2315 .filter_map(|literal| match literal {
2316 EirBodyLiteral::Epistemic(modal) => Some(modal),
2317 EirBodyLiteral::Relational { .. }
2318 | EirBodyLiteral::Constraint
2319 | EirBodyLiteral::Binding => None,
2320 })
2321 {
2322 let flattened = flatten_epistemic_literal(modal)?;
2323 let Some(expected) = target_arities.get(&flattened.atom.predicate) else {
2324 continue;
2327 };
2328 if expected.contains(&flattened.atom.arity) {
2329 continue;
2330 }
2331
2332 let expected_description = if expected.len() == 1 {
2333 format!(
2334 "target arity {}",
2335 expected.first().expect("one target arity")
2336 )
2337 } else {
2338 format!("target arities {expected:?}")
2339 };
2340 return Err(XlogError::UnsupportedEpistemicConstruct {
2341 construct: "epistemic modal tuple key".to_string(),
2342 context: format!(
2343 "modal target `{}` has {expected_description}, but its tuple key flattens to \
2344 binding arity {}; use one scalar key term per target column",
2345 flattened.atom.predicate, flattened.atom.arity
2346 ),
2347 });
2348 }
2349 Ok(())
2350}
2351
2352fn non_modal_relation_arities(program: &Program) -> BTreeMap<String, BTreeSet<usize>> {
2353 let mut arities = BTreeMap::new();
2354 for declaration in &program.predicates {
2355 arities
2356 .entry(declaration.name.clone())
2357 .or_insert_with(BTreeSet::new)
2358 .insert(declaration.arity());
2359 }
2360 for rule in &program.rules {
2361 record_predicate_signature(&mut arities, &rule.head);
2362 record_non_modal_body_signatures(&mut arities, &rule.body);
2363 }
2364 for constraint in &program.constraints {
2365 record_non_modal_body_signatures(&mut arities, &constraint.body);
2366 }
2367 for query in &program.queries {
2368 record_predicate_signature(&mut arities, &query.atom);
2369 }
2370 for fact in &program.prob_facts {
2371 record_predicate_signature(&mut arities, &fact.atom);
2372 }
2373 for disjunction in &program.annotated_disjunctions {
2374 for choice in &disjunction.choices {
2375 record_predicate_signature(&mut arities, &choice.atom);
2376 }
2377 }
2378 for evidence in &program.evidence {
2379 record_predicate_signature(&mut arities, &evidence.atom);
2380 }
2381 for query in &program.prob_queries {
2382 record_predicate_signature(&mut arities, &query.atom);
2383 }
2384 for declaration in &program.neural_predicates {
2385 record_predicate_signature(&mut arities, &declaration.predicate);
2386 }
2387 for rule in &program.learnable_rules {
2388 record_predicate_signature(&mut arities, &rule.head);
2389 record_non_modal_body_signatures(&mut arities, &rule.body);
2390 }
2391 arities
2392}
2393
2394fn record_non_modal_body_signatures(
2395 signatures: &mut BTreeMap<String, BTreeSet<usize>>,
2396 body: &[BodyLiteral],
2397) {
2398 for literal in body {
2399 if let BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) = literal {
2400 record_predicate_signature(signatures, atom);
2401 }
2402 }
2403}
2404
2405fn rewrite_modal_literals_for_source_validation(
2411 body: &mut Vec<BodyLiteral>,
2412 invariant: &InvariantRelations<'_>,
2413 determined: &EpistemicallyDeterminedPredicates,
2414) {
2415 let mut non_binding_modals = Vec::new();
2416 body.retain_mut(|literal| {
2417 let BodyLiteral::Epistemic(modal) = literal else {
2418 return true;
2419 };
2420 if modal.atom.terms.iter().any(|term| {
2421 !matches!(
2422 term,
2423 Term::Variable(_)
2424 | Term::Anonymous
2425 | Term::Integer(_)
2426 | Term::Float(_)
2427 | Term::String(_)
2428 | Term::Symbol(_)
2429 )
2430 }) {
2431 *literal = BodyLiteral::Comparison(Comparison {
2435 left: Term::Integer(1),
2436 op: CompOp::Eq,
2437 right: Term::Integer(1),
2438 });
2439 return true;
2440 }
2441 if !modal.negated
2442 && (invariant.is_invariant(&modal.atom.predicate)
2443 || determined.contains(&modal.atom.predicate))
2444 {
2445 *literal = BodyLiteral::Positive(modal.atom.clone());
2446 true
2447 } else {
2448 non_binding_modals.push(BodyLiteral::Negated(modal.atom.clone()));
2449 false
2450 }
2451 });
2452 body.extend(non_binding_modals);
2453}
2454
2455fn non_epistemic_bound_variables(body: &[BodyLiteral]) -> BTreeSet<String> {
2460 let mut bound = BTreeSet::new();
2461 for literal in body {
2462 let BodyLiteral::Positive(atom) = literal else {
2463 continue;
2464 };
2465 bound.extend(
2466 atom.variables()
2467 .into_iter()
2468 .filter(|name| *name != "_")
2469 .map(str::to_string),
2470 );
2471 }
2472
2473 for literal in body {
2474 let BodyLiteral::IsExpr(binding) = literal else {
2475 continue;
2476 };
2477 if binding
2478 .expr
2479 .variables()
2480 .iter()
2481 .all(|name| bound.contains(*name))
2482 {
2483 bound.insert(binding.target.clone());
2484 }
2485 }
2486
2487 bound
2488}
2489
2490fn validate_modal_variable_bindings(
2495 body: &[BodyLiteral],
2496 invariant: &InvariantRelations<'_>,
2497 determined: &EpistemicallyDeterminedPredicates,
2498) -> Result<()> {
2499 let mut bound = non_epistemic_bound_variables(body);
2500
2501 for literal in body {
2506 let BodyLiteral::Epistemic(modal) = literal else {
2507 continue;
2508 };
2509 let may_bind = !modal.negated
2510 && (invariant.is_invariant(&modal.atom.predicate)
2511 || determined.contains(&modal.atom.predicate));
2512 if may_bind {
2513 bound.extend(
2514 modal
2515 .atom
2516 .variables()
2517 .into_iter()
2518 .filter(|name| *name != "_")
2519 .map(str::to_string),
2520 );
2521 }
2522 }
2523
2524 for literal in body {
2525 let BodyLiteral::Epistemic(modal) = literal else {
2526 continue;
2527 };
2528 let may_bind = !modal.negated
2529 && (invariant.is_invariant(&modal.atom.predicate)
2530 || determined.contains(&modal.atom.predicate));
2531 for variable in modal.atom.variables() {
2532 if variable == "_" {
2533 continue;
2534 }
2535 if !may_bind && !bound.contains(variable) {
2536 return Err(XlogError::UnsafeVariable(variable.to_string()));
2537 }
2538 }
2539 }
2540 Ok(())
2541}
2542
2543pub fn reduce_epistemic_program_to_ordinary(program: &Program) -> Result<Program> {
2560 let prepared = prepare_root_authored_constraint_identity(program)?;
2561 if let Some(reduced) = try_reduce_case_a_recursive_epistemic_program(&prepared)? {
2562 return Ok(reduced);
2563 }
2564 reduce_epistemic_program_to_ordinary_inner(&prepared, &BTreeSet::new(), &BTreeMap::new())
2565}
2566
2567pub fn reduce_epistemic_program_to_ordinary_for_stratified_schema(
2589 program: &Program,
2590) -> Result<Program> {
2591 let prepared = prepare_root_authored_constraint_identity(program)?;
2592 let determined = EpistemicallyDeterminedPredicates::analyze(&prepared);
2593 let path_specific_rules = stratified_schema_reduction_overrides(&prepared)?;
2594 reduce_epistemic_program_to_ordinary_inner(
2595 &prepared,
2596 &determined.determined,
2597 &path_specific_rules,
2598 )
2599}
2600
2601fn prepare_root_authored_constraint_identity(program: &Program) -> Result<Program> {
2602 let mut prepared = program.clone();
2603 if prepared.authored_constraint_source_bound.is_some() {
2604 prepared.validate_prepared_authored_constraint_identity()?;
2605 } else {
2606 prepared.prepare_authored_constraint_identity_at_root()?;
2607 }
2608 Ok(prepared)
2609}
2610
2611fn reduce_epistemic_program_to_ordinary_inner(
2618 program: &Program,
2619 schema_only_determined_resolve: &BTreeSet<String>,
2620 path_specific_rules: &BTreeMap<usize, crate::ast::Rule>,
2621) -> Result<Program> {
2622 let path_specific_rule_indices = path_specific_rules.keys().copied().collect::<BTreeSet<_>>();
2623 validate_epistemic_relation_shapes(program, &path_specific_rule_indices)?;
2624
2625 let removed_rule_indices = faeel_unfounded_exact_tuple_self_support_rule_indices(program);
2646 let removed_rule_index_set = removed_rule_indices
2647 .iter()
2648 .copied()
2649 .collect::<BTreeSet<_>>();
2650 let active_original_rule_indices = (0..program.rules.len())
2651 .filter(|index| !removed_rule_index_set.contains(index))
2652 .collect::<Vec<_>>();
2653 let mut reduced = program_without_rule_indices(program, &removed_rule_indices);
2654
2655 let invariant = InvariantRelations::analyze(program);
2679
2680 let mut augmented_rule_original_arities = BTreeMap::new();
2686
2687 for ((rule_index, rule), original_rule_index) in reduced
2688 .rules
2689 .iter_mut()
2690 .enumerate()
2691 .zip(active_original_rule_indices)
2692 {
2693 if let Some(path_specific_rule) = path_specific_rules.get(&original_rule_index) {
2694 *rule = path_specific_rule.clone();
2695 continue;
2696 }
2697 let original_head_arity = rule.head.arity();
2698 let modal_only_output_variables = modal_only_bound_output_variables(rule);
2706 append_body_local_tuple_key_variables_to_head(rule);
2707 if rule.head.arity() > original_head_arity {
2708 augmented_rule_original_arities.insert(rule_index, original_head_arity);
2709 }
2710 let was_fact = rule.body.is_empty();
2711 let had_epistemic_body = rule
2712 .body
2713 .iter()
2714 .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)));
2715 for lit in &mut rule.body {
2725 if let BodyLiteral::Epistemic(modal) = lit {
2726 if resolves_augmented_head_variable(
2732 modal,
2733 &modal_only_output_variables,
2734 &invariant,
2735 schema_only_determined_resolve,
2736 ) {
2737 *lit = BodyLiteral::Positive(modal.atom.clone());
2738 }
2739 }
2740 }
2741 rule.body
2742 .retain(|lit| !matches!(lit, BodyLiteral::Epistemic(_)));
2743 if !was_fact && had_epistemic_body && rule.body.is_empty() {
2744 rule.body.push(BodyLiteral::Comparison(Comparison {
2745 left: Term::Integer(1),
2746 op: CompOp::Eq,
2747 right: Term::Integer(1),
2748 }));
2749 }
2750 }
2751 qualify_extensional_multi_arity_predicates(&mut reduced, program, &removed_rule_index_set);
2759
2760 let augmented_signatures =
2761 reconcile_augmented_head_declarations(&mut reduced, &augmented_rule_original_arities)?;
2762
2763 if !augmented_signatures.is_empty() {
2773 reduced.queries.retain(|query| {
2774 !augmented_signatures.contains_key(&(query.atom.predicate.clone(), query.atom.arity()))
2775 });
2776 }
2777
2778 reduced.constraints.retain(|constraint| {
2785 !constraint
2786 .body
2787 .iter()
2788 .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
2789 });
2790
2791 Ok(reduced)
2792}
2793
2794pub fn reduce_case_a_epistemic_program_to_ordinary(program: &Program) -> Program {
2812 let mut reduced = program.clone();
2813 for rule in &mut reduced.rules {
2814 resolve_recursive_epistemic_rule_modals(rule);
2815 }
2816 reduced.constraints.retain(|constraint| {
2820 !constraint
2821 .body
2822 .iter()
2823 .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
2824 });
2825 qualify_extensional_multi_arity_predicates(&mut reduced, program, &BTreeSet::new());
2826 reduced
2827}
2828
2829fn reduce_founded_epistemic_program_to_ordinary(program: &Program) -> Program {
2839 let mut reduced = program.clone();
2840 for rule in &mut reduced.rules {
2841 resolve_recursive_epistemic_rule_modals(rule);
2842 }
2843 for constraint in &mut reduced.constraints {
2844 for literal in &mut constraint.body {
2845 let BodyLiteral::Epistemic(modal) = literal else {
2846 continue;
2847 };
2848 *literal = if modal.negated {
2849 BodyLiteral::Negated(modal.atom.clone())
2850 } else {
2851 BodyLiteral::Positive(modal.atom.clone())
2852 };
2853 }
2854 }
2855 qualify_extensional_multi_arity_predicates(&mut reduced, program, &BTreeSet::new());
2856 reduced
2857}
2858
2859fn resolve_recursive_epistemic_rule_modals(rule: &mut crate::ast::Rule) {
2860 for literal in &mut rule.body {
2861 if let BodyLiteral::Epistemic(modal) = literal {
2862 *literal = if modal.negated {
2863 BodyLiteral::Negated(modal.atom.clone())
2864 } else {
2865 BodyLiteral::Positive(modal.atom.clone())
2866 };
2867 }
2868 }
2869}
2870
2871fn modal_only_bound_output_variables(rule: &crate::ast::Rule) -> BTreeSet<String> {
2882 let positively_bound = non_epistemic_bound_variables(&rule.body);
2883
2884 let mut modal_only = BTreeSet::new();
2887 let mut consider = |name: &str| {
2888 if name != "_" && !positively_bound.contains(name) {
2889 modal_only.insert(name.to_string());
2890 }
2891 };
2892 for term in &rule.head.terms {
2893 if let Term::Variable(name) = term {
2894 consider(name);
2895 }
2896 }
2897 for lit in &rule.body {
2898 if let BodyLiteral::Epistemic(lit) = lit {
2899 for term in &lit.atom.terms {
2900 if let Term::Variable(name) = term {
2901 consider(name);
2902 }
2903 }
2904 }
2905 }
2906 modal_only
2907}
2908
2909fn modal_atom_binds_output_variable(
2913 modal: &EpistemicLiteral,
2914 modal_only_output_variables: &BTreeSet<String>,
2915) -> bool {
2916 modal.atom.terms.iter().any(
2917 |term| matches!(term, Term::Variable(name) if modal_only_output_variables.contains(name)),
2918 )
2919}
2920
2921fn resolves_augmented_head_variable(
2922 modal: &EpistemicLiteral,
2923 modal_only_output_variables: &BTreeSet<String>,
2924 invariant: &InvariantRelations,
2925 schema_only_determined_resolve: &BTreeSet<String>,
2926) -> bool {
2927 !modal.negated
2928 && (invariant.is_invariant(&modal.atom.predicate)
2929 || schema_only_determined_resolve.contains(&modal.atom.predicate))
2930 && modal_atom_binds_output_variable(modal, modal_only_output_variables)
2931}
2932
2933fn record_predicate_signature(
2934 signatures: &mut BTreeMap<String, BTreeSet<usize>>,
2935 atom: &crate::ast::Atom,
2936) {
2937 signatures
2938 .entry(atom.predicate.clone())
2939 .or_default()
2940 .insert(atom.arity());
2941}
2942
2943fn record_body_predicate_signatures(
2944 signatures: &mut BTreeMap<String, BTreeSet<usize>>,
2945 body: &[BodyLiteral],
2946) {
2947 for literal in body {
2948 match literal {
2949 BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) => {
2950 record_predicate_signature(signatures, atom);
2951 }
2952 BodyLiteral::Epistemic(modal) => {
2953 let eir_terms = modal
2954 .atom
2955 .terms
2956 .iter()
2957 .map(convert_term)
2958 .collect::<Vec<_>>();
2959 let arity = flatten_structured_key_terms(&modal.atom.predicate, &eir_terms)
2965 .map(|(arity, _, _)| arity)
2966 .unwrap_or_else(|_| modal.atom.arity());
2967 signatures
2968 .entry(modal.atom.predicate.clone())
2969 .or_default()
2970 .insert(arity);
2971 }
2972 BodyLiteral::Comparison(_) | BodyLiteral::IsExpr(_) | BodyLiteral::Univ(_) => {}
2973 }
2974 }
2975}
2976
2977fn collect_epistemic_relation_identities(
2978 program: &Program,
2979 removed_rules: &BTreeSet<usize>,
2980) -> (BTreeMap<String, BTreeSet<usize>>, BTreeSet<String>) {
2981 let mut source_arities: BTreeMap<String, BTreeSet<usize>> = BTreeMap::new();
2982 for declaration in &program.predicates {
2983 source_arities
2984 .entry(declaration.name.clone())
2985 .or_default()
2986 .insert(declaration.arity());
2987 }
2988
2989 let mut derived_predicates = BTreeSet::new();
2990 for (index, rule) in program.rules.iter().enumerate() {
2991 if removed_rules.contains(&index) {
2992 continue;
2993 }
2994 record_predicate_signature(&mut source_arities, &rule.head);
2995 record_body_predicate_signatures(&mut source_arities, &rule.body);
2996 if !rule.body.is_empty() {
2997 derived_predicates.insert(rule.head.predicate.clone());
2998 }
2999 }
3000 for constraint in &program.constraints {
3001 record_body_predicate_signatures(&mut source_arities, &constraint.body);
3002 }
3003 for query in &program.queries {
3004 record_predicate_signature(&mut source_arities, &query.atom);
3005 }
3006 for fact in &program.prob_facts {
3007 record_predicate_signature(&mut source_arities, &fact.atom);
3008 }
3009 for disjunction in &program.annotated_disjunctions {
3010 for choice in &disjunction.choices {
3011 record_predicate_signature(&mut source_arities, &choice.atom);
3012 }
3013 }
3014 for evidence in &program.evidence {
3015 record_predicate_signature(&mut source_arities, &evidence.atom);
3016 }
3017 for query in &program.prob_queries {
3018 record_predicate_signature(&mut source_arities, &query.atom);
3019 }
3020 for declaration in &program.neural_predicates {
3021 record_predicate_signature(&mut source_arities, &declaration.predicate);
3022 }
3023 for rule in &program.learnable_rules {
3024 record_predicate_signature(&mut source_arities, &rule.head);
3025 record_body_predicate_signatures(&mut source_arities, &rule.body);
3026 if !rule.body.is_empty() {
3027 derived_predicates.insert(rule.head.predicate.clone());
3028 }
3029 }
3030
3031 (source_arities, derived_predicates)
3032}
3033
3034fn all_multi_arity_predicates(program: &Program) -> BTreeSet<String> {
3035 collect_epistemic_relation_identities(program, &BTreeSet::new())
3036 .0
3037 .into_iter()
3038 .filter_map(|(predicate, arities)| (arities.len() > 1).then_some(predicate))
3039 .collect()
3040}
3041
3042fn qualify_atom_for_extensional_multi_arity(
3043 atom: &mut crate::ast::Atom,
3044 predicates: &BTreeSet<String>,
3045) {
3046 if predicates.contains(&atom.predicate) {
3047 atom.predicate = format!("{}/{}", atom.predicate, atom.arity());
3048 }
3049}
3050
3051fn qualify_body_for_extensional_multi_arity(
3052 body: &mut [BodyLiteral],
3053 predicates: &BTreeSet<String>,
3054) {
3055 for literal in body {
3056 match literal {
3057 BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) => {
3058 qualify_atom_for_extensional_multi_arity(atom, predicates);
3059 }
3060 BodyLiteral::Epistemic(modal) => {
3061 qualify_atom_for_extensional_multi_arity(&mut modal.atom, predicates);
3062 }
3063 BodyLiteral::Comparison(_) | BodyLiteral::IsExpr(_) | BodyLiteral::Univ(_) => {}
3064 }
3065 }
3066}
3067
3068pub fn epistemic_extensional_multi_arity_predicates(program: &Program) -> BTreeSet<String> {
3076 let removed_rules = faeel_unfounded_exact_tuple_self_support_rule_indices(program)
3077 .into_iter()
3078 .collect::<BTreeSet<_>>();
3079 extensional_multi_arity_predicates(program, &removed_rules)
3080}
3081
3082fn extensional_multi_arity_predicates(
3083 program: &Program,
3084 removed_rules: &BTreeSet<usize>,
3085) -> BTreeSet<String> {
3086 let (source_arities, derived_predicates) =
3087 collect_epistemic_relation_identities(program, removed_rules);
3088 source_arities
3089 .into_iter()
3090 .filter_map(|(predicate, arities)| {
3091 (arities.len() > 1 && !derived_predicates.contains(&predicate)).then_some(predicate)
3092 })
3093 .collect()
3094}
3095
3096fn qualify_extensional_multi_arity_predicates(
3103 reduced: &mut Program,
3104 source: &Program,
3105 removed_rules: &BTreeSet<usize>,
3106) {
3107 let predicates = extensional_multi_arity_predicates(source, removed_rules);
3108 qualify_predicate_signatures(reduced, &predicates);
3109}
3110
3111fn qualify_predicate_signatures(reduced: &mut Program, predicates: &BTreeSet<String>) {
3116 if predicates.is_empty() {
3117 return;
3118 }
3119
3120 for declaration in &mut reduced.predicates {
3121 if predicates.contains(&declaration.name) {
3122 declaration.name = format!("{}/{}", declaration.name, declaration.arity());
3123 }
3124 }
3125 for rule in &mut reduced.rules {
3126 qualify_atom_for_extensional_multi_arity(&mut rule.head, predicates);
3127 qualify_body_for_extensional_multi_arity(&mut rule.body, predicates);
3128 }
3129 for constraint in &mut reduced.constraints {
3130 qualify_body_for_extensional_multi_arity(&mut constraint.body, predicates);
3131 }
3132 for query in &mut reduced.queries {
3133 qualify_atom_for_extensional_multi_arity(&mut query.atom, predicates);
3134 }
3135 for fact in &mut reduced.prob_facts {
3136 qualify_atom_for_extensional_multi_arity(&mut fact.atom, predicates);
3137 }
3138 for disjunction in &mut reduced.annotated_disjunctions {
3139 for choice in &mut disjunction.choices {
3140 qualify_atom_for_extensional_multi_arity(&mut choice.atom, predicates);
3141 }
3142 }
3143 for evidence in &mut reduced.evidence {
3144 qualify_atom_for_extensional_multi_arity(&mut evidence.atom, predicates);
3145 }
3146 for query in &mut reduced.prob_queries {
3147 qualify_atom_for_extensional_multi_arity(&mut query.atom, predicates);
3148 }
3149 for declaration in &mut reduced.neural_predicates {
3150 qualify_atom_for_extensional_multi_arity(&mut declaration.predicate, predicates);
3151 }
3152 for rule in &mut reduced.learnable_rules {
3153 qualify_atom_for_extensional_multi_arity(&mut rule.head, predicates);
3154 qualify_body_for_extensional_multi_arity(&mut rule.body, predicates);
3155 }
3156}
3157
3158fn validate_epistemic_derived_relation_identity(
3168 program: &Program,
3169 removed_rules: &BTreeSet<usize>,
3170) -> Result<BTreeMap<String, BTreeSet<usize>>> {
3171 let (source_arities, derived_predicates) =
3172 collect_epistemic_relation_identities(program, removed_rules);
3173 for predicate in derived_predicates {
3174 let arities = source_arities
3175 .get(&predicate)
3176 .expect("derived predicate has a source signature");
3177 if arities.len() > 1 {
3178 return Err(XlogError::UnsupportedEpistemicConstruct {
3179 construct: "epistemic derived predicate schema".to_string(),
3180 context: format!(
3181 "derived predicate `{predicate}` uses multiple source arities {arities:?}; \
3182 epistemic derived relations require one source signature per predicate name"
3183 ),
3184 });
3185 }
3186 }
3187
3188 Ok(source_arities)
3189}
3190
3191fn validate_epistemic_relation_shapes(
3200 program: &Program,
3201 non_augmenting_rule_indices: &BTreeSet<usize>,
3202) -> Result<()> {
3203 let removed_rules = faeel_unfounded_exact_tuple_self_support_rule_indices(program)
3204 .into_iter()
3205 .collect::<BTreeSet<_>>();
3206 let active_rules = program
3207 .rules
3208 .iter()
3209 .enumerate()
3210 .filter(|(index, _)| {
3211 !removed_rules.contains(index) && !non_augmenting_rule_indices.contains(index)
3212 })
3213 .collect::<Vec<_>>();
3214
3215 validate_epistemic_derived_relation_identity(program, &removed_rules)?;
3216
3217 let mut reduced_rule_arities = Vec::with_capacity(active_rules.len());
3218 let mut augmented_targets: BTreeMap<(String, usize), usize> = BTreeMap::new();
3219
3220 for (_, rule) in &active_rules {
3221 let original_signature = (rule.head.predicate.clone(), rule.head.arity());
3222 let mut reduced_rule = (*rule).clone();
3223 append_body_local_tuple_key_variables_to_head(&mut reduced_rule);
3224 let reduced_arity = reduced_rule.head.arity();
3225
3226 if reduced_arity > original_signature.1 {
3227 augmented_targets
3228 .entry(original_signature.clone())
3229 .and_modify(|target| *target = (*target).max(reduced_arity))
3230 .or_insert(reduced_arity);
3231 }
3232 reduced_rule_arities.push((original_signature, reduced_arity));
3233 }
3234
3235 for ((predicate, original_arity), target_arity) in augmented_targets {
3236 let arities = reduced_rule_arities
3237 .iter()
3238 .filter(|((candidate, arity), _)| candidate == &predicate && *arity == original_arity)
3239 .map(|(_, arity)| *arity)
3240 .collect::<BTreeSet<_>>();
3241 if arities.len() != 1 || !arities.contains(&target_arity) {
3242 return Err(XlogError::UnsupportedEpistemicConstruct {
3243 construct: "epistemic augmented predicate schema".to_string(),
3244 context: format!(
3245 "rules defining `{predicate}/{original_arity}` lower to incompatible \
3246 internal arities {arities:?}; every clause for one predicate signature \
3247 must bind the same augmented tuple shape"
3248 ),
3249 });
3250 }
3251
3252 for query in program.queries.iter().filter(|query| {
3253 query.atom.predicate == predicate && query.atom.arity() == original_arity
3254 }) {
3255 let mut variables = BTreeSet::new();
3256 let unconstrained = query.atom.terms.iter().all(|term| match term {
3257 Term::Variable(name) => name != "_" && variables.insert(name.as_str()),
3258 Term::Anonymous
3259 | Term::Integer(_)
3260 | Term::Float(_)
3261 | Term::String(_)
3262 | Term::Symbol(_)
3263 | Term::List(_)
3264 | Term::Cons { .. }
3265 | Term::Compound { .. }
3266 | Term::PredRef(_)
3267 | Term::Aggregate(_) => false,
3268 });
3269 if !unconstrained {
3270 return Err(XlogError::UnsupportedEpistemicConstruct {
3271 construct: "epistemic augmented head query".to_string(),
3272 context: format!(
3273 "query `{predicate}/{original_arity}` is not a tuple of distinct named \
3274 variables; an augmented epistemic head can currently surface only \
3275 queries whose arguments are distinct named variables"
3276 ),
3277 });
3278 }
3279 }
3280 }
3281
3282 let eir = build_eir(program)?;
3283 let mut clauses_by_signature: BTreeMap<(String, usize), Vec<(usize, &crate::ast::Rule)>> =
3284 BTreeMap::new();
3285 for (rule_index, rule) in active_rules {
3286 clauses_by_signature
3287 .entry((rule.head.predicate.clone(), rule.head.arity()))
3288 .or_default()
3289 .push((rule_index, rule));
3290 }
3291 for ((predicate, arity), clauses) in clauses_by_signature {
3292 if clauses.len() > 1
3293 && clauses.iter().any(|(_, rule)| {
3294 rule.body
3295 .iter()
3296 .any(|literal| matches!(literal, BodyLiteral::Epistemic(_)))
3297 })
3298 && !epistemic_rule_union_gates_are_redundant(program, &eir, &clauses)
3299 {
3300 return Err(XlogError::UnsupportedEpistemicConstruct {
3301 construct: "epistemic rule-union materialization".to_string(),
3302 context: format!(
3303 "predicate `{predicate}/{arity}` has multiple defining clauses and at least \
3304 one epistemic clause; single-pass materialization cannot preserve \
3305 per-clause modal provenance, so it cannot safely filter the clause union"
3306 ),
3307 });
3308 }
3309 }
3310
3311 Ok(())
3312}
3313
3314fn epistemic_rule_union_gates_are_redundant(
3336 program: &Program,
3337 eir: &EirProgram,
3338 clauses: &[(usize, &crate::ast::Rule)],
3339) -> bool {
3340 let invariant = InvariantRelations::analyze(program);
3341 let mut normalized_conjunctions = clauses.iter().map(|(rule_index, _)| {
3342 eir.rules
3343 .get(*rule_index)
3344 .and_then(|rule| normalized_rule_union_gates(rule, &invariant))
3345 });
3346 if let Some(Some(first)) = normalized_conjunctions.next() {
3347 if !first.is_empty()
3348 && normalized_conjunctions.all(|candidate| {
3349 candidate.is_some_and(|candidate| rule_union_gate_sets_equal(&first, &candidate))
3350 })
3351 {
3352 return true;
3353 }
3354 }
3355
3356 let epistemic_clauses = clauses
3357 .iter()
3358 .filter(|(_, rule)| {
3359 rule.body
3360 .iter()
3361 .any(|literal| matches!(literal, BodyLiteral::Epistemic(_)))
3362 })
3363 .collect::<Vec<_>>();
3364 if epistemic_clauses.is_empty() {
3365 return true;
3366 }
3367
3368 let every_gate_is_unconditionally_true = epistemic_clauses.iter().all(|(rule_index, _)| {
3373 eir.rules.get(*rule_index).is_some_and(|eir_rule| {
3374 let modal_literals = eir_rule
3375 .body
3376 .iter()
3377 .filter_map(|literal| match literal {
3378 EirBodyLiteral::Epistemic(modal) => Some(modal),
3379 _ => None,
3380 })
3381 .collect::<Vec<_>>();
3382 !modal_literals.is_empty()
3383 && modal_literals.iter().all(|modal| {
3384 !modal.negated && has_unconditional_ground_founded_support(eir, &modal.atom)
3385 })
3386 })
3387 });
3388 if every_gate_is_unconditionally_true {
3389 return true;
3390 }
3391
3392 if epistemic_clauses.len() != 1 {
3393 return false;
3394 }
3395
3396 let (rule_index, _) = epistemic_clauses[0];
3397 let Some(eir_rule) = eir.rules.get(*rule_index) else {
3398 return false;
3399 };
3400 let modal_literals = eir_rule
3401 .body
3402 .iter()
3403 .filter_map(|literal| match literal {
3404 EirBodyLiteral::Epistemic(modal) => Some(modal),
3405 _ => None,
3406 })
3407 .collect::<Vec<_>>();
3408
3409 !modal_literals.is_empty()
3410 && modal_literals.iter().all(|modal| {
3411 !modal.negated
3412 && (has_unconditional_ground_founded_support(eir, &modal.atom)
3413 || (modal.atom == eir_rule.head
3414 && eir_head_is_bijective_variable_tuple(&eir_rule.head)
3415 && ((eir.mode == EirEpistemicMode::G91
3416 && modal.op == EirEpistemicOp::Possible)
3417 || has_tuple_level_independent_founded_support(
3418 eir,
3419 eir_rule,
3420 &modal.atom,
3421 ))))
3422 })
3423}
3424
3425#[derive(Debug, Clone, PartialEq, Eq)]
3426enum RuleUnionGateTerm {
3427 OutputColumn(usize),
3428 Literal(EirTerm),
3429}
3430
3431#[derive(Debug, Clone, PartialEq, Eq)]
3432struct RuleUnionGate {
3433 predicate: String,
3434 arity: usize,
3435 terms: Vec<RuleUnionGateTerm>,
3436 op: Option<EirEpistemicOp>,
3437 negated: bool,
3438}
3439
3440fn normalized_rule_union_gates(
3445 rule: &xlog_ir::EirRule,
3446 invariant: &InvariantRelations<'_>,
3447) -> Option<Vec<RuleUnionGate>> {
3448 let output_terms = augmented_eir_head_terms(rule);
3449 let mut gates = Vec::new();
3450 for literal in &rule.body {
3451 let EirBodyLiteral::Epistemic(modal) = literal else {
3452 continue;
3453 };
3454 let bound_columns = bound_output_columns_for_terms(&modal.atom.terms, &output_terms);
3455 let terms = modal
3456 .atom
3457 .terms
3458 .iter()
3459 .zip(bound_columns)
3460 .map(|(term, output_column)| match (term, output_column) {
3461 (EirTerm::Variable(_), Some(column)) => {
3462 Some(RuleUnionGateTerm::OutputColumn(column))
3463 }
3464 (
3465 term @ (EirTerm::Anonymous
3466 | EirTerm::Integer(_)
3467 | EirTerm::FloatBits(_)
3468 | EirTerm::String(_)
3469 | EirTerm::Symbol(_)
3470 | EirTerm::PredRef(_)),
3471 None,
3472 ) => Some(RuleUnionGateTerm::Literal(term.clone())),
3473 _ => None,
3474 })
3475 .collect::<Option<Vec<_>>>()?;
3476 let gate = RuleUnionGate {
3477 predicate: modal.atom.predicate.clone(),
3478 arity: modal.atom.arity,
3479 terms,
3480 op: (!invariant.is_invariant(&modal.atom.predicate)).then_some(modal.op),
3481 negated: modal.negated,
3482 };
3483 if !gates.contains(&gate) {
3484 gates.push(gate);
3485 }
3486 }
3487 Some(gates)
3488}
3489
3490fn rule_union_gate_sets_equal(left: &[RuleUnionGate], right: &[RuleUnionGate]) -> bool {
3491 left.len() == right.len() && left.iter().all(|gate| right.contains(gate))
3492}
3493
3494fn eir_head_is_bijective_variable_tuple(head: &xlog_ir::EirAtom) -> bool {
3495 let mut variables = BTreeSet::new();
3496 head.terms.iter().all(|term| match term {
3497 EirTerm::Variable(name) => variables.insert(name),
3498 _ => false,
3499 })
3500}
3501
3502fn reconcile_augmented_head_declarations(
3517 reduced: &mut Program,
3518 augmented_rule_original_arities: &BTreeMap<usize, usize>,
3519) -> Result<BTreeMap<(String, usize), usize>> {
3520 use crate::ast::{PredColumn, TypeRef};
3521
3522 let mut augmented_signatures: BTreeMap<(String, usize), usize> = BTreeMap::new();
3526 let mut inferred_types: BTreeMap<(String, usize), Vec<Option<TypeRef>>> = BTreeMap::new();
3527
3528 let mut lowerer = Lowerer::new();
3534 lowerer.infer_schemas(reduced)?;
3535 let schemas = lowerer.schemas().clone();
3536
3537 for (rule_index, rule) in reduced.rules.iter().enumerate() {
3538 if rule.body.is_empty() {
3539 continue;
3540 }
3541 let Some(&original_arity) = augmented_rule_original_arities.get(&rule_index) else {
3543 continue;
3544 };
3545 let arity = rule.head.terms.len();
3546 if arity <= original_arity {
3547 continue;
3548 }
3549 let signature = (rule.head.predicate.clone(), original_arity);
3550 let entry = augmented_signatures.entry(signature.clone()).or_insert(0);
3551 if arity > *entry {
3552 *entry = arity;
3553 }
3554 let types = inferred_types
3555 .entry(signature)
3556 .or_insert_with(|| vec![None; arity]);
3557 if types.len() < arity {
3558 types.resize(arity, None);
3559 }
3560 let variable_types = lowerer.infer_rule_variable_types(rule, |atom, index| {
3561 schemas
3562 .get(&atom.predicate)
3563 .and_then(|schema| schema.column_type(index))
3564 })?;
3565
3566 for (col, term) in rule.head.terms.iter().enumerate() {
3569 if types[col].is_some() {
3570 continue;
3571 }
3572 let Term::Variable(head_var) = term else {
3573 continue;
3574 };
3575 if let Some((typ, _)) = variable_types.get(head_var) {
3576 types[col] = Some(TypeRef::Scalar(*typ));
3577 }
3578 }
3579 }
3580
3581 for decl in &mut reduced.predicates {
3582 let signature = (decl.name.clone(), decl.arity());
3583 let Some(&target_arity) = augmented_signatures.get(&signature) else {
3584 continue;
3585 };
3586 let mut columns = decl.schema_columns();
3587 if target_arity <= columns.len() {
3588 continue;
3589 }
3590 let inferred = inferred_types.get(&signature);
3591 for col in columns.len()..target_arity {
3592 let typ = inferred
3593 .and_then(|types| types.get(col))
3594 .and_then(|t| t.clone())
3595 .unwrap_or(TypeRef::Scalar(xlog_core::ScalarType::U32));
3597 columns.push(PredColumn { name: None, typ });
3598 }
3599 decl.types = columns.iter().map(|column| column.typ.clone()).collect();
3600 decl.columns = columns;
3601 }
3602
3603 Ok(augmented_signatures)
3604}
3605
3606fn append_body_local_tuple_key_variables_to_head(rule: &mut crate::ast::Rule) {
3607 let mut hidden_variables = Vec::new();
3608 for lit in &rule.body {
3609 let BodyLiteral::Epistemic(lit) = lit else {
3610 continue;
3611 };
3612 for term in &lit.atom.terms {
3613 let Term::Variable(variable) = term else {
3614 continue;
3615 };
3616 if variable == "_" {
3617 continue;
3618 }
3619 let already_in_head = rule
3620 .head
3621 .terms
3622 .iter()
3623 .any(|head_term| matches!(head_term, Term::Variable(name) if name == variable));
3624 if !already_in_head && !hidden_variables.iter().any(|name| name == variable) {
3625 hidden_variables.push(variable.clone());
3626 }
3627 }
3628 }
3629 for variable in hidden_variables {
3630 rule.head.terms.push(Term::Variable(variable));
3631 }
3632}
3633
3634fn wcoj_status_for_reduction(
3635 positive_relational_atoms: &[xlog_ir::EirAtom],
3636 has_negated_relational_atom: bool,
3637) -> EpistemicWcojReductionStatus {
3638 if !has_negated_relational_atom
3639 && positive_relational_atoms_are_supported_wcoj_shape(positive_relational_atoms)
3640 {
3641 EpistemicWcojReductionStatus::RequiresPlannerEligibility
3642 } else {
3643 EpistemicWcojReductionStatus::NotWcojCandidate
3644 }
3645}
3646
3647fn positive_relational_atoms_are_supported_wcoj_shape(atoms: &[xlog_ir::EirAtom]) -> bool {
3648 let mut edges: BTreeSet<(String, String)> = BTreeSet::new();
3649 let mut degrees: BTreeMap<String, usize> = BTreeMap::new();
3650 for atom in atoms {
3651 if atom.arity != 2 || atom.terms.len() != 2 {
3652 return false;
3653 }
3654 let Some(left) = eir_variable_name(&atom.terms[0]) else {
3655 return false;
3656 };
3657 let Some(right) = eir_variable_name(&atom.terms[1]) else {
3658 return false;
3659 };
3660 if left == right {
3661 return false;
3662 }
3663 let edge = if left < right {
3664 (left.to_string(), right.to_string())
3665 } else {
3666 (right.to_string(), left.to_string())
3667 };
3668 if !edges.insert(edge.clone()) {
3669 return false;
3670 }
3671 *degrees.entry(edge.0).or_insert(0) += 1;
3672 *degrees.entry(edge.1).or_insert(0) += 1;
3673 }
3674
3675 match edges.len() {
3676 3 => degrees.len() == 3 && degrees.values().all(|degree| *degree == 2),
3677 4 => degrees.len() == 4 && degrees.values().all(|degree| *degree == 2),
3678 10 | 15 | 21 | 28 => {
3679 let variable_count = degrees.len();
3680 (5..=8).contains(&variable_count)
3681 && edges.len() == variable_count * (variable_count - 1) / 2
3682 && degrees.values().all(|degree| *degree == variable_count - 1)
3683 }
3684 _ => false,
3685 }
3686}
3687
3688fn eir_variable_name(term: &EirTerm) -> Option<&str> {
3689 match term {
3690 EirTerm::Variable(name) => Some(name.as_str()),
3691 _ => None,
3692 }
3693}
3694
3695#[derive(Debug, Clone, PartialEq, Eq)]
3697pub enum FaeelCandidateResult {
3698 Model,
3700 NoModel(FaeelNoModelReason),
3702}
3703
3704#[derive(Debug, Clone, PartialEq, Eq)]
3706pub enum FaeelNoModelReason {
3707 UnfoundedPossible {
3709 predicate: String,
3711 arity: usize,
3713 },
3714 Contradiction {
3716 predicate: String,
3718 arity: usize,
3720 },
3721 UnsatisfiedLiteral {
3723 predicate: String,
3725 arity: usize,
3727 },
3728}
3729
3730#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3732pub struct GeneratePropagateTestConfig {
3733 pub max_candidates: usize,
3735}
3736
3737#[derive(Debug, Clone, Default, PartialEq, Eq)]
3739pub struct GeneratePropagateTestTrace {
3740 pub generated: usize,
3742 pub guesses: usize,
3744 pub propagated: usize,
3746 pub pruned: usize,
3748 pub reduced_program_models: usize,
3750 pub tested: usize,
3752 pub accepted: usize,
3754 pub accepted_world_views: usize,
3756 pub rejected: usize,
3758 pub rejection_reasons: Vec<FaeelNoModelReason>,
3760}
3761
3762#[derive(Debug, Clone, PartialEq, Eq)]
3764pub struct GeneratePropagateTestOutcome {
3765 pub trace: GeneratePropagateTestTrace,
3767 pub accepted_candidate_indices: Vec<usize>,
3769 pub rejected_candidate_indices: Vec<usize>,
3771}
3772
3773#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
3779pub enum EpistemicComponentMergeReason {
3780 SharedHeadPredicate {
3783 predicate: String,
3785 },
3786 DerivedPredicate {
3789 predicate: String,
3792 },
3793 SharedModalPredicate {
3796 predicate: String,
3798 },
3799 Constraint {
3802 predicates: Vec<String>,
3804 },
3805}
3806
3807#[derive(Debug, Clone, PartialEq, Eq)]
3809pub struct EpistemicDependencyComponent {
3810 pub predicates: Vec<String>,
3812 pub rule_indices: Vec<usize>,
3814 pub merge_reasons: Vec<EpistemicComponentMergeReason>,
3819}
3820
3821#[derive(Debug, Clone, PartialEq, Eq)]
3823pub struct EpistemicDependencyGraph {
3824 pub components: Vec<EpistemicDependencyComponent>,
3826}
3827
3828#[derive(Debug, Clone, PartialEq, Eq)]
3830pub struct EpistemicSplitPlan {
3831 pub components: Vec<EpistemicDependencyComponent>,
3833}
3834
3835impl EpistemicSplitPlan {
3836 pub fn recomposed_rule_indices(&self) -> Vec<usize> {
3838 let mut indices: Vec<usize> = self
3839 .components
3840 .iter()
3841 .flat_map(|component| component.rule_indices.iter().copied())
3842 .collect();
3843 indices.sort_unstable();
3844 indices
3845 }
3846}
3847
3848#[derive(Debug, Clone)]
3850pub struct EpistemicSplitExecutableComponent {
3851 pub component: EpistemicDependencyComponent,
3853 pub executable: EpistemicExecutablePlan,
3855}
3856
3857#[derive(Debug, Clone)]
3859pub struct EpistemicSplitExecutablePlan {
3860 pub split_plan: EpistemicSplitPlan,
3862 pub components: Vec<EpistemicSplitExecutableComponent>,
3864}
3865
3866impl EpistemicSplitExecutablePlan {
3867 pub fn recomposed_rule_indices(&self) -> Vec<usize> {
3877 let mut indices: Vec<usize> = self
3878 .components
3879 .iter()
3880 .flat_map(|component| component.component.rule_indices.iter().copied())
3881 .collect();
3882 indices.sort_unstable();
3883 indices
3884 }
3885
3886 pub fn planned_recomposed_rule_indices(&self) -> Vec<usize> {
3889 self.split_plan.recomposed_rule_indices()
3890 }
3891
3892 pub fn recomposed_components(&self) -> Vec<&EpistemicSplitExecutableComponent> {
3894 let mut components: Vec<_> = self.components.iter().collect();
3895 components.sort_by_key(|component| {
3896 component
3897 .component
3898 .rule_indices
3899 .iter()
3900 .copied()
3901 .min()
3902 .unwrap_or(usize::MAX)
3903 });
3904 components
3905 }
3906}
3907
3908pub fn evaluate_epistemic_literal(
3910 mode: EpistemicMode,
3911 lit: &EpistemicLiteral,
3912 interpretation: &EpistemicInterpretation,
3913) -> TruthValue {
3914 let value = match lit.op {
3915 EpistemicOp::Know => interpretation.contains_known(&lit.atom),
3916 EpistemicOp::Possible => match mode {
3917 EpistemicMode::G91 => {
3918 interpretation.contains_known(&lit.atom)
3919 || interpretation.contains_possible(&lit.atom)
3920 }
3921 EpistemicMode::Faeel => interpretation.contains_known(&lit.atom),
3922 },
3923 };
3924
3925 TruthValue::from_bool(if lit.negated { !value } else { value })
3926}
3927
3928pub fn evaluate_faeel_candidate(
3930 program: &Program,
3931 interpretation: &EpistemicInterpretation,
3932) -> Result<FaeelCandidateResult> {
3933 evaluate_epistemic_candidate(program, interpretation, EpistemicMode::Faeel)
3934}
3935
3936pub fn evaluate_epistemic_candidate(
3938 program: &Program,
3939 interpretation: &EpistemicInterpretation,
3940 mode: EpistemicMode,
3941) -> Result<FaeelCandidateResult> {
3942 reject_gpt_epistemic_constraints(program)?;
3943 if let Some((predicate, arity)) = interpretation.first_contradiction() {
3944 return Ok(FaeelCandidateResult::NoModel(
3945 FaeelNoModelReason::Contradiction { predicate, arity },
3946 ));
3947 }
3948
3949 for rule in &program.rules {
3950 for body_lit in &rule.body {
3951 let BodyLiteral::Epistemic(lit) = body_lit else {
3952 continue;
3953 };
3954 if interpretation.contains_known(&lit.atom)
3955 && interpretation.contains_rejected(&lit.atom)
3956 {
3957 return Ok(FaeelCandidateResult::NoModel(
3958 FaeelNoModelReason::Contradiction {
3959 predicate: lit.atom.predicate.clone(),
3960 arity: lit.atom.arity(),
3961 },
3962 ));
3963 }
3964 if mode == EpistemicMode::Faeel
3965 && lit.op == EpistemicOp::Possible
3966 && interpretation.contains_possible(&lit.atom)
3967 && !interpretation.contains_known(&lit.atom)
3968 {
3969 return Ok(FaeelCandidateResult::NoModel(
3970 FaeelNoModelReason::UnfoundedPossible {
3971 predicate: lit.atom.predicate.clone(),
3972 arity: lit.atom.arity(),
3973 },
3974 ));
3975 }
3976 if evaluate_epistemic_literal(mode, lit, interpretation) == TruthValue::False {
3977 return Ok(FaeelCandidateResult::NoModel(
3978 FaeelNoModelReason::UnsatisfiedLiteral {
3979 predicate: lit.atom.predicate.clone(),
3980 arity: lit.atom.arity(),
3981 },
3982 ));
3983 }
3984 }
3985 }
3986
3987 Ok(FaeelCandidateResult::Model)
3988}
3989
3990pub fn run_generate_propagate_test(
3992 program: &Program,
3993 candidates: Vec<EpistemicInterpretation>,
3994 config: GeneratePropagateTestConfig,
3995) -> Result<GeneratePropagateTestOutcome> {
3996 run_generate_propagate_test_with_mode(
3997 program,
3998 candidates,
3999 config,
4000 program.directives.epistemic_mode_or_default(),
4001 )
4002}
4003
4004pub fn run_generate_propagate_test_with_mode(
4006 program: &Program,
4007 candidates: Vec<EpistemicInterpretation>,
4008 config: GeneratePropagateTestConfig,
4009 mode: EpistemicMode,
4010) -> Result<GeneratePropagateTestOutcome> {
4011 reject_gpt_epistemic_constraints(program)?;
4012 if candidates.len() > config.max_candidates {
4013 return Err(xlog_core::XlogError::ResourceExhausted {
4014 context: "epistemic GPT candidate guard".to_string(),
4015 estimated_bytes: candidates.len() as u64,
4016 budget_bytes: config.max_candidates as u64,
4017 });
4018 }
4019
4020 let generated = candidates.len();
4021 let guesses = candidates
4022 .iter()
4023 .map(EpistemicInterpretation::epistemic_guess_count)
4024 .sum();
4025 let mut propagated_candidates = Vec::new();
4026 let mut rejection_reasons = Vec::new();
4027 let mut rejected_candidate_indices = Vec::new();
4028 for (idx, candidate) in candidates.into_iter().enumerate() {
4029 if let Some((predicate, arity)) = candidate.first_contradiction() {
4030 rejection_reasons.push(FaeelNoModelReason::Contradiction { predicate, arity });
4031 rejected_candidate_indices.push(idx);
4032 } else {
4033 propagated_candidates.push((idx, candidate));
4034 }
4035 }
4036
4037 let mut trace = GeneratePropagateTestTrace {
4038 generated,
4039 guesses,
4040 propagated: propagated_candidates.len(),
4041 pruned: generated.saturating_sub(propagated_candidates.len()),
4042 reduced_program_models: propagated_candidates.len(),
4043 rejection_reasons,
4044 ..GeneratePropagateTestTrace::default()
4045 };
4046 let mut accepted_candidate_indices = Vec::new();
4047
4048 for (idx, candidate) in &propagated_candidates {
4049 trace.tested += 1;
4050 match evaluate_epistemic_candidate(program, candidate, mode)? {
4051 FaeelCandidateResult::Model => {
4052 trace.accepted += 1;
4053 trace.accepted_world_views += 1;
4054 accepted_candidate_indices.push(*idx);
4055 }
4056 FaeelCandidateResult::NoModel(reason) => {
4057 trace.rejected += 1;
4058 trace.rejection_reasons.push(reason);
4059 rejected_candidate_indices.push(*idx);
4060 }
4061 }
4062 }
4063
4064 Ok(GeneratePropagateTestOutcome {
4065 trace,
4066 accepted_candidate_indices,
4067 rejected_candidate_indices,
4068 })
4069}
4070
4071pub fn build_epistemic_dependency_graph(program: &Program) -> Result<EpistemicDependencyGraph> {
4073 if program.rules.is_empty() {
4074 return Ok(EpistemicDependencyGraph { components: vec![] });
4075 }
4076
4077 let mut parents: Vec<usize> = (0..program.rules.len()).collect();
4078 let mut rule_predicates = Vec::with_capacity(program.rules.len());
4079 let mut head_owner: BTreeMap<String, usize> = BTreeMap::new();
4080 let mut merge_log: Vec<(usize, EpistemicComponentMergeReason)> = Vec::new();
4084
4085 for (idx, rule) in program.rules.iter().enumerate() {
4086 if rule.body.is_empty() {
4087 continue;
4088 }
4089 if let Some(owner) = head_owner.get(&rule.head.predicate).copied() {
4090 union_components(&mut parents, owner, idx);
4091 merge_log.push((
4092 idx,
4093 EpistemicComponentMergeReason::SharedHeadPredicate {
4094 predicate: rule.head.predicate.clone(),
4095 },
4096 ));
4097 } else {
4098 head_owner.insert(rule.head.predicate.clone(), idx);
4099 }
4100 }
4101
4102 let mut modal_owner: BTreeMap<EpistemicAtomKey, usize> = BTreeMap::new();
4103 for (idx, rule) in program.rules.iter().enumerate() {
4104 let mut predicates = BTreeSet::new();
4105 predicates.insert(rule.head.predicate.clone());
4106 for lit in &rule.body {
4107 if let BodyLiteral::Epistemic(lit) = lit {
4108 let key =
4109 EpistemicAtomKey::from_arity(lit.atom.predicate.clone(), lit.atom.arity());
4110 if let Some(owner) = modal_owner.get(&key).copied() {
4111 union_components(&mut parents, owner, idx);
4112 merge_log.push((
4113 idx,
4114 EpistemicComponentMergeReason::SharedModalPredicate {
4115 predicate: format!("{}/{}", lit.atom.predicate, lit.atom.arity()),
4116 },
4117 ));
4118 } else {
4119 modal_owner.insert(key, idx);
4120 }
4121 }
4122 if let Some(atom) = lit.atom() {
4123 if let Some(owner) = head_owner.get(&atom.predicate).copied() {
4124 if owner != idx {
4125 union_components(&mut parents, owner, idx);
4126 merge_log.push((
4127 idx,
4128 EpistemicComponentMergeReason::DerivedPredicate {
4129 predicate: atom.predicate.clone(),
4130 },
4131 ));
4132 }
4133 }
4134 predicates.insert(atom.predicate.clone());
4135 }
4136 }
4137
4138 rule_predicates.push(predicates);
4139 }
4140
4141 let mut constraint_predicates = Vec::with_capacity(program.constraints.len());
4142 for constraint in &program.constraints {
4143 let predicates = constraint_predicate_set(constraint);
4144 let mut owners = predicates
4145 .iter()
4146 .filter_map(|predicate| head_owner.get(predicate).copied());
4147 if let Some(first_owner) = owners.next() {
4148 let mut coalesced_any = false;
4149 for owner in owners {
4150 if find_component(&mut parents, first_owner) != find_component(&mut parents, owner)
4151 {
4152 coalesced_any = true;
4153 }
4154 union_components(&mut parents, first_owner, owner);
4155 }
4156 if coalesced_any {
4157 let constraint_heads: Vec<String> = predicates
4158 .iter()
4159 .filter(|predicate| head_owner.contains_key(*predicate))
4160 .cloned()
4161 .collect();
4162 merge_log.push((
4163 first_owner,
4164 EpistemicComponentMergeReason::Constraint {
4165 predicates: constraint_heads,
4166 },
4167 ));
4168 }
4169 }
4170 constraint_predicates.push(predicates);
4171 }
4172
4173 let mut grouped: BTreeMap<usize, (BTreeSet<String>, Vec<usize>)> = BTreeMap::new();
4174 for (idx, predicates) in rule_predicates.into_iter().enumerate() {
4175 let root = find_component(&mut parents, idx);
4176 let entry = grouped
4177 .entry(root)
4178 .or_insert_with(|| (BTreeSet::new(), vec![]));
4179 entry.0.extend(predicates);
4180 entry.1.push(idx);
4181 }
4182 for predicates in constraint_predicates {
4183 let Some(root) = predicates
4184 .iter()
4185 .filter_map(|predicate| head_owner.get(predicate).copied())
4186 .map(|idx| find_component(&mut parents, idx))
4187 .next()
4188 else {
4189 continue;
4190 };
4191 grouped
4192 .entry(root)
4193 .or_insert_with(|| (BTreeSet::new(), vec![]))
4194 .0
4195 .extend(predicates);
4196 }
4197
4198 let mut reasons_by_root: BTreeMap<usize, BTreeSet<EpistemicComponentMergeReason>> =
4200 BTreeMap::new();
4201 for (touched_idx, reason) in merge_log {
4202 let root = find_component(&mut parents, touched_idx);
4203 reasons_by_root.entry(root).or_default().insert(reason);
4204 }
4205
4206 let mut components: Vec<EpistemicDependencyComponent> = grouped
4207 .into_iter()
4208 .map(|(root, (predicates, mut rule_indices))| {
4209 rule_indices.sort_unstable();
4210 let merge_reasons = reasons_by_root
4211 .remove(&root)
4212 .map(|reasons| reasons.into_iter().collect())
4213 .unwrap_or_default();
4214 EpistemicDependencyComponent {
4215 predicates: predicates.into_iter().collect(),
4216 rule_indices,
4217 merge_reasons,
4218 }
4219 })
4220 .collect();
4221 components.sort_by(|a, b| a.predicates.cmp(&b.predicates));
4222 Ok(EpistemicDependencyGraph { components })
4223}
4224
4225fn constraint_predicate_set(constraint: &Constraint) -> BTreeSet<String> {
4226 constraint
4227 .body
4228 .iter()
4229 .filter_map(|lit| lit.atom().map(|atom| atom.predicate.clone()))
4230 .collect()
4231}
4232
4233fn find_component(parents: &mut [usize], idx: usize) -> usize {
4234 if parents[idx] != idx {
4235 let root = find_component(parents, parents[idx]);
4236 parents[idx] = root;
4237 }
4238 parents[idx]
4239}
4240
4241fn union_components(parents: &mut [usize], left: usize, right: usize) {
4242 let left_root = find_component(parents, left);
4243 let right_root = find_component(parents, right);
4244 if left_root != right_root {
4245 parents[right_root] = left_root;
4246 }
4247}
4248
4249#[derive(Debug, Clone)]
4254pub struct EpistemicStratum {
4255 pub head_predicates: Vec<String>,
4257 pub rule_indices: Vec<usize>,
4259 pub program: Program,
4263}
4264
4265#[derive(Debug, Clone)]
4272pub struct EpistemicStratifiedPlan {
4273 pub strata: Vec<EpistemicStratum>,
4275 pub ordinary_post_program: Program,
4278}
4279
4280struct EpistemicallyDeterminedPredicates {
4295 determined: BTreeSet<String>,
4296}
4297
4298impl EpistemicallyDeterminedPredicates {
4299 fn analyze(program: &Program) -> Self {
4300 let invariant = InvariantRelations::analyze(program);
4301
4302 let mut derived_heads: BTreeSet<&str> = BTreeSet::new();
4304 for rule in &program.rules {
4305 if !rule.body.is_empty() {
4306 derived_heads.insert(rule.head.predicate.as_str());
4307 }
4308 }
4309
4310 let mut determined: BTreeSet<String> = BTreeSet::new();
4326 let mut changed = true;
4327 while changed {
4328 changed = false;
4329 for head in &derived_heads {
4330 if determined.contains(*head) {
4331 continue;
4332 }
4333 if Self::head_is_determined(program, head, &invariant, &derived_heads, &determined)
4334 {
4335 determined.insert((*head).to_string());
4336 changed = true;
4337 }
4338 }
4339 }
4340
4341 Self { determined }
4342 }
4343
4344 fn head_is_determined(
4347 program: &Program,
4348 head: &str,
4349 invariant: &InvariantRelations,
4350 derived_heads: &BTreeSet<&str>,
4351 determined: &BTreeSet<String>,
4352 ) -> bool {
4353 let mut defined = false;
4354 for rule in &program.rules {
4355 if rule.head.predicate != head || rule.body.is_empty() {
4356 continue;
4357 }
4358 defined = true;
4359 for lit in &rule.body {
4360 let referenced = match lit {
4361 BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) => {
4362 atom.predicate.as_str()
4363 }
4364 BodyLiteral::Epistemic(modal) => modal.atom.predicate.as_str(),
4365 BodyLiteral::Comparison(_) | BodyLiteral::IsExpr(_) | BodyLiteral::Univ(_) => {
4366 continue
4367 }
4368 };
4369 if referenced == head {
4370 return false;
4373 }
4374 let ok = invariant.is_invariant(referenced)
4375 || determined.contains(referenced)
4376 || !derived_heads.contains(referenced);
4378 if !ok {
4379 return false;
4380 }
4381 }
4382 }
4383 defined
4384 }
4385
4386 fn contains(&self, predicate: &str) -> bool {
4387 self.determined.contains(predicate)
4388 }
4389}
4390
4391pub fn try_plan_stratified_epistemic_program(
4415 program: &Program,
4416) -> Result<Option<EpistemicStratifiedPlan>> {
4417 let prepared = prepare_root_authored_constraint_identity(program)?;
4418 let program = &prepared;
4419 let determined = EpistemicallyDeterminedPredicates::analyze(program);
4420
4421 let mut needs_stratification = false;
4425 for rule in &program.rules {
4426 for lit in &rule.body {
4427 if let BodyLiteral::Epistemic(modal) = lit {
4428 if determined.contains(modal.atom.predicate.as_str())
4429 && modal.atom.predicate != rule.head.predicate
4430 {
4431 needs_stratification = true;
4432 }
4433 }
4434 }
4435 }
4436 if !needs_stratification {
4437 return Ok(None);
4438 }
4439 let removed_rules = faeel_unfounded_exact_tuple_self_support_rule_indices(program)
4440 .into_iter()
4441 .collect::<BTreeSet<_>>();
4442 validate_epistemic_derived_relation_identity(program, &removed_rules)?;
4443
4444 let stratum_level = assign_epistemic_strata(program, &determined)?;
4449 let Some(stratum_level) = stratum_level else {
4450 return Ok(None);
4451 };
4452
4453 let mut levels: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
4455 for (idx, rule) in program.rules.iter().enumerate() {
4456 let has_epistemic = rule
4457 .body
4458 .iter()
4459 .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)));
4460 if !has_epistemic {
4461 continue;
4462 }
4463 let Some(level) = stratum_level.get(rule.head.predicate.as_str()) else {
4464 return Ok(None);
4467 };
4468 levels.entry(*level).or_default().push(idx);
4469 }
4470
4471 if levels.len() < 2 {
4472 return Ok(None);
4475 }
4476
4477 let mut strata = Vec::with_capacity(levels.len());
4478 for (_level, rule_indices) in levels {
4479 let head_predicates: Vec<String> = rule_indices
4480 .iter()
4481 .filter_map(|idx| program.rules.get(*idx))
4482 .map(|rule| rule.head.predicate.clone())
4483 .collect::<BTreeSet<_>>()
4484 .into_iter()
4485 .collect();
4486 let stratum_program =
4487 build_stratum_subprogram(program, &rule_indices, &head_predicates, &stratum_level)?;
4488 strata.push(EpistemicStratum {
4489 head_predicates,
4490 rule_indices,
4491 program: stratum_program,
4492 });
4493 }
4494
4495 for stratum in &strata {
4500 if try_reduce_case_a_recursive_epistemic_program(&stratum.program)?.is_none() {
4501 validate_epistemic_relation_shapes(&stratum.program, &BTreeSet::new())?;
4502 }
4503 }
4504
4505 let ordinary_post_program = build_post_stratification_ordinary_program(program);
4506 ordinary_post_program.validate_prepared_authored_constraint_identity()?;
4507
4508 Ok(Some(EpistemicStratifiedPlan {
4509 strata,
4510 ordinary_post_program,
4511 }))
4512}
4513
4514fn build_post_stratification_ordinary_program(program: &Program) -> Program {
4522 let mut post = program.clone();
4523 post.rules.retain(|rule| {
4524 rule.body
4525 .iter()
4526 .all(|literal| !matches!(literal, BodyLiteral::Epistemic(_)))
4527 });
4528 post.constraints.retain(|constraint| {
4529 constraint
4530 .body
4531 .iter()
4532 .all(|literal| !matches!(literal, BodyLiteral::Epistemic(_)))
4533 });
4534 post
4535}
4536
4537fn stratified_schema_reduction_overrides(
4545 program: &Program,
4546) -> Result<BTreeMap<usize, crate::ast::Rule>> {
4547 let Some(plan) = try_plan_stratified_epistemic_program(program)? else {
4548 return Ok(BTreeMap::new());
4549 };
4550
4551 let mut overrides = BTreeMap::new();
4552 for stratum in plan.strata {
4553 if try_reduce_case_a_recursive_epistemic_program(&stratum.program)?.is_none() {
4554 continue;
4555 }
4556 for rule_index in stratum.rule_indices {
4557 let mut rule = program.rules.get(rule_index).cloned().ok_or_else(|| {
4558 XlogError::Compilation(format!(
4559 "stratified epistemic rule index {rule_index} is outside the source program"
4560 ))
4561 })?;
4562 resolve_recursive_epistemic_rule_modals(&mut rule);
4563 overrides.insert(rule_index, rule);
4564 }
4565 }
4566 Ok(overrides)
4567}
4568
4569fn assign_epistemic_strata(
4577 program: &Program,
4578 determined: &EpistemicallyDeterminedPredicates,
4579) -> Result<Option<BTreeMap<String, usize>>> {
4580 let mut epistemic_heads: BTreeSet<&str> = BTreeSet::new();
4582 for rule in &program.rules {
4583 if rule
4584 .body
4585 .iter()
4586 .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
4587 {
4588 epistemic_heads.insert(rule.head.predicate.as_str());
4589 }
4590 }
4591
4592 let mut modal_edges: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
4604 for rule in &program.rules {
4605 let head = rule.head.predicate.as_str();
4606 for lit in &rule.body {
4607 if let BodyLiteral::Epistemic(modal) = lit {
4608 let target = modal.atom.predicate.as_str();
4609 if epistemic_heads.contains(target) {
4610 if !determined.contains(target) {
4611 return Ok(None);
4614 }
4615 modal_edges.entry(head).or_default().insert(target);
4616 } else if determined.contains(target) {
4617 let support =
4621 epistemic_support_of_determined_ordinary(program, target, &epistemic_heads);
4622 if support.is_empty() {
4623 return Ok(None);
4627 }
4628 let entry = modal_edges.entry(head).or_default();
4629 for support_head in support {
4630 entry.insert(support_head);
4631 }
4632 }
4633 }
4634 }
4635 }
4636
4637 let mut level: BTreeMap<String, usize> = BTreeMap::new();
4640 fn visit<'a>(
4641 head: &'a str,
4642 modal_edges: &BTreeMap<&'a str, BTreeSet<&'a str>>,
4643 level: &mut BTreeMap<String, usize>,
4644 active: &mut BTreeSet<&'a str>,
4645 ) -> Result<usize> {
4646 if let Some(l) = level.get(head) {
4647 return Ok(*l);
4648 }
4649 if !active.insert(head) {
4650 return Err(recursive_epistemic_rejection(
4653 "stratified epistemic planning encountered a modal dependency cycle",
4654 ));
4655 }
4656 let mut l = 0;
4657 if let Some(targets) = modal_edges.get(head) {
4658 for target in targets {
4659 let tl = visit(target, modal_edges, level, active)?;
4660 l = l.max(tl + 1);
4661 }
4662 }
4663 active.remove(head);
4664 level.insert(head.to_string(), l);
4665 Ok(l)
4666 }
4667
4668 for head in &epistemic_heads {
4669 visit(head, &modal_edges, &mut level, &mut BTreeSet::new())?;
4670 }
4671
4672 Ok(Some(level))
4673}
4674
4675fn epistemic_support_of_determined_ordinary<'a>(
4683 program: &'a Program,
4684 predicate: &'a str,
4685 epistemic_heads: &BTreeSet<&'a str>,
4686) -> BTreeSet<&'a str> {
4687 let mut support: BTreeSet<&'a str> = BTreeSet::new();
4688 let mut seen: BTreeSet<&'a str> = BTreeSet::new();
4689 let mut stack: Vec<&'a str> = vec![predicate];
4690 while let Some(current) = stack.pop() {
4691 if !seen.insert(current) {
4692 continue;
4693 }
4694 for rule in &program.rules {
4695 if rule.head.predicate != current || rule.body.is_empty() {
4696 continue;
4697 }
4698 for lit in &rule.body {
4699 let referenced = match lit {
4700 BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) => {
4701 atom.predicate.as_str()
4702 }
4703 BodyLiteral::Epistemic(_)
4706 | BodyLiteral::Comparison(_)
4707 | BodyLiteral::IsExpr(_)
4708 | BodyLiteral::Univ(_) => continue,
4709 };
4710 if epistemic_heads.contains(referenced) {
4711 support.insert(referenced);
4712 } else {
4713 stack.push(referenced);
4715 }
4716 }
4717 }
4718 if epistemic_heads.contains(current) && current != predicate {
4720 support.insert(current);
4721 }
4722 }
4723 support
4724}
4725
4726fn build_stratum_subprogram(
4736 program: &Program,
4737 rule_indices: &[usize],
4738 head_predicates: &[String],
4739 stratum_level: &BTreeMap<String, usize>,
4740) -> Result<Program> {
4741 let this_level = head_predicates
4742 .iter()
4743 .filter_map(|h| stratum_level.get(h))
4744 .copied()
4745 .max()
4746 .unwrap_or(0);
4747
4748 let lower_epistemic_heads: BTreeSet<&str> = stratum_level
4751 .iter()
4752 .filter(|(_, level)| **level < this_level)
4753 .map(|(head, _)| head.as_str())
4754 .collect();
4755
4756 let all_epistemic_heads: BTreeSet<&str> = program
4759 .rules
4760 .iter()
4761 .filter(|rule| {
4762 rule.body
4763 .iter()
4764 .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
4765 })
4766 .map(|rule| rule.head.predicate.as_str())
4767 .collect();
4768
4769 let own_rule_indices: BTreeSet<usize> = rule_indices.iter().copied().collect();
4770
4771 let mut stratum = program.clone();
4772 stratum.rules = program
4773 .rules
4774 .iter()
4775 .enumerate()
4776 .filter_map(|(idx, rule)| {
4777 if own_rule_indices.contains(&idx) {
4778 return Some(rule.clone());
4779 }
4780 if lower_epistemic_heads.contains(rule.head.predicate.as_str()) {
4782 return None;
4783 }
4784 let has_epistemic = rule
4787 .body
4788 .iter()
4789 .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)));
4790 if has_epistemic && !own_rule_indices.contains(&idx) {
4791 return None;
4793 }
4794 let support = epistemic_support_of_determined_ordinary(
4805 program,
4806 rule.head.predicate.as_str(),
4807 &all_epistemic_heads,
4808 );
4809 if support
4810 .iter()
4811 .any(|h| stratum_level.get(*h).copied().unwrap_or(0) >= this_level)
4812 {
4813 return None;
4814 }
4815 Some(rule.clone())
4816 })
4817 .collect();
4818
4819 let head_set: BTreeSet<&str> = head_predicates.iter().map(String::as_str).collect();
4824 stratum.queries.clear();
4825
4826 stratum.constraints = program
4830 .constraints
4831 .iter()
4832 .filter(|constraint| {
4833 let is_ordinary = constraint
4834 .body
4835 .iter()
4836 .all(|literal| !matches!(literal, BodyLiteral::Epistemic(_)));
4837 if is_ordinary {
4838 return false;
4839 }
4840 constraint_predicate_set(constraint)
4841 .iter()
4842 .all(|p| head_set.contains(p.as_str()) || !is_program_head(program, p))
4843 })
4844 .cloned()
4845 .collect();
4846
4847 Ok(stratum)
4848}
4849
4850fn is_program_head(program: &Program, predicate: &str) -> bool {
4851 program
4852 .rules
4853 .iter()
4854 .any(|rule| !rule.body.is_empty() && rule.head.predicate == predicate)
4855}
4856
4857pub fn split_epistemic_program(program: &Program) -> Result<EpistemicSplitPlan> {
4865 Ok(EpistemicSplitPlan {
4879 components: build_epistemic_dependency_graph(program)?.components,
4880 })
4881}
4882
4883pub fn compile_epistemic_gpu_split_execution(
4885 program: &Program,
4886) -> Result<EpistemicSplitExecutablePlan> {
4887 compile_epistemic_gpu_split_execution_with_stats_snapshot(program, None)
4888}
4889
4890pub fn compile_epistemic_gpu_split_execution_with_stats_snapshot(
4897 program: &Program,
4898 stats_snapshot: Option<&StatsSnapshot>,
4899) -> Result<EpistemicSplitExecutablePlan> {
4900 let mut prepared = program.clone();
4901 if prepared.authored_constraint_source_bound.is_some() {
4902 prepared.validate_prepared_authored_constraint_identity()?;
4903 } else {
4904 prepared.prepare_authored_constraint_identity_at_root()?;
4905 }
4906 let program = &prepared;
4907 validate_epistemic_relation_shapes(program, &BTreeSet::new())?;
4908 reject_epistemic_constraints(program)?;
4909 let split_plan = split_epistemic_program(program)?;
4910 let mut components = Vec::new();
4911
4912 for component in &split_plan.components {
4913 if !component_has_epistemic_rule(program, component) {
4914 continue;
4915 }
4916
4917 let coupling = classify_cross_component_modal_coupling(program, component)?;
4926
4927 let component_program = split_component_program(program, component)?;
4928 let executable = compile_epistemic_gpu_execution_inner(
4929 &component_program,
4930 stats_snapshot,
4931 coupling.allows_multiple_output_heads(),
4932 )?;
4933 components.push(EpistemicSplitExecutableComponent {
4934 component: component.clone(),
4935 executable,
4936 });
4937 }
4938
4939 if components.is_empty() {
4940 return Err(XlogError::UnsupportedEpistemicConstruct {
4941 construct: "epistemic GPU split execution".to_string(),
4942 context: "requires at least one epistemic split component".to_string(),
4943 });
4944 }
4945
4946 Ok(EpistemicSplitExecutablePlan {
4947 split_plan,
4948 components,
4949 })
4950}
4951
4952fn component_has_epistemic_rule(
4953 program: &Program,
4954 component: &EpistemicDependencyComponent,
4955) -> bool {
4956 component
4957 .rule_indices
4958 .iter()
4959 .filter_map(|idx| program.rules.get(*idx))
4960 .any(|rule| {
4961 rule.body
4962 .iter()
4963 .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
4964 })
4965}
4966
4967fn component_epistemic_output_heads(
4976 program: &Program,
4977 component: &EpistemicDependencyComponent,
4978) -> Vec<String> {
4979 let mut heads: BTreeSet<String> = BTreeSet::new();
4980 for idx in &component.rule_indices {
4981 let Some(rule) = program.rules.get(*idx) else {
4982 continue;
4983 };
4984 let has_epistemic_body = rule
4985 .body
4986 .iter()
4987 .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)));
4988 if has_epistemic_body {
4989 heads.insert(rule.head.predicate.clone());
4990 }
4991 }
4992 heads.into_iter().collect()
4993}
4994
4995fn format_component_merge_reasons(component: &EpistemicDependencyComponent) -> String {
5003 if component.merge_reasons.is_empty() {
5004 return "no recorded coalesce reason".to_string();
5005 }
5006 component
5007 .merge_reasons
5008 .iter()
5009 .map(|reason| match reason {
5010 EpistemicComponentMergeReason::SharedHeadPredicate { predicate } => {
5011 format!("SharedHeadPredicate({predicate})")
5012 }
5013 EpistemicComponentMergeReason::DerivedPredicate { predicate } => {
5014 format!("DerivedPredicate({predicate})")
5015 }
5016 EpistemicComponentMergeReason::SharedModalPredicate { predicate } => {
5017 format!("SharedModalPredicate({predicate})")
5018 }
5019 EpistemicComponentMergeReason::Constraint { predicates } => {
5020 format!("Constraint({})", predicates.join(", "))
5021 }
5022 })
5023 .collect::<Vec<_>>()
5024 .join(", ")
5025}
5026
5027#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5029enum CrossComponentCoupling {
5030 JointSolvable,
5034}
5035
5036impl CrossComponentCoupling {
5037 fn allows_multiple_output_heads(self) -> bool {
5040 match self {
5041 CrossComponentCoupling::JointSolvable => true,
5042 }
5043 }
5044}
5045
5046fn epistemic_tainted_predicates<'a>(
5084 program: &'a Program,
5085 component: &EpistemicDependencyComponent,
5086 epistemic_heads: &'a [String],
5087) -> BTreeSet<&'a str> {
5088 let mut tainted: BTreeSet<&str> = epistemic_heads.iter().map(String::as_str).collect();
5089 let mut changed = true;
5092 while changed {
5093 changed = false;
5094 for idx in &component.rule_indices {
5095 let Some(rule) = program.rules.get(*idx) else {
5096 continue;
5097 };
5098 if tainted.contains(rule.head.predicate.as_str()) {
5099 continue;
5100 }
5101 let body_touches_tainted = rule.body.iter().any(|lit| {
5105 lit.atom()
5106 .map(|atom| tainted.contains(atom.predicate.as_str()))
5107 .unwrap_or(false)
5108 });
5109 if body_touches_tainted {
5110 tainted.insert(rule.head.predicate.as_str());
5111 changed = true;
5112 }
5113 }
5114 }
5115 tainted
5116}
5117
5118fn classify_cross_component_modal_coupling(
5119 program: &Program,
5120 component: &EpistemicDependencyComponent,
5121) -> Result<CrossComponentCoupling> {
5122 let epistemic_heads = component_epistemic_output_heads(program, component);
5123 if epistemic_heads.len() <= 1 {
5124 return Ok(CrossComponentCoupling::JointSolvable);
5125 }
5126
5127 let tainted = epistemic_tainted_predicates(program, component, &epistemic_heads);
5139
5140 let mut nested_modal_predicates: BTreeSet<String> = BTreeSet::new();
5141 for idx in &component.rule_indices {
5142 let Some(rule) = program.rules.get(*idx) else {
5143 continue;
5144 };
5145 for lit in &rule.body {
5146 if let BodyLiteral::Epistemic(modal) = lit {
5147 if tainted.contains(modal.atom.predicate.as_str()) {
5148 nested_modal_predicates.insert(format!(
5149 "{}/{}",
5150 modal.atom.predicate,
5151 modal.atom.arity()
5152 ));
5153 }
5154 }
5155 }
5156 }
5157
5158 if nested_modal_predicates.is_empty() {
5159 return Ok(CrossComponentCoupling::JointSolvable);
5164 }
5165
5166 Err(XlogError::UnsupportedEpistemicConstruct {
5167 construct: "cross-component epistemic coupling".to_string(),
5168 context: format!(
5169 "epistemic output heads {:?} are coupled into a single dependency \
5170 component (reasons: {}) through nested modal literals over \
5171 epistemic-derived predicates {:?}; the modal truth of an \
5172 epistemic-derived head depends on another head's accepted world view, \
5173 so a single joint world-view enumeration would mis-evaluate the \
5174 nested modality and an independent split would be unsound, so this \
5175 fails closed",
5176 epistemic_heads,
5177 format_component_merge_reasons(component),
5178 nested_modal_predicates.into_iter().collect::<Vec<_>>(),
5179 ),
5180 })
5181}
5182
5183fn split_component_program(
5184 program: &Program,
5185 component: &EpistemicDependencyComponent,
5186) -> Result<Program> {
5187 let mut component_program = program.clone();
5188 let component_predicates: BTreeSet<&str> =
5189 component.predicates.iter().map(String::as_str).collect();
5190 let component_rule_indices: BTreeSet<usize> = component.rule_indices.iter().copied().collect();
5191 let head_predicates: BTreeSet<&str> = program
5192 .rules
5193 .iter()
5194 .map(|rule| rule.head.predicate.as_str())
5195 .collect();
5196 component_program.rules = program
5197 .rules
5198 .iter()
5199 .enumerate()
5200 .filter_map(|(idx, rule)| {
5201 (component_rule_indices.contains(&idx)
5202 || (rule.body.is_empty()
5203 && component_predicates.contains(rule.head.predicate.as_str())))
5204 .then_some(rule.clone())
5205 })
5206 .collect();
5207 component_program.constraints = program
5208 .constraints
5209 .iter()
5210 .filter(|constraint| {
5211 let predicates = constraint_predicate_set(constraint);
5212 let has_component_owned_predicate = predicates
5213 .iter()
5214 .any(|predicate| head_predicates.contains(predicate.as_str()));
5215 !has_component_owned_predicate
5216 || predicates
5217 .iter()
5218 .all(|predicate| component_predicates.contains(predicate.as_str()))
5219 })
5220 .cloned()
5221 .collect();
5222 Ok(component_program)
5223}
5224
5225#[cfg(test)]
5226mod tests {
5227 use super::*;
5228 use crate::ast::{PredColumn, PredDecl, TypeRef};
5229 use crate::parse_program;
5230
5231 #[test]
5232 fn augmented_head_reconciliation_preserves_columns_only_declarations() {
5233 let symbol = TypeRef::Scalar(xlog_core::ScalarType::Symbol);
5234 let wide_integer = TypeRef::Scalar(xlog_core::ScalarType::I64);
5235 let mut program = Program::new();
5236 program.predicates = vec![
5237 PredDecl {
5238 name: "result".to_string(),
5239 types: Vec::new(),
5240 columns: vec![PredColumn {
5241 name: Some("key".to_string()),
5242 typ: symbol.clone(),
5243 }],
5244 is_private: false,
5245 },
5246 PredDecl {
5247 name: "source".to_string(),
5248 types: Vec::new(),
5249 columns: vec![
5250 PredColumn {
5251 name: Some("key".to_string()),
5252 typ: symbol.clone(),
5253 },
5254 PredColumn {
5255 name: Some("value".to_string()),
5256 typ: wide_integer.clone(),
5257 },
5258 ],
5259 is_private: false,
5260 },
5261 ];
5262 program.rules.push(crate::ast::Rule {
5263 head: Atom {
5264 predicate: "result".to_string(),
5265 terms: vec![
5266 Term::Variable("Key".to_string()),
5267 Term::Variable("Value".to_string()),
5268 ],
5269 },
5270 body: vec![BodyLiteral::Positive(Atom {
5271 predicate: "source".to_string(),
5272 terms: vec![
5273 Term::Variable("Key".to_string()),
5274 Term::Variable("Value".to_string()),
5275 ],
5276 })],
5277 });
5278 let resolved = BTreeMap::from([(0, 1)]);
5279
5280 let widened = reconcile_augmented_head_declarations(&mut program, &resolved)
5281 .expect("reconcile augmented declaration");
5282 let declaration = program
5283 .predicates
5284 .iter()
5285 .find(|declaration| declaration.name == "result")
5286 .unwrap();
5287
5288 assert_eq!(widened.get(&("result".to_string(), 1)), Some(&2));
5289 assert_eq!(declaration.arity(), 2);
5290 assert_eq!(
5291 declaration.types,
5292 vec![symbol.clone(), wide_integer.clone()]
5293 );
5294 assert_eq!(
5295 declaration
5296 .columns
5297 .iter()
5298 .map(|column| column.typ.clone())
5299 .collect::<Vec<_>>(),
5300 vec![symbol, wide_integer]
5301 );
5302 }
5303
5304 #[test]
5305 fn augmented_head_reconciliation_uses_body_declarations_by_name_and_arity() {
5306 let program = parse_program(
5307 r#"
5308 #pragma epistemic_mode = faeel
5309 pred node(symbol).
5310 pred source(symbol, i64).
5311 pred source(u32).
5312 pred result(symbol).
5313 node(key).
5314 source(key, 5000000000).
5315 source(1).
5316 result(X) :- node(X), know source(X, Y).
5317 "#,
5318 )
5319 .expect("parse same-name multi-arity schema fixture");
5320
5321 let reduced = reduce_epistemic_program_to_ordinary_for_stratified_schema(&program)
5322 .expect("same-name body declarations should reduce");
5323 let result = reduced
5324 .predicates
5325 .iter()
5326 .find(|declaration| declaration.name == "result")
5327 .expect("missing result declaration");
5328 let types = result
5329 .schema_columns()
5330 .into_iter()
5331 .map(|column| column.typ)
5332 .collect::<Vec<_>>();
5333
5334 assert_eq!(
5335 types,
5336 vec![
5337 TypeRef::Scalar(xlog_core::ScalarType::Symbol),
5338 TypeRef::Scalar(xlog_core::ScalarType::I64),
5339 ]
5340 );
5341 assert!(reduced
5342 .predicates
5343 .iter()
5344 .any(|declaration| declaration.name == "source/2"));
5345 assert!(reduced
5346 .predicates
5347 .iter()
5348 .any(|declaration| declaration.name == "source/1"));
5349 crate::compile::Compiler::new()
5350 .compile_program(&reduced)
5351 .expect("arity-qualified reduced program should compile");
5352 compile_epistemic_gpu_execution(&program)
5353 .expect("production epistemic compilation should use the exact source signature");
5354 }
5355
5356 #[test]
5357 fn augmented_head_reconciliation_uses_undeclared_fixed_point_schemas() {
5358 let sources = [
5359 r#"
5360 #pragma epistemic_mode = faeel
5361 pred node(symbol).
5362 pred result(symbol).
5363 node(key).
5364 edge(key, 5000000000).
5365 result(X) :- node(X), know edge(X, Y).
5366 "#,
5367 r#"
5368 #pragma epistemic_mode = faeel
5369 pred node(symbol).
5370 pred result(symbol).
5371 node(key).
5372 raw(key, 5000000000).
5373 edge(X, Y) :- raw(X, Y).
5374 result(X) :- node(X), know edge(X, Y).
5375 "#,
5376 ];
5377
5378 for source in sources {
5379 let program = parse_program(source).expect("parse inferred-schema fixture");
5380 let reduced = reduce_epistemic_program_to_ordinary(&program)
5381 .expect("inferred modal source should reduce");
5382 let result = reduced
5383 .predicates
5384 .iter()
5385 .find(|declaration| declaration.name == "result")
5386 .expect("missing result declaration");
5387 assert_eq!(
5388 result
5389 .schema_columns()
5390 .into_iter()
5391 .map(|column| column.typ)
5392 .collect::<Vec<_>>(),
5393 vec![
5394 TypeRef::Scalar(xlog_core::ScalarType::Symbol),
5395 TypeRef::Scalar(xlog_core::ScalarType::I64),
5396 ]
5397 );
5398 crate::compile::Compiler::new()
5399 .compile_program(&reduced)
5400 .expect("fixed-point inferred hidden-column type should compile");
5401 }
5402 }
5403
5404 #[test]
5405 fn augmented_head_reconciliation_uses_arithmetic_binding_type() {
5406 let program = parse_program(
5407 r#"
5408 #pragma epistemic_mode = faeel
5409 pred node(symbol).
5410 pred allowed(u64).
5411 pred result(symbol).
5412 node(key).
5413 allowed(1).
5414 result(X) :- node(X), Y is cast(1, u64), not know allowed(Y).
5415 "#,
5416 )
5417 .expect("parse arithmetic-binding fixture");
5418
5419 let reduced = reduce_epistemic_program_to_ordinary(&program)
5420 .expect("arithmetic-bound hidden column should reduce");
5421 let result = reduced
5422 .predicates
5423 .iter()
5424 .find(|declaration| declaration.name == "result")
5425 .expect("missing result declaration");
5426 assert_eq!(
5427 result
5428 .schema_columns()
5429 .into_iter()
5430 .map(|column| column.typ)
5431 .collect::<Vec<_>>(),
5432 vec![
5433 TypeRef::Scalar(xlog_core::ScalarType::Symbol),
5434 TypeRef::Scalar(xlog_core::ScalarType::U64),
5435 ]
5436 );
5437 crate::compile::Compiler::new()
5438 .compile_program(&reduced)
5439 .expect("arithmetic-bound hidden-column type should compile");
5440 }
5441
5442 #[test]
5443 fn augmented_head_reconciliation_widens_only_the_original_signature() {
5444 let mut reduced = parse_program(
5445 r#"
5446 pred edge(symbol, i64).
5447 pred triple(symbol, i64, u32).
5448 pred result(symbol, i64, u32).
5449 pred result(symbol).
5450 result(X, Y, Z) :- triple(X, Y, Z).
5451 result(X, Y) :- edge(X, Y).
5452 ?- result(A, B, C).
5453 ?- result(X).
5454 "#,
5455 )
5456 .expect("parse exact-signature reconciliation fixture");
5457
5458 let augmented =
5459 reconcile_augmented_head_declarations(&mut reduced, &BTreeMap::from([(1, 1)]))
5460 .expect("reconcile exact augmented signature");
5461 let declaration_arities = reduced
5462 .predicates
5463 .iter()
5464 .filter(|declaration| declaration.name == "result")
5465 .map(PredDecl::arity)
5466 .collect::<Vec<_>>();
5467 let rule_arities = reduced
5468 .rules
5469 .iter()
5470 .filter(|rule| rule.head.predicate == "result")
5471 .map(|rule| rule.head.arity())
5472 .collect::<Vec<_>>();
5473 let query_arities = reduced
5474 .queries
5475 .iter()
5476 .filter(|query| query.atom.predicate == "result")
5477 .map(|query| query.atom.arity())
5478 .collect::<Vec<_>>();
5479
5480 assert_eq!(augmented.get(&("result".to_string(), 1)), Some(&2));
5481 assert_eq!(declaration_arities, vec![3, 2]);
5482 assert_eq!(rule_arities, vec![3, 2]);
5483 assert_eq!(query_arities, vec![3, 1]);
5484 }
5485
5486 #[test]
5487 fn augmented_undeclared_head_removes_its_original_query() {
5488 let program = parse_program(
5489 r#"
5490 #pragma epistemic_mode = faeel
5491 node(key).
5492 edge(key, 5000000000).
5493 result(X) :- node(X), know edge(X, Y).
5494 ?- result(X).
5495 "#,
5496 )
5497 .expect("parse undeclared augmented-head fixture");
5498
5499 let reduced = reduce_epistemic_program_to_ordinary_for_stratified_schema(&program)
5500 .expect("undeclared augmented head should reduce");
5501
5502 assert!(reduced
5503 .rules
5504 .iter()
5505 .any(|rule| rule.head.predicate == "result" && rule.head.arity() == 2));
5506 assert!(!reduced
5507 .queries
5508 .iter()
5509 .any(|query| query.atom.predicate == "result" && query.atom.arity() == 1));
5510 }
5511
5512 #[test]
5513 fn divergent_augmented_head_arities_are_rejected_before_reduction() {
5514 let different_modal_widths = r#"
5515 #pragma epistemic_mode = faeel
5516 pred node(symbol).
5517 pred edge(symbol, i64).
5518 pred triple(symbol, i64, u32).
5519 pred result(symbol).
5520 node(key).
5521 edge(key, 5000000000).
5522 triple(key, 5000000000, 1).
5523 result(X) :- node(X), know edge(X, Y).
5524 result(X) :- node(X), know triple(X, Y, Z).
5525 "#;
5526 let ordinary_sibling = r#"
5527 #pragma epistemic_mode = faeel
5528 pred base(symbol).
5529 pred node(symbol).
5530 pred edge(symbol, i64).
5531 pred result(symbol).
5532 base(key).
5533 node(key).
5534 edge(key, 5000000000).
5535 result(X) :- base(X).
5536 result(X) :- node(X), know edge(X, Y).
5537 "#;
5538
5539 for source in [different_modal_widths, ordinary_sibling] {
5540 let program = parse_program(source).expect("parse divergent augmented-head fixture");
5541 let errors = [
5542 plan_epistemic_gpu_execution(&program)
5543 .expect_err("GPU planning must reject divergent reduced arities"),
5544 reduce_epistemic_program_to_ordinary(&program)
5545 .expect_err("execution reduction must reject divergent arities"),
5546 reduce_epistemic_program_to_ordinary_for_stratified_schema(&program)
5547 .expect_err("schema reduction must reject divergent arities"),
5548 ];
5549
5550 for error in errors {
5551 let message = error.to_string();
5552 assert!(message.contains("epistemic augmented predicate schema"));
5553 assert!(message.contains("result/1"), "{message}");
5554 assert!(
5555 message.contains("incompatible internal arities"),
5556 "{message}"
5557 );
5558 }
5559 }
5560 }
5561
5562 #[test]
5563 fn single_pass_epistemic_rule_unions_without_clause_provenance_are_rejected() {
5564 let fixtures = [
5565 r#"
5566 #pragma epistemic_mode = faeel
5567 pred p().
5568 pred q().
5569 pred result(symbol).
5570 q().
5571 result(a) :- know p().
5572 result(b) :- know q().
5573 ?- result(X).
5574 "#,
5575 r#"
5576 #pragma epistemic_mode = faeel
5577 pred q().
5578 pred result(symbol).
5579 result(a).
5580 result(b) :- know q().
5581 ?- result(X).
5582 "#,
5583 ];
5584
5585 for source in fixtures {
5586 let program = parse_program(source).expect("parse epistemic rule-union fixture");
5587 let errors = [
5588 plan_epistemic_gpu_execution(&program)
5589 .expect_err("GPU planning must reject a provenance-free rule union"),
5590 reduce_epistemic_program_to_ordinary(&program)
5591 .expect_err("execution reduction must reject a provenance-free rule union"),
5592 reduce_epistemic_program_to_ordinary_for_stratified_schema(&program)
5593 .expect_err("schema reduction must reject a provenance-free rule union"),
5594 ];
5595 for error in errors {
5596 let message = error.to_string();
5597 assert!(message.contains("epistemic rule-union materialization"));
5598 assert!(message.contains("result/1"), "{message}");
5599 assert!(message.contains("per-clause modal provenance"), "{message}");
5600 }
5601 }
5602 }
5603
5604 #[test]
5605 fn equivalent_epistemic_rule_union_filters_are_distributive() {
5606 let program = parse_program(
5607 r#"
5608 #pragma epistemic_mode = faeel
5609 pred target(u32).
5610 pred left(u32).
5611 pred right(u32).
5612 pred result(u32).
5613 target(1).
5614 target(2).
5615 left(1).
5616 right(2).
5617 result(X) :- left(X), know target(X).
5618 result(Y) :- right(Y), possible target(Y).
5619 ?- result(Value).
5620 "#,
5621 )
5622 .expect("parse equivalent-filter rule union");
5623
5624 plan_epistemic_gpu_execution(&program)
5625 .expect("equivalent invariant modal filters must distribute over the rule union");
5626 reduce_epistemic_program_to_ordinary(&program)
5627 .expect("execution reduction must preserve an equivalent-filter rule union");
5628 reduce_epistemic_program_to_ordinary_for_stratified_schema(&program)
5629 .expect("schema reduction must preserve an equivalent-filter rule union");
5630 }
5631
5632 #[test]
5633 fn independently_founded_ground_gates_are_distributive_across_rule_union() {
5634 let program = parse_program(
5635 r#"
5636 #pragma epistemic_mode = g91
5637 pred left(u32).
5638 pred right(u32).
5639 pred result(u32).
5640 left(1).
5641 right(1).
5642 result(1) :- possible left(1).
5643 result(2) :- possible right(1).
5644 ?- result(Value).
5645 "#,
5646 )
5647 .expect("parse independently founded ground-gate rule union");
5648
5649 plan_epistemic_gpu_execution(&program)
5650 .expect("independently founded ground gates must distribute over the rule union");
5651 reduce_epistemic_program_to_ordinary(&program)
5652 .expect("execution reduction must preserve independently founded ground gates");
5653 }
5654
5655 #[test]
5656 fn g91_exact_head_possible_union_preserves_compatibility_self_support() {
5657 let program = parse_program(
5658 r#"
5659 #pragma epistemic_mode = g91
5660 pred seed(u32).
5661 pred node(u32).
5662 pred p(u32).
5663 seed(1).
5664 node(2).
5665 p(X) :- seed(X).
5666 p(X) :- node(X), possible p(X).
5667 ?- p(X).
5668 "#,
5669 )
5670 .expect("parse G91 exact-head possibility union");
5671
5672 assert_eq!(
5673 classify_recursive_epistemic_program(&program).unwrap(),
5674 RecursiveEpistemicClass::ModalCycle
5675 );
5676 let prepared = prepare_epistemic_program(&program).expect("validate G91 source");
5677 let compatibility = try_prepare_g91_compatibility_reduction(&prepared)
5678 .expect("G91 modal cycle must be admitted")
5679 .expect("G91 modal cycle must use an explicit compatibility fixpoint");
5680 Compiler::new()
5681 .compile_program(compatibility.upper_bound_program())
5682 .expect("G91 upper-bound program must compile");
5683 Compiler::new()
5684 .compile_program(compatibility.refinement_program())
5685 .expect("declared G91 snapshot program must compile");
5686 assert_eq!(compatibility.snapshot_relations().len(), 1);
5687 }
5688
5689 #[test]
5690 fn g91_compatibility_applies_to_exact_tuples_across_one_recursive_component() {
5691 let program = parse_program(
5692 r#"
5693 #pragma epistemic_mode = g91
5694 pred domain(u32).
5695 pred p(u32).
5696 pred q(u32).
5697 domain(1).
5698 p(X) :- domain(X), possible q(X).
5699 q(X) :- domain(X), possible p(X).
5700 ?- p(X).
5701 ?- q(X).
5702 "#,
5703 )
5704 .expect("parse mutual G91 modal component");
5705
5706 let prepared = prepare_epistemic_program(&program).expect("validate mutual G91 source");
5707 let compatibility = try_prepare_g91_compatibility_reduction(&prepared)
5708 .expect("mutual G91 modal component must be admitted")
5709 .expect("mutual G91 modal component must use compatibility iteration");
5710 for rule in compatibility
5711 .upper_bound_program()
5712 .rules
5713 .iter()
5714 .filter(|rule| matches!(rule.head.predicate.as_str(), "p" | "q"))
5715 {
5716 assert!(
5717 rule.body
5718 .iter()
5719 .any(|literal| matches!(literal, BodyLiteral::Comparison(_))),
5720 "the exact tuple compatibility edge must become a tautological conjunct: {rule:?}"
5721 );
5722 assert!(
5723 !rule.body.iter().any(|literal| {
5724 matches!(literal, BodyLiteral::Positive(atom) if atom.predicate == "p" || atom.predicate == "q")
5725 }),
5726 "a compatibility edge must not become an ordinary recursive join: {rule:?}"
5727 );
5728 }
5729 for rule in compatibility
5730 .refinement_program()
5731 .rules
5732 .iter()
5733 .filter(|rule| matches!(rule.head.predicate.as_str(), "p" | "q"))
5734 {
5735 assert!(rule.body.iter().any(|literal| {
5736 matches!(literal, BodyLiteral::Positive(atom) if atom.predicate.starts_with("__xlog_g91_snapshot_"))
5737 }));
5738 assert!(!rule
5739 .body
5740 .iter()
5741 .any(|literal| matches!(literal, BodyLiteral::Epistemic(_))));
5742 }
5743 }
5744
5745 #[test]
5746 fn g91_snapshot_names_avoid_programmatic_relation_collisions() {
5747 let mut program = parse_program("pred p(u32).").expect("parse source relation");
5748 program.predicates.push(PredDecl {
5749 name: "__xlog_g91_snapshot_p".to_string(),
5750 types: vec![TypeRef::Scalar(xlog_core::ScalarType::U32)],
5751 columns: vec![PredColumn {
5752 name: None,
5753 typ: TypeRef::Scalar(xlog_core::ScalarType::U32),
5754 }],
5755 is_private: false,
5756 });
5757 let target = "p".to_string();
5758 let names = g91_snapshot_relation_names(&program, std::iter::once(&target));
5759 assert_eq!(
5760 names.get("p").map(String::as_str),
5761 Some("__xlog_g91_snapshot_p_0")
5762 );
5763 }
5764
5765 #[test]
5766 fn g91_compatibility_rejects_recursive_aggregation_in_the_selected_component() {
5767 let program = parse_program(
5768 r#"
5769 #pragma epistemic_mode = g91
5770 pred seed(u32).
5771 pred p(u32).
5772 pred totals(u64).
5773 seed(1).
5774 p(X) :- seed(X), possible p(X).
5775 p(X) :- seed(X), totals(_).
5776 totals(count(X)) :- p(X).
5777 ?- p(X).
5778 "#,
5779 )
5780 .expect("parse recursive aggregate compatibility fixture");
5781
5782 let prepared = prepare_epistemic_program(&program).expect("validate G91 source");
5783 let error = try_prepare_g91_compatibility_reduction(&prepared)
5784 .expect_err("recursive aggregation makes compatibility refinement non-monotone");
5785 let message = error.to_string();
5786 assert!(message.contains("Gelfond-1991 compatibility"), "{message}");
5787 assert!(message.contains("aggregate"), "{message}");
5788 assert!(message.contains("totals"), "{message}");
5789 }
5790
5791 #[test]
5792 fn g91_compatibility_rejects_recursive_negation_in_the_selected_component() {
5793 for negated_dependency in [
5794 "not blocked(X)",
5795 "not possible blocked(X)",
5796 "not know blocked(X)",
5797 ] {
5798 let program = parse_program(&format!(
5799 r#"
5800 #pragma epistemic_mode = g91
5801 pred seed(u32).
5802 pred p(u32).
5803 pred blocked(u32).
5804 seed(1).
5805 p(X) :- seed(X), possible p(X).
5806 p(X) :- seed(X), {negated_dependency}.
5807 blocked(X) :- p(X).
5808 ?- p(X).
5809 "#,
5810 ))
5811 .expect("parse recursive negation compatibility fixture");
5812
5813 let prepared = prepare_epistemic_program(&program).expect("validate G91 source");
5814 let error = match try_prepare_g91_compatibility_reduction(&prepared) {
5815 Err(error) => error,
5816 Ok(_) => panic!(
5817 "recursive dependency `{negated_dependency}` must make compatibility \
5818 refinement non-monotone"
5819 ),
5820 };
5821 let message = error.to_string();
5822 assert!(message.contains("Gelfond-1991 compatibility"), "{message}");
5823 assert!(message.contains("negation"), "{message}");
5824 assert!(message.contains("p"), "{message}");
5825 }
5826 }
5827
5828 #[test]
5829 fn source_validation_combines_modal_and_arithmetic_type_evidence_before_elision() {
5830 let program = parse_program(
5831 r#"
5832 #pragma epistemic_mode = faeel
5833 pred p(u32).
5834 p(X) :- X is cast(1, u64), possible p(X).
5835 ?- p(X).
5836 "#,
5837 )
5838 .expect("parse arithmetic and modal type-conflict fixture");
5839
5840 let error = prepare_epistemic_program(&program)
5841 .expect_err("foundedness must not hide the authored type conflict");
5842 let message = error.to_string();
5843 assert!(message.contains("Type mismatch"), "{message}");
5844 assert!(
5845 message.contains("U32") && message.contains("U64"),
5846 "{message}"
5847 );
5848 }
5849
5850 #[test]
5851 fn source_validation_uses_lowerer_arithmetic_order_before_elision() {
5852 let program = parse_program(
5853 r#"
5854 #pragma epistemic_mode = faeel
5855 pred p(i64).
5856 p(X) :- X is Y + 1, Y is 1, possible p(X).
5857 "#,
5858 )
5859 .expect("parse reversed arithmetic dependency fixture");
5860
5861 let error = prepare_epistemic_program(&program)
5862 .expect_err("a later arithmetic binding cannot retroactively validate an earlier one");
5863 assert!(
5864 error.to_string().contains("variable X not bound"),
5865 "{error}"
5866 );
5867 }
5868
5869 #[test]
5870 fn source_validation_rejects_structured_modal_arity_before_rule_elision() {
5871 for mode in ["faeel", "g91"] {
5872 let program = parse_program(&format!(
5873 r#"
5874 #pragma epistemic_mode = {mode}
5875 pred p(list<symbol>).
5876 p([a, b]) :- possible p([a, b]).
5877 ?- p(X).
5878 "#
5879 ))
5880 .expect("parse structured exact-self-support fixture");
5881
5882 let error = prepare_epistemic_program(&program)
5883 .expect_err("a flattened modal key must match its authored target arity");
5884 let message = error.to_string();
5885 assert!(message.contains("epistemic modal tuple key"), "{message}");
5886 assert!(message.contains("target arity 1"), "{message}");
5887 assert!(message.contains("binding arity 2"), "{message}");
5888 }
5889 }
5890
5891 #[test]
5892 fn source_validation_accepts_structured_key_matching_flat_target_arity() {
5893 let program = parse_program(
5894 r#"
5895 #pragma epistemic_mode = faeel
5896 pred host(u32, u32).
5897 pred watched(u32, u32).
5898 pred out(u32, u32).
5899 host(1, 2).
5900 watched(1, 2).
5901 out(X, Y) :- host(X, Y), know watched([X, Y]).
5902 ?- out(X, Y).
5903 "#,
5904 )
5905 .expect("parse matching structured modal key fixture");
5906
5907 validate_epistemic_source_program(&program)
5908 .expect("a two-element structured key must address a binary target");
5909
5910 let multi_arity = epistemic_extensional_multi_arity_predicates(&program);
5911 assert!(
5912 !multi_arity.contains("watched"),
5913 "a two-column structured modal key and watched/2 are one signature"
5914 );
5915 }
5916
5917 #[test]
5918 fn invariant_analysis_treats_shared_acyclic_dependencies_as_a_diamond() {
5919 let program = parse_program(
5920 r#"
5921 pred base(u32).
5922 pred left(u32).
5923 pred right(u32).
5924 pred joined(u32).
5925 pred out(u32).
5926 base(1).
5927 left(X) :- base(X).
5928 right(X) :- base(X).
5929 joined(X) :- left(X), right(X).
5930 out(X) :- possible joined(X).
5931 ?- out(X).
5932 "#,
5933 )
5934 .expect("parse invariant diamond fixture");
5935
5936 let invariant = InvariantRelations::analyze(&program);
5937 assert!(invariant.is_invariant("joined"));
5938 validate_epistemic_source_program(&program)
5939 .expect("the positive modal over an acyclic diamond must bind its output");
5940 reduce_epistemic_program_to_ordinary(&program)
5941 .expect("the invariant modal binder must reduce to an ordinary join");
5942 }
5943
5944 #[test]
5945 fn positive_modal_binders_are_independent_of_conjunct_order() {
5946 for mode in ["faeel", "g91"] {
5947 for modal_body in [
5948 "possible p(X), possible base(X)",
5949 "possible base(X), possible p(X)",
5950 ] {
5951 let program = parse_program(&format!(
5952 r#"
5953 #pragma epistemic_mode = {mode}
5954 pred base(u32).
5955 pred p(u32).
5956 base(1).
5957 p(X) :- {modal_body}.
5958 ?- p(X).
5959 "#
5960 ))
5961 .expect("parse modal binder ordering fixture");
5962
5963 validate_epistemic_source_program(&program).unwrap_or_else(|error| {
5964 panic!("{mode} body `{modal_body}` must be range-restricted: {error}")
5965 });
5966 }
5967 }
5968 }
5969
5970 #[test]
5971 fn negated_exact_modal_cycles_are_never_removed_as_foundedness_elision() {
5972 let program = parse_program(
5973 r#"
5974 #pragma epistemic_mode = faeel
5975 pred p().
5976 p() :- not possible p().
5977 ?- p().
5978 "#,
5979 )
5980 .expect("parse negated exact modal cycle");
5981
5982 let prepared = prepare_epistemic_program(&program)
5983 .expect("negated modal cycle must survive source preparation");
5984 assert!(!prepared.removed_unfounded_rules());
5985 assert!(prepared.active_program().rules.iter().any(|rule| {
5986 rule.body
5987 .iter()
5988 .any(|literal| matches!(literal, BodyLiteral::Epistemic(modal) if modal.negated))
5989 }));
5990 }
5991
5992 #[test]
5993 fn modal_fixpoint_reduction_preserves_non_bijective_sibling_unions() {
5994 let fixtures = [
5995 r#"
5996 #pragma epistemic_mode = faeel
5997 pred domain(symbol).
5998 pred other(symbol, symbol).
5999 pred p(symbol, symbol).
6000 domain(a).
6001 other(c, d).
6002 p(X, X) :- domain(X).
6003 p(A, B) :- other(A, B).
6004 p(X, X) :- domain(X), know p(X, X).
6005 ?- p(A, B).
6006 "#,
6007 r#"
6008 #pragma epistemic_mode = faeel
6009 pred left(symbol).
6010 pred right(symbol).
6011 pred p(symbol, symbol).
6012 left(x).
6013 right(y).
6014 p(a, X) :- left(X).
6015 p(b, Y) :- right(Y).
6016 p(a, X) :- left(X), know p(a, X).
6017 ?- p(A, B).
6018 "#,
6019 ];
6020
6021 for source in fixtures {
6022 let program = parse_program(source).expect("parse non-bijective rule union");
6023 assert_eq!(
6024 classify_recursive_epistemic_program(&program).unwrap(),
6025 RecursiveEpistemicClass::ModalCycle
6026 );
6027 let reduced = try_reduce_case_a_recursive_epistemic_program(&program)
6028 .expect("modal-cycle sibling union must be admitted")
6029 .expect("modal-cycle sibling union must reduce to a fixpoint");
6030 Compiler::new()
6031 .compile_program(&reduced)
6032 .expect("ordinary fixpoint preserves per-clause sibling rows");
6033 }
6034 }
6035
6036 #[test]
6037 fn constrained_support_does_not_found_a_wider_modal_domain() {
6038 let program = parse_program(
6039 r#"
6040 #pragma epistemic_mode = faeel
6041 pred seed(u32).
6042 pred p(u32).
6043 seed(1).
6044 seed(2).
6045 p(X) :- seed(X), X = 1.
6046 p(X) :- seed(X), possible p(X).
6047 ?- p(X).
6048 "#,
6049 )
6050 .expect("parse constrained foundedness program");
6051
6052 let reduced = reduce_epistemic_program_to_ordinary(&program)
6053 .expect("the unfounded modal clause must be removed, not rejected");
6054 assert_eq!(
6055 reduced
6056 .rules
6057 .iter()
6058 .filter(|rule| rule.head.predicate == "p" && !rule.body.is_empty())
6059 .count(),
6060 1,
6061 "a constrained support clause cannot found the wider self-support domain"
6062 );
6063 }
6064
6065 #[test]
6066 fn derived_predicate_source_arity_collisions_are_rejected() {
6067 let fixtures = [
6068 r#"
6069 #pragma epistemic_mode = faeel
6070 unary(a).
6071 binary(a, b).
6072 result(X) :- unary(X), know unary(X).
6073 result(X, Y) :- binary(X, Y), know binary(X, Y).
6074 "#,
6075 r#"
6076 #pragma epistemic_mode = faeel
6077 node(key).
6078 edge(key, 5000000000).
6079 result(X) :- node(X), know edge(X, Y).
6080 ?- result(A, B).
6081 "#,
6082 r#"
6083 #pragma epistemic_mode = faeel
6084 node(key).
6085 edge(key, 5000000000).
6086 result(X) :- node(X), know edge(X, Y).
6087 observer(X) :- result(X, Y).
6088 "#,
6089 ];
6090
6091 for source in fixtures {
6092 let program = parse_program(source).expect("parse source-arity collision fixture");
6093 let errors = [
6094 plan_epistemic_gpu_execution(&program)
6095 .expect_err("GPU planning must reject derived source-arity collisions"),
6096 reduce_epistemic_program_to_ordinary(&program)
6097 .expect_err("execution reduction must reject source-arity collisions"),
6098 reduce_epistemic_program_to_ordinary_for_stratified_schema(&program)
6099 .expect_err("schema reduction must reject source-arity collisions"),
6100 ];
6101 for error in errors {
6102 let message = error.to_string();
6103 assert!(
6104 message.contains("epistemic derived predicate schema"),
6105 "{message}"
6106 );
6107 assert!(message.contains("result"), "{message}");
6108 assert!(message.contains("{1, 2}"), "{message}");
6109 }
6110 }
6111 }
6112
6113 #[test]
6114 fn constrained_augmented_head_queries_are_rejected() {
6115 let fixtures = [
6116 r#"
6117 #pragma epistemic_mode = faeel
6118 node(key).
6119 edge(key, 5000000000).
6120 result(X) :- node(X), know edge(X, Y).
6121 ?- result(other).
6122 "#,
6123 r#"
6124 #pragma epistemic_mode = faeel
6125 pair(left, right).
6126 edge(left, 5000000000).
6127 result(X, Y) :- pair(X, Y), know edge(X, Z).
6128 ?- result(Value, Value).
6129 "#,
6130 r#"
6131 #pragma epistemic_mode = faeel
6132 node(key).
6133 edge(key, 5000000000).
6134 allowed(5000000000).
6135 result(X) :- node(X), edge(X, Y), know allowed(Y).
6136 ?- result(key).
6137 "#,
6138 ];
6139
6140 for source in fixtures {
6141 let program = parse_program(source).expect("parse constrained-query fixture");
6142 let errors = [
6143 plan_epistemic_gpu_execution(&program)
6144 .expect_err("GPU planning must reject a constrained augmented query"),
6145 reduce_epistemic_program_to_ordinary(&program)
6146 .expect_err("execution reduction must reject a constrained augmented query"),
6147 reduce_epistemic_program_to_ordinary_for_stratified_schema(&program)
6148 .expect_err("schema reduction must reject a constrained augmented query"),
6149 ];
6150 for error in errors {
6151 let message = error.to_string();
6152 assert!(
6153 message.contains("epistemic augmented head query"),
6154 "{message}"
6155 );
6156 assert!(message.contains("distinct named variables"), "{message}");
6157 }
6158 }
6159 }
6160
6161 #[test]
6162 fn removed_unfounded_sibling_does_not_create_an_augmented_arity_conflict() {
6163 let program = parse_program(
6164 r#"
6165 #pragma epistemic_mode = faeel
6166 pred node(symbol).
6167 pred edge(symbol, i64).
6168 pred result(symbol).
6169 node(key).
6170 edge(key, 5000000000).
6171 result(X) :- node(X), know edge(X, Y).
6172 result(key) :- possible result(key).
6173 "#,
6174 )
6175 .expect("parse foundedness fixture");
6176
6177 let reduced = reduce_epistemic_program_to_ordinary(&program)
6178 .expect("removed unfounded support must not affect the surviving relation shape");
6179 let result_rules = reduced
6180 .rules
6181 .iter()
6182 .filter(|rule| rule.head.predicate == "result")
6183 .collect::<Vec<_>>();
6184 assert_eq!(result_rules.len(), 1);
6185 assert_eq!(result_rules[0].head.arity(), 1);
6186 }
6187
6188 #[test]
6189 fn ordinary_bound_appended_columns_participate_in_shape_validation() {
6190 let program = parse_program(
6191 r#"
6192 #pragma epistemic_mode = faeel
6193 pred node(symbol).
6194 pred edge(symbol, i64).
6195 pred allowed(i64).
6196 pred result(symbol).
6197 node(key).
6198 edge(key, 5000000000).
6199 allowed(5000000000).
6200 result(X) :- node(X).
6201 result(X) :- node(X), edge(X, Y), know allowed(Y).
6202 ?- result(X).
6203 "#,
6204 )
6205 .expect("parse ordinary-bound augmentation fixture");
6206
6207 let errors = [
6208 plan_epistemic_gpu_execution(&program)
6209 .expect_err("GPU planning must reject divergent internal arities"),
6210 reduce_epistemic_program_to_ordinary(&program)
6211 .expect_err("execution reduction must reject divergent internal arities"),
6212 reduce_epistemic_program_to_ordinary_for_stratified_schema(&program)
6213 .expect_err("schema reduction must reject divergent internal arities"),
6214 ];
6215 for error in errors {
6216 let message = error.to_string();
6217 assert!(
6218 message.contains("epistemic augmented predicate schema"),
6219 "{message}"
6220 );
6221 assert!(message.contains("result/1"), "{message}");
6222 assert!(message.contains("{1, 2}"), "{message}");
6223 }
6224 }
6225
6226 #[test]
6227 fn ordinary_bound_appended_columns_widen_the_internal_declaration() {
6228 let program = parse_program(
6229 r#"
6230 #pragma epistemic_mode = faeel
6231 pred node(symbol).
6232 pred edge(symbol, i64).
6233 pred allowed(i64).
6234 pred result(symbol).
6235 node(key).
6236 edge(key, 5000000000).
6237 allowed(5000000000).
6238 result(X) :- node(X), edge(X, Y), know allowed(Y).
6239 ?- result(X).
6240 "#,
6241 )
6242 .expect("parse ordinary-bound declaration fixture");
6243
6244 let reduced = reduce_epistemic_program_to_ordinary(&program)
6245 .expect("uniform ordinary-bound augmentation should reduce");
6246 let declaration = reduced
6247 .predicates
6248 .iter()
6249 .find(|declaration| declaration.name == "result")
6250 .expect("missing result declaration");
6251 assert_eq!(declaration.arity(), 2);
6252 crate::compile::Compiler::new()
6253 .compile_program(&reduced)
6254 .expect("reconciled ordinary-bound augmentation should compile");
6255 }
6256
6257 #[test]
6258 fn stratified_schema_reduction_uses_the_recursive_stratum_reducer() {
6259 let program = parse_program(
6260 r#"
6261 #pragma epistemic_mode = faeel
6262 pred node(u32).
6263 pred edge(u32, u32).
6264 pred accepted_edge(u32, u32).
6265 pred reach(u32, u32).
6266 node(1).
6267 node(2).
6268 node(3).
6269 edge(1, 2).
6270 edge(2, 3).
6271 accepted_edge(X, Y) :- node(X), node(Y), know edge(X, Y).
6272 reach(X, Y) :- node(X), node(Y), know accepted_edge(X, Y).
6273 reach(X, Z) :- reach(X, Y), node(Z), know accepted_edge(Y, Z).
6274 ?- reach(X, Z).
6275 "#,
6276 )
6277 .expect("parse stratified recursive fixture");
6278
6279 let plan = try_plan_stratified_epistemic_program(&program)
6280 .expect("stratified planning should succeed")
6281 .expect("fixture requires stratified execution");
6282 assert_eq!(plan.strata.len(), 2);
6283
6284 let reduced = reduce_epistemic_program_to_ordinary_for_stratified_schema(&program)
6285 .expect("schema reduction should follow each stratum's executable reducer");
6286 assert!(reduced
6287 .rules
6288 .iter()
6289 .filter(|rule| rule.head.predicate == "reach")
6290 .all(|rule| rule.head.arity() == 2));
6291 assert_eq!(
6292 reduced
6293 .predicates
6294 .iter()
6295 .find(|declaration| declaration.name == "reach")
6296 .expect("missing reach declaration")
6297 .arity(),
6298 2
6299 );
6300 crate::compile::Compiler::new()
6301 .compile_program(&reduced)
6302 .expect("path-aligned stratified schema program should compile");
6303 }
6304
6305 #[test]
6306 fn high_arity_epistemic_adapter_reduction_is_not_wcoj_required() {
6307 let program = parse_program(
6308 r#"
6309 pred case_variant(u32, u32).
6310 pred case_domain_variant(u32, u32, u32).
6311 pred domain_adapter_root(u32, u32, u32).
6312 pred domain_adapter_intervention(u32, u32, u32).
6313 pred domain_candidate_seed(u32, u32, u32, u32).
6314 pred heldout_label_seed(u32, u32).
6315 pred blocked_candidate(u32, u32, u32).
6316 pred generated_candidate(u32, u32, u32, u32, u32).
6317
6318 generated_candidate(Case, Variant, Candidate, Root, Intervention) :-
6319 case_domain_variant(Case, Variant, Domain),
6320 domain_adapter_root(Domain, Candidate, Root),
6321 domain_adapter_intervention(Domain, Candidate, Intervention),
6322 domain_candidate_seed(Domain, Candidate, Root, Intervention),
6323 know domain_candidate_seed(Domain, Candidate, Root, Intervention),
6324 possible case_variant(Case, Variant),
6325 not know heldout_label_seed(Case, Candidate),
6326 not possible blocked_candidate(Case, Variant, Candidate).
6327 "#,
6328 )
6329 .expect("parse high-arity adapter epistemic program");
6330
6331 let plan = plan_epistemic_gpu_execution(&program)
6332 .expect("plan high-arity adapter epistemic program");
6333
6334 assert_eq!(plan.reductions.len(), 1);
6335 assert_eq!(
6336 plan.reductions[0].wcoj_status,
6337 EpistemicWcojReductionStatus::NotWcojCandidate
6338 );
6339 }
6340
6341 #[test]
6342 fn binary_triangle_epistemic_reduction_still_requires_wcoj() {
6343 let program = parse_program(
6344 r#"
6345 pred xy(u32, u32).
6346 pred yz(u32, u32).
6347 pred xz(u32, u32).
6348 pred tri(u32, u32, u32).
6349
6350 tri(X, Y, Z) :-
6351 xy(X, Y),
6352 yz(Y, Z),
6353 xz(X, Z),
6354 know xy(X, Y).
6355 "#,
6356 )
6357 .expect("parse binary triangle epistemic program");
6358
6359 let plan =
6360 plan_epistemic_gpu_execution(&program).expect("plan binary triangle epistemic program");
6361
6362 assert_eq!(plan.reductions.len(), 1);
6363 assert_eq!(
6364 plan.reductions[0].wcoj_status,
6365 EpistemicWcojReductionStatus::RequiresPlannerEligibility
6366 );
6367 }
6368
6369 #[test]
6370 fn binary_eight_clique_epistemic_reduction_requires_wcoj() {
6371 let program = parse_program(
6372 r#"
6373 pred edge(u32, u32).
6374 pred clique8(u32, u32, u32, u32, u32, u32, u32, u32).
6375
6376 clique8(A, B, C, D, E, F, G, H) :-
6377 edge(A, B),
6378 edge(A, C),
6379 edge(A, D),
6380 edge(A, E),
6381 edge(A, F),
6382 edge(A, G),
6383 edge(A, H),
6384 edge(B, C),
6385 edge(B, D),
6386 edge(B, E),
6387 edge(B, F),
6388 edge(B, G),
6389 edge(B, H),
6390 edge(C, D),
6391 edge(C, E),
6392 edge(C, F),
6393 edge(C, G),
6394 edge(C, H),
6395 edge(D, E),
6396 edge(D, F),
6397 edge(D, G),
6398 edge(D, H),
6399 edge(E, F),
6400 edge(E, G),
6401 edge(E, H),
6402 edge(F, G),
6403 edge(F, H),
6404 edge(G, H),
6405 know edge(A, B).
6406 "#,
6407 )
6408 .expect("parse binary eight-clique epistemic program");
6409
6410 let plan = plan_epistemic_gpu_execution(&program)
6411 .expect("plan binary eight-clique epistemic program");
6412
6413 assert_eq!(plan.reductions.len(), 1);
6414 assert_eq!(
6415 plan.reductions[0].wcoj_status,
6416 EpistemicWcojReductionStatus::RequiresPlannerEligibility
6417 );
6418 }
6419}