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::{positive_body_bound_variables, 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(
2014 predicate: &str,
2015 terms: &[EirTerm],
2016) -> Result<(usize, Vec<EirTerm>, Vec<usize>)> {
2017 let mut flattened: Vec<EirTerm> = Vec::with_capacity(terms.len());
2018 for term in terms {
2019 match term {
2020 EirTerm::List(items) => {
2021 flatten_structured_elements(predicate, "list", items, &mut flattened)?;
2022 }
2023 EirTerm::Compound { functor, args } => {
2024 flatten_structured_elements(
2025 predicate,
2026 &format!("compound {functor}/{}", args.len()),
2027 args,
2028 &mut flattened,
2029 )?;
2030 }
2031 EirTerm::Cons { .. } => {
2032 return Err(XlogError::UnsupportedEpistemicConstruct {
2033 construct: "modal tuple-key cons pattern".to_string(),
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 });
2040 }
2041 EirTerm::PredRef(name) => {
2042 return Err(XlogError::UnsupportedEpistemicConstruct {
2043 construct: "modal tuple-key predicate reference".to_string(),
2044 context: format!(
2045 "modal tuple-key for {predicate} uses predref `{name}`, which has no \
2046 finite, typed GPU key-column encoding"
2047 ),
2048 });
2049 }
2050 EirTerm::Aggregate { op, variable } => {
2051 return Err(XlogError::UnsupportedEpistemicConstruct {
2052 construct: "modal tuple-key aggregate".to_string(),
2053 context: format!(
2054 "modal tuple-key for {predicate} uses aggregate `{op}({variable})`, whose \
2055 value is not a finite, typed GPU key-column tuple"
2056 ),
2057 });
2058 }
2059 scalar => flattened.push(scalar.clone()),
2060 }
2061 }
2062
2063 let arity = flattened.len();
2064 let key_columns = (0..arity).collect();
2065 Ok((arity, flattened, key_columns))
2066}
2067
2068fn flatten_structured_elements(
2074 predicate: &str,
2075 shape: &str,
2076 elements: &[EirTerm],
2077 flattened: &mut Vec<EirTerm>,
2078) -> Result<()> {
2079 for element in elements {
2080 if eir_term_is_scalar_key_element(element) {
2081 flattened.push(element.clone());
2082 } else {
2083 return Err(XlogError::UnsupportedEpistemicConstruct {
2084 construct: "nested modal tuple-key structure".to_string(),
2085 context: format!(
2086 "modal tuple-key for {predicate} nests a non-scalar element {element:?} inside \
2087 a {shape}; only fixed-arity structures of scalar/Symbol-typed elements have a \
2088 finite, typed GPU key-column encoding"
2089 ),
2090 });
2091 }
2092 }
2093 Ok(())
2094}
2095
2096fn bound_output_columns_for_terms(
2097 key_terms: &[EirTerm],
2098 output_terms: &[EirTerm],
2099) -> Vec<Option<usize>> {
2100 key_terms
2101 .iter()
2102 .map(|term| match term {
2103 EirTerm::Variable(variable) => output_terms.iter().position(
2104 |head_term| matches!(head_term, EirTerm::Variable(name) if name == variable),
2105 ),
2106 _ => None,
2107 })
2108 .collect()
2109}
2110
2111fn augmented_eir_head_terms(rule: &xlog_ir::EirRule) -> Vec<EirTerm> {
2112 let mut output_terms = rule.head.terms.clone();
2113 for lit in &rule.body {
2114 let EirBodyLiteral::Epistemic(lit) = lit else {
2115 continue;
2116 };
2117 let key_terms = flatten_structured_key_terms(&lit.atom.predicate, &lit.atom.terms)
2123 .map(|(_, terms, _)| terms)
2124 .unwrap_or_else(|_| lit.atom.terms.clone());
2125 for term in &key_terms {
2126 let EirTerm::Variable(variable) = term else {
2127 continue;
2128 };
2129 if !output_terms
2130 .iter()
2131 .any(|head_term| matches!(head_term, EirTerm::Variable(name) if name == variable))
2132 {
2133 output_terms.push(EirTerm::Variable(variable.clone()));
2134 }
2135 }
2136 }
2137 output_terms
2138}
2139
2140fn final_output_columns_for_eir(eir: &EirProgram) -> Option<Vec<usize>> {
2141 let mut final_columns = Vec::new();
2142 let mut needs_projection = false;
2143 for rule in &eir.rules {
2144 if !rule
2145 .body
2146 .iter()
2147 .any(|lit| matches!(lit, EirBodyLiteral::Epistemic(_)))
2148 {
2149 continue;
2150 }
2151 let augmented_len = augmented_eir_head_terms(rule).len();
2152 if augmented_len > rule.head.terms.len() {
2153 needs_projection = true;
2154 }
2155 if final_columns.is_empty() {
2156 final_columns = (0..rule.head.terms.len()).collect();
2157 }
2158 }
2159 if needs_projection {
2160 Some(final_columns)
2161 } else {
2162 None
2163 }
2164}
2165
2166fn faeel_unfounded_exact_tuple_self_support_rule_indices(program: &Program) -> Vec<usize> {
2184 let Ok(eir) = build_eir(program) else {
2185 return Vec::new();
2186 };
2187 if eir.mode != EirEpistemicMode::Faeel {
2188 return Vec::new();
2189 }
2190 let mut indices = Vec::new();
2191 for (index, (rule, eir_rule)) in program.rules.iter().zip(&eir.rules).enumerate() {
2192 let modal_only_output_variables = modal_only_bound_output_variables(rule);
2193 let drop = eir_rule.body.iter().any(|lit| {
2194 let EirBodyLiteral::Epistemic(modal) = lit else {
2195 return false;
2196 };
2197 if modal.negated
2198 || modal.atom.predicate != eir_rule.head.predicate
2199 || modal.atom.arity != eir_rule.head.arity
2200 || modal.atom.terms != eir_rule.head.terms
2201 {
2202 return false;
2203 }
2204 if has_independent_founded_support(&eir, &modal.atom)
2207 || has_tuple_level_independent_founded_support(&eir, eir_rule, &modal.atom)
2208 {
2209 return false;
2210 }
2211 if modal
2215 .atom
2216 .terms
2217 .iter()
2218 .any(|term| matches!(term, EirTerm::Variable(name) if modal_only_output_variables.contains(name)))
2219 {
2220 return false;
2221 }
2222 true
2223 });
2224 if drop {
2225 indices.push(index);
2226 }
2227 }
2228 indices
2229}
2230
2231fn program_without_rule_indices(program: &Program, removed_rule_indices: &[usize]) -> Program {
2232 if removed_rule_indices.is_empty() {
2233 return program.clone();
2234 }
2235
2236 let removed_rule_indices = removed_rule_indices
2237 .iter()
2238 .copied()
2239 .collect::<BTreeSet<_>>();
2240 let mut filtered = program.clone();
2241 filtered.rules = program
2242 .rules
2243 .iter()
2244 .enumerate()
2245 .filter(|(index, _)| !removed_rule_indices.contains(index))
2246 .map(|(_, rule)| rule.clone())
2247 .collect();
2248 filtered
2249}
2250
2251pub fn validate_epistemic_source_program(program: &Program) -> Result<()> {
2261 let prepared = prepare_root_authored_constraint_identity(program)?;
2262 validate_prepared_epistemic_source_program(&prepared)
2263}
2264
2265fn validate_prepared_epistemic_source_program(program: &Program) -> Result<()> {
2266 validate_authored_modal_key_shapes(program)?;
2267 let invariant = InvariantRelations::analyze(program);
2268 let determined = EpistemicallyDeterminedPredicates::analyze(program);
2269 for rule in &program.rules {
2270 validate_modal_variable_bindings(&rule.body, &invariant, &determined)?;
2271 }
2272 for constraint in &program.constraints {
2273 validate_modal_variable_bindings(&constraint.body, &invariant, &determined)?;
2274 }
2275
2276 let mut validation = program.clone();
2277 for rule in &mut validation.rules {
2278 rewrite_modal_literals_for_source_validation(&mut rule.body, &invariant, &determined);
2279 }
2280 for constraint in &mut validation.constraints {
2281 rewrite_modal_literals_for_source_validation(&mut constraint.body, &invariant, &determined);
2282 }
2283
2284 let multi_arity_predicates = all_multi_arity_predicates(program);
2285 qualify_predicate_signatures(&mut validation, &multi_arity_predicates);
2286 Compiler::new().validate_program_without_stratification(&validation)
2287}
2288
2289fn validate_authored_modal_key_shapes(program: &Program) -> Result<()> {
2299 let target_arities = non_modal_relation_arities(program);
2300 let eir = build_eir(program)?;
2301 for modal in eir
2302 .rules
2303 .iter()
2304 .flat_map(|rule| &rule.body)
2305 .chain(
2306 eir.constraints
2307 .iter()
2308 .flat_map(|constraint| &constraint.body),
2309 )
2310 .filter_map(|literal| match literal {
2311 EirBodyLiteral::Epistemic(modal) => Some(modal),
2312 EirBodyLiteral::Relational { .. }
2313 | EirBodyLiteral::Constraint
2314 | EirBodyLiteral::Binding => None,
2315 })
2316 {
2317 let flattened = flatten_epistemic_literal(modal)?;
2318 let Some(expected) = target_arities.get(&flattened.atom.predicate) else {
2319 continue;
2322 };
2323 if expected.contains(&flattened.atom.arity) {
2324 continue;
2325 }
2326
2327 let expected_description = if expected.len() == 1 {
2328 format!(
2329 "target arity {}",
2330 expected.first().expect("one target arity")
2331 )
2332 } else {
2333 format!("target arities {expected:?}")
2334 };
2335 return Err(XlogError::UnsupportedEpistemicConstruct {
2336 construct: "epistemic modal tuple key".to_string(),
2337 context: format!(
2338 "modal target `{}` has {expected_description}, but its tuple key flattens to \
2339 binding arity {}; use one scalar key term per target column",
2340 flattened.atom.predicate, flattened.atom.arity
2341 ),
2342 });
2343 }
2344 Ok(())
2345}
2346
2347fn non_modal_relation_arities(program: &Program) -> BTreeMap<String, BTreeSet<usize>> {
2348 let mut arities = BTreeMap::new();
2349 for declaration in &program.predicates {
2350 arities
2351 .entry(declaration.name.clone())
2352 .or_insert_with(BTreeSet::new)
2353 .insert(declaration.arity());
2354 }
2355 for rule in &program.rules {
2356 record_predicate_signature(&mut arities, &rule.head);
2357 record_non_modal_body_signatures(&mut arities, &rule.body);
2358 }
2359 for constraint in &program.constraints {
2360 record_non_modal_body_signatures(&mut arities, &constraint.body);
2361 }
2362 for query in &program.queries {
2363 record_predicate_signature(&mut arities, &query.atom);
2364 }
2365 for fact in &program.prob_facts {
2366 record_predicate_signature(&mut arities, &fact.atom);
2367 }
2368 for disjunction in &program.annotated_disjunctions {
2369 for choice in &disjunction.choices {
2370 record_predicate_signature(&mut arities, &choice.atom);
2371 }
2372 }
2373 for evidence in &program.evidence {
2374 record_predicate_signature(&mut arities, &evidence.atom);
2375 }
2376 for query in &program.prob_queries {
2377 record_predicate_signature(&mut arities, &query.atom);
2378 }
2379 for declaration in &program.neural_predicates {
2380 record_predicate_signature(&mut arities, &declaration.predicate);
2381 }
2382 for rule in &program.learnable_rules {
2383 record_predicate_signature(&mut arities, &rule.head);
2384 record_non_modal_body_signatures(&mut arities, &rule.body);
2385 }
2386 arities
2387}
2388
2389fn record_non_modal_body_signatures(
2390 signatures: &mut BTreeMap<String, BTreeSet<usize>>,
2391 body: &[BodyLiteral],
2392) {
2393 for literal in body {
2394 if let BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) = literal {
2395 record_predicate_signature(signatures, atom);
2396 }
2397 }
2398}
2399
2400fn rewrite_modal_literals_for_source_validation(
2406 body: &mut Vec<BodyLiteral>,
2407 invariant: &InvariantRelations<'_>,
2408 determined: &EpistemicallyDeterminedPredicates,
2409) {
2410 let mut non_binding_modals = Vec::new();
2411 body.retain_mut(|literal| {
2412 let BodyLiteral::Epistemic(modal) = literal else {
2413 return true;
2414 };
2415 if modal.atom.terms.iter().any(|term| {
2416 !matches!(
2417 term,
2418 Term::Variable(_)
2419 | Term::Anonymous
2420 | Term::Integer(_)
2421 | Term::Float(_)
2422 | Term::String(_)
2423 | Term::Symbol(_)
2424 )
2425 }) {
2426 *literal = BodyLiteral::Comparison(Comparison {
2430 left: Term::Integer(1),
2431 op: CompOp::Eq,
2432 right: Term::Integer(1),
2433 });
2434 return true;
2435 }
2436 if !modal.negated
2437 && (invariant.is_invariant(&modal.atom.predicate)
2438 || determined.contains(&modal.atom.predicate))
2439 {
2440 *literal = BodyLiteral::Positive(modal.atom.clone());
2441 true
2442 } else {
2443 non_binding_modals.push(BodyLiteral::Negated(modal.atom.clone()));
2444 false
2445 }
2446 });
2447 body.extend(non_binding_modals);
2448}
2449
2450fn non_epistemic_bound_variables(body: &[BodyLiteral]) -> BTreeSet<String> {
2455 let mut bound = positive_body_bound_variables(body);
2456
2457 for literal in body {
2458 let BodyLiteral::IsExpr(binding) = literal else {
2459 continue;
2460 };
2461 if binding
2462 .expr
2463 .variables()
2464 .iter()
2465 .all(|name| bound.contains(*name))
2466 {
2467 bound.insert(binding.target.clone());
2468 }
2469 }
2470
2471 bound
2472}
2473
2474fn validate_modal_variable_bindings(
2479 body: &[BodyLiteral],
2480 invariant: &InvariantRelations<'_>,
2481 determined: &EpistemicallyDeterminedPredicates,
2482) -> Result<()> {
2483 let mut bound = non_epistemic_bound_variables(body);
2484
2485 for literal in body {
2490 let BodyLiteral::Epistemic(modal) = literal else {
2491 continue;
2492 };
2493 let may_bind = !modal.negated
2494 && (invariant.is_invariant(&modal.atom.predicate)
2495 || determined.contains(&modal.atom.predicate));
2496 if may_bind {
2497 bound.extend(
2498 modal
2499 .atom
2500 .variables()
2501 .into_iter()
2502 .filter(|name| *name != "_")
2503 .map(str::to_string),
2504 );
2505 }
2506 }
2507
2508 for literal in body {
2509 let BodyLiteral::Epistemic(modal) = literal else {
2510 continue;
2511 };
2512 let may_bind = !modal.negated
2513 && (invariant.is_invariant(&modal.atom.predicate)
2514 || determined.contains(&modal.atom.predicate));
2515 for variable in modal.atom.variables() {
2516 if variable == "_" {
2517 continue;
2518 }
2519 if !may_bind && !bound.contains(variable) {
2520 return Err(XlogError::UnsafeVariable(variable.to_string()));
2521 }
2522 }
2523 }
2524 Ok(())
2525}
2526
2527pub fn reduce_epistemic_program_to_ordinary(program: &Program) -> Result<Program> {
2544 let prepared = prepare_root_authored_constraint_identity(program)?;
2545 if let Some(reduced) = try_reduce_case_a_recursive_epistemic_program(&prepared)? {
2546 return Ok(reduced);
2547 }
2548 reduce_epistemic_program_to_ordinary_inner(&prepared, &BTreeSet::new(), &BTreeMap::new())
2549}
2550
2551pub fn reduce_epistemic_program_to_ordinary_for_stratified_schema(
2573 program: &Program,
2574) -> Result<Program> {
2575 let prepared = prepare_root_authored_constraint_identity(program)?;
2576 let determined = EpistemicallyDeterminedPredicates::analyze(&prepared);
2577 let path_specific_rules = stratified_schema_reduction_overrides(&prepared)?;
2578 reduce_epistemic_program_to_ordinary_inner(
2579 &prepared,
2580 &determined.determined,
2581 &path_specific_rules,
2582 )
2583}
2584
2585fn prepare_root_authored_constraint_identity(program: &Program) -> Result<Program> {
2586 let mut prepared = program.clone();
2587 if prepared.authored_constraint_source_bound.is_some() {
2588 prepared.validate_prepared_authored_constraint_identity()?;
2589 } else {
2590 prepared.prepare_authored_constraint_identity_at_root()?;
2591 }
2592 Ok(prepared)
2593}
2594
2595fn reduce_epistemic_program_to_ordinary_inner(
2602 program: &Program,
2603 schema_only_determined_resolve: &BTreeSet<String>,
2604 path_specific_rules: &BTreeMap<usize, crate::ast::Rule>,
2605) -> Result<Program> {
2606 let path_specific_rule_indices = path_specific_rules.keys().copied().collect::<BTreeSet<_>>();
2607 validate_epistemic_relation_shapes(program, &path_specific_rule_indices)?;
2608
2609 let removed_rule_indices = faeel_unfounded_exact_tuple_self_support_rule_indices(program);
2630 let removed_rule_index_set = removed_rule_indices
2631 .iter()
2632 .copied()
2633 .collect::<BTreeSet<_>>();
2634 let active_original_rule_indices = (0..program.rules.len())
2635 .filter(|index| !removed_rule_index_set.contains(index))
2636 .collect::<Vec<_>>();
2637 let mut reduced = program_without_rule_indices(program, &removed_rule_indices);
2638
2639 let invariant = InvariantRelations::analyze(program);
2663
2664 let mut augmented_rule_original_arities = BTreeMap::new();
2670
2671 for ((rule_index, rule), original_rule_index) in reduced
2672 .rules
2673 .iter_mut()
2674 .enumerate()
2675 .zip(active_original_rule_indices)
2676 {
2677 if let Some(path_specific_rule) = path_specific_rules.get(&original_rule_index) {
2678 *rule = path_specific_rule.clone();
2679 continue;
2680 }
2681 let original_head_arity = rule.head.arity();
2682 let modal_only_output_variables = modal_only_bound_output_variables(rule);
2690 append_body_local_tuple_key_variables_to_head(rule);
2691 if rule.head.arity() > original_head_arity {
2692 augmented_rule_original_arities.insert(rule_index, original_head_arity);
2693 }
2694 let was_fact = rule.body.is_empty();
2695 let had_epistemic_body = rule
2696 .body
2697 .iter()
2698 .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)));
2699 for lit in &mut rule.body {
2709 if let BodyLiteral::Epistemic(modal) = lit {
2710 if resolves_augmented_head_variable(
2716 modal,
2717 &modal_only_output_variables,
2718 &invariant,
2719 schema_only_determined_resolve,
2720 ) {
2721 *lit = BodyLiteral::Positive(modal.atom.clone());
2722 }
2723 }
2724 }
2725 rule.body
2726 .retain(|lit| !matches!(lit, BodyLiteral::Epistemic(_)));
2727 if !was_fact && had_epistemic_body && rule.body.is_empty() {
2728 rule.body.push(BodyLiteral::Comparison(Comparison {
2729 left: Term::Integer(1),
2730 op: CompOp::Eq,
2731 right: Term::Integer(1),
2732 }));
2733 }
2734 }
2735 qualify_extensional_multi_arity_predicates(&mut reduced, program, &removed_rule_index_set);
2743
2744 let augmented_signatures =
2745 reconcile_augmented_head_declarations(&mut reduced, &augmented_rule_original_arities)?;
2746
2747 if !augmented_signatures.is_empty() {
2757 reduced.queries.retain(|query| {
2758 !augmented_signatures.contains_key(&(query.atom.predicate.clone(), query.atom.arity()))
2759 });
2760 }
2761
2762 reduced.constraints.retain(|constraint| {
2769 !constraint
2770 .body
2771 .iter()
2772 .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
2773 });
2774
2775 Ok(reduced)
2776}
2777
2778pub fn reduce_case_a_epistemic_program_to_ordinary(program: &Program) -> Program {
2796 let mut reduced = program.clone();
2797 for rule in &mut reduced.rules {
2798 resolve_recursive_epistemic_rule_modals(rule);
2799 }
2800 reduced.constraints.retain(|constraint| {
2804 !constraint
2805 .body
2806 .iter()
2807 .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
2808 });
2809 qualify_extensional_multi_arity_predicates(&mut reduced, program, &BTreeSet::new());
2810 reduced
2811}
2812
2813fn reduce_founded_epistemic_program_to_ordinary(program: &Program) -> Program {
2823 let mut reduced = program.clone();
2824 for rule in &mut reduced.rules {
2825 resolve_recursive_epistemic_rule_modals(rule);
2826 }
2827 for constraint in &mut reduced.constraints {
2828 for literal in &mut constraint.body {
2829 let BodyLiteral::Epistemic(modal) = literal else {
2830 continue;
2831 };
2832 *literal = if modal.negated {
2833 BodyLiteral::Negated(modal.atom.clone())
2834 } else {
2835 BodyLiteral::Positive(modal.atom.clone())
2836 };
2837 }
2838 }
2839 qualify_extensional_multi_arity_predicates(&mut reduced, program, &BTreeSet::new());
2840 reduced
2841}
2842
2843fn resolve_recursive_epistemic_rule_modals(rule: &mut crate::ast::Rule) {
2844 for literal in &mut rule.body {
2845 if let BodyLiteral::Epistemic(modal) = literal {
2846 *literal = if modal.negated {
2847 BodyLiteral::Negated(modal.atom.clone())
2848 } else {
2849 BodyLiteral::Positive(modal.atom.clone())
2850 };
2851 }
2852 }
2853}
2854
2855fn modal_only_bound_output_variables(rule: &crate::ast::Rule) -> BTreeSet<String> {
2866 let positively_bound = non_epistemic_bound_variables(&rule.body);
2867
2868 let mut modal_only = BTreeSet::new();
2871 let mut consider = |name: &str| {
2872 if name != "_" && !positively_bound.contains(name) {
2873 modal_only.insert(name.to_string());
2874 }
2875 };
2876 for term in &rule.head.terms {
2877 if let Term::Variable(name) = term {
2878 consider(name);
2879 }
2880 }
2881 for lit in &rule.body {
2882 if let BodyLiteral::Epistemic(lit) = lit {
2883 for term in &lit.atom.terms {
2884 if let Term::Variable(name) = term {
2885 consider(name);
2886 }
2887 }
2888 }
2889 }
2890 modal_only
2891}
2892
2893fn modal_atom_binds_output_variable(
2897 modal: &EpistemicLiteral,
2898 modal_only_output_variables: &BTreeSet<String>,
2899) -> bool {
2900 modal.atom.terms.iter().any(
2901 |term| matches!(term, Term::Variable(name) if modal_only_output_variables.contains(name)),
2902 )
2903}
2904
2905fn resolves_augmented_head_variable(
2906 modal: &EpistemicLiteral,
2907 modal_only_output_variables: &BTreeSet<String>,
2908 invariant: &InvariantRelations,
2909 schema_only_determined_resolve: &BTreeSet<String>,
2910) -> bool {
2911 !modal.negated
2912 && (invariant.is_invariant(&modal.atom.predicate)
2913 || schema_only_determined_resolve.contains(&modal.atom.predicate))
2914 && modal_atom_binds_output_variable(modal, modal_only_output_variables)
2915}
2916
2917fn record_predicate_signature(
2918 signatures: &mut BTreeMap<String, BTreeSet<usize>>,
2919 atom: &crate::ast::Atom,
2920) {
2921 signatures
2922 .entry(atom.predicate.clone())
2923 .or_default()
2924 .insert(atom.arity());
2925}
2926
2927fn record_body_predicate_signatures(
2928 signatures: &mut BTreeMap<String, BTreeSet<usize>>,
2929 body: &[BodyLiteral],
2930) {
2931 for literal in body {
2932 match literal {
2933 BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) => {
2934 record_predicate_signature(signatures, atom);
2935 }
2936 BodyLiteral::Epistemic(modal) => {
2937 let eir_terms = modal
2938 .atom
2939 .terms
2940 .iter()
2941 .map(convert_term)
2942 .collect::<Vec<_>>();
2943 let arity = flatten_structured_key_terms(&modal.atom.predicate, &eir_terms)
2949 .map(|(arity, _, _)| arity)
2950 .unwrap_or_else(|_| modal.atom.arity());
2951 signatures
2952 .entry(modal.atom.predicate.clone())
2953 .or_default()
2954 .insert(arity);
2955 }
2956 BodyLiteral::Comparison(_) | BodyLiteral::IsExpr(_) | BodyLiteral::Univ(_) => {}
2957 }
2958 }
2959}
2960
2961fn collect_epistemic_relation_identities(
2962 program: &Program,
2963 removed_rules: &BTreeSet<usize>,
2964) -> (BTreeMap<String, BTreeSet<usize>>, BTreeSet<String>) {
2965 let mut source_arities: BTreeMap<String, BTreeSet<usize>> = BTreeMap::new();
2966 for declaration in &program.predicates {
2967 source_arities
2968 .entry(declaration.name.clone())
2969 .or_default()
2970 .insert(declaration.arity());
2971 }
2972
2973 let mut derived_predicates = BTreeSet::new();
2974 for (index, rule) in program.rules.iter().enumerate() {
2975 if removed_rules.contains(&index) {
2976 continue;
2977 }
2978 record_predicate_signature(&mut source_arities, &rule.head);
2979 record_body_predicate_signatures(&mut source_arities, &rule.body);
2980 if !rule.body.is_empty() {
2981 derived_predicates.insert(rule.head.predicate.clone());
2982 }
2983 }
2984 for constraint in &program.constraints {
2985 record_body_predicate_signatures(&mut source_arities, &constraint.body);
2986 }
2987 for query in &program.queries {
2988 record_predicate_signature(&mut source_arities, &query.atom);
2989 }
2990 for fact in &program.prob_facts {
2991 record_predicate_signature(&mut source_arities, &fact.atom);
2992 }
2993 for disjunction in &program.annotated_disjunctions {
2994 for choice in &disjunction.choices {
2995 record_predicate_signature(&mut source_arities, &choice.atom);
2996 }
2997 }
2998 for evidence in &program.evidence {
2999 record_predicate_signature(&mut source_arities, &evidence.atom);
3000 }
3001 for query in &program.prob_queries {
3002 record_predicate_signature(&mut source_arities, &query.atom);
3003 }
3004 for declaration in &program.neural_predicates {
3005 record_predicate_signature(&mut source_arities, &declaration.predicate);
3006 }
3007 for rule in &program.learnable_rules {
3008 record_predicate_signature(&mut source_arities, &rule.head);
3009 record_body_predicate_signatures(&mut source_arities, &rule.body);
3010 if !rule.body.is_empty() {
3011 derived_predicates.insert(rule.head.predicate.clone());
3012 }
3013 }
3014
3015 (source_arities, derived_predicates)
3016}
3017
3018fn all_multi_arity_predicates(program: &Program) -> BTreeSet<String> {
3019 collect_epistemic_relation_identities(program, &BTreeSet::new())
3020 .0
3021 .into_iter()
3022 .filter_map(|(predicate, arities)| (arities.len() > 1).then_some(predicate))
3023 .collect()
3024}
3025
3026fn qualify_atom_for_extensional_multi_arity(
3027 atom: &mut crate::ast::Atom,
3028 predicates: &BTreeSet<String>,
3029) {
3030 if predicates.contains(&atom.predicate) {
3031 atom.predicate = format!("{}/{}", atom.predicate, atom.arity());
3032 }
3033}
3034
3035fn qualify_body_for_extensional_multi_arity(
3036 body: &mut [BodyLiteral],
3037 predicates: &BTreeSet<String>,
3038) {
3039 for literal in body {
3040 match literal {
3041 BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) => {
3042 qualify_atom_for_extensional_multi_arity(atom, predicates);
3043 }
3044 BodyLiteral::Epistemic(modal) => {
3045 qualify_atom_for_extensional_multi_arity(&mut modal.atom, predicates);
3046 }
3047 BodyLiteral::Comparison(_) | BodyLiteral::IsExpr(_) | BodyLiteral::Univ(_) => {}
3048 }
3049 }
3050}
3051
3052pub fn epistemic_extensional_multi_arity_predicates(program: &Program) -> BTreeSet<String> {
3060 let removed_rules = faeel_unfounded_exact_tuple_self_support_rule_indices(program)
3061 .into_iter()
3062 .collect::<BTreeSet<_>>();
3063 extensional_multi_arity_predicates(program, &removed_rules)
3064}
3065
3066fn extensional_multi_arity_predicates(
3067 program: &Program,
3068 removed_rules: &BTreeSet<usize>,
3069) -> BTreeSet<String> {
3070 let (source_arities, derived_predicates) =
3071 collect_epistemic_relation_identities(program, removed_rules);
3072 source_arities
3073 .into_iter()
3074 .filter_map(|(predicate, arities)| {
3075 (arities.len() > 1 && !derived_predicates.contains(&predicate)).then_some(predicate)
3076 })
3077 .collect()
3078}
3079
3080fn qualify_extensional_multi_arity_predicates(
3087 reduced: &mut Program,
3088 source: &Program,
3089 removed_rules: &BTreeSet<usize>,
3090) {
3091 let predicates = extensional_multi_arity_predicates(source, removed_rules);
3092 qualify_predicate_signatures(reduced, &predicates);
3093}
3094
3095fn qualify_predicate_signatures(reduced: &mut Program, predicates: &BTreeSet<String>) {
3100 if predicates.is_empty() {
3101 return;
3102 }
3103
3104 for declaration in &mut reduced.predicates {
3105 if predicates.contains(&declaration.name) {
3106 declaration.name = format!("{}/{}", declaration.name, declaration.arity());
3107 }
3108 }
3109 for rule in &mut reduced.rules {
3110 qualify_atom_for_extensional_multi_arity(&mut rule.head, predicates);
3111 qualify_body_for_extensional_multi_arity(&mut rule.body, predicates);
3112 }
3113 for constraint in &mut reduced.constraints {
3114 qualify_body_for_extensional_multi_arity(&mut constraint.body, predicates);
3115 }
3116 for query in &mut reduced.queries {
3117 qualify_atom_for_extensional_multi_arity(&mut query.atom, predicates);
3118 }
3119 for fact in &mut reduced.prob_facts {
3120 qualify_atom_for_extensional_multi_arity(&mut fact.atom, predicates);
3121 }
3122 for disjunction in &mut reduced.annotated_disjunctions {
3123 for choice in &mut disjunction.choices {
3124 qualify_atom_for_extensional_multi_arity(&mut choice.atom, predicates);
3125 }
3126 }
3127 for evidence in &mut reduced.evidence {
3128 qualify_atom_for_extensional_multi_arity(&mut evidence.atom, predicates);
3129 }
3130 for query in &mut reduced.prob_queries {
3131 qualify_atom_for_extensional_multi_arity(&mut query.atom, predicates);
3132 }
3133 for declaration in &mut reduced.neural_predicates {
3134 qualify_atom_for_extensional_multi_arity(&mut declaration.predicate, predicates);
3135 }
3136 for rule in &mut reduced.learnable_rules {
3137 qualify_atom_for_extensional_multi_arity(&mut rule.head, predicates);
3138 qualify_body_for_extensional_multi_arity(&mut rule.body, predicates);
3139 }
3140}
3141
3142fn validate_epistemic_derived_relation_identity(
3152 program: &Program,
3153 removed_rules: &BTreeSet<usize>,
3154) -> Result<BTreeMap<String, BTreeSet<usize>>> {
3155 let (source_arities, derived_predicates) =
3156 collect_epistemic_relation_identities(program, removed_rules);
3157 for predicate in derived_predicates {
3158 let arities = source_arities
3159 .get(&predicate)
3160 .expect("derived predicate has a source signature");
3161 if arities.len() > 1 {
3162 return Err(XlogError::UnsupportedEpistemicConstruct {
3163 construct: "epistemic derived predicate schema".to_string(),
3164 context: format!(
3165 "derived predicate `{predicate}` uses multiple source arities {arities:?}; \
3166 epistemic derived relations require one source signature per predicate name"
3167 ),
3168 });
3169 }
3170 }
3171
3172 Ok(source_arities)
3173}
3174
3175fn validate_epistemic_relation_shapes(
3184 program: &Program,
3185 non_augmenting_rule_indices: &BTreeSet<usize>,
3186) -> Result<()> {
3187 let removed_rules = faeel_unfounded_exact_tuple_self_support_rule_indices(program)
3188 .into_iter()
3189 .collect::<BTreeSet<_>>();
3190 let active_rules = program
3191 .rules
3192 .iter()
3193 .enumerate()
3194 .filter(|(index, _)| {
3195 !removed_rules.contains(index) && !non_augmenting_rule_indices.contains(index)
3196 })
3197 .collect::<Vec<_>>();
3198
3199 validate_epistemic_derived_relation_identity(program, &removed_rules)?;
3200
3201 let mut reduced_rule_arities = Vec::with_capacity(active_rules.len());
3202 let mut augmented_targets: BTreeMap<(String, usize), usize> = BTreeMap::new();
3203
3204 for (_, rule) in &active_rules {
3205 let original_signature = (rule.head.predicate.clone(), rule.head.arity());
3206 let mut reduced_rule = (*rule).clone();
3207 append_body_local_tuple_key_variables_to_head(&mut reduced_rule);
3208 let reduced_arity = reduced_rule.head.arity();
3209
3210 if reduced_arity > original_signature.1 {
3211 augmented_targets
3212 .entry(original_signature.clone())
3213 .and_modify(|target| *target = (*target).max(reduced_arity))
3214 .or_insert(reduced_arity);
3215 }
3216 reduced_rule_arities.push((original_signature, reduced_arity));
3217 }
3218
3219 for ((predicate, original_arity), target_arity) in augmented_targets {
3220 let arities = reduced_rule_arities
3221 .iter()
3222 .filter(|((candidate, arity), _)| candidate == &predicate && *arity == original_arity)
3223 .map(|(_, arity)| *arity)
3224 .collect::<BTreeSet<_>>();
3225 if arities.len() != 1 || !arities.contains(&target_arity) {
3226 return Err(XlogError::UnsupportedEpistemicConstruct {
3227 construct: "epistemic augmented predicate schema".to_string(),
3228 context: format!(
3229 "rules defining `{predicate}/{original_arity}` lower to incompatible \
3230 internal arities {arities:?}; every clause for one predicate signature \
3231 must bind the same augmented tuple shape"
3232 ),
3233 });
3234 }
3235
3236 for query in program.queries.iter().filter(|query| {
3237 query.atom.predicate == predicate && query.atom.arity() == original_arity
3238 }) {
3239 let mut variables = BTreeSet::new();
3240 let unconstrained = query.atom.terms.iter().all(|term| match term {
3241 Term::Variable(name) => name != "_" && variables.insert(name.as_str()),
3242 Term::Anonymous
3243 | Term::Integer(_)
3244 | Term::Float(_)
3245 | Term::String(_)
3246 | Term::Symbol(_)
3247 | Term::List(_)
3248 | Term::Cons { .. }
3249 | Term::Compound { .. }
3250 | Term::PredRef(_)
3251 | Term::Aggregate(_) => false,
3252 });
3253 if !unconstrained {
3254 return Err(XlogError::UnsupportedEpistemicConstruct {
3255 construct: "epistemic augmented head query".to_string(),
3256 context: format!(
3257 "query `{predicate}/{original_arity}` is not a tuple of distinct named \
3258 variables; an augmented epistemic head can currently surface only \
3259 queries whose arguments are distinct named variables"
3260 ),
3261 });
3262 }
3263 }
3264 }
3265
3266 let eir = build_eir(program)?;
3267 let mut clauses_by_signature: BTreeMap<(String, usize), Vec<(usize, &crate::ast::Rule)>> =
3268 BTreeMap::new();
3269 for (rule_index, rule) in active_rules {
3270 clauses_by_signature
3271 .entry((rule.head.predicate.clone(), rule.head.arity()))
3272 .or_default()
3273 .push((rule_index, rule));
3274 }
3275 for ((predicate, arity), clauses) in clauses_by_signature {
3276 if clauses.len() > 1
3277 && clauses.iter().any(|(_, rule)| {
3278 rule.body
3279 .iter()
3280 .any(|literal| matches!(literal, BodyLiteral::Epistemic(_)))
3281 })
3282 && !epistemic_rule_union_gates_are_redundant(program, &eir, &clauses)
3283 {
3284 return Err(XlogError::UnsupportedEpistemicConstruct {
3285 construct: "epistemic rule-union materialization".to_string(),
3286 context: format!(
3287 "predicate `{predicate}/{arity}` has multiple defining clauses and at least \
3288 one epistemic clause; single-pass materialization cannot preserve \
3289 per-clause modal provenance, so it cannot safely filter the clause union"
3290 ),
3291 });
3292 }
3293 }
3294
3295 Ok(())
3296}
3297
3298fn epistemic_rule_union_gates_are_redundant(
3320 program: &Program,
3321 eir: &EirProgram,
3322 clauses: &[(usize, &crate::ast::Rule)],
3323) -> bool {
3324 let invariant = InvariantRelations::analyze(program);
3325 let mut normalized_conjunctions = clauses.iter().map(|(rule_index, _)| {
3326 eir.rules
3327 .get(*rule_index)
3328 .and_then(|rule| normalized_rule_union_gates(rule, &invariant))
3329 });
3330 if let Some(Some(first)) = normalized_conjunctions.next() {
3331 if !first.is_empty()
3332 && normalized_conjunctions.all(|candidate| {
3333 candidate.is_some_and(|candidate| rule_union_gate_sets_equal(&first, &candidate))
3334 })
3335 {
3336 return true;
3337 }
3338 }
3339
3340 let epistemic_clauses = clauses
3341 .iter()
3342 .filter(|(_, rule)| {
3343 rule.body
3344 .iter()
3345 .any(|literal| matches!(literal, BodyLiteral::Epistemic(_)))
3346 })
3347 .collect::<Vec<_>>();
3348 if epistemic_clauses.is_empty() {
3349 return true;
3350 }
3351
3352 let every_gate_is_unconditionally_true = epistemic_clauses.iter().all(|(rule_index, _)| {
3357 eir.rules.get(*rule_index).is_some_and(|eir_rule| {
3358 let modal_literals = eir_rule
3359 .body
3360 .iter()
3361 .filter_map(|literal| match literal {
3362 EirBodyLiteral::Epistemic(modal) => Some(modal),
3363 _ => None,
3364 })
3365 .collect::<Vec<_>>();
3366 !modal_literals.is_empty()
3367 && modal_literals.iter().all(|modal| {
3368 !modal.negated && has_unconditional_ground_founded_support(eir, &modal.atom)
3369 })
3370 })
3371 });
3372 if every_gate_is_unconditionally_true {
3373 return true;
3374 }
3375
3376 if epistemic_clauses.len() != 1 {
3377 return false;
3378 }
3379
3380 let (rule_index, _) = epistemic_clauses[0];
3381 let Some(eir_rule) = eir.rules.get(*rule_index) else {
3382 return false;
3383 };
3384 let modal_literals = eir_rule
3385 .body
3386 .iter()
3387 .filter_map(|literal| match literal {
3388 EirBodyLiteral::Epistemic(modal) => Some(modal),
3389 _ => None,
3390 })
3391 .collect::<Vec<_>>();
3392
3393 !modal_literals.is_empty()
3394 && modal_literals.iter().all(|modal| {
3395 !modal.negated
3396 && (has_unconditional_ground_founded_support(eir, &modal.atom)
3397 || (modal.atom == eir_rule.head
3398 && eir_head_is_bijective_variable_tuple(&eir_rule.head)
3399 && ((eir.mode == EirEpistemicMode::G91
3400 && modal.op == EirEpistemicOp::Possible)
3401 || has_tuple_level_independent_founded_support(
3402 eir,
3403 eir_rule,
3404 &modal.atom,
3405 ))))
3406 })
3407}
3408
3409#[derive(Debug, Clone, PartialEq, Eq)]
3410enum RuleUnionGateTerm {
3411 OutputColumn(usize),
3412 Literal(EirTerm),
3413}
3414
3415#[derive(Debug, Clone, PartialEq, Eq)]
3416struct RuleUnionGate {
3417 predicate: String,
3418 arity: usize,
3419 terms: Vec<RuleUnionGateTerm>,
3420 op: Option<EirEpistemicOp>,
3421 negated: bool,
3422}
3423
3424fn normalized_rule_union_gates(
3429 rule: &xlog_ir::EirRule,
3430 invariant: &InvariantRelations<'_>,
3431) -> Option<Vec<RuleUnionGate>> {
3432 let output_terms = augmented_eir_head_terms(rule);
3433 let mut gates = Vec::new();
3434 for literal in &rule.body {
3435 let EirBodyLiteral::Epistemic(modal) = literal else {
3436 continue;
3437 };
3438 let bound_columns = bound_output_columns_for_terms(&modal.atom.terms, &output_terms);
3439 let terms = modal
3440 .atom
3441 .terms
3442 .iter()
3443 .zip(bound_columns)
3444 .map(|(term, output_column)| match (term, output_column) {
3445 (EirTerm::Variable(_), Some(column)) => {
3446 Some(RuleUnionGateTerm::OutputColumn(column))
3447 }
3448 (
3449 term @ (EirTerm::Anonymous
3450 | EirTerm::Integer(_)
3451 | EirTerm::FloatBits(_)
3452 | EirTerm::String(_)
3453 | EirTerm::Symbol(_)
3454 | EirTerm::PredRef(_)),
3455 None,
3456 ) => Some(RuleUnionGateTerm::Literal(term.clone())),
3457 _ => None,
3458 })
3459 .collect::<Option<Vec<_>>>()?;
3460 let gate = RuleUnionGate {
3461 predicate: modal.atom.predicate.clone(),
3462 arity: modal.atom.arity,
3463 terms,
3464 op: (!invariant.is_invariant(&modal.atom.predicate)).then_some(modal.op),
3465 negated: modal.negated,
3466 };
3467 if !gates.contains(&gate) {
3468 gates.push(gate);
3469 }
3470 }
3471 Some(gates)
3472}
3473
3474fn rule_union_gate_sets_equal(left: &[RuleUnionGate], right: &[RuleUnionGate]) -> bool {
3475 left.len() == right.len() && left.iter().all(|gate| right.contains(gate))
3476}
3477
3478fn eir_head_is_bijective_variable_tuple(head: &xlog_ir::EirAtom) -> bool {
3479 let mut variables = BTreeSet::new();
3480 head.terms.iter().all(|term| match term {
3481 EirTerm::Variable(name) => variables.insert(name),
3482 _ => false,
3483 })
3484}
3485
3486fn reconcile_augmented_head_declarations(
3501 reduced: &mut Program,
3502 augmented_rule_original_arities: &BTreeMap<usize, usize>,
3503) -> Result<BTreeMap<(String, usize), usize>> {
3504 use crate::ast::{PredColumn, TypeRef};
3505
3506 let mut augmented_signatures: BTreeMap<(String, usize), usize> = BTreeMap::new();
3510 let mut inferred_types: BTreeMap<(String, usize), Vec<Option<TypeRef>>> = BTreeMap::new();
3511
3512 let mut lowerer = Lowerer::new();
3518 lowerer.infer_schemas(reduced)?;
3519 let schemas = lowerer.schemas().clone();
3520
3521 for (rule_index, rule) in reduced.rules.iter().enumerate() {
3522 if rule.body.is_empty() {
3523 continue;
3524 }
3525 let Some(&original_arity) = augmented_rule_original_arities.get(&rule_index) else {
3527 continue;
3528 };
3529 let arity = rule.head.terms.len();
3530 if arity <= original_arity {
3531 continue;
3532 }
3533 let signature = (rule.head.predicate.clone(), original_arity);
3534 let entry = augmented_signatures.entry(signature.clone()).or_insert(0);
3535 if arity > *entry {
3536 *entry = arity;
3537 }
3538 let types = inferred_types
3539 .entry(signature)
3540 .or_insert_with(|| vec![None; arity]);
3541 if types.len() < arity {
3542 types.resize(arity, None);
3543 }
3544 let variable_types = lowerer.infer_rule_variable_types(rule, |atom, index| {
3545 schemas
3546 .get(&atom.predicate)
3547 .and_then(|schema| schema.column_type(index))
3548 })?;
3549
3550 for (col, term) in rule.head.terms.iter().enumerate() {
3553 if types[col].is_some() {
3554 continue;
3555 }
3556 let Term::Variable(head_var) = term else {
3557 continue;
3558 };
3559 if let Some((typ, _)) = variable_types.get(head_var) {
3560 types[col] = Some(TypeRef::Scalar(*typ));
3561 }
3562 }
3563 }
3564
3565 for decl in &mut reduced.predicates {
3566 let signature = (decl.name.clone(), decl.arity());
3567 let Some(&target_arity) = augmented_signatures.get(&signature) else {
3568 continue;
3569 };
3570 let mut columns = decl.schema_columns();
3571 if target_arity <= columns.len() {
3572 continue;
3573 }
3574 let inferred = inferred_types.get(&signature);
3575 for col in columns.len()..target_arity {
3576 let typ = inferred
3577 .and_then(|types| types.get(col))
3578 .and_then(|t| t.clone())
3579 .unwrap_or(TypeRef::Scalar(xlog_core::ScalarType::U32));
3581 columns.push(PredColumn { name: None, typ });
3582 }
3583 decl.types = columns.iter().map(|column| column.typ.clone()).collect();
3584 decl.columns = columns;
3585 }
3586
3587 Ok(augmented_signatures)
3588}
3589
3590fn append_body_local_tuple_key_variables_to_head(rule: &mut crate::ast::Rule) {
3591 let mut hidden_variables = Vec::new();
3592 for lit in &rule.body {
3593 let BodyLiteral::Epistemic(lit) = lit else {
3594 continue;
3595 };
3596 for term in &lit.atom.terms {
3597 let Term::Variable(variable) = term else {
3598 continue;
3599 };
3600 if variable == "_" {
3601 continue;
3602 }
3603 let already_in_head = rule
3604 .head
3605 .terms
3606 .iter()
3607 .any(|head_term| matches!(head_term, Term::Variable(name) if name == variable));
3608 if !already_in_head && !hidden_variables.iter().any(|name| name == variable) {
3609 hidden_variables.push(variable.clone());
3610 }
3611 }
3612 }
3613 for variable in hidden_variables {
3614 rule.head.terms.push(Term::Variable(variable));
3615 }
3616}
3617
3618fn wcoj_status_for_reduction(
3619 positive_relational_atoms: &[xlog_ir::EirAtom],
3620 has_negated_relational_atom: bool,
3621) -> EpistemicWcojReductionStatus {
3622 if !has_negated_relational_atom
3623 && positive_relational_atoms_are_supported_wcoj_shape(positive_relational_atoms)
3624 {
3625 EpistemicWcojReductionStatus::RequiresPlannerEligibility
3626 } else {
3627 EpistemicWcojReductionStatus::NotWcojCandidate
3628 }
3629}
3630
3631fn positive_relational_atoms_are_supported_wcoj_shape(atoms: &[xlog_ir::EirAtom]) -> bool {
3632 let mut edges: BTreeSet<(String, String)> = BTreeSet::new();
3633 let mut degrees: BTreeMap<String, usize> = BTreeMap::new();
3634 for atom in atoms {
3635 if atom.arity != 2 || atom.terms.len() != 2 {
3636 return false;
3637 }
3638 let Some(left) = eir_variable_name(&atom.terms[0]) else {
3639 return false;
3640 };
3641 let Some(right) = eir_variable_name(&atom.terms[1]) else {
3642 return false;
3643 };
3644 if left == right {
3645 return false;
3646 }
3647 let edge = if left < right {
3648 (left.to_string(), right.to_string())
3649 } else {
3650 (right.to_string(), left.to_string())
3651 };
3652 if !edges.insert(edge.clone()) {
3653 return false;
3654 }
3655 *degrees.entry(edge.0).or_insert(0) += 1;
3656 *degrees.entry(edge.1).or_insert(0) += 1;
3657 }
3658
3659 match edges.len() {
3660 3 => degrees.len() == 3 && degrees.values().all(|degree| *degree == 2),
3661 4 => degrees.len() == 4 && degrees.values().all(|degree| *degree == 2),
3662 10 | 15 | 21 | 28 => {
3663 let variable_count = degrees.len();
3664 (5..=8).contains(&variable_count)
3665 && edges.len() == variable_count * (variable_count - 1) / 2
3666 && degrees.values().all(|degree| *degree == variable_count - 1)
3667 }
3668 _ => false,
3669 }
3670}
3671
3672fn eir_variable_name(term: &EirTerm) -> Option<&str> {
3673 match term {
3674 EirTerm::Variable(name) => Some(name.as_str()),
3675 _ => None,
3676 }
3677}
3678
3679#[derive(Debug, Clone, PartialEq, Eq)]
3681pub enum FaeelCandidateResult {
3682 Model,
3684 NoModel(FaeelNoModelReason),
3686}
3687
3688#[derive(Debug, Clone, PartialEq, Eq)]
3690pub enum FaeelNoModelReason {
3691 UnfoundedPossible {
3693 predicate: String,
3695 arity: usize,
3697 },
3698 Contradiction {
3700 predicate: String,
3702 arity: usize,
3704 },
3705 UnsatisfiedLiteral {
3707 predicate: String,
3709 arity: usize,
3711 },
3712}
3713
3714#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3716pub struct GeneratePropagateTestConfig {
3717 pub max_candidates: usize,
3719}
3720
3721#[derive(Debug, Clone, Default, PartialEq, Eq)]
3723pub struct GeneratePropagateTestTrace {
3724 pub generated: usize,
3726 pub guesses: usize,
3728 pub propagated: usize,
3730 pub pruned: usize,
3732 pub reduced_program_models: usize,
3734 pub tested: usize,
3736 pub accepted: usize,
3738 pub accepted_world_views: usize,
3740 pub rejected: usize,
3742 pub rejection_reasons: Vec<FaeelNoModelReason>,
3744}
3745
3746#[derive(Debug, Clone, PartialEq, Eq)]
3748pub struct GeneratePropagateTestOutcome {
3749 pub trace: GeneratePropagateTestTrace,
3751 pub accepted_candidate_indices: Vec<usize>,
3753 pub rejected_candidate_indices: Vec<usize>,
3755}
3756
3757#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
3763pub enum EpistemicComponentMergeReason {
3764 SharedHeadPredicate {
3767 predicate: String,
3769 },
3770 DerivedPredicate {
3773 predicate: String,
3776 },
3777 SharedModalPredicate {
3780 predicate: String,
3782 },
3783 Constraint {
3786 predicates: Vec<String>,
3788 },
3789}
3790
3791#[derive(Debug, Clone, PartialEq, Eq)]
3793pub struct EpistemicDependencyComponent {
3794 pub predicates: Vec<String>,
3796 pub rule_indices: Vec<usize>,
3798 pub merge_reasons: Vec<EpistemicComponentMergeReason>,
3803}
3804
3805#[derive(Debug, Clone, PartialEq, Eq)]
3807pub struct EpistemicDependencyGraph {
3808 pub components: Vec<EpistemicDependencyComponent>,
3810}
3811
3812#[derive(Debug, Clone, PartialEq, Eq)]
3814pub struct EpistemicSplitPlan {
3815 pub components: Vec<EpistemicDependencyComponent>,
3817}
3818
3819impl EpistemicSplitPlan {
3820 pub fn recomposed_rule_indices(&self) -> Vec<usize> {
3822 let mut indices: Vec<usize> = self
3823 .components
3824 .iter()
3825 .flat_map(|component| component.rule_indices.iter().copied())
3826 .collect();
3827 indices.sort_unstable();
3828 indices
3829 }
3830}
3831
3832#[derive(Debug, Clone)]
3834pub struct EpistemicSplitExecutableComponent {
3835 pub component: EpistemicDependencyComponent,
3837 pub executable: EpistemicExecutablePlan,
3839}
3840
3841#[derive(Debug, Clone)]
3843pub struct EpistemicSplitExecutablePlan {
3844 pub split_plan: EpistemicSplitPlan,
3846 pub components: Vec<EpistemicSplitExecutableComponent>,
3848}
3849
3850impl EpistemicSplitExecutablePlan {
3851 pub fn recomposed_rule_indices(&self) -> Vec<usize> {
3861 let mut indices: Vec<usize> = self
3862 .components
3863 .iter()
3864 .flat_map(|component| component.component.rule_indices.iter().copied())
3865 .collect();
3866 indices.sort_unstable();
3867 indices
3868 }
3869
3870 pub fn planned_recomposed_rule_indices(&self) -> Vec<usize> {
3873 self.split_plan.recomposed_rule_indices()
3874 }
3875
3876 pub fn recomposed_components(&self) -> Vec<&EpistemicSplitExecutableComponent> {
3878 let mut components: Vec<_> = self.components.iter().collect();
3879 components.sort_by_key(|component| {
3880 component
3881 .component
3882 .rule_indices
3883 .iter()
3884 .copied()
3885 .min()
3886 .unwrap_or(usize::MAX)
3887 });
3888 components
3889 }
3890}
3891
3892pub fn evaluate_epistemic_literal(
3894 mode: EpistemicMode,
3895 lit: &EpistemicLiteral,
3896 interpretation: &EpistemicInterpretation,
3897) -> TruthValue {
3898 let value = match lit.op {
3899 EpistemicOp::Know => interpretation.contains_known(&lit.atom),
3900 EpistemicOp::Possible => match mode {
3901 EpistemicMode::G91 => {
3902 interpretation.contains_known(&lit.atom)
3903 || interpretation.contains_possible(&lit.atom)
3904 }
3905 EpistemicMode::Faeel => interpretation.contains_known(&lit.atom),
3906 },
3907 };
3908
3909 TruthValue::from_bool(if lit.negated { !value } else { value })
3910}
3911
3912pub fn evaluate_faeel_candidate(
3914 program: &Program,
3915 interpretation: &EpistemicInterpretation,
3916) -> Result<FaeelCandidateResult> {
3917 evaluate_epistemic_candidate(program, interpretation, EpistemicMode::Faeel)
3918}
3919
3920pub fn evaluate_epistemic_candidate(
3922 program: &Program,
3923 interpretation: &EpistemicInterpretation,
3924 mode: EpistemicMode,
3925) -> Result<FaeelCandidateResult> {
3926 let prepared = prepare_root_authored_constraint_identity(program)?;
3927 evaluate_prepared_epistemic_candidate(&prepared, interpretation, mode)
3928}
3929
3930fn evaluate_prepared_epistemic_candidate(
3931 program: &Program,
3932 interpretation: &EpistemicInterpretation,
3933 mode: EpistemicMode,
3934) -> Result<FaeelCandidateResult> {
3935 reject_gpt_epistemic_constraints(program)?;
3936 if let Some((predicate, arity)) = interpretation.first_contradiction() {
3937 return Ok(FaeelCandidateResult::NoModel(
3938 FaeelNoModelReason::Contradiction { predicate, arity },
3939 ));
3940 }
3941
3942 for rule in &program.rules {
3943 for body_lit in &rule.body {
3944 let BodyLiteral::Epistemic(lit) = body_lit else {
3945 continue;
3946 };
3947 if interpretation.contains_known(&lit.atom)
3948 && interpretation.contains_rejected(&lit.atom)
3949 {
3950 return Ok(FaeelCandidateResult::NoModel(
3951 FaeelNoModelReason::Contradiction {
3952 predicate: lit.atom.predicate.clone(),
3953 arity: lit.atom.arity(),
3954 },
3955 ));
3956 }
3957 if mode == EpistemicMode::Faeel
3958 && lit.op == EpistemicOp::Possible
3959 && interpretation.contains_possible(&lit.atom)
3960 && !interpretation.contains_known(&lit.atom)
3961 {
3962 return Ok(FaeelCandidateResult::NoModel(
3963 FaeelNoModelReason::UnfoundedPossible {
3964 predicate: lit.atom.predicate.clone(),
3965 arity: lit.atom.arity(),
3966 },
3967 ));
3968 }
3969 if evaluate_epistemic_literal(mode, lit, interpretation) == TruthValue::False {
3970 return Ok(FaeelCandidateResult::NoModel(
3971 FaeelNoModelReason::UnsatisfiedLiteral {
3972 predicate: lit.atom.predicate.clone(),
3973 arity: lit.atom.arity(),
3974 },
3975 ));
3976 }
3977 }
3978 }
3979
3980 Ok(FaeelCandidateResult::Model)
3981}
3982
3983pub fn run_generate_propagate_test(
3985 program: &Program,
3986 candidates: Vec<EpistemicInterpretation>,
3987 config: GeneratePropagateTestConfig,
3988) -> Result<GeneratePropagateTestOutcome> {
3989 run_generate_propagate_test_with_mode(
3990 program,
3991 candidates,
3992 config,
3993 program.directives.epistemic_mode_or_default(),
3994 )
3995}
3996
3997pub fn run_generate_propagate_test_with_mode(
3999 program: &Program,
4000 candidates: Vec<EpistemicInterpretation>,
4001 config: GeneratePropagateTestConfig,
4002 mode: EpistemicMode,
4003) -> Result<GeneratePropagateTestOutcome> {
4004 let prepared = prepare_root_authored_constraint_identity(program)?;
4005 reject_gpt_epistemic_constraints(&prepared)?;
4006 if candidates.len() > config.max_candidates {
4007 return Err(xlog_core::XlogError::CapacityExceeded {
4008 context: "epistemic GPT candidate guard".to_string(),
4009 required: candidates.len() as u64,
4010 limit: config.max_candidates as u64,
4011 unit: "candidates".to_string(),
4012 });
4013 }
4014
4015 let generated = candidates.len();
4016 let guesses = candidates
4017 .iter()
4018 .map(EpistemicInterpretation::epistemic_guess_count)
4019 .sum();
4020 let mut propagated_candidates = Vec::new();
4021 let mut rejection_reasons = Vec::new();
4022 let mut rejected_candidate_indices = Vec::new();
4023 for (idx, candidate) in candidates.into_iter().enumerate() {
4024 if let Some((predicate, arity)) = candidate.first_contradiction() {
4025 rejection_reasons.push(FaeelNoModelReason::Contradiction { predicate, arity });
4026 rejected_candidate_indices.push(idx);
4027 } else {
4028 propagated_candidates.push((idx, candidate));
4029 }
4030 }
4031
4032 let mut trace = GeneratePropagateTestTrace {
4033 generated,
4034 guesses,
4035 propagated: propagated_candidates.len(),
4036 pruned: generated.saturating_sub(propagated_candidates.len()),
4037 reduced_program_models: propagated_candidates.len(),
4038 rejection_reasons,
4039 ..GeneratePropagateTestTrace::default()
4040 };
4041 let mut accepted_candidate_indices = Vec::new();
4042
4043 for (idx, candidate) in &propagated_candidates {
4044 trace.tested += 1;
4045 match evaluate_prepared_epistemic_candidate(&prepared, candidate, mode)? {
4046 FaeelCandidateResult::Model => {
4047 trace.accepted += 1;
4048 trace.accepted_world_views += 1;
4049 accepted_candidate_indices.push(*idx);
4050 }
4051 FaeelCandidateResult::NoModel(reason) => {
4052 trace.rejected += 1;
4053 trace.rejection_reasons.push(reason);
4054 rejected_candidate_indices.push(*idx);
4055 }
4056 }
4057 }
4058
4059 Ok(GeneratePropagateTestOutcome {
4060 trace,
4061 accepted_candidate_indices,
4062 rejected_candidate_indices,
4063 })
4064}
4065
4066pub fn build_epistemic_dependency_graph(program: &Program) -> Result<EpistemicDependencyGraph> {
4068 if program.rules.is_empty() {
4069 return Ok(EpistemicDependencyGraph { components: vec![] });
4070 }
4071
4072 let mut parents: Vec<usize> = (0..program.rules.len()).collect();
4073 let mut rule_predicates = Vec::with_capacity(program.rules.len());
4074 let mut head_owner: BTreeMap<String, usize> = BTreeMap::new();
4075 let mut merge_log: Vec<(usize, EpistemicComponentMergeReason)> = Vec::new();
4079
4080 for (idx, rule) in program.rules.iter().enumerate() {
4081 if rule.body.is_empty() {
4082 continue;
4083 }
4084 if let Some(owner) = head_owner.get(&rule.head.predicate).copied() {
4085 union_components(&mut parents, owner, idx);
4086 merge_log.push((
4087 idx,
4088 EpistemicComponentMergeReason::SharedHeadPredicate {
4089 predicate: rule.head.predicate.clone(),
4090 },
4091 ));
4092 } else {
4093 head_owner.insert(rule.head.predicate.clone(), idx);
4094 }
4095 }
4096
4097 let mut modal_owner: BTreeMap<EpistemicAtomKey, usize> = BTreeMap::new();
4098 for (idx, rule) in program.rules.iter().enumerate() {
4099 let mut predicates = BTreeSet::new();
4100 predicates.insert(rule.head.predicate.clone());
4101 for lit in &rule.body {
4102 if let BodyLiteral::Epistemic(lit) = lit {
4103 let key =
4104 EpistemicAtomKey::from_arity(lit.atom.predicate.clone(), lit.atom.arity());
4105 if let Some(owner) = modal_owner.get(&key).copied() {
4106 union_components(&mut parents, owner, idx);
4107 merge_log.push((
4108 idx,
4109 EpistemicComponentMergeReason::SharedModalPredicate {
4110 predicate: format!("{}/{}", lit.atom.predicate, lit.atom.arity()),
4111 },
4112 ));
4113 } else {
4114 modal_owner.insert(key, idx);
4115 }
4116 }
4117 if let Some(atom) = lit.atom() {
4118 if let Some(owner) = head_owner.get(&atom.predicate).copied() {
4119 if owner != idx {
4120 union_components(&mut parents, owner, idx);
4121 merge_log.push((
4122 idx,
4123 EpistemicComponentMergeReason::DerivedPredicate {
4124 predicate: atom.predicate.clone(),
4125 },
4126 ));
4127 }
4128 }
4129 predicates.insert(atom.predicate.clone());
4130 }
4131 }
4132
4133 rule_predicates.push(predicates);
4134 }
4135
4136 let mut constraint_predicates = Vec::with_capacity(program.constraints.len());
4137 for constraint in &program.constraints {
4138 let predicates = constraint_predicate_set(constraint);
4139 let mut owners = predicates
4140 .iter()
4141 .filter_map(|predicate| head_owner.get(predicate).copied());
4142 if let Some(first_owner) = owners.next() {
4143 let mut coalesced_any = false;
4144 for owner in owners {
4145 if find_component(&mut parents, first_owner) != find_component(&mut parents, owner)
4146 {
4147 coalesced_any = true;
4148 }
4149 union_components(&mut parents, first_owner, owner);
4150 }
4151 if coalesced_any {
4152 let constraint_heads: Vec<String> = predicates
4153 .iter()
4154 .filter(|predicate| head_owner.contains_key(*predicate))
4155 .cloned()
4156 .collect();
4157 merge_log.push((
4158 first_owner,
4159 EpistemicComponentMergeReason::Constraint {
4160 predicates: constraint_heads,
4161 },
4162 ));
4163 }
4164 }
4165 constraint_predicates.push(predicates);
4166 }
4167
4168 let mut grouped: BTreeMap<usize, (BTreeSet<String>, Vec<usize>)> = BTreeMap::new();
4169 for (idx, predicates) in rule_predicates.into_iter().enumerate() {
4170 let root = find_component(&mut parents, idx);
4171 let entry = grouped
4172 .entry(root)
4173 .or_insert_with(|| (BTreeSet::new(), vec![]));
4174 entry.0.extend(predicates);
4175 entry.1.push(idx);
4176 }
4177 for predicates in constraint_predicates {
4178 let Some(root) = predicates
4179 .iter()
4180 .filter_map(|predicate| head_owner.get(predicate).copied())
4181 .map(|idx| find_component(&mut parents, idx))
4182 .next()
4183 else {
4184 continue;
4185 };
4186 grouped
4187 .entry(root)
4188 .or_insert_with(|| (BTreeSet::new(), vec![]))
4189 .0
4190 .extend(predicates);
4191 }
4192
4193 let mut reasons_by_root: BTreeMap<usize, BTreeSet<EpistemicComponentMergeReason>> =
4195 BTreeMap::new();
4196 for (touched_idx, reason) in merge_log {
4197 let root = find_component(&mut parents, touched_idx);
4198 reasons_by_root.entry(root).or_default().insert(reason);
4199 }
4200
4201 let mut components: Vec<EpistemicDependencyComponent> = grouped
4202 .into_iter()
4203 .map(|(root, (predicates, mut rule_indices))| {
4204 rule_indices.sort_unstable();
4205 let merge_reasons = reasons_by_root
4206 .remove(&root)
4207 .map(|reasons| reasons.into_iter().collect())
4208 .unwrap_or_default();
4209 EpistemicDependencyComponent {
4210 predicates: predicates.into_iter().collect(),
4211 rule_indices,
4212 merge_reasons,
4213 }
4214 })
4215 .collect();
4216 components.sort_by(|a, b| a.predicates.cmp(&b.predicates));
4217 Ok(EpistemicDependencyGraph { components })
4218}
4219
4220fn constraint_predicate_set(constraint: &Constraint) -> BTreeSet<String> {
4221 constraint
4222 .body
4223 .iter()
4224 .filter_map(|lit| lit.atom().map(|atom| atom.predicate.clone()))
4225 .collect()
4226}
4227
4228fn find_component(parents: &mut [usize], idx: usize) -> usize {
4229 if parents[idx] != idx {
4230 let root = find_component(parents, parents[idx]);
4231 parents[idx] = root;
4232 }
4233 parents[idx]
4234}
4235
4236fn union_components(parents: &mut [usize], left: usize, right: usize) {
4237 let left_root = find_component(parents, left);
4238 let right_root = find_component(parents, right);
4239 if left_root != right_root {
4240 parents[right_root] = left_root;
4241 }
4242}
4243
4244#[derive(Debug, Clone)]
4249pub struct EpistemicStratum {
4250 pub head_predicates: Vec<String>,
4252 pub rule_indices: Vec<usize>,
4254 pub program: Program,
4258}
4259
4260#[derive(Debug, Clone)]
4267pub struct EpistemicStratifiedPlan {
4268 pub strata: Vec<EpistemicStratum>,
4270 pub ordinary_post_program: Program,
4273}
4274
4275struct EpistemicallyDeterminedPredicates {
4290 determined: BTreeSet<String>,
4291}
4292
4293impl EpistemicallyDeterminedPredicates {
4294 fn analyze(program: &Program) -> Self {
4295 let invariant = InvariantRelations::analyze(program);
4296
4297 let mut derived_heads: BTreeSet<&str> = BTreeSet::new();
4299 for rule in &program.rules {
4300 if !rule.body.is_empty() {
4301 derived_heads.insert(rule.head.predicate.as_str());
4302 }
4303 }
4304
4305 let mut determined: BTreeSet<String> = BTreeSet::new();
4321 let mut changed = true;
4322 while changed {
4323 changed = false;
4324 for head in &derived_heads {
4325 if determined.contains(*head) {
4326 continue;
4327 }
4328 if Self::head_is_determined(program, head, &invariant, &derived_heads, &determined)
4329 {
4330 determined.insert((*head).to_string());
4331 changed = true;
4332 }
4333 }
4334 }
4335
4336 Self { determined }
4337 }
4338
4339 fn head_is_determined(
4342 program: &Program,
4343 head: &str,
4344 invariant: &InvariantRelations,
4345 derived_heads: &BTreeSet<&str>,
4346 determined: &BTreeSet<String>,
4347 ) -> bool {
4348 let mut defined = false;
4349 for rule in &program.rules {
4350 if rule.head.predicate != head || rule.body.is_empty() {
4351 continue;
4352 }
4353 defined = true;
4354 for lit in &rule.body {
4355 let referenced = match lit {
4356 BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) => {
4357 atom.predicate.as_str()
4358 }
4359 BodyLiteral::Epistemic(modal) => modal.atom.predicate.as_str(),
4360 BodyLiteral::Comparison(_) | BodyLiteral::IsExpr(_) | BodyLiteral::Univ(_) => {
4361 continue
4362 }
4363 };
4364 if referenced == head {
4365 return false;
4368 }
4369 let ok = invariant.is_invariant(referenced)
4370 || determined.contains(referenced)
4371 || !derived_heads.contains(referenced);
4373 if !ok {
4374 return false;
4375 }
4376 }
4377 }
4378 defined
4379 }
4380
4381 fn contains(&self, predicate: &str) -> bool {
4382 self.determined.contains(predicate)
4383 }
4384}
4385
4386pub fn try_plan_stratified_epistemic_program(
4410 program: &Program,
4411) -> Result<Option<EpistemicStratifiedPlan>> {
4412 let prepared = prepare_root_authored_constraint_identity(program)?;
4413 let program = &prepared;
4414 let determined = EpistemicallyDeterminedPredicates::analyze(program);
4415
4416 let mut needs_stratification = false;
4420 for rule in &program.rules {
4421 for lit in &rule.body {
4422 if let BodyLiteral::Epistemic(modal) = lit {
4423 if determined.contains(modal.atom.predicate.as_str())
4424 && modal.atom.predicate != rule.head.predicate
4425 {
4426 needs_stratification = true;
4427 }
4428 }
4429 }
4430 }
4431 if !needs_stratification {
4432 return Ok(None);
4433 }
4434 let removed_rules = faeel_unfounded_exact_tuple_self_support_rule_indices(program)
4435 .into_iter()
4436 .collect::<BTreeSet<_>>();
4437 validate_epistemic_derived_relation_identity(program, &removed_rules)?;
4438
4439 let stratum_level = assign_epistemic_strata(program, &determined)?;
4444 let Some(stratum_level) = stratum_level else {
4445 return Ok(None);
4446 };
4447
4448 let mut levels: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
4450 for (idx, rule) in program.rules.iter().enumerate() {
4451 let has_epistemic = rule
4452 .body
4453 .iter()
4454 .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)));
4455 if !has_epistemic {
4456 continue;
4457 }
4458 let Some(level) = stratum_level.get(rule.head.predicate.as_str()) else {
4459 return Ok(None);
4462 };
4463 levels.entry(*level).or_default().push(idx);
4464 }
4465
4466 if levels.len() < 2 {
4467 return Ok(None);
4470 }
4471
4472 let mut strata = Vec::with_capacity(levels.len());
4473 for (_level, rule_indices) in levels {
4474 let head_predicates: Vec<String> = rule_indices
4475 .iter()
4476 .filter_map(|idx| program.rules.get(*idx))
4477 .map(|rule| rule.head.predicate.clone())
4478 .collect::<BTreeSet<_>>()
4479 .into_iter()
4480 .collect();
4481 let stratum_program =
4482 build_stratum_subprogram(program, &rule_indices, &head_predicates, &stratum_level)?;
4483 strata.push(EpistemicStratum {
4484 head_predicates,
4485 rule_indices,
4486 program: stratum_program,
4487 });
4488 }
4489
4490 for stratum in &strata {
4495 if try_reduce_case_a_recursive_epistemic_program(&stratum.program)?.is_none() {
4496 validate_epistemic_relation_shapes(&stratum.program, &BTreeSet::new())?;
4497 }
4498 }
4499
4500 let ordinary_post_program = build_post_stratification_ordinary_program(program);
4501 ordinary_post_program.validate_prepared_authored_constraint_identity()?;
4502
4503 Ok(Some(EpistemicStratifiedPlan {
4504 strata,
4505 ordinary_post_program,
4506 }))
4507}
4508
4509fn build_post_stratification_ordinary_program(program: &Program) -> Program {
4517 let mut post = program.clone();
4518 post.rules.retain(|rule| {
4519 rule.body
4520 .iter()
4521 .all(|literal| !matches!(literal, BodyLiteral::Epistemic(_)))
4522 });
4523 post.constraints.retain(|constraint| {
4524 constraint
4525 .body
4526 .iter()
4527 .all(|literal| !matches!(literal, BodyLiteral::Epistemic(_)))
4528 });
4529 post
4530}
4531
4532fn stratified_schema_reduction_overrides(
4540 program: &Program,
4541) -> Result<BTreeMap<usize, crate::ast::Rule>> {
4542 let Some(plan) = try_plan_stratified_epistemic_program(program)? else {
4543 return Ok(BTreeMap::new());
4544 };
4545
4546 let mut overrides = BTreeMap::new();
4547 for stratum in plan.strata {
4548 if try_reduce_case_a_recursive_epistemic_program(&stratum.program)?.is_none() {
4549 continue;
4550 }
4551 for rule_index in stratum.rule_indices {
4552 let mut rule = program.rules.get(rule_index).cloned().ok_or_else(|| {
4553 XlogError::Compilation(format!(
4554 "stratified epistemic rule index {rule_index} is outside the source program"
4555 ))
4556 })?;
4557 resolve_recursive_epistemic_rule_modals(&mut rule);
4558 overrides.insert(rule_index, rule);
4559 }
4560 }
4561 Ok(overrides)
4562}
4563
4564fn assign_epistemic_strata(
4572 program: &Program,
4573 determined: &EpistemicallyDeterminedPredicates,
4574) -> Result<Option<BTreeMap<String, usize>>> {
4575 let mut epistemic_heads: BTreeSet<&str> = BTreeSet::new();
4577 for rule in &program.rules {
4578 if rule
4579 .body
4580 .iter()
4581 .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
4582 {
4583 epistemic_heads.insert(rule.head.predicate.as_str());
4584 }
4585 }
4586
4587 let mut modal_edges: BTreeMap<&str, BTreeSet<&str>> = BTreeMap::new();
4599 for rule in &program.rules {
4600 let head = rule.head.predicate.as_str();
4601 for lit in &rule.body {
4602 if let BodyLiteral::Epistemic(modal) = lit {
4603 let target = modal.atom.predicate.as_str();
4604 if epistemic_heads.contains(target) {
4605 if !determined.contains(target) {
4606 return Ok(None);
4609 }
4610 modal_edges.entry(head).or_default().insert(target);
4611 } else if determined.contains(target) {
4612 let support =
4616 epistemic_support_of_determined_ordinary(program, target, &epistemic_heads);
4617 if support.is_empty() {
4618 return Ok(None);
4622 }
4623 let entry = modal_edges.entry(head).or_default();
4624 for support_head in support {
4625 entry.insert(support_head);
4626 }
4627 }
4628 }
4629 }
4630 }
4631
4632 let mut level: BTreeMap<String, usize> = BTreeMap::new();
4635 fn visit<'a>(
4636 head: &'a str,
4637 modal_edges: &BTreeMap<&'a str, BTreeSet<&'a str>>,
4638 level: &mut BTreeMap<String, usize>,
4639 active: &mut BTreeSet<&'a str>,
4640 ) -> Result<usize> {
4641 if let Some(l) = level.get(head) {
4642 return Ok(*l);
4643 }
4644 if !active.insert(head) {
4645 return Err(recursive_epistemic_rejection(
4648 "stratified epistemic planning encountered a modal dependency cycle",
4649 ));
4650 }
4651 let mut l = 0;
4652 if let Some(targets) = modal_edges.get(head) {
4653 for target in targets {
4654 let tl = visit(target, modal_edges, level, active)?;
4655 l = l.max(tl + 1);
4656 }
4657 }
4658 active.remove(head);
4659 level.insert(head.to_string(), l);
4660 Ok(l)
4661 }
4662
4663 for head in &epistemic_heads {
4664 visit(head, &modal_edges, &mut level, &mut BTreeSet::new())?;
4665 }
4666
4667 Ok(Some(level))
4668}
4669
4670fn epistemic_support_of_determined_ordinary<'a>(
4678 program: &'a Program,
4679 predicate: &'a str,
4680 epistemic_heads: &BTreeSet<&'a str>,
4681) -> BTreeSet<&'a str> {
4682 let mut support: BTreeSet<&'a str> = BTreeSet::new();
4683 let mut seen: BTreeSet<&'a str> = BTreeSet::new();
4684 let mut stack: Vec<&'a str> = vec![predicate];
4685 while let Some(current) = stack.pop() {
4686 if !seen.insert(current) {
4687 continue;
4688 }
4689 for rule in &program.rules {
4690 if rule.head.predicate != current || rule.body.is_empty() {
4691 continue;
4692 }
4693 for lit in &rule.body {
4694 let referenced = match lit {
4695 BodyLiteral::Positive(atom) | BodyLiteral::Negated(atom) => {
4696 atom.predicate.as_str()
4697 }
4698 BodyLiteral::Epistemic(_)
4701 | BodyLiteral::Comparison(_)
4702 | BodyLiteral::IsExpr(_)
4703 | BodyLiteral::Univ(_) => continue,
4704 };
4705 if epistemic_heads.contains(referenced) {
4706 support.insert(referenced);
4707 } else {
4708 stack.push(referenced);
4710 }
4711 }
4712 }
4713 if epistemic_heads.contains(current) && current != predicate {
4715 support.insert(current);
4716 }
4717 }
4718 support
4719}
4720
4721fn build_stratum_subprogram(
4731 program: &Program,
4732 rule_indices: &[usize],
4733 head_predicates: &[String],
4734 stratum_level: &BTreeMap<String, usize>,
4735) -> Result<Program> {
4736 let this_level = head_predicates
4737 .iter()
4738 .filter_map(|h| stratum_level.get(h))
4739 .copied()
4740 .max()
4741 .unwrap_or(0);
4742
4743 let lower_epistemic_heads: BTreeSet<&str> = stratum_level
4746 .iter()
4747 .filter(|(_, level)| **level < this_level)
4748 .map(|(head, _)| head.as_str())
4749 .collect();
4750
4751 let all_epistemic_heads: BTreeSet<&str> = program
4754 .rules
4755 .iter()
4756 .filter(|rule| {
4757 rule.body
4758 .iter()
4759 .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
4760 })
4761 .map(|rule| rule.head.predicate.as_str())
4762 .collect();
4763
4764 let own_rule_indices: BTreeSet<usize> = rule_indices.iter().copied().collect();
4765
4766 let mut stratum = program.clone();
4767 stratum.rules = program
4768 .rules
4769 .iter()
4770 .enumerate()
4771 .filter_map(|(idx, rule)| {
4772 if own_rule_indices.contains(&idx) {
4773 return Some(rule.clone());
4774 }
4775 if lower_epistemic_heads.contains(rule.head.predicate.as_str()) {
4777 return None;
4778 }
4779 let has_epistemic = rule
4782 .body
4783 .iter()
4784 .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)));
4785 if has_epistemic && !own_rule_indices.contains(&idx) {
4786 return None;
4788 }
4789 let support = epistemic_support_of_determined_ordinary(
4800 program,
4801 rule.head.predicate.as_str(),
4802 &all_epistemic_heads,
4803 );
4804 if support
4805 .iter()
4806 .any(|h| stratum_level.get(*h).copied().unwrap_or(0) >= this_level)
4807 {
4808 return None;
4809 }
4810 Some(rule.clone())
4811 })
4812 .collect();
4813
4814 let head_set: BTreeSet<&str> = head_predicates.iter().map(String::as_str).collect();
4819 stratum.queries.clear();
4820
4821 stratum.constraints = program
4825 .constraints
4826 .iter()
4827 .filter(|constraint| {
4828 let is_ordinary = constraint
4829 .body
4830 .iter()
4831 .all(|literal| !matches!(literal, BodyLiteral::Epistemic(_)));
4832 if is_ordinary {
4833 return false;
4834 }
4835 constraint_predicate_set(constraint)
4836 .iter()
4837 .all(|p| head_set.contains(p.as_str()) || !is_program_head(program, p))
4838 })
4839 .cloned()
4840 .collect();
4841
4842 Ok(stratum)
4843}
4844
4845fn is_program_head(program: &Program, predicate: &str) -> bool {
4846 program
4847 .rules
4848 .iter()
4849 .any(|rule| !rule.body.is_empty() && rule.head.predicate == predicate)
4850}
4851
4852pub fn split_epistemic_program(program: &Program) -> Result<EpistemicSplitPlan> {
4860 Ok(EpistemicSplitPlan {
4874 components: build_epistemic_dependency_graph(program)?.components,
4875 })
4876}
4877
4878pub fn compile_epistemic_gpu_split_execution(
4880 program: &Program,
4881) -> Result<EpistemicSplitExecutablePlan> {
4882 compile_epistemic_gpu_split_execution_with_stats_snapshot(program, None)
4883}
4884
4885pub fn compile_epistemic_gpu_split_execution_with_stats_snapshot(
4892 program: &Program,
4893 stats_snapshot: Option<&StatsSnapshot>,
4894) -> Result<EpistemicSplitExecutablePlan> {
4895 let mut prepared = program.clone();
4896 if prepared.authored_constraint_source_bound.is_some() {
4897 prepared.validate_prepared_authored_constraint_identity()?;
4898 } else {
4899 prepared.prepare_authored_constraint_identity_at_root()?;
4900 }
4901 let program = &prepared;
4902 validate_epistemic_relation_shapes(program, &BTreeSet::new())?;
4903 reject_epistemic_constraints(program)?;
4904 let split_plan = split_epistemic_program(program)?;
4905 let mut components = Vec::new();
4906
4907 for component in &split_plan.components {
4908 if !component_has_epistemic_rule(program, component) {
4909 continue;
4910 }
4911
4912 let coupling = classify_cross_component_modal_coupling(program, component)?;
4921
4922 let component_program = split_component_program(program, component)?;
4923 let executable = compile_epistemic_gpu_execution_inner(
4924 &component_program,
4925 stats_snapshot,
4926 coupling.allows_multiple_output_heads(),
4927 )?;
4928 components.push(EpistemicSplitExecutableComponent {
4929 component: component.clone(),
4930 executable,
4931 });
4932 }
4933
4934 if components.is_empty() {
4935 return Err(XlogError::UnsupportedEpistemicConstruct {
4936 construct: "epistemic GPU split execution".to_string(),
4937 context: "requires at least one epistemic split component".to_string(),
4938 });
4939 }
4940
4941 Ok(EpistemicSplitExecutablePlan {
4942 split_plan,
4943 components,
4944 })
4945}
4946
4947fn component_has_epistemic_rule(
4948 program: &Program,
4949 component: &EpistemicDependencyComponent,
4950) -> bool {
4951 component
4952 .rule_indices
4953 .iter()
4954 .filter_map(|idx| program.rules.get(*idx))
4955 .any(|rule| {
4956 rule.body
4957 .iter()
4958 .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)))
4959 })
4960}
4961
4962fn component_epistemic_output_heads(
4971 program: &Program,
4972 component: &EpistemicDependencyComponent,
4973) -> Vec<String> {
4974 let mut heads: BTreeSet<String> = BTreeSet::new();
4975 for idx in &component.rule_indices {
4976 let Some(rule) = program.rules.get(*idx) else {
4977 continue;
4978 };
4979 let has_epistemic_body = rule
4980 .body
4981 .iter()
4982 .any(|lit| matches!(lit, BodyLiteral::Epistemic(_)));
4983 if has_epistemic_body {
4984 heads.insert(rule.head.predicate.clone());
4985 }
4986 }
4987 heads.into_iter().collect()
4988}
4989
4990fn format_component_merge_reasons(component: &EpistemicDependencyComponent) -> String {
4998 if component.merge_reasons.is_empty() {
4999 return "no recorded coalesce reason".to_string();
5000 }
5001 component
5002 .merge_reasons
5003 .iter()
5004 .map(|reason| match reason {
5005 EpistemicComponentMergeReason::SharedHeadPredicate { predicate } => {
5006 format!("SharedHeadPredicate({predicate})")
5007 }
5008 EpistemicComponentMergeReason::DerivedPredicate { predicate } => {
5009 format!("DerivedPredicate({predicate})")
5010 }
5011 EpistemicComponentMergeReason::SharedModalPredicate { predicate } => {
5012 format!("SharedModalPredicate({predicate})")
5013 }
5014 EpistemicComponentMergeReason::Constraint { predicates } => {
5015 format!("Constraint({})", predicates.join(", "))
5016 }
5017 })
5018 .collect::<Vec<_>>()
5019 .join(", ")
5020}
5021
5022#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5024enum CrossComponentCoupling {
5025 JointSolvable,
5029}
5030
5031impl CrossComponentCoupling {
5032 fn allows_multiple_output_heads(self) -> bool {
5035 match self {
5036 CrossComponentCoupling::JointSolvable => true,
5037 }
5038 }
5039}
5040
5041fn epistemic_tainted_predicates<'a>(
5079 program: &'a Program,
5080 component: &EpistemicDependencyComponent,
5081 epistemic_heads: &'a [String],
5082) -> BTreeSet<&'a str> {
5083 let mut tainted: BTreeSet<&str> = epistemic_heads.iter().map(String::as_str).collect();
5084 let mut changed = true;
5087 while changed {
5088 changed = false;
5089 for idx in &component.rule_indices {
5090 let Some(rule) = program.rules.get(*idx) else {
5091 continue;
5092 };
5093 if tainted.contains(rule.head.predicate.as_str()) {
5094 continue;
5095 }
5096 let body_touches_tainted = rule.body.iter().any(|lit| {
5100 lit.atom()
5101 .map(|atom| tainted.contains(atom.predicate.as_str()))
5102 .unwrap_or(false)
5103 });
5104 if body_touches_tainted {
5105 tainted.insert(rule.head.predicate.as_str());
5106 changed = true;
5107 }
5108 }
5109 }
5110 tainted
5111}
5112
5113fn classify_cross_component_modal_coupling(
5114 program: &Program,
5115 component: &EpistemicDependencyComponent,
5116) -> Result<CrossComponentCoupling> {
5117 let epistemic_heads = component_epistemic_output_heads(program, component);
5118 if epistemic_heads.len() <= 1 {
5119 return Ok(CrossComponentCoupling::JointSolvable);
5120 }
5121
5122 let tainted = epistemic_tainted_predicates(program, component, &epistemic_heads);
5134
5135 let mut nested_modal_predicates: BTreeSet<String> = BTreeSet::new();
5136 for idx in &component.rule_indices {
5137 let Some(rule) = program.rules.get(*idx) else {
5138 continue;
5139 };
5140 for lit in &rule.body {
5141 if let BodyLiteral::Epistemic(modal) = lit {
5142 if tainted.contains(modal.atom.predicate.as_str()) {
5143 nested_modal_predicates.insert(format!(
5144 "{}/{}",
5145 modal.atom.predicate,
5146 modal.atom.arity()
5147 ));
5148 }
5149 }
5150 }
5151 }
5152
5153 if nested_modal_predicates.is_empty() {
5154 return Ok(CrossComponentCoupling::JointSolvable);
5159 }
5160
5161 Err(XlogError::UnsupportedEpistemicConstruct {
5162 construct: "cross-component epistemic coupling".to_string(),
5163 context: format!(
5164 "epistemic output heads {:?} are coupled into a single dependency \
5165 component (reasons: {}) through nested modal literals over \
5166 epistemic-derived predicates {:?}; the modal truth of an \
5167 epistemic-derived head depends on another head's accepted world view, \
5168 so a single joint world-view enumeration would mis-evaluate the \
5169 nested modality and an independent split would be unsound, so this \
5170 fails closed",
5171 epistemic_heads,
5172 format_component_merge_reasons(component),
5173 nested_modal_predicates.into_iter().collect::<Vec<_>>(),
5174 ),
5175 })
5176}
5177
5178fn split_component_program(
5179 program: &Program,
5180 component: &EpistemicDependencyComponent,
5181) -> Result<Program> {
5182 let mut component_program = program.clone();
5183 let component_predicates: BTreeSet<&str> =
5184 component.predicates.iter().map(String::as_str).collect();
5185 let component_rule_indices: BTreeSet<usize> = component.rule_indices.iter().copied().collect();
5186 let head_predicates: BTreeSet<&str> = program
5187 .rules
5188 .iter()
5189 .map(|rule| rule.head.predicate.as_str())
5190 .collect();
5191 component_program.rules = program
5192 .rules
5193 .iter()
5194 .enumerate()
5195 .filter_map(|(idx, rule)| {
5196 (component_rule_indices.contains(&idx)
5197 || (rule.body.is_empty()
5198 && component_predicates.contains(rule.head.predicate.as_str())))
5199 .then_some(rule.clone())
5200 })
5201 .collect();
5202 component_program.constraints = program
5203 .constraints
5204 .iter()
5205 .filter(|constraint| {
5206 let predicates = constraint_predicate_set(constraint);
5207 let has_component_owned_predicate = predicates
5208 .iter()
5209 .any(|predicate| head_predicates.contains(predicate.as_str()));
5210 !has_component_owned_predicate
5211 || predicates
5212 .iter()
5213 .all(|predicate| component_predicates.contains(predicate.as_str()))
5214 })
5215 .cloned()
5216 .collect();
5217 Ok(component_program)
5218}
5219
5220#[cfg(test)]
5221mod tests {
5222 use super::*;
5223 use crate::ast::{PredColumn, PredDecl, TypeRef};
5224 use crate::parse_program;
5225
5226 #[test]
5227 fn augmented_head_reconciliation_preserves_columns_only_declarations() {
5228 let symbol = TypeRef::Scalar(xlog_core::ScalarType::Symbol);
5229 let wide_integer = TypeRef::Scalar(xlog_core::ScalarType::I64);
5230 let mut program = Program::new();
5231 program.predicates = vec![
5232 PredDecl {
5233 name: "result".to_string(),
5234 types: Vec::new(),
5235 columns: vec![PredColumn {
5236 name: Some("key".to_string()),
5237 typ: symbol.clone(),
5238 }],
5239 is_private: false,
5240 },
5241 PredDecl {
5242 name: "source".to_string(),
5243 types: Vec::new(),
5244 columns: vec![
5245 PredColumn {
5246 name: Some("key".to_string()),
5247 typ: symbol.clone(),
5248 },
5249 PredColumn {
5250 name: Some("value".to_string()),
5251 typ: wide_integer.clone(),
5252 },
5253 ],
5254 is_private: false,
5255 },
5256 ];
5257 program.rules.push(crate::ast::Rule {
5258 head: Atom {
5259 predicate: "result".to_string(),
5260 terms: vec![
5261 Term::Variable("Key".to_string()),
5262 Term::Variable("Value".to_string()),
5263 ],
5264 },
5265 body: vec![BodyLiteral::Positive(Atom {
5266 predicate: "source".to_string(),
5267 terms: vec![
5268 Term::Variable("Key".to_string()),
5269 Term::Variable("Value".to_string()),
5270 ],
5271 })],
5272 });
5273 let resolved = BTreeMap::from([(0, 1)]);
5274
5275 let widened = reconcile_augmented_head_declarations(&mut program, &resolved)
5276 .expect("reconcile augmented declaration");
5277 let declaration = program
5278 .predicates
5279 .iter()
5280 .find(|declaration| declaration.name == "result")
5281 .unwrap();
5282
5283 assert_eq!(widened.get(&("result".to_string(), 1)), Some(&2));
5284 assert_eq!(declaration.arity(), 2);
5285 assert_eq!(
5286 declaration.types,
5287 vec![symbol.clone(), wide_integer.clone()]
5288 );
5289 assert_eq!(
5290 declaration
5291 .columns
5292 .iter()
5293 .map(|column| column.typ.clone())
5294 .collect::<Vec<_>>(),
5295 vec![symbol, wide_integer]
5296 );
5297 }
5298
5299 #[test]
5300 fn augmented_head_reconciliation_uses_body_declarations_by_name_and_arity() {
5301 let program = parse_program(
5302 r#"
5303 #pragma epistemic_mode = faeel
5304 pred node(symbol).
5305 pred source(symbol, i64).
5306 pred source(u32).
5307 pred result(symbol).
5308 node(key).
5309 source(key, 5000000000).
5310 source(1).
5311 result(X) :- node(X), know source(X, Y).
5312 "#,
5313 )
5314 .expect("parse same-name multi-arity schema fixture");
5315
5316 let reduced = reduce_epistemic_program_to_ordinary_for_stratified_schema(&program)
5317 .expect("same-name body declarations should reduce");
5318 let result = reduced
5319 .predicates
5320 .iter()
5321 .find(|declaration| declaration.name == "result")
5322 .expect("missing result declaration");
5323 let types = result
5324 .schema_columns()
5325 .into_iter()
5326 .map(|column| column.typ)
5327 .collect::<Vec<_>>();
5328
5329 assert_eq!(
5330 types,
5331 vec![
5332 TypeRef::Scalar(xlog_core::ScalarType::Symbol),
5333 TypeRef::Scalar(xlog_core::ScalarType::I64),
5334 ]
5335 );
5336 assert!(reduced
5337 .predicates
5338 .iter()
5339 .any(|declaration| declaration.name == "source/2"));
5340 assert!(reduced
5341 .predicates
5342 .iter()
5343 .any(|declaration| declaration.name == "source/1"));
5344 crate::compile::Compiler::new()
5345 .compile_program(&reduced)
5346 .expect("arity-qualified reduced program should compile");
5347 compile_epistemic_gpu_execution(&program)
5348 .expect("production epistemic compilation should use the exact source signature");
5349 }
5350
5351 #[test]
5352 fn augmented_head_reconciliation_uses_undeclared_fixed_point_schemas() {
5353 let sources = [
5354 r#"
5355 #pragma epistemic_mode = faeel
5356 pred node(symbol).
5357 pred result(symbol).
5358 node(key).
5359 edge(key, 5000000000).
5360 result(X) :- node(X), know edge(X, Y).
5361 "#,
5362 r#"
5363 #pragma epistemic_mode = faeel
5364 pred node(symbol).
5365 pred result(symbol).
5366 node(key).
5367 raw(key, 5000000000).
5368 edge(X, Y) :- raw(X, Y).
5369 result(X) :- node(X), know edge(X, Y).
5370 "#,
5371 ];
5372
5373 for source in sources {
5374 let program = parse_program(source).expect("parse inferred-schema fixture");
5375 let reduced = reduce_epistemic_program_to_ordinary(&program)
5376 .expect("inferred modal source should reduce");
5377 let result = reduced
5378 .predicates
5379 .iter()
5380 .find(|declaration| declaration.name == "result")
5381 .expect("missing result declaration");
5382 assert_eq!(
5383 result
5384 .schema_columns()
5385 .into_iter()
5386 .map(|column| column.typ)
5387 .collect::<Vec<_>>(),
5388 vec![
5389 TypeRef::Scalar(xlog_core::ScalarType::Symbol),
5390 TypeRef::Scalar(xlog_core::ScalarType::I64),
5391 ]
5392 );
5393 crate::compile::Compiler::new()
5394 .compile_program(&reduced)
5395 .expect("fixed-point inferred hidden-column type should compile");
5396 }
5397 }
5398
5399 #[test]
5400 fn augmented_head_reconciliation_uses_arithmetic_binding_type() {
5401 let program = parse_program(
5402 r#"
5403 #pragma epistemic_mode = faeel
5404 pred node(symbol).
5405 pred allowed(u64).
5406 pred result(symbol).
5407 node(key).
5408 allowed(1).
5409 result(X) :- node(X), Y is cast(1, u64), not know allowed(Y).
5410 "#,
5411 )
5412 .expect("parse arithmetic-binding fixture");
5413
5414 let reduced = reduce_epistemic_program_to_ordinary(&program)
5415 .expect("arithmetic-bound hidden column should reduce");
5416 let result = reduced
5417 .predicates
5418 .iter()
5419 .find(|declaration| declaration.name == "result")
5420 .expect("missing result declaration");
5421 assert_eq!(
5422 result
5423 .schema_columns()
5424 .into_iter()
5425 .map(|column| column.typ)
5426 .collect::<Vec<_>>(),
5427 vec![
5428 TypeRef::Scalar(xlog_core::ScalarType::Symbol),
5429 TypeRef::Scalar(xlog_core::ScalarType::U64),
5430 ]
5431 );
5432 crate::compile::Compiler::new()
5433 .compile_program(&reduced)
5434 .expect("arithmetic-bound hidden-column type should compile");
5435 }
5436
5437 #[test]
5438 fn augmented_head_reconciliation_widens_only_the_original_signature() {
5439 let mut reduced = parse_program(
5440 r#"
5441 pred edge(symbol, i64).
5442 pred triple(symbol, i64, u32).
5443 pred result(symbol, i64, u32).
5444 pred result(symbol).
5445 result(X, Y, Z) :- triple(X, Y, Z).
5446 result(X, Y) :- edge(X, Y).
5447 ?- result(A, B, C).
5448 ?- result(X).
5449 "#,
5450 )
5451 .expect("parse exact-signature reconciliation fixture");
5452
5453 let augmented =
5454 reconcile_augmented_head_declarations(&mut reduced, &BTreeMap::from([(1, 1)]))
5455 .expect("reconcile exact augmented signature");
5456 let declaration_arities = reduced
5457 .predicates
5458 .iter()
5459 .filter(|declaration| declaration.name == "result")
5460 .map(PredDecl::arity)
5461 .collect::<Vec<_>>();
5462 let rule_arities = reduced
5463 .rules
5464 .iter()
5465 .filter(|rule| rule.head.predicate == "result")
5466 .map(|rule| rule.head.arity())
5467 .collect::<Vec<_>>();
5468 let query_arities = reduced
5469 .queries
5470 .iter()
5471 .filter(|query| query.atom.predicate == "result")
5472 .map(|query| query.atom.arity())
5473 .collect::<Vec<_>>();
5474
5475 assert_eq!(augmented.get(&("result".to_string(), 1)), Some(&2));
5476 assert_eq!(declaration_arities, vec![3, 2]);
5477 assert_eq!(rule_arities, vec![3, 2]);
5478 assert_eq!(query_arities, vec![3, 1]);
5479 }
5480
5481 #[test]
5482 fn augmented_undeclared_head_removes_its_original_query() {
5483 let program = parse_program(
5484 r#"
5485 #pragma epistemic_mode = faeel
5486 node(key).
5487 edge(key, 5000000000).
5488 result(X) :- node(X), know edge(X, Y).
5489 ?- result(X).
5490 "#,
5491 )
5492 .expect("parse undeclared augmented-head fixture");
5493
5494 let reduced = reduce_epistemic_program_to_ordinary_for_stratified_schema(&program)
5495 .expect("undeclared augmented head should reduce");
5496
5497 assert!(reduced
5498 .rules
5499 .iter()
5500 .any(|rule| rule.head.predicate == "result" && rule.head.arity() == 2));
5501 assert!(!reduced
5502 .queries
5503 .iter()
5504 .any(|query| query.atom.predicate == "result" && query.atom.arity() == 1));
5505 }
5506
5507 #[test]
5508 fn divergent_augmented_head_arities_are_rejected_before_reduction() {
5509 let different_modal_widths = r#"
5510 #pragma epistemic_mode = faeel
5511 pred node(symbol).
5512 pred edge(symbol, i64).
5513 pred triple(symbol, i64, u32).
5514 pred result(symbol).
5515 node(key).
5516 edge(key, 5000000000).
5517 triple(key, 5000000000, 1).
5518 result(X) :- node(X), know edge(X, Y).
5519 result(X) :- node(X), know triple(X, Y, Z).
5520 "#;
5521 let ordinary_sibling = r#"
5522 #pragma epistemic_mode = faeel
5523 pred base(symbol).
5524 pred node(symbol).
5525 pred edge(symbol, i64).
5526 pred result(symbol).
5527 base(key).
5528 node(key).
5529 edge(key, 5000000000).
5530 result(X) :- base(X).
5531 result(X) :- node(X), know edge(X, Y).
5532 "#;
5533
5534 for source in [different_modal_widths, ordinary_sibling] {
5535 let program = parse_program(source).expect("parse divergent augmented-head fixture");
5536 let errors = [
5537 plan_epistemic_gpu_execution(&program)
5538 .expect_err("GPU planning must reject divergent reduced arities"),
5539 reduce_epistemic_program_to_ordinary(&program)
5540 .expect_err("execution reduction must reject divergent arities"),
5541 reduce_epistemic_program_to_ordinary_for_stratified_schema(&program)
5542 .expect_err("schema reduction must reject divergent arities"),
5543 ];
5544
5545 for error in errors {
5546 let message = error.to_string();
5547 assert!(message.contains("epistemic augmented predicate schema"));
5548 assert!(message.contains("result/1"), "{message}");
5549 assert!(
5550 message.contains("incompatible internal arities"),
5551 "{message}"
5552 );
5553 }
5554 }
5555 }
5556
5557 #[test]
5558 fn single_pass_epistemic_rule_unions_without_clause_provenance_are_rejected() {
5559 let fixtures = [
5560 r#"
5561 #pragma epistemic_mode = faeel
5562 pred p().
5563 pred q().
5564 pred result(symbol).
5565 q().
5566 result(a) :- know p().
5567 result(b) :- know q().
5568 ?- result(X).
5569 "#,
5570 r#"
5571 #pragma epistemic_mode = faeel
5572 pred q().
5573 pred result(symbol).
5574 result(a).
5575 result(b) :- know q().
5576 ?- result(X).
5577 "#,
5578 ];
5579
5580 for source in fixtures {
5581 let program = parse_program(source).expect("parse epistemic rule-union fixture");
5582 let errors = [
5583 plan_epistemic_gpu_execution(&program)
5584 .expect_err("GPU planning must reject a provenance-free rule union"),
5585 reduce_epistemic_program_to_ordinary(&program)
5586 .expect_err("execution reduction must reject a provenance-free rule union"),
5587 reduce_epistemic_program_to_ordinary_for_stratified_schema(&program)
5588 .expect_err("schema reduction must reject a provenance-free rule union"),
5589 ];
5590 for error in errors {
5591 let message = error.to_string();
5592 assert!(message.contains("epistemic rule-union materialization"));
5593 assert!(message.contains("result/1"), "{message}");
5594 assert!(message.contains("per-clause modal provenance"), "{message}");
5595 }
5596 }
5597 }
5598
5599 #[test]
5600 fn equivalent_epistemic_rule_union_filters_are_distributive() {
5601 let program = parse_program(
5602 r#"
5603 #pragma epistemic_mode = faeel
5604 pred target(u32).
5605 pred left(u32).
5606 pred right(u32).
5607 pred result(u32).
5608 target(1).
5609 target(2).
5610 left(1).
5611 right(2).
5612 result(X) :- left(X), know target(X).
5613 result(Y) :- right(Y), possible target(Y).
5614 ?- result(Value).
5615 "#,
5616 )
5617 .expect("parse equivalent-filter rule union");
5618
5619 plan_epistemic_gpu_execution(&program)
5620 .expect("equivalent invariant modal filters must distribute over the rule union");
5621 reduce_epistemic_program_to_ordinary(&program)
5622 .expect("execution reduction must preserve an equivalent-filter rule union");
5623 reduce_epistemic_program_to_ordinary_for_stratified_schema(&program)
5624 .expect("schema reduction must preserve an equivalent-filter rule union");
5625 }
5626
5627 #[test]
5628 fn independently_founded_ground_gates_are_distributive_across_rule_union() {
5629 let program = parse_program(
5630 r#"
5631 #pragma epistemic_mode = g91
5632 pred left(u32).
5633 pred right(u32).
5634 pred result(u32).
5635 left(1).
5636 right(1).
5637 result(1) :- possible left(1).
5638 result(2) :- possible right(1).
5639 ?- result(Value).
5640 "#,
5641 )
5642 .expect("parse independently founded ground-gate rule union");
5643
5644 plan_epistemic_gpu_execution(&program)
5645 .expect("independently founded ground gates must distribute over the rule union");
5646 reduce_epistemic_program_to_ordinary(&program)
5647 .expect("execution reduction must preserve independently founded ground gates");
5648 }
5649
5650 #[test]
5651 fn g91_exact_head_possible_union_preserves_compatibility_self_support() {
5652 let program = parse_program(
5653 r#"
5654 #pragma epistemic_mode = g91
5655 pred seed(u32).
5656 pred node(u32).
5657 pred p(u32).
5658 seed(1).
5659 node(2).
5660 p(X) :- seed(X).
5661 p(X) :- node(X), possible p(X).
5662 ?- p(X).
5663 "#,
5664 )
5665 .expect("parse G91 exact-head possibility union");
5666
5667 assert_eq!(
5668 classify_recursive_epistemic_program(&program).unwrap(),
5669 RecursiveEpistemicClass::ModalCycle
5670 );
5671 let prepared = prepare_epistemic_program(&program).expect("validate G91 source");
5672 let compatibility = try_prepare_g91_compatibility_reduction(&prepared)
5673 .expect("G91 modal cycle must be admitted")
5674 .expect("G91 modal cycle must use an explicit compatibility fixpoint");
5675 Compiler::new()
5676 .compile_program(compatibility.upper_bound_program())
5677 .expect("G91 upper-bound program must compile");
5678 Compiler::new()
5679 .compile_program(compatibility.refinement_program())
5680 .expect("declared G91 snapshot program must compile");
5681 assert_eq!(compatibility.snapshot_relations().len(), 1);
5682 }
5683
5684 #[test]
5685 fn g91_compatibility_applies_to_exact_tuples_across_one_recursive_component() {
5686 let program = parse_program(
5687 r#"
5688 #pragma epistemic_mode = g91
5689 pred domain(u32).
5690 pred p(u32).
5691 pred q(u32).
5692 domain(1).
5693 p(X) :- domain(X), possible q(X).
5694 q(X) :- domain(X), possible p(X).
5695 ?- p(X).
5696 ?- q(X).
5697 "#,
5698 )
5699 .expect("parse mutual G91 modal component");
5700
5701 let prepared = prepare_epistemic_program(&program).expect("validate mutual G91 source");
5702 let compatibility = try_prepare_g91_compatibility_reduction(&prepared)
5703 .expect("mutual G91 modal component must be admitted")
5704 .expect("mutual G91 modal component must use compatibility iteration");
5705 for rule in compatibility
5706 .upper_bound_program()
5707 .rules
5708 .iter()
5709 .filter(|rule| matches!(rule.head.predicate.as_str(), "p" | "q"))
5710 {
5711 assert!(
5712 rule.body
5713 .iter()
5714 .any(|literal| matches!(literal, BodyLiteral::Comparison(_))),
5715 "the exact tuple compatibility edge must become a tautological conjunct: {rule:?}"
5716 );
5717 assert!(
5718 !rule.body.iter().any(|literal| {
5719 matches!(literal, BodyLiteral::Positive(atom) if atom.predicate == "p" || atom.predicate == "q")
5720 }),
5721 "a compatibility edge must not become an ordinary recursive join: {rule:?}"
5722 );
5723 }
5724 for rule in compatibility
5725 .refinement_program()
5726 .rules
5727 .iter()
5728 .filter(|rule| matches!(rule.head.predicate.as_str(), "p" | "q"))
5729 {
5730 assert!(rule.body.iter().any(|literal| {
5731 matches!(literal, BodyLiteral::Positive(atom) if atom.predicate.starts_with("__xlog_g91_snapshot_"))
5732 }));
5733 assert!(!rule
5734 .body
5735 .iter()
5736 .any(|literal| matches!(literal, BodyLiteral::Epistemic(_))));
5737 }
5738 }
5739
5740 #[test]
5741 fn g91_snapshot_names_avoid_programmatic_relation_collisions() {
5742 let mut program = parse_program("pred p(u32).").expect("parse source relation");
5743 program.predicates.push(PredDecl {
5744 name: "__xlog_g91_snapshot_p".to_string(),
5745 types: vec![TypeRef::Scalar(xlog_core::ScalarType::U32)],
5746 columns: vec![PredColumn {
5747 name: None,
5748 typ: TypeRef::Scalar(xlog_core::ScalarType::U32),
5749 }],
5750 is_private: false,
5751 });
5752 let target = "p".to_string();
5753 let names = g91_snapshot_relation_names(&program, std::iter::once(&target));
5754 assert_eq!(
5755 names.get("p").map(String::as_str),
5756 Some("__xlog_g91_snapshot_p_0")
5757 );
5758 }
5759
5760 #[test]
5761 fn g91_compatibility_rejects_recursive_aggregation_in_the_selected_component() {
5762 let program = parse_program(
5763 r#"
5764 #pragma epistemic_mode = g91
5765 pred seed(u32).
5766 pred p(u32).
5767 pred totals(u64).
5768 seed(1).
5769 p(X) :- seed(X), possible p(X).
5770 p(X) :- seed(X), totals(_).
5771 totals(count(X)) :- p(X).
5772 ?- p(X).
5773 "#,
5774 )
5775 .expect("parse recursive aggregate compatibility fixture");
5776
5777 let prepared = prepare_epistemic_program(&program).expect("validate G91 source");
5778 let error = try_prepare_g91_compatibility_reduction(&prepared)
5779 .expect_err("recursive aggregation makes compatibility refinement non-monotone");
5780 let message = error.to_string();
5781 assert!(message.contains("Gelfond-1991 compatibility"), "{message}");
5782 assert!(message.contains("aggregate"), "{message}");
5783 assert!(message.contains("totals"), "{message}");
5784 }
5785
5786 #[test]
5787 fn g91_compatibility_rejects_recursive_negation_in_the_selected_component() {
5788 for negated_dependency in [
5789 "not blocked(X)",
5790 "not possible blocked(X)",
5791 "not know blocked(X)",
5792 ] {
5793 let program = parse_program(&format!(
5794 r#"
5795 #pragma epistemic_mode = g91
5796 pred seed(u32).
5797 pred p(u32).
5798 pred blocked(u32).
5799 seed(1).
5800 p(X) :- seed(X), possible p(X).
5801 p(X) :- seed(X), {negated_dependency}.
5802 blocked(X) :- p(X).
5803 ?- p(X).
5804 "#,
5805 ))
5806 .expect("parse recursive negation compatibility fixture");
5807
5808 let prepared = prepare_epistemic_program(&program).expect("validate G91 source");
5809 let error = match try_prepare_g91_compatibility_reduction(&prepared) {
5810 Err(error) => error,
5811 Ok(_) => panic!(
5812 "recursive dependency `{negated_dependency}` must make compatibility \
5813 refinement non-monotone"
5814 ),
5815 };
5816 let message = error.to_string();
5817 assert!(message.contains("Gelfond-1991 compatibility"), "{message}");
5818 assert!(message.contains("negation"), "{message}");
5819 assert!(message.contains("p"), "{message}");
5820 }
5821 }
5822
5823 #[test]
5824 fn source_validation_combines_modal_and_arithmetic_type_evidence_before_elision() {
5825 let program = parse_program(
5826 r#"
5827 #pragma epistemic_mode = faeel
5828 pred p(u32).
5829 p(X) :- X is cast(1, u64), possible p(X).
5830 ?- p(X).
5831 "#,
5832 )
5833 .expect("parse arithmetic and modal type-conflict fixture");
5834
5835 let error = prepare_epistemic_program(&program)
5836 .expect_err("foundedness must not hide the authored type conflict");
5837 let message = error.to_string();
5838 assert!(message.contains("Type mismatch"), "{message}");
5839 assert!(
5840 message.contains("U32") && message.contains("U64"),
5841 "{message}"
5842 );
5843 }
5844
5845 #[test]
5846 fn source_validation_uses_lowerer_arithmetic_order_before_elision() {
5847 let program = parse_program(
5848 r#"
5849 #pragma epistemic_mode = faeel
5850 pred p(i64).
5851 p(X) :- X is Y + 1, Y is 1, possible p(X).
5852 "#,
5853 )
5854 .expect("parse reversed arithmetic dependency fixture");
5855
5856 let error = prepare_epistemic_program(&program)
5857 .expect_err("a later arithmetic binding cannot retroactively validate an earlier one");
5858 assert!(
5859 error.to_string().contains("variable X not bound"),
5860 "{error}"
5861 );
5862 }
5863
5864 #[test]
5865 fn source_validation_rejects_structured_modal_arity_before_rule_elision() {
5866 for mode in ["faeel", "g91"] {
5867 let program = parse_program(&format!(
5868 r#"
5869 #pragma epistemic_mode = {mode}
5870 pred p(list<symbol>).
5871 p([a, b]) :- possible p([a, b]).
5872 ?- p(X).
5873 "#
5874 ))
5875 .expect("parse structured exact-self-support fixture");
5876
5877 let error = prepare_epistemic_program(&program)
5878 .expect_err("a flattened modal key must match its authored target arity");
5879 let message = error.to_string();
5880 assert!(message.contains("epistemic modal tuple key"), "{message}");
5881 assert!(message.contains("target arity 1"), "{message}");
5882 assert!(message.contains("binding arity 2"), "{message}");
5883 }
5884 }
5885
5886 #[test]
5887 fn source_validation_accepts_structured_key_matching_flat_target_arity() {
5888 let program = parse_program(
5889 r#"
5890 #pragma epistemic_mode = faeel
5891 pred host(u32, u32).
5892 pred watched(u32, u32).
5893 pred out(u32, u32).
5894 host(1, 2).
5895 watched(1, 2).
5896 out(X, Y) :- host(X, Y), know watched([X, Y]).
5897 ?- out(X, Y).
5898 "#,
5899 )
5900 .expect("parse matching structured modal key fixture");
5901
5902 validate_epistemic_source_program(&program)
5903 .expect("a two-element structured key must address a binary target");
5904
5905 let multi_arity = epistemic_extensional_multi_arity_predicates(&program);
5906 assert!(
5907 !multi_arity.contains("watched"),
5908 "a two-column structured modal key and watched/2 are one signature"
5909 );
5910 }
5911
5912 #[test]
5913 fn invariant_analysis_treats_shared_acyclic_dependencies_as_a_diamond() {
5914 let program = parse_program(
5915 r#"
5916 pred base(u32).
5917 pred left(u32).
5918 pred right(u32).
5919 pred joined(u32).
5920 pred out(u32).
5921 base(1).
5922 left(X) :- base(X).
5923 right(X) :- base(X).
5924 joined(X) :- left(X), right(X).
5925 out(X) :- possible joined(X).
5926 ?- out(X).
5927 "#,
5928 )
5929 .expect("parse invariant diamond fixture");
5930
5931 let invariant = InvariantRelations::analyze(&program);
5932 assert!(invariant.is_invariant("joined"));
5933 validate_epistemic_source_program(&program)
5934 .expect("the positive modal over an acyclic diamond must bind its output");
5935 reduce_epistemic_program_to_ordinary(&program)
5936 .expect("the invariant modal binder must reduce to an ordinary join");
5937 }
5938
5939 #[test]
5940 fn positive_modal_binders_are_independent_of_conjunct_order() {
5941 for mode in ["faeel", "g91"] {
5942 for modal_body in [
5943 "possible p(X), possible base(X)",
5944 "possible base(X), possible p(X)",
5945 ] {
5946 let program = parse_program(&format!(
5947 r#"
5948 #pragma epistemic_mode = {mode}
5949 pred base(u32).
5950 pred p(u32).
5951 base(1).
5952 p(X) :- {modal_body}.
5953 ?- p(X).
5954 "#
5955 ))
5956 .expect("parse modal binder ordering fixture");
5957
5958 validate_epistemic_source_program(&program).unwrap_or_else(|error| {
5959 panic!("{mode} body `{modal_body}` must be range-restricted: {error}")
5960 });
5961 }
5962 }
5963 }
5964
5965 #[test]
5966 fn negated_exact_modal_cycles_are_never_removed_as_foundedness_elision() {
5967 let program = parse_program(
5968 r#"
5969 #pragma epistemic_mode = faeel
5970 pred p().
5971 p() :- not possible p().
5972 ?- p().
5973 "#,
5974 )
5975 .expect("parse negated exact modal cycle");
5976
5977 let prepared = prepare_epistemic_program(&program)
5978 .expect("negated modal cycle must survive source preparation");
5979 assert!(!prepared.removed_unfounded_rules());
5980 assert!(prepared.active_program().rules.iter().any(|rule| {
5981 rule.body
5982 .iter()
5983 .any(|literal| matches!(literal, BodyLiteral::Epistemic(modal) if modal.negated))
5984 }));
5985 }
5986
5987 #[test]
5988 fn modal_fixpoint_reduction_preserves_non_bijective_sibling_unions() {
5989 let fixtures = [
5990 r#"
5991 #pragma epistemic_mode = faeel
5992 pred domain(symbol).
5993 pred other(symbol, symbol).
5994 pred p(symbol, symbol).
5995 domain(a).
5996 other(c, d).
5997 p(X, X) :- domain(X).
5998 p(A, B) :- other(A, B).
5999 p(X, X) :- domain(X), know p(X, X).
6000 ?- p(A, B).
6001 "#,
6002 r#"
6003 #pragma epistemic_mode = faeel
6004 pred left(symbol).
6005 pred right(symbol).
6006 pred p(symbol, symbol).
6007 left(x).
6008 right(y).
6009 p(a, X) :- left(X).
6010 p(b, Y) :- right(Y).
6011 p(a, X) :- left(X), know p(a, X).
6012 ?- p(A, B).
6013 "#,
6014 ];
6015
6016 for source in fixtures {
6017 let program = parse_program(source).expect("parse non-bijective rule union");
6018 assert_eq!(
6019 classify_recursive_epistemic_program(&program).unwrap(),
6020 RecursiveEpistemicClass::ModalCycle
6021 );
6022 let reduced = try_reduce_case_a_recursive_epistemic_program(&program)
6023 .expect("modal-cycle sibling union must be admitted")
6024 .expect("modal-cycle sibling union must reduce to a fixpoint");
6025 Compiler::new()
6026 .compile_program(&reduced)
6027 .expect("ordinary fixpoint preserves per-clause sibling rows");
6028 }
6029 }
6030
6031 #[test]
6032 fn constrained_support_does_not_found_a_wider_modal_domain() {
6033 let program = parse_program(
6034 r#"
6035 #pragma epistemic_mode = faeel
6036 pred seed(u32).
6037 pred p(u32).
6038 seed(1).
6039 seed(2).
6040 p(X) :- seed(X), X = 1.
6041 p(X) :- seed(X), possible p(X).
6042 ?- p(X).
6043 "#,
6044 )
6045 .expect("parse constrained foundedness program");
6046
6047 let reduced = reduce_epistemic_program_to_ordinary(&program)
6048 .expect("the unfounded modal clause must be removed, not rejected");
6049 assert_eq!(
6050 reduced
6051 .rules
6052 .iter()
6053 .filter(|rule| rule.head.predicate == "p" && !rule.body.is_empty())
6054 .count(),
6055 1,
6056 "a constrained support clause cannot found the wider self-support domain"
6057 );
6058 }
6059
6060 #[test]
6061 fn derived_predicate_source_arity_collisions_are_rejected() {
6062 let fixtures = [
6063 r#"
6064 #pragma epistemic_mode = faeel
6065 unary(a).
6066 binary(a, b).
6067 result(X) :- unary(X), know unary(X).
6068 result(X, Y) :- binary(X, Y), know binary(X, Y).
6069 "#,
6070 r#"
6071 #pragma epistemic_mode = faeel
6072 node(key).
6073 edge(key, 5000000000).
6074 result(X) :- node(X), know edge(X, Y).
6075 ?- result(A, B).
6076 "#,
6077 r#"
6078 #pragma epistemic_mode = faeel
6079 node(key).
6080 edge(key, 5000000000).
6081 result(X) :- node(X), know edge(X, Y).
6082 observer(X) :- result(X, Y).
6083 "#,
6084 ];
6085
6086 for source in fixtures {
6087 let program = parse_program(source).expect("parse source-arity collision fixture");
6088 let errors = [
6089 plan_epistemic_gpu_execution(&program)
6090 .expect_err("GPU planning must reject derived source-arity collisions"),
6091 reduce_epistemic_program_to_ordinary(&program)
6092 .expect_err("execution reduction must reject source-arity collisions"),
6093 reduce_epistemic_program_to_ordinary_for_stratified_schema(&program)
6094 .expect_err("schema reduction must reject source-arity collisions"),
6095 ];
6096 for error in errors {
6097 let message = error.to_string();
6098 assert!(
6099 message.contains("epistemic derived predicate schema"),
6100 "{message}"
6101 );
6102 assert!(message.contains("result"), "{message}");
6103 assert!(message.contains("{1, 2}"), "{message}");
6104 }
6105 }
6106 }
6107
6108 #[test]
6109 fn constrained_augmented_head_queries_are_rejected() {
6110 let fixtures = [
6111 r#"
6112 #pragma epistemic_mode = faeel
6113 node(key).
6114 edge(key, 5000000000).
6115 result(X) :- node(X), know edge(X, Y).
6116 ?- result(other).
6117 "#,
6118 r#"
6119 #pragma epistemic_mode = faeel
6120 pair(left, right).
6121 edge(left, 5000000000).
6122 result(X, Y) :- pair(X, Y), know edge(X, Z).
6123 ?- result(Value, Value).
6124 "#,
6125 r#"
6126 #pragma epistemic_mode = faeel
6127 node(key).
6128 edge(key, 5000000000).
6129 allowed(5000000000).
6130 result(X) :- node(X), edge(X, Y), know allowed(Y).
6131 ?- result(key).
6132 "#,
6133 ];
6134
6135 for source in fixtures {
6136 let program = parse_program(source).expect("parse constrained-query fixture");
6137 let errors = [
6138 plan_epistemic_gpu_execution(&program)
6139 .expect_err("GPU planning must reject a constrained augmented query"),
6140 reduce_epistemic_program_to_ordinary(&program)
6141 .expect_err("execution reduction must reject a constrained augmented query"),
6142 reduce_epistemic_program_to_ordinary_for_stratified_schema(&program)
6143 .expect_err("schema reduction must reject a constrained augmented query"),
6144 ];
6145 for error in errors {
6146 let message = error.to_string();
6147 assert!(
6148 message.contains("epistemic augmented head query"),
6149 "{message}"
6150 );
6151 assert!(message.contains("distinct named variables"), "{message}");
6152 }
6153 }
6154 }
6155
6156 #[test]
6157 fn removed_unfounded_sibling_does_not_create_an_augmented_arity_conflict() {
6158 let program = parse_program(
6159 r#"
6160 #pragma epistemic_mode = faeel
6161 pred node(symbol).
6162 pred edge(symbol, i64).
6163 pred result(symbol).
6164 node(key).
6165 edge(key, 5000000000).
6166 result(X) :- node(X), know edge(X, Y).
6167 result(key) :- possible result(key).
6168 "#,
6169 )
6170 .expect("parse foundedness fixture");
6171
6172 let reduced = reduce_epistemic_program_to_ordinary(&program)
6173 .expect("removed unfounded support must not affect the surviving relation shape");
6174 let result_rules = reduced
6175 .rules
6176 .iter()
6177 .filter(|rule| rule.head.predicate == "result")
6178 .collect::<Vec<_>>();
6179 assert_eq!(result_rules.len(), 1);
6180 assert_eq!(result_rules[0].head.arity(), 1);
6181 }
6182
6183 #[test]
6184 fn ordinary_bound_appended_columns_participate_in_shape_validation() {
6185 let program = parse_program(
6186 r#"
6187 #pragma epistemic_mode = faeel
6188 pred node(symbol).
6189 pred edge(symbol, i64).
6190 pred allowed(i64).
6191 pred result(symbol).
6192 node(key).
6193 edge(key, 5000000000).
6194 allowed(5000000000).
6195 result(X) :- node(X).
6196 result(X) :- node(X), edge(X, Y), know allowed(Y).
6197 ?- result(X).
6198 "#,
6199 )
6200 .expect("parse ordinary-bound augmentation fixture");
6201
6202 let errors = [
6203 plan_epistemic_gpu_execution(&program)
6204 .expect_err("GPU planning must reject divergent internal arities"),
6205 reduce_epistemic_program_to_ordinary(&program)
6206 .expect_err("execution reduction must reject divergent internal arities"),
6207 reduce_epistemic_program_to_ordinary_for_stratified_schema(&program)
6208 .expect_err("schema reduction must reject divergent internal arities"),
6209 ];
6210 for error in errors {
6211 let message = error.to_string();
6212 assert!(
6213 message.contains("epistemic augmented predicate schema"),
6214 "{message}"
6215 );
6216 assert!(message.contains("result/1"), "{message}");
6217 assert!(message.contains("{1, 2}"), "{message}");
6218 }
6219 }
6220
6221 #[test]
6222 fn ordinary_bound_appended_columns_widen_the_internal_declaration() {
6223 let program = parse_program(
6224 r#"
6225 #pragma epistemic_mode = faeel
6226 pred node(symbol).
6227 pred edge(symbol, i64).
6228 pred allowed(i64).
6229 pred result(symbol).
6230 node(key).
6231 edge(key, 5000000000).
6232 allowed(5000000000).
6233 result(X) :- node(X), edge(X, Y), know allowed(Y).
6234 ?- result(X).
6235 "#,
6236 )
6237 .expect("parse ordinary-bound declaration fixture");
6238
6239 let reduced = reduce_epistemic_program_to_ordinary(&program)
6240 .expect("uniform ordinary-bound augmentation should reduce");
6241 let declaration = reduced
6242 .predicates
6243 .iter()
6244 .find(|declaration| declaration.name == "result")
6245 .expect("missing result declaration");
6246 assert_eq!(declaration.arity(), 2);
6247 crate::compile::Compiler::new()
6248 .compile_program(&reduced)
6249 .expect("reconciled ordinary-bound augmentation should compile");
6250 }
6251
6252 #[test]
6253 fn stratified_schema_reduction_uses_the_recursive_stratum_reducer() {
6254 let program = parse_program(
6255 r#"
6256 #pragma epistemic_mode = faeel
6257 pred node(u32).
6258 pred edge(u32, u32).
6259 pred accepted_edge(u32, u32).
6260 pred reach(u32, u32).
6261 node(1).
6262 node(2).
6263 node(3).
6264 edge(1, 2).
6265 edge(2, 3).
6266 accepted_edge(X, Y) :- node(X), node(Y), know edge(X, Y).
6267 reach(X, Y) :- node(X), node(Y), know accepted_edge(X, Y).
6268 reach(X, Z) :- reach(X, Y), node(Z), know accepted_edge(Y, Z).
6269 ?- reach(X, Z).
6270 "#,
6271 )
6272 .expect("parse stratified recursive fixture");
6273
6274 let plan = try_plan_stratified_epistemic_program(&program)
6275 .expect("stratified planning should succeed")
6276 .expect("fixture requires stratified execution");
6277 assert_eq!(plan.strata.len(), 2);
6278
6279 let reduced = reduce_epistemic_program_to_ordinary_for_stratified_schema(&program)
6280 .expect("schema reduction should follow each stratum's executable reducer");
6281 assert!(reduced
6282 .rules
6283 .iter()
6284 .filter(|rule| rule.head.predicate == "reach")
6285 .all(|rule| rule.head.arity() == 2));
6286 assert_eq!(
6287 reduced
6288 .predicates
6289 .iter()
6290 .find(|declaration| declaration.name == "reach")
6291 .expect("missing reach declaration")
6292 .arity(),
6293 2
6294 );
6295 crate::compile::Compiler::new()
6296 .compile_program(&reduced)
6297 .expect("path-aligned stratified schema program should compile");
6298 }
6299
6300 #[test]
6301 fn high_arity_epistemic_adapter_reduction_is_not_wcoj_required() {
6302 let program = parse_program(
6303 r#"
6304 pred case_variant(u32, u32).
6305 pred case_domain_variant(u32, u32, u32).
6306 pred domain_adapter_root(u32, u32, u32).
6307 pred domain_adapter_intervention(u32, u32, u32).
6308 pred domain_candidate_seed(u32, u32, u32, u32).
6309 pred heldout_label_seed(u32, u32).
6310 pred blocked_candidate(u32, u32, u32).
6311 pred generated_candidate(u32, u32, u32, u32, u32).
6312
6313 generated_candidate(Case, Variant, Candidate, Root, Intervention) :-
6314 case_domain_variant(Case, Variant, Domain),
6315 domain_adapter_root(Domain, Candidate, Root),
6316 domain_adapter_intervention(Domain, Candidate, Intervention),
6317 domain_candidate_seed(Domain, Candidate, Root, Intervention),
6318 know domain_candidate_seed(Domain, Candidate, Root, Intervention),
6319 possible case_variant(Case, Variant),
6320 not know heldout_label_seed(Case, Candidate),
6321 not possible blocked_candidate(Case, Variant, Candidate).
6322 "#,
6323 )
6324 .expect("parse high-arity adapter epistemic program");
6325
6326 let plan = plan_epistemic_gpu_execution(&program)
6327 .expect("plan high-arity adapter epistemic program");
6328
6329 assert_eq!(plan.reductions.len(), 1);
6330 assert_eq!(
6331 plan.reductions[0].wcoj_status,
6332 EpistemicWcojReductionStatus::NotWcojCandidate
6333 );
6334 }
6335
6336 #[test]
6337 fn binary_triangle_epistemic_reduction_still_requires_wcoj() {
6338 let program = parse_program(
6339 r#"
6340 pred xy(u32, u32).
6341 pred yz(u32, u32).
6342 pred xz(u32, u32).
6343 pred tri(u32, u32, u32).
6344
6345 tri(X, Y, Z) :-
6346 xy(X, Y),
6347 yz(Y, Z),
6348 xz(X, Z),
6349 know xy(X, Y).
6350 "#,
6351 )
6352 .expect("parse binary triangle epistemic program");
6353
6354 let plan =
6355 plan_epistemic_gpu_execution(&program).expect("plan binary triangle epistemic program");
6356
6357 assert_eq!(plan.reductions.len(), 1);
6358 assert_eq!(
6359 plan.reductions[0].wcoj_status,
6360 EpistemicWcojReductionStatus::RequiresPlannerEligibility
6361 );
6362 }
6363
6364 #[test]
6365 fn binary_eight_clique_epistemic_reduction_requires_wcoj() {
6366 let program = parse_program(
6367 r#"
6368 pred edge(u32, u32).
6369 pred clique8(u32, u32, u32, u32, u32, u32, u32, u32).
6370
6371 clique8(A, B, C, D, E, F, G, H) :-
6372 edge(A, B),
6373 edge(A, C),
6374 edge(A, D),
6375 edge(A, E),
6376 edge(A, F),
6377 edge(A, G),
6378 edge(A, H),
6379 edge(B, C),
6380 edge(B, D),
6381 edge(B, E),
6382 edge(B, F),
6383 edge(B, G),
6384 edge(B, H),
6385 edge(C, D),
6386 edge(C, E),
6387 edge(C, F),
6388 edge(C, G),
6389 edge(C, H),
6390 edge(D, E),
6391 edge(D, F),
6392 edge(D, G),
6393 edge(D, H),
6394 edge(E, F),
6395 edge(E, G),
6396 edge(E, H),
6397 edge(F, G),
6398 edge(F, H),
6399 edge(G, H),
6400 know edge(A, B).
6401 "#,
6402 )
6403 .expect("parse binary eight-clique epistemic program");
6404
6405 let plan = plan_epistemic_gpu_execution(&program)
6406 .expect("plan binary eight-clique epistemic program");
6407
6408 assert_eq!(plan.reductions.len(), 1);
6409 assert_eq!(
6410 plan.reductions[0].wcoj_status,
6411 EpistemicWcojReductionStatus::RequiresPlannerEligibility
6412 );
6413 }
6414}