1use crate::pir::{PirGraph, PirNodeId};
41use crate::provenance::Value;
42use std::collections::{HashMap, HashSet};
43use xlog_core::{Result, XlogError};
44
45#[derive(Debug, Clone, PartialEq, Eq, Hash)]
47pub struct WfsAtom {
48 pub predicate: String,
50 pub args: Vec<Value>,
52}
53
54impl WfsAtom {
55 pub fn new(predicate: impl Into<String>, args: Vec<Value>) -> Self {
57 Self {
58 predicate: predicate.into(),
59 args,
60 }
61 }
62}
63
64impl std::fmt::Display for WfsAtom {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 write!(f, "{}(", self.predicate)?;
67 for (i, arg) in self.args.iter().enumerate() {
68 if i > 0 {
69 write!(f, ", ")?;
70 }
71 match arg {
72 Value::I64(v) => write!(f, "{}", v)?,
73 Value::F64(v) => write!(f, "{}", f64::from_bits(*v))?,
74 Value::Symbol(v) => write!(f, "sym{}", v)?,
75 Value::String(v) => write!(f, "\"{}\"", v)?,
76 }
77 }
78 write!(f, ")")
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84pub enum TruthValue {
85 True,
87 False,
89 Undefined,
91}
92
93impl TruthValue {
94 pub fn is_defined(&self) -> bool {
96 matches!(self, TruthValue::True | TruthValue::False)
97 }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Hash)]
102pub enum WfsLiteral {
103 Positive(WfsAtom),
105 Negative(WfsAtom),
107}
108
109impl WfsLiteral {
110 pub fn atom(&self) -> &WfsAtom {
112 match self {
113 WfsLiteral::Positive(a) | WfsLiteral::Negative(a) => a,
114 }
115 }
116
117 pub fn is_positive(&self) -> bool {
119 matches!(self, WfsLiteral::Positive(_))
120 }
121
122 pub fn is_negative(&self) -> bool {
124 matches!(self, WfsLiteral::Negative(_))
125 }
126}
127
128#[derive(Debug, Clone)]
133pub struct WfsRule {
134 pub head: WfsAtom,
136 pub body: Vec<WfsLiteral>,
138 pub provenance: PirNodeId,
141}
142
143impl WfsRule {
144 pub fn new(head: WfsAtom, body: Vec<WfsLiteral>, provenance: PirNodeId) -> Self {
146 Self {
147 head,
148 body,
149 provenance,
150 }
151 }
152
153 fn is_satisfied(
159 &self,
160 true_set: &HashSet<WfsAtom>,
161 false_set: &HashSet<WfsAtom>,
162 scc_atoms: &HashSet<WfsAtom>,
163 ) -> Option<bool> {
164 for lit in &self.body {
167 match lit {
168 WfsLiteral::Positive(atom) => {
169 if false_set.contains(atom) {
170 return Some(false);
171 }
172 if true_set.contains(atom) {
173 continue; }
175 if scc_atoms.contains(atom) {
177 return None; }
179 return Some(false);
181 }
182 WfsLiteral::Negative(atom) => {
183 if true_set.contains(atom) {
184 return Some(false);
185 }
186 if false_set.contains(atom) {
187 continue; }
189 if scc_atoms.contains(atom) {
191 return None; }
193 continue;
196 }
197 }
198 }
199 Some(true)
200 }
201}
202
203#[derive(Debug, Clone)]
205pub struct WfsResult {
206 pub true_set: HashMap<WfsAtom, PirNodeId>,
208 pub false_set: HashSet<WfsAtom>,
210 }
212
213impl WfsResult {
214 pub fn new() -> Self {
216 Self {
217 true_set: HashMap::new(),
218 false_set: HashSet::new(),
219 }
220 }
221
222 pub fn truth_value(&self, atom: &WfsAtom) -> TruthValue {
224 if self.true_set.contains_key(atom) {
225 TruthValue::True
226 } else if self.false_set.contains(atom) {
227 TruthValue::False
228 } else {
229 TruthValue::Undefined
230 }
231 }
232
233 pub fn provenance(&self, atom: &WfsAtom) -> Option<PirNodeId> {
235 self.true_set.get(atom).copied()
236 }
237
238 pub fn is_undefined(&self, atom: &WfsAtom) -> bool {
240 !self.true_set.contains_key(atom) && !self.false_set.contains(atom)
241 }
242
243 pub fn undefined_atoms<'a>(
245 &self,
246 atoms: impl Iterator<Item = &'a WfsAtom>,
247 ) -> Vec<&'a WfsAtom> {
248 atoms.filter(|a| self.is_undefined(a)).collect()
249 }
250}
251
252impl Default for WfsResult {
253 fn default() -> Self {
254 Self::new()
255 }
256}
257
258#[derive(Debug, Clone)]
262#[non_exhaustive]
263pub struct WfsConfig {
264 pub max_iterations: usize,
266}
267
268impl Default for WfsConfig {
269 fn default() -> Self {
270 Self {
271 max_iterations: 1000,
272 }
273 }
274}
275
276pub struct WfsContext<'a> {
280 rules: &'a [WfsRule],
282 scc_atoms: HashSet<WfsAtom>,
284 rules_by_head: HashMap<WfsAtom, Vec<usize>>,
286 pir: &'a mut PirGraph,
288}
289
290impl<'a> WfsContext<'a> {
291 fn new(rules: &'a [WfsRule], pir: &'a mut PirGraph) -> Self {
293 let mut scc_atoms = HashSet::new();
295 let mut rules_by_head: HashMap<WfsAtom, Vec<usize>> = HashMap::new();
296
297 for (i, rule) in rules.iter().enumerate() {
298 scc_atoms.insert(rule.head.clone());
299 rules_by_head.entry(rule.head.clone()).or_default().push(i);
300 }
301
302 Self {
303 rules,
304 scc_atoms,
305 rules_by_head,
306 pir,
307 }
308 }
309}
310
311fn compute_unfounded_set(
319 ctx: &WfsContext,
320 true_set: &HashSet<WfsAtom>,
321 false_set: &HashSet<WfsAtom>,
322) -> HashSet<WfsAtom> {
323 let mut unfounded: HashSet<WfsAtom> = ctx
325 .scc_atoms
326 .iter()
327 .filter(|a| !true_set.contains(*a))
328 .cloned()
329 .collect();
330
331 loop {
333 let mut changed = false;
334 let mut to_remove = Vec::new();
335
336 for atom in &unfounded {
337 if let Some(rule_indices) = ctx.rules_by_head.get(atom) {
339 for &rule_idx in rule_indices {
340 let rule = &ctx.rules[rule_idx];
341 if rule_is_potentially_supporting(
342 rule,
343 true_set,
344 false_set,
345 &unfounded,
346 &ctx.scc_atoms,
347 ) {
348 to_remove.push(atom.clone());
349 changed = true;
350 break;
351 }
352 }
353 }
354 }
355
356 for atom in to_remove {
357 unfounded.remove(&atom);
358 }
359
360 if !changed {
361 break;
362 }
363 }
364
365 unfounded
366}
367
368fn rule_is_potentially_supporting(
379 rule: &WfsRule,
380 true_set: &HashSet<WfsAtom>,
381 false_set: &HashSet<WfsAtom>,
382 unfounded: &HashSet<WfsAtom>,
383 scc_atoms: &HashSet<WfsAtom>,
384) -> bool {
385 for lit in &rule.body {
386 match lit {
387 WfsLiteral::Positive(atom) => {
388 if false_set.contains(atom) {
390 return false;
391 }
392 if true_set.contains(atom) {
394 continue;
395 }
396 if scc_atoms.contains(atom) {
399 if unfounded.contains(atom) {
400 return false;
401 }
402 continue;
404 }
405 return false;
408 }
409 WfsLiteral::Negative(atom) => {
410 if true_set.contains(atom) {
412 return false;
413 }
414 }
415 }
416 }
417 true
418}
419
420fn derive_consequences(
428 ctx: &mut WfsContext,
429 true_set: &HashSet<WfsAtom>,
430 false_set: &HashSet<WfsAtom>,
431 existing_provenance: &HashMap<WfsAtom, PirNodeId>,
432) -> HashMap<WfsAtom, PirNodeId> {
433 let mut new_true: HashMap<WfsAtom, PirNodeId> = HashMap::new();
434
435 for rule in ctx.rules.iter() {
436 if true_set.contains(&rule.head) || false_set.contains(&rule.head) {
438 continue;
439 }
440
441 if let Some(true) = rule.is_satisfied(true_set, false_set, &ctx.scc_atoms) {
443 let mut prov_parts = vec![rule.provenance];
445
446 for lit in &rule.body {
447 if let WfsLiteral::Positive(atom) = lit {
448 if let Some(&atom_prov) = existing_provenance.get(atom) {
449 prov_parts.push(atom_prov);
450 }
451 }
454 }
455
456 let rule_prov = if prov_parts.len() == 1 {
457 prov_parts[0]
458 } else {
459 ctx.pir.and(prov_parts)
460 };
461
462 let entry = new_true
464 .entry(rule.head.clone())
465 .or_insert_with(|| ctx.pir.const_false());
466 *entry = ctx.pir.or(vec![*entry, rule_prov]);
467 }
468 }
469
470 new_true
471}
472
473pub fn evaluate_wfs_rules(
502 rules: &[WfsRule],
503 pir: &mut PirGraph,
504 config: &WfsConfig,
505) -> Result<WfsResult> {
506 if rules.is_empty() {
507 return Ok(WfsResult::new());
508 }
509
510 let mut ctx = WfsContext::new(rules, pir);
511
512 let mut true_set: HashSet<WfsAtom> = HashSet::new();
513 let mut true_provenance: HashMap<WfsAtom, PirNodeId> = HashMap::new();
514 let mut false_set: HashSet<WfsAtom> = HashSet::new();
515
516 for iteration in 0..config.max_iterations {
517 let unfounded = compute_unfounded_set(&ctx, &true_set, &false_set);
519
520 let new_false: Vec<WfsAtom> = unfounded
522 .into_iter()
523 .filter(|a| !false_set.contains(a))
524 .collect();
525 let new_false_count = new_false.len();
526 false_set.extend(new_false);
527
528 let new_true = derive_consequences(&mut ctx, &true_set, &false_set, &true_provenance);
530 let new_true_count = new_true.len();
531
532 for (atom, prov) in new_true {
534 if !true_set.contains(&atom) {
535 true_set.insert(atom.clone());
536 let entry = true_provenance
538 .entry(atom)
539 .or_insert_with(|| ctx.pir.const_false());
540 *entry = ctx.pir.or(vec![*entry, prov]);
541 }
542 }
543
544 if new_false_count == 0 && new_true_count == 0 {
546 break;
547 }
548
549 if iteration == config.max_iterations - 1 {
550 return Err(XlogError::Execution(format!(
551 "WFS evaluation did not converge after {} iterations",
552 config.max_iterations
553 )));
554 }
555 }
556
557 Ok(WfsResult {
558 true_set: true_provenance,
559 false_set,
560 })
561}
562
563pub fn evaluate_wfs_with_rules(rules: Vec<WfsRule>, pir: &mut PirGraph) -> Result<WfsResult> {
571 evaluate_wfs_rules(&rules, pir, &WfsConfig::default())
572}
573
574#[cfg(test)]
580fn evaluate_wfs_scc_with_config(
581 scc_predicates: &[String],
582 pir: &mut PirGraph,
583 config: &WfsConfig,
584) -> Result<WfsResult> {
585 if scc_predicates.is_empty() {
586 return Ok(WfsResult::new());
587 }
588
589 let false_set: HashSet<WfsAtom> = scc_predicates
590 .iter()
591 .map(|p| WfsAtom::new(p.clone(), vec![]))
592 .collect();
593
594 let _ = config;
595 let _ = pir;
596
597 Ok(WfsResult {
598 true_set: HashMap::new(),
599 false_set,
600 })
601}
602
603#[cfg(test)]
604mod tests {
605 use super::*;
606 use crate::pir::LeafId;
607
608 fn prop(name: &str) -> WfsAtom {
610 WfsAtom::new(name, vec![])
611 }
612
613 fn atom(name: &str, args: &[i64]) -> WfsAtom {
615 WfsAtom::new(name, args.iter().map(|&i| Value::I64(i)).collect())
616 }
617
618 #[test]
619 fn test_wfs_config_default() {
620 let config = WfsConfig::default();
621 assert_eq!(config.max_iterations, 1000);
622 }
623
624 #[test]
625 fn test_wfs_result_default() {
626 let result = WfsResult::default();
627 assert!(result.true_set.is_empty());
628 assert!(result.false_set.is_empty());
629 }
630
631 #[test]
632 fn test_wfs_result_truth_value() {
633 let mut result = WfsResult::new();
634 let atom = prop("p");
635
636 assert_eq!(result.truth_value(&atom), TruthValue::Undefined);
638
639 result.false_set.insert(atom.clone());
641 assert_eq!(result.truth_value(&atom), TruthValue::False);
642
643 result.false_set.remove(&atom);
645 let mut pir = PirGraph::new();
646 let node_id = pir.lit(LeafId::new(0));
647 result.true_set.insert(atom.clone(), node_id);
648 assert_eq!(result.truth_value(&atom), TruthValue::True);
649 }
650
651 #[test]
652 fn test_wfs_atom_equality() {
653 let atom1 = WfsAtom::new("p", vec![Value::I64(1)]);
654 let atom2 = WfsAtom::new("p", vec![Value::I64(1)]);
655 let atom3 = WfsAtom::new("p", vec![Value::I64(2)]);
656
657 assert_eq!(atom1, atom2);
658 assert_ne!(atom1, atom3);
659 }
660
661 #[test]
662 fn test_wfs_empty_rules() {
663 let mut pir = PirGraph::new();
664 let result = evaluate_wfs_rules(&[], &mut pir, &WfsConfig::default()).unwrap();
665 assert!(result.true_set.is_empty());
666 assert!(result.false_set.is_empty());
667 }
668
669 #[test]
670 fn test_wfs_simple_fact() {
671 let mut pir = PirGraph::new();
674 let const_true = pir.const_true();
675
676 let rules = vec![WfsRule::new(prop("p"), vec![], const_true)];
677
678 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
679
680 assert_eq!(result.truth_value(&prop("p")), TruthValue::True);
681 assert!(result.provenance(&prop("p")).is_some());
682 }
683
684 #[test]
685 fn test_wfs_simple_negation() {
686 let mut pir = PirGraph::new();
690 let const_true = pir.const_true();
691
692 let rules = vec![
693 WfsRule::new(prop("p"), vec![WfsLiteral::Negative(prop("q"))], const_true),
694 WfsRule::new(prop("q"), vec![WfsLiteral::Negative(prop("p"))], const_true),
695 ];
696
697 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
698
699 assert_eq!(result.truth_value(&prop("p")), TruthValue::Undefined);
701 assert_eq!(result.truth_value(&prop("q")), TruthValue::Undefined);
702 }
703
704 #[test]
705 fn test_wfs_asymmetric_negation() {
706 let mut pir = PirGraph::new();
709 let const_true = pir.const_true();
710
711 let rules = vec![
712 WfsRule::new(prop("p"), vec![WfsLiteral::Negative(prop("q"))], const_true),
713 WfsRule::new(prop("q"), vec![], const_true),
714 ];
715
716 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
717
718 assert_eq!(result.truth_value(&prop("q")), TruthValue::True);
719 assert_eq!(result.truth_value(&prop("p")), TruthValue::False);
720 }
721
722 #[test]
723 fn test_wfs_three_way_cycle() {
724 let mut pir = PirGraph::new();
727 let const_true = pir.const_true();
728
729 let rules = vec![
730 WfsRule::new(prop("p"), vec![WfsLiteral::Negative(prop("q"))], const_true),
731 WfsRule::new(prop("q"), vec![WfsLiteral::Negative(prop("r"))], const_true),
732 WfsRule::new(prop("r"), vec![WfsLiteral::Negative(prop("p"))], const_true),
733 ];
734
735 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
736
737 assert_eq!(result.truth_value(&prop("p")), TruthValue::Undefined);
739 assert_eq!(result.truth_value(&prop("q")), TruthValue::Undefined);
740 assert_eq!(result.truth_value(&prop("r")), TruthValue::Undefined);
741 }
742
743 #[test]
744 fn test_wfs_win_lose() {
745 let mut pir = PirGraph::new();
753 let const_true = pir.const_true();
754
755 let rules = vec![
757 WfsRule::new(
758 atom("win", &[1]),
759 vec![WfsLiteral::Negative(atom("win", &[2]))],
760 const_true, ),
762 WfsRule::new(
763 atom("win", &[2]),
764 vec![WfsLiteral::Negative(atom("win", &[1]))],
765 const_true, ),
767 ];
768
769 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
770
771 assert_eq!(
773 result.truth_value(&atom("win", &[1])),
774 TruthValue::Undefined
775 );
776 assert_eq!(
777 result.truth_value(&atom("win", &[2])),
778 TruthValue::Undefined
779 );
780 }
781
782 #[test]
783 fn test_wfs_win_with_base_case() {
784 let mut pir = PirGraph::new();
798 let const_true = pir.const_true();
799
800 let rules = vec![
807 WfsRule::new(
808 atom("win", &[1]),
809 vec![WfsLiteral::Negative(atom("win", &[2]))],
810 const_true,
811 ),
812 WfsRule::new(
814 atom("win", &[2]),
815 vec![WfsLiteral::Positive(atom("win", &[3]))],
816 const_true,
817 ),
818 ];
819
820 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
821
822 assert_eq!(result.truth_value(&atom("win", &[1])), TruthValue::True);
826 assert_eq!(result.truth_value(&atom("win", &[2])), TruthValue::False);
827 }
828
829 #[test]
830 fn test_wfs_chain_with_grounding() {
831 let mut pir = PirGraph::new();
838 let const_true = pir.const_true();
839
840 let rules = vec![
841 WfsRule::new(prop("a"), vec![], const_true),
842 WfsRule::new(prop("b"), vec![WfsLiteral::Negative(prop("a"))], const_true),
843 WfsRule::new(prop("c"), vec![WfsLiteral::Negative(prop("b"))], const_true),
844 ];
845
846 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
847
848 assert_eq!(result.truth_value(&prop("a")), TruthValue::True);
849 assert_eq!(result.truth_value(&prop("b")), TruthValue::False);
850 assert_eq!(result.truth_value(&prop("c")), TruthValue::True);
851 }
852
853 #[test]
854 fn test_wfs_multiple_rules_same_head() {
855 let mut pir = PirGraph::new();
860 let const_true = pir.const_true();
861
862 let rules = vec![
863 WfsRule::new(prop("q"), vec![], const_true),
864 WfsRule::new(prop("r"), vec![], const_true),
865 WfsRule::new(prop("p"), vec![WfsLiteral::Positive(prop("q"))], const_true),
866 WfsRule::new(prop("p"), vec![WfsLiteral::Negative(prop("r"))], const_true),
867 ];
868
869 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
870
871 assert_eq!(result.truth_value(&prop("q")), TruthValue::True);
872 assert_eq!(result.truth_value(&prop("r")), TruthValue::True);
873 assert_eq!(result.truth_value(&prop("p")), TruthValue::True);
874 }
875
876 #[test]
877 fn test_wfs_provenance_tracking() {
878 let mut pir = PirGraph::new();
881 let leaf = pir.lit(LeafId::new(42));
882
883 let rules = vec![WfsRule::new(prop("p"), vec![], leaf)];
884
885 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
886
887 let prov = result.provenance(&prop("p"));
888 assert!(prov.is_some());
889 assert!(prov.unwrap().as_u32() > 0);
892 }
893
894 #[test]
895 fn test_wfs_combined_provenance() {
896 let mut pir = PirGraph::new();
899 let leaf_q = pir.lit(LeafId::new(1));
900 let const_true = pir.const_true();
901
902 let rules = vec![
903 WfsRule::new(prop("q"), vec![], leaf_q),
904 WfsRule::new(prop("p"), vec![WfsLiteral::Positive(prop("q"))], const_true),
905 ];
906
907 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
908
909 assert_eq!(result.truth_value(&prop("q")), TruthValue::True);
910 assert_eq!(result.truth_value(&prop("p")), TruthValue::True);
911
912 let p_prov = result.provenance(&prop("p")).unwrap();
915 assert!(p_prov.as_u32() > 0);
917 }
918
919 #[test]
920 fn test_wfs_no_rules_for_predicate() {
921 let result = evaluate_wfs_scc_with_config(
923 &["p".to_string(), "q".to_string()],
924 &mut PirGraph::new(),
925 &WfsConfig::default(),
926 )
927 .unwrap();
928
929 assert_eq!(result.truth_value(&prop("p")), TruthValue::False);
930 assert_eq!(result.truth_value(&prop("q")), TruthValue::False);
931 }
932
933 #[test]
934 fn test_wfs_positive_cycle() {
935 let mut pir = PirGraph::new();
938 let const_true = pir.const_true();
939
940 let rules = vec![
941 WfsRule::new(prop("p"), vec![WfsLiteral::Positive(prop("q"))], const_true),
942 WfsRule::new(prop("q"), vec![WfsLiteral::Positive(prop("p"))], const_true),
943 ];
944
945 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
946
947 assert_eq!(result.truth_value(&prop("p")), TruthValue::False);
949 assert_eq!(result.truth_value(&prop("q")), TruthValue::False);
950 }
951
952 #[test]
953 fn test_wfs_positive_cycle_with_base() {
954 let mut pir = PirGraph::new();
957 let const_true = pir.const_true();
958
959 let rules = vec![
960 WfsRule::new(prop("p"), vec![], const_true), WfsRule::new(prop("p"), vec![WfsLiteral::Positive(prop("q"))], const_true),
962 WfsRule::new(prop("q"), vec![WfsLiteral::Positive(prop("p"))], const_true),
963 ];
964
965 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
966
967 assert_eq!(result.truth_value(&prop("p")), TruthValue::True);
968 assert_eq!(result.truth_value(&prop("q")), TruthValue::True);
969 }
970
971 #[test]
972 fn test_wfs_mixed_dependencies() {
973 let mut pir = PirGraph::new();
983 let const_true = pir.const_true();
984
985 let rules = vec![
986 WfsRule::new(prop("a"), vec![], const_true),
987 WfsRule::new(prop("b"), vec![WfsLiteral::Positive(prop("a"))], const_true),
988 WfsRule::new(prop("c"), vec![WfsLiteral::Negative(prop("b"))], const_true),
989 WfsRule::new(
990 prop("d"),
991 vec![
992 WfsLiteral::Positive(prop("c")),
993 WfsLiteral::Negative(prop("e")),
994 ],
995 const_true,
996 ),
997 WfsRule::new(prop("e"), vec![WfsLiteral::Negative(prop("d"))], const_true),
998 ];
999
1000 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
1001
1002 assert_eq!(result.truth_value(&prop("a")), TruthValue::True);
1003 assert_eq!(result.truth_value(&prop("b")), TruthValue::True);
1004 assert_eq!(result.truth_value(&prop("c")), TruthValue::False);
1005 assert_eq!(result.truth_value(&prop("d")), TruthValue::False);
1006 assert_eq!(result.truth_value(&prop("e")), TruthValue::True);
1007 }
1008
1009 #[test]
1010 fn test_wfs_undefined_atoms_method() {
1011 let mut result = WfsResult::new();
1012 let p = prop("p");
1013 let q = prop("q");
1014 let r = prop("r");
1015
1016 result.true_set.insert(p.clone(), PirNodeId::from(0));
1017 result.false_set.insert(q.clone());
1018 let atoms = vec![&p, &q, &r];
1021 let undefined = result.undefined_atoms(atoms.into_iter());
1022
1023 assert_eq!(undefined.len(), 1);
1024 assert_eq!(undefined[0], &r);
1025 }
1026
1027 impl From<u32> for PirNodeId {
1029 fn from(v: u32) -> Self {
1030 unsafe { std::mem::transmute(v) }
1033 }
1034 }
1035
1036 #[test]
1037 fn test_wfs_even_odd_cycle() {
1038 let mut pir = PirGraph::new();
1051 let const_true = pir.const_true();
1052
1053 let rules = vec![
1054 WfsRule::new(atom("even", &[0]), vec![], const_true),
1056 WfsRule::new(
1060 atom("odd", &[1]),
1061 vec![WfsLiteral::Positive(atom("even", &[0]))],
1062 const_true,
1063 ),
1064 WfsRule::new(
1066 atom("even", &[2]),
1067 vec![WfsLiteral::Positive(atom("odd", &[1]))],
1068 const_true,
1069 ),
1070 WfsRule::new(
1072 atom("odd", &[3]),
1073 vec![WfsLiteral::Positive(atom("even", &[2]))],
1074 const_true,
1075 ),
1076 ];
1077
1078 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
1079
1080 assert_eq!(result.truth_value(&atom("even", &[0])), TruthValue::True);
1081 assert_eq!(result.truth_value(&atom("odd", &[1])), TruthValue::True);
1082 assert_eq!(result.truth_value(&atom("even", &[2])), TruthValue::True);
1083 assert_eq!(result.truth_value(&atom("odd", &[3])), TruthValue::True);
1084 }
1085
1086 #[test]
1087 fn test_wfs_default_logic() {
1088 let mut pir = PirGraph::new();
1101 let const_true = pir.const_true();
1102
1103 let rules = vec![
1105 WfsRule::new(atom("penguin", &[1]), vec![], const_true), WfsRule::new(
1109 atom("bird", &[1]),
1110 vec![WfsLiteral::Positive(atom("penguin", &[1]))],
1111 const_true,
1112 ),
1113 WfsRule::new(
1115 atom("ab", &[1]),
1116 vec![WfsLiteral::Positive(atom("penguin", &[1]))],
1117 const_true,
1118 ),
1119 WfsRule::new(
1121 atom("flies", &[1]),
1122 vec![
1123 WfsLiteral::Positive(atom("bird", &[1])),
1124 WfsLiteral::Negative(atom("ab", &[1])),
1125 ],
1126 const_true,
1127 ),
1128 ];
1129
1130 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
1131
1132 assert_eq!(result.truth_value(&atom("penguin", &[1])), TruthValue::True);
1133 assert_eq!(result.truth_value(&atom("bird", &[1])), TruthValue::True);
1134 assert_eq!(result.truth_value(&atom("ab", &[1])), TruthValue::True);
1135 assert_eq!(result.truth_value(&atom("flies", &[1])), TruthValue::False);
1136 }
1137
1138 #[test]
1139 fn test_wfs_non_ground_birds() {
1140 let mut pir = PirGraph::new();
1151 let const_true = pir.const_true();
1152
1153 let rules = vec![
1154 WfsRule::new(atom("penguin", &[1]), vec![], const_true), WfsRule::new(atom("eagle", &[2]), vec![], const_true), WfsRule::new(
1159 atom("bird", &[1]),
1160 vec![WfsLiteral::Positive(atom("penguin", &[1]))],
1161 const_true,
1162 ),
1163 WfsRule::new(
1165 atom("bird", &[2]),
1166 vec![WfsLiteral::Positive(atom("eagle", &[2]))],
1167 const_true,
1168 ),
1169 WfsRule::new(
1171 atom("ab", &[1]),
1172 vec![WfsLiteral::Positive(atom("penguin", &[1]))],
1173 const_true,
1174 ),
1175 WfsRule::new(
1177 atom("flies", &[1]),
1178 vec![
1179 WfsLiteral::Positive(atom("bird", &[1])),
1180 WfsLiteral::Negative(atom("ab", &[1])),
1181 ],
1182 const_true,
1183 ),
1184 WfsRule::new(
1187 atom("flies", &[2]),
1188 vec![
1189 WfsLiteral::Positive(atom("bird", &[2])),
1190 WfsLiteral::Negative(atom("ab", &[2])),
1191 ],
1192 const_true,
1193 ),
1194 ];
1195
1196 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
1197
1198 assert_eq!(result.truth_value(&atom("penguin", &[1])), TruthValue::True);
1200 assert_eq!(result.truth_value(&atom("bird", &[1])), TruthValue::True);
1201 assert_eq!(result.truth_value(&atom("ab", &[1])), TruthValue::True);
1202 assert_eq!(result.truth_value(&atom("flies", &[1])), TruthValue::False);
1203
1204 assert_eq!(result.truth_value(&atom("eagle", &[2])), TruthValue::True);
1206 assert_eq!(result.truth_value(&atom("bird", &[2])), TruthValue::True);
1207 assert_eq!(result.truth_value(&atom("flies", &[2])), TruthValue::True);
1211 }
1212
1213 #[test]
1214 fn test_wfs_probabilistic_provenance_or() {
1215 let mut pir = PirGraph::new();
1219 let leaf_q = pir.lit(LeafId::new(1));
1220 let leaf_r = pir.lit(LeafId::new(2));
1221
1222 let rules = vec![
1223 WfsRule::new(prop("q"), vec![], leaf_q),
1224 WfsRule::new(prop("r"), vec![], leaf_r),
1225 WfsRule::new(
1226 prop("p"),
1227 vec![WfsLiteral::Positive(prop("q"))],
1228 pir.const_true(),
1229 ),
1230 WfsRule::new(
1231 prop("p"),
1232 vec![WfsLiteral::Positive(prop("r"))],
1233 pir.const_true(),
1234 ),
1235 ];
1236
1237 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
1238
1239 assert_eq!(result.truth_value(&prop("p")), TruthValue::True);
1240 assert_eq!(result.truth_value(&prop("q")), TruthValue::True);
1241 assert_eq!(result.truth_value(&prop("r")), TruthValue::True);
1242
1243 let p_prov = result.provenance(&prop("p")).unwrap();
1245 assert!(p_prov.as_u32() > 0);
1247 }
1248
1249 #[test]
1250 fn test_wfs_stable_model_unique() {
1251 let mut pir = PirGraph::new();
1257 let const_true = pir.const_true();
1258
1259 let rules = vec![
1260 WfsRule::new(prop("d"), vec![], const_true),
1261 WfsRule::new(
1262 prop("a"),
1263 vec![
1264 WfsLiteral::Negative(prop("b")),
1265 WfsLiteral::Negative(prop("c")),
1266 ],
1267 const_true,
1268 ),
1269 WfsRule::new(
1270 prop("b"),
1271 vec![
1272 WfsLiteral::Negative(prop("a")),
1273 WfsLiteral::Negative(prop("c")),
1274 ],
1275 const_true,
1276 ),
1277 WfsRule::new(
1278 prop("c"),
1279 vec![
1280 WfsLiteral::Negative(prop("a")),
1281 WfsLiteral::Negative(prop("b")),
1282 ],
1283 const_true,
1284 ),
1285 ];
1286
1287 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
1288
1289 assert_eq!(result.truth_value(&prop("d")), TruthValue::True);
1290 assert_eq!(result.truth_value(&prop("a")), TruthValue::Undefined);
1292 assert_eq!(result.truth_value(&prop("b")), TruthValue::Undefined);
1293 assert_eq!(result.truth_value(&prop("c")), TruthValue::Undefined);
1294 }
1295
1296 #[test]
1297 fn test_wfs_hamiltonian_cycle_like() {
1298 let mut pir = PirGraph::new();
1305 let const_true = pir.const_true();
1306
1307 let rules = vec![
1308 WfsRule::new(atom("edge", &[1, 2]), vec![], const_true),
1310 WfsRule::new(
1312 atom("in", &[1, 2]),
1313 vec![
1314 WfsLiteral::Positive(atom("edge", &[1, 2])),
1315 WfsLiteral::Negative(atom("out", &[1, 2])),
1316 ],
1317 const_true,
1318 ),
1319 WfsRule::new(
1321 atom("out", &[1, 2]),
1322 vec![
1323 WfsLiteral::Positive(atom("edge", &[1, 2])),
1324 WfsLiteral::Negative(atom("in", &[1, 2])),
1325 ],
1326 const_true,
1327 ),
1328 ];
1329
1330 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
1331
1332 assert_eq!(result.truth_value(&atom("edge", &[1, 2])), TruthValue::True);
1333 assert_eq!(
1335 result.truth_value(&atom("in", &[1, 2])),
1336 TruthValue::Undefined
1337 );
1338 assert_eq!(
1339 result.truth_value(&atom("out", &[1, 2])),
1340 TruthValue::Undefined
1341 );
1342 }
1343
1344 #[test]
1345 fn test_wfs_partial_grounding() {
1346 let mut pir = PirGraph::new();
1349 let const_true = pir.const_true();
1350
1351 let rules = vec![WfsRule::new(
1353 prop("p"),
1354 vec![WfsLiteral::Positive(prop("q"))],
1355 const_true,
1356 )];
1357
1358 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
1359
1360 assert_eq!(result.truth_value(&prop("p")), TruthValue::False);
1363 assert_eq!(result.truth_value(&prop("q")), TruthValue::Undefined);
1365 }
1366}