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,
49 pub args: Vec<Value>,
50}
51
52impl WfsAtom {
53 pub fn new(predicate: impl Into<String>, args: Vec<Value>) -> Self {
55 Self {
56 predicate: predicate.into(),
57 args,
58 }
59 }
60}
61
62impl std::fmt::Display for WfsAtom {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 write!(f, "{}(", self.predicate)?;
65 for (i, arg) in self.args.iter().enumerate() {
66 if i > 0 {
67 write!(f, ", ")?;
68 }
69 match arg {
70 Value::I64(v) => write!(f, "{}", v)?,
71 Value::F64(v) => write!(f, "{}", f64::from_bits(*v))?,
72 Value::Symbol(v) => write!(f, "sym{}", v)?,
73 Value::String(v) => write!(f, "\"{}\"", v)?,
74 }
75 }
76 write!(f, ")")
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum TruthValue {
83 True,
84 False,
85 Undefined,
86}
87
88impl TruthValue {
89 pub fn is_defined(&self) -> bool {
91 matches!(self, TruthValue::True | TruthValue::False)
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Hash)]
97pub enum WfsLiteral {
98 Positive(WfsAtom),
100 Negative(WfsAtom),
102}
103
104impl WfsLiteral {
105 pub fn atom(&self) -> &WfsAtom {
107 match self {
108 WfsLiteral::Positive(a) | WfsLiteral::Negative(a) => a,
109 }
110 }
111
112 pub fn is_positive(&self) -> bool {
114 matches!(self, WfsLiteral::Positive(_))
115 }
116
117 pub fn is_negative(&self) -> bool {
119 matches!(self, WfsLiteral::Negative(_))
120 }
121}
122
123#[derive(Debug, Clone)]
128pub struct WfsRule {
129 pub head: WfsAtom,
131 pub body: Vec<WfsLiteral>,
133 pub provenance: PirNodeId,
136}
137
138impl WfsRule {
139 pub fn new(head: WfsAtom, body: Vec<WfsLiteral>, provenance: PirNodeId) -> Self {
141 Self {
142 head,
143 body,
144 provenance,
145 }
146 }
147
148 fn is_satisfied(
154 &self,
155 true_set: &HashSet<WfsAtom>,
156 false_set: &HashSet<WfsAtom>,
157 scc_atoms: &HashSet<WfsAtom>,
158 ) -> Option<bool> {
159 for lit in &self.body {
162 match lit {
163 WfsLiteral::Positive(atom) => {
164 if false_set.contains(atom) {
165 return Some(false);
166 }
167 if true_set.contains(atom) {
168 continue; }
170 if scc_atoms.contains(atom) {
172 return None; }
174 return Some(false);
176 }
177 WfsLiteral::Negative(atom) => {
178 if true_set.contains(atom) {
179 return Some(false);
180 }
181 if false_set.contains(atom) {
182 continue; }
184 if scc_atoms.contains(atom) {
186 return None; }
188 continue;
191 }
192 }
193 }
194 Some(true)
195 }
196
197 #[allow(dead_code)] pub fn is_definitely_unsatisfiable(
202 &self,
203 true_set: &HashSet<WfsAtom>,
204 false_set: &HashSet<WfsAtom>,
205 ) -> bool {
206 for lit in &self.body {
207 match lit {
208 WfsLiteral::Positive(atom) => {
209 if false_set.contains(atom) {
210 return true;
211 }
212 }
213 WfsLiteral::Negative(atom) => {
214 if true_set.contains(atom) {
215 return true;
216 }
217 }
218 }
219 }
220 false
221 }
222}
223
224#[derive(Debug, Clone)]
226pub struct WfsResult {
227 pub true_set: HashMap<WfsAtom, PirNodeId>,
229 pub false_set: HashSet<WfsAtom>,
231 }
233
234impl WfsResult {
235 pub fn new() -> Self {
237 Self {
238 true_set: HashMap::new(),
239 false_set: HashSet::new(),
240 }
241 }
242
243 pub fn truth_value(&self, atom: &WfsAtom) -> TruthValue {
245 if self.true_set.contains_key(atom) {
246 TruthValue::True
247 } else if self.false_set.contains(atom) {
248 TruthValue::False
249 } else {
250 TruthValue::Undefined
251 }
252 }
253
254 pub fn provenance(&self, atom: &WfsAtom) -> Option<PirNodeId> {
256 self.true_set.get(atom).copied()
257 }
258
259 pub fn is_undefined(&self, atom: &WfsAtom) -> bool {
261 !self.true_set.contains_key(atom) && !self.false_set.contains(atom)
262 }
263
264 pub fn undefined_atoms<'a>(
266 &self,
267 atoms: impl Iterator<Item = &'a WfsAtom>,
268 ) -> Vec<&'a WfsAtom> {
269 atoms.filter(|a| self.is_undefined(a)).collect()
270 }
271}
272
273impl Default for WfsResult {
274 fn default() -> Self {
275 Self::new()
276 }
277}
278
279#[derive(Debug, Clone)]
283#[non_exhaustive]
284pub struct WfsConfig {
285 pub max_iterations: usize,
287}
288
289impl Default for WfsConfig {
290 fn default() -> Self {
291 Self {
292 max_iterations: 1000,
293 }
294 }
295}
296
297pub struct WfsContext<'a> {
301 rules: &'a [WfsRule],
303 scc_atoms: HashSet<WfsAtom>,
305 rules_by_head: HashMap<WfsAtom, Vec<usize>>,
307 pir: &'a mut PirGraph,
309}
310
311impl<'a> WfsContext<'a> {
312 fn new(rules: &'a [WfsRule], pir: &'a mut PirGraph) -> Self {
314 let mut scc_atoms = HashSet::new();
316 let mut rules_by_head: HashMap<WfsAtom, Vec<usize>> = HashMap::new();
317
318 for (i, rule) in rules.iter().enumerate() {
319 scc_atoms.insert(rule.head.clone());
320 rules_by_head.entry(rule.head.clone()).or_default().push(i);
321 }
322
323 Self {
324 rules,
325 scc_atoms,
326 rules_by_head,
327 pir,
328 }
329 }
330}
331
332fn compute_unfounded_set(
340 ctx: &WfsContext,
341 true_set: &HashSet<WfsAtom>,
342 false_set: &HashSet<WfsAtom>,
343) -> HashSet<WfsAtom> {
344 let mut unfounded: HashSet<WfsAtom> = ctx
346 .scc_atoms
347 .iter()
348 .filter(|a| !true_set.contains(*a))
349 .cloned()
350 .collect();
351
352 loop {
354 let mut changed = false;
355 let mut to_remove = Vec::new();
356
357 for atom in &unfounded {
358 if let Some(rule_indices) = ctx.rules_by_head.get(atom) {
360 for &rule_idx in rule_indices {
361 let rule = &ctx.rules[rule_idx];
362 if rule_is_potentially_supporting(
363 rule,
364 true_set,
365 false_set,
366 &unfounded,
367 &ctx.scc_atoms,
368 ) {
369 to_remove.push(atom.clone());
370 changed = true;
371 break;
372 }
373 }
374 }
375 }
376
377 for atom in to_remove {
378 unfounded.remove(&atom);
379 }
380
381 if !changed {
382 break;
383 }
384 }
385
386 unfounded
387}
388
389fn rule_is_potentially_supporting(
400 rule: &WfsRule,
401 true_set: &HashSet<WfsAtom>,
402 false_set: &HashSet<WfsAtom>,
403 unfounded: &HashSet<WfsAtom>,
404 scc_atoms: &HashSet<WfsAtom>,
405) -> bool {
406 for lit in &rule.body {
407 match lit {
408 WfsLiteral::Positive(atom) => {
409 if false_set.contains(atom) {
411 return false;
412 }
413 if true_set.contains(atom) {
415 continue;
416 }
417 if scc_atoms.contains(atom) {
420 if unfounded.contains(atom) {
421 return false;
422 }
423 continue;
425 }
426 return false;
429 }
430 WfsLiteral::Negative(atom) => {
431 if true_set.contains(atom) {
433 return false;
434 }
435 }
436 }
437 }
438 true
439}
440
441fn derive_consequences(
449 ctx: &mut WfsContext,
450 true_set: &HashSet<WfsAtom>,
451 false_set: &HashSet<WfsAtom>,
452 existing_provenance: &HashMap<WfsAtom, PirNodeId>,
453) -> HashMap<WfsAtom, PirNodeId> {
454 let mut new_true: HashMap<WfsAtom, PirNodeId> = HashMap::new();
455
456 for rule in ctx.rules.iter() {
457 if true_set.contains(&rule.head) || false_set.contains(&rule.head) {
459 continue;
460 }
461
462 if let Some(true) = rule.is_satisfied(true_set, false_set, &ctx.scc_atoms) {
464 let mut prov_parts = vec![rule.provenance];
466
467 for lit in &rule.body {
468 if let WfsLiteral::Positive(atom) = lit {
469 if let Some(&atom_prov) = existing_provenance.get(atom) {
470 prov_parts.push(atom_prov);
471 }
472 }
475 }
476
477 let rule_prov = if prov_parts.len() == 1 {
478 prov_parts[0]
479 } else {
480 ctx.pir.and(prov_parts)
481 };
482
483 let entry = new_true
485 .entry(rule.head.clone())
486 .or_insert_with(|| ctx.pir.const_false());
487 *entry = ctx.pir.or(vec![*entry, rule_prov]);
488 }
489 }
490
491 new_true
492}
493
494pub fn evaluate_wfs_rules(
523 rules: &[WfsRule],
524 pir: &mut PirGraph,
525 config: &WfsConfig,
526) -> Result<WfsResult> {
527 if rules.is_empty() {
528 return Ok(WfsResult::new());
529 }
530
531 let mut ctx = WfsContext::new(rules, pir);
532
533 let mut true_set: HashSet<WfsAtom> = HashSet::new();
534 let mut true_provenance: HashMap<WfsAtom, PirNodeId> = HashMap::new();
535 let mut false_set: HashSet<WfsAtom> = HashSet::new();
536
537 for iteration in 0..config.max_iterations {
538 let unfounded = compute_unfounded_set(&ctx, &true_set, &false_set);
540
541 let new_false: Vec<WfsAtom> = unfounded
543 .into_iter()
544 .filter(|a| !false_set.contains(a))
545 .collect();
546 let new_false_count = new_false.len();
547 false_set.extend(new_false);
548
549 let new_true = derive_consequences(&mut ctx, &true_set, &false_set, &true_provenance);
551 let new_true_count = new_true.len();
552
553 for (atom, prov) in new_true {
555 if !true_set.contains(&atom) {
556 true_set.insert(atom.clone());
557 let entry = true_provenance
559 .entry(atom)
560 .or_insert_with(|| ctx.pir.const_false());
561 *entry = ctx.pir.or(vec![*entry, prov]);
562 }
563 }
564
565 if new_false_count == 0 && new_true_count == 0 {
567 break;
568 }
569
570 if iteration == config.max_iterations - 1 {
571 return Err(XlogError::Execution(format!(
572 "WFS evaluation did not converge after {} iterations",
573 config.max_iterations
574 )));
575 }
576 }
577
578 Ok(WfsResult {
579 true_set: true_provenance,
580 false_set,
581 })
582}
583
584pub fn evaluate_wfs_with_rules(rules: Vec<WfsRule>, pir: &mut PirGraph) -> Result<WfsResult> {
592 evaluate_wfs_rules(&rules, pir, &WfsConfig::default())
593}
594
595#[cfg(test)]
601fn evaluate_wfs_scc_with_config(
602 scc_predicates: &[String],
603 pir: &mut PirGraph,
604 config: &WfsConfig,
605) -> Result<WfsResult> {
606 if scc_predicates.is_empty() {
607 return Ok(WfsResult::new());
608 }
609
610 let false_set: HashSet<WfsAtom> = scc_predicates
611 .iter()
612 .map(|p| WfsAtom::new(p.clone(), vec![]))
613 .collect();
614
615 let _ = config;
616 let _ = pir;
617
618 Ok(WfsResult {
619 true_set: HashMap::new(),
620 false_set,
621 })
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627 use crate::pir::LeafId;
628
629 fn prop(name: &str) -> WfsAtom {
631 WfsAtom::new(name, vec![])
632 }
633
634 fn atom(name: &str, args: &[i64]) -> WfsAtom {
636 WfsAtom::new(name, args.iter().map(|&i| Value::I64(i)).collect())
637 }
638
639 #[test]
640 fn test_wfs_config_default() {
641 let config = WfsConfig::default();
642 assert_eq!(config.max_iterations, 1000);
643 }
644
645 #[test]
646 fn test_wfs_result_default() {
647 let result = WfsResult::default();
648 assert!(result.true_set.is_empty());
649 assert!(result.false_set.is_empty());
650 }
651
652 #[test]
653 fn test_wfs_result_truth_value() {
654 let mut result = WfsResult::new();
655 let atom = prop("p");
656
657 assert_eq!(result.truth_value(&atom), TruthValue::Undefined);
659
660 result.false_set.insert(atom.clone());
662 assert_eq!(result.truth_value(&atom), TruthValue::False);
663
664 result.false_set.remove(&atom);
666 let mut pir = PirGraph::new();
667 let node_id = pir.lit(LeafId::new(0));
668 result.true_set.insert(atom.clone(), node_id);
669 assert_eq!(result.truth_value(&atom), TruthValue::True);
670 }
671
672 #[test]
673 fn test_wfs_atom_equality() {
674 let atom1 = WfsAtom::new("p", vec![Value::I64(1)]);
675 let atom2 = WfsAtom::new("p", vec![Value::I64(1)]);
676 let atom3 = WfsAtom::new("p", vec![Value::I64(2)]);
677
678 assert_eq!(atom1, atom2);
679 assert_ne!(atom1, atom3);
680 }
681
682 #[test]
683 fn test_wfs_empty_rules() {
684 let mut pir = PirGraph::new();
685 let result = evaluate_wfs_rules(&[], &mut pir, &WfsConfig::default()).unwrap();
686 assert!(result.true_set.is_empty());
687 assert!(result.false_set.is_empty());
688 }
689
690 #[test]
691 fn test_wfs_simple_fact() {
692 let mut pir = PirGraph::new();
695 let const_true = pir.const_true();
696
697 let rules = vec![WfsRule::new(prop("p"), vec![], const_true)];
698
699 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
700
701 assert_eq!(result.truth_value(&prop("p")), TruthValue::True);
702 assert!(result.provenance(&prop("p")).is_some());
703 }
704
705 #[test]
706 fn test_wfs_simple_negation() {
707 let mut pir = PirGraph::new();
711 let const_true = pir.const_true();
712
713 let rules = vec![
714 WfsRule::new(prop("p"), vec![WfsLiteral::Negative(prop("q"))], const_true),
715 WfsRule::new(prop("q"), vec![WfsLiteral::Negative(prop("p"))], const_true),
716 ];
717
718 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
719
720 assert_eq!(result.truth_value(&prop("p")), TruthValue::Undefined);
722 assert_eq!(result.truth_value(&prop("q")), TruthValue::Undefined);
723 }
724
725 #[test]
726 fn test_wfs_asymmetric_negation() {
727 let mut pir = PirGraph::new();
730 let const_true = pir.const_true();
731
732 let rules = vec![
733 WfsRule::new(prop("p"), vec![WfsLiteral::Negative(prop("q"))], const_true),
734 WfsRule::new(prop("q"), vec![], const_true),
735 ];
736
737 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
738
739 assert_eq!(result.truth_value(&prop("q")), TruthValue::True);
740 assert_eq!(result.truth_value(&prop("p")), TruthValue::False);
741 }
742
743 #[test]
744 fn test_wfs_three_way_cycle() {
745 let mut pir = PirGraph::new();
748 let const_true = pir.const_true();
749
750 let rules = vec![
751 WfsRule::new(prop("p"), vec![WfsLiteral::Negative(prop("q"))], const_true),
752 WfsRule::new(prop("q"), vec![WfsLiteral::Negative(prop("r"))], const_true),
753 WfsRule::new(prop("r"), vec![WfsLiteral::Negative(prop("p"))], const_true),
754 ];
755
756 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
757
758 assert_eq!(result.truth_value(&prop("p")), TruthValue::Undefined);
760 assert_eq!(result.truth_value(&prop("q")), TruthValue::Undefined);
761 assert_eq!(result.truth_value(&prop("r")), TruthValue::Undefined);
762 }
763
764 #[test]
765 fn test_wfs_win_lose() {
766 let mut pir = PirGraph::new();
774 let const_true = pir.const_true();
775
776 let rules = vec![
778 WfsRule::new(
779 atom("win", &[1]),
780 vec![WfsLiteral::Negative(atom("win", &[2]))],
781 const_true, ),
783 WfsRule::new(
784 atom("win", &[2]),
785 vec![WfsLiteral::Negative(atom("win", &[1]))],
786 const_true, ),
788 ];
789
790 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
791
792 assert_eq!(
794 result.truth_value(&atom("win", &[1])),
795 TruthValue::Undefined
796 );
797 assert_eq!(
798 result.truth_value(&atom("win", &[2])),
799 TruthValue::Undefined
800 );
801 }
802
803 #[test]
804 fn test_wfs_win_with_base_case() {
805 let mut pir = PirGraph::new();
819 let const_true = pir.const_true();
820
821 let rules = vec![
828 WfsRule::new(
829 atom("win", &[1]),
830 vec![WfsLiteral::Negative(atom("win", &[2]))],
831 const_true,
832 ),
833 WfsRule::new(
835 atom("win", &[2]),
836 vec![WfsLiteral::Positive(atom("win", &[3]))],
837 const_true,
838 ),
839 ];
840
841 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
842
843 assert_eq!(result.truth_value(&atom("win", &[1])), TruthValue::True);
847 assert_eq!(result.truth_value(&atom("win", &[2])), TruthValue::False);
848 }
849
850 #[test]
851 fn test_wfs_chain_with_grounding() {
852 let mut pir = PirGraph::new();
859 let const_true = pir.const_true();
860
861 let rules = vec![
862 WfsRule::new(prop("a"), vec![], const_true),
863 WfsRule::new(prop("b"), vec![WfsLiteral::Negative(prop("a"))], const_true),
864 WfsRule::new(prop("c"), vec![WfsLiteral::Negative(prop("b"))], const_true),
865 ];
866
867 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
868
869 assert_eq!(result.truth_value(&prop("a")), TruthValue::True);
870 assert_eq!(result.truth_value(&prop("b")), TruthValue::False);
871 assert_eq!(result.truth_value(&prop("c")), TruthValue::True);
872 }
873
874 #[test]
875 fn test_wfs_multiple_rules_same_head() {
876 let mut pir = PirGraph::new();
881 let const_true = pir.const_true();
882
883 let rules = vec![
884 WfsRule::new(prop("q"), vec![], const_true),
885 WfsRule::new(prop("r"), vec![], const_true),
886 WfsRule::new(prop("p"), vec![WfsLiteral::Positive(prop("q"))], const_true),
887 WfsRule::new(prop("p"), vec![WfsLiteral::Negative(prop("r"))], const_true),
888 ];
889
890 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
891
892 assert_eq!(result.truth_value(&prop("q")), TruthValue::True);
893 assert_eq!(result.truth_value(&prop("r")), TruthValue::True);
894 assert_eq!(result.truth_value(&prop("p")), TruthValue::True);
895 }
896
897 #[test]
898 fn test_wfs_provenance_tracking() {
899 let mut pir = PirGraph::new();
902 let leaf = pir.lit(LeafId::new(42));
903
904 let rules = vec![WfsRule::new(prop("p"), vec![], leaf)];
905
906 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
907
908 let prov = result.provenance(&prop("p"));
909 assert!(prov.is_some());
910 assert!(prov.unwrap().as_u32() > 0);
913 }
914
915 #[test]
916 fn test_wfs_combined_provenance() {
917 let mut pir = PirGraph::new();
920 let leaf_q = pir.lit(LeafId::new(1));
921 let const_true = pir.const_true();
922
923 let rules = vec![
924 WfsRule::new(prop("q"), vec![], leaf_q),
925 WfsRule::new(prop("p"), vec![WfsLiteral::Positive(prop("q"))], const_true),
926 ];
927
928 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
929
930 assert_eq!(result.truth_value(&prop("q")), TruthValue::True);
931 assert_eq!(result.truth_value(&prop("p")), TruthValue::True);
932
933 let p_prov = result.provenance(&prop("p")).unwrap();
936 assert!(p_prov.as_u32() > 0);
938 }
939
940 #[test]
941 fn test_wfs_no_rules_for_predicate() {
942 let result = evaluate_wfs_scc_with_config(
944 &["p".to_string(), "q".to_string()],
945 &mut PirGraph::new(),
946 &WfsConfig::default(),
947 )
948 .unwrap();
949
950 assert_eq!(result.truth_value(&prop("p")), TruthValue::False);
951 assert_eq!(result.truth_value(&prop("q")), TruthValue::False);
952 }
953
954 #[test]
955 fn test_wfs_positive_cycle() {
956 let mut pir = PirGraph::new();
959 let const_true = pir.const_true();
960
961 let rules = vec![
962 WfsRule::new(prop("p"), vec![WfsLiteral::Positive(prop("q"))], const_true),
963 WfsRule::new(prop("q"), vec![WfsLiteral::Positive(prop("p"))], const_true),
964 ];
965
966 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
967
968 assert_eq!(result.truth_value(&prop("p")), TruthValue::False);
970 assert_eq!(result.truth_value(&prop("q")), TruthValue::False);
971 }
972
973 #[test]
974 fn test_wfs_positive_cycle_with_base() {
975 let mut pir = PirGraph::new();
978 let const_true = pir.const_true();
979
980 let rules = vec![
981 WfsRule::new(prop("p"), vec![], const_true), WfsRule::new(prop("p"), vec![WfsLiteral::Positive(prop("q"))], const_true),
983 WfsRule::new(prop("q"), vec![WfsLiteral::Positive(prop("p"))], const_true),
984 ];
985
986 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
987
988 assert_eq!(result.truth_value(&prop("p")), TruthValue::True);
989 assert_eq!(result.truth_value(&prop("q")), TruthValue::True);
990 }
991
992 #[test]
993 fn test_wfs_mixed_dependencies() {
994 let mut pir = PirGraph::new();
1004 let const_true = pir.const_true();
1005
1006 let rules = vec![
1007 WfsRule::new(prop("a"), vec![], const_true),
1008 WfsRule::new(prop("b"), vec![WfsLiteral::Positive(prop("a"))], const_true),
1009 WfsRule::new(prop("c"), vec![WfsLiteral::Negative(prop("b"))], const_true),
1010 WfsRule::new(
1011 prop("d"),
1012 vec![
1013 WfsLiteral::Positive(prop("c")),
1014 WfsLiteral::Negative(prop("e")),
1015 ],
1016 const_true,
1017 ),
1018 WfsRule::new(prop("e"), vec![WfsLiteral::Negative(prop("d"))], const_true),
1019 ];
1020
1021 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
1022
1023 assert_eq!(result.truth_value(&prop("a")), TruthValue::True);
1024 assert_eq!(result.truth_value(&prop("b")), TruthValue::True);
1025 assert_eq!(result.truth_value(&prop("c")), TruthValue::False);
1026 assert_eq!(result.truth_value(&prop("d")), TruthValue::False);
1027 assert_eq!(result.truth_value(&prop("e")), TruthValue::True);
1028 }
1029
1030 #[test]
1031 fn test_wfs_undefined_atoms_method() {
1032 let mut result = WfsResult::new();
1033 let p = prop("p");
1034 let q = prop("q");
1035 let r = prop("r");
1036
1037 result.true_set.insert(p.clone(), PirNodeId::from(0));
1038 result.false_set.insert(q.clone());
1039 let atoms = vec![&p, &q, &r];
1042 let undefined = result.undefined_atoms(atoms.into_iter());
1043
1044 assert_eq!(undefined.len(), 1);
1045 assert_eq!(undefined[0], &r);
1046 }
1047
1048 impl From<u32> for PirNodeId {
1050 fn from(v: u32) -> Self {
1051 unsafe { std::mem::transmute(v) }
1054 }
1055 }
1056
1057 #[test]
1058 fn test_wfs_even_odd_cycle() {
1059 let mut pir = PirGraph::new();
1072 let const_true = pir.const_true();
1073
1074 let rules = vec![
1075 WfsRule::new(atom("even", &[0]), vec![], const_true),
1077 WfsRule::new(
1081 atom("odd", &[1]),
1082 vec![WfsLiteral::Positive(atom("even", &[0]))],
1083 const_true,
1084 ),
1085 WfsRule::new(
1087 atom("even", &[2]),
1088 vec![WfsLiteral::Positive(atom("odd", &[1]))],
1089 const_true,
1090 ),
1091 WfsRule::new(
1093 atom("odd", &[3]),
1094 vec![WfsLiteral::Positive(atom("even", &[2]))],
1095 const_true,
1096 ),
1097 ];
1098
1099 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
1100
1101 assert_eq!(result.truth_value(&atom("even", &[0])), TruthValue::True);
1102 assert_eq!(result.truth_value(&atom("odd", &[1])), TruthValue::True);
1103 assert_eq!(result.truth_value(&atom("even", &[2])), TruthValue::True);
1104 assert_eq!(result.truth_value(&atom("odd", &[3])), TruthValue::True);
1105 }
1106
1107 #[test]
1108 fn test_wfs_default_logic() {
1109 let mut pir = PirGraph::new();
1122 let const_true = pir.const_true();
1123
1124 let rules = vec![
1126 WfsRule::new(atom("penguin", &[1]), vec![], const_true), WfsRule::new(
1130 atom("bird", &[1]),
1131 vec![WfsLiteral::Positive(atom("penguin", &[1]))],
1132 const_true,
1133 ),
1134 WfsRule::new(
1136 atom("ab", &[1]),
1137 vec![WfsLiteral::Positive(atom("penguin", &[1]))],
1138 const_true,
1139 ),
1140 WfsRule::new(
1142 atom("flies", &[1]),
1143 vec![
1144 WfsLiteral::Positive(atom("bird", &[1])),
1145 WfsLiteral::Negative(atom("ab", &[1])),
1146 ],
1147 const_true,
1148 ),
1149 ];
1150
1151 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
1152
1153 assert_eq!(result.truth_value(&atom("penguin", &[1])), TruthValue::True);
1154 assert_eq!(result.truth_value(&atom("bird", &[1])), TruthValue::True);
1155 assert_eq!(result.truth_value(&atom("ab", &[1])), TruthValue::True);
1156 assert_eq!(result.truth_value(&atom("flies", &[1])), TruthValue::False);
1157 }
1158
1159 #[test]
1160 fn test_wfs_non_ground_birds() {
1161 let mut pir = PirGraph::new();
1172 let const_true = pir.const_true();
1173
1174 let rules = vec![
1175 WfsRule::new(atom("penguin", &[1]), vec![], const_true), WfsRule::new(atom("eagle", &[2]), vec![], const_true), WfsRule::new(
1180 atom("bird", &[1]),
1181 vec![WfsLiteral::Positive(atom("penguin", &[1]))],
1182 const_true,
1183 ),
1184 WfsRule::new(
1186 atom("bird", &[2]),
1187 vec![WfsLiteral::Positive(atom("eagle", &[2]))],
1188 const_true,
1189 ),
1190 WfsRule::new(
1192 atom("ab", &[1]),
1193 vec![WfsLiteral::Positive(atom("penguin", &[1]))],
1194 const_true,
1195 ),
1196 WfsRule::new(
1198 atom("flies", &[1]),
1199 vec![
1200 WfsLiteral::Positive(atom("bird", &[1])),
1201 WfsLiteral::Negative(atom("ab", &[1])),
1202 ],
1203 const_true,
1204 ),
1205 WfsRule::new(
1208 atom("flies", &[2]),
1209 vec![
1210 WfsLiteral::Positive(atom("bird", &[2])),
1211 WfsLiteral::Negative(atom("ab", &[2])),
1212 ],
1213 const_true,
1214 ),
1215 ];
1216
1217 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
1218
1219 assert_eq!(result.truth_value(&atom("penguin", &[1])), TruthValue::True);
1221 assert_eq!(result.truth_value(&atom("bird", &[1])), TruthValue::True);
1222 assert_eq!(result.truth_value(&atom("ab", &[1])), TruthValue::True);
1223 assert_eq!(result.truth_value(&atom("flies", &[1])), TruthValue::False);
1224
1225 assert_eq!(result.truth_value(&atom("eagle", &[2])), TruthValue::True);
1227 assert_eq!(result.truth_value(&atom("bird", &[2])), TruthValue::True);
1228 assert_eq!(result.truth_value(&atom("flies", &[2])), TruthValue::True);
1232 }
1233
1234 #[test]
1235 fn test_wfs_probabilistic_provenance_or() {
1236 let mut pir = PirGraph::new();
1240 let leaf_q = pir.lit(LeafId::new(1));
1241 let leaf_r = pir.lit(LeafId::new(2));
1242
1243 let rules = vec![
1244 WfsRule::new(prop("q"), vec![], leaf_q),
1245 WfsRule::new(prop("r"), vec![], leaf_r),
1246 WfsRule::new(
1247 prop("p"),
1248 vec![WfsLiteral::Positive(prop("q"))],
1249 pir.const_true(),
1250 ),
1251 WfsRule::new(
1252 prop("p"),
1253 vec![WfsLiteral::Positive(prop("r"))],
1254 pir.const_true(),
1255 ),
1256 ];
1257
1258 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
1259
1260 assert_eq!(result.truth_value(&prop("p")), TruthValue::True);
1261 assert_eq!(result.truth_value(&prop("q")), TruthValue::True);
1262 assert_eq!(result.truth_value(&prop("r")), TruthValue::True);
1263
1264 let p_prov = result.provenance(&prop("p")).unwrap();
1266 assert!(p_prov.as_u32() > 0);
1268 }
1269
1270 #[test]
1271 fn test_wfs_stable_model_unique() {
1272 let mut pir = PirGraph::new();
1278 let const_true = pir.const_true();
1279
1280 let rules = vec![
1281 WfsRule::new(prop("d"), vec![], const_true),
1282 WfsRule::new(
1283 prop("a"),
1284 vec![
1285 WfsLiteral::Negative(prop("b")),
1286 WfsLiteral::Negative(prop("c")),
1287 ],
1288 const_true,
1289 ),
1290 WfsRule::new(
1291 prop("b"),
1292 vec![
1293 WfsLiteral::Negative(prop("a")),
1294 WfsLiteral::Negative(prop("c")),
1295 ],
1296 const_true,
1297 ),
1298 WfsRule::new(
1299 prop("c"),
1300 vec![
1301 WfsLiteral::Negative(prop("a")),
1302 WfsLiteral::Negative(prop("b")),
1303 ],
1304 const_true,
1305 ),
1306 ];
1307
1308 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
1309
1310 assert_eq!(result.truth_value(&prop("d")), TruthValue::True);
1311 assert_eq!(result.truth_value(&prop("a")), TruthValue::Undefined);
1313 assert_eq!(result.truth_value(&prop("b")), TruthValue::Undefined);
1314 assert_eq!(result.truth_value(&prop("c")), TruthValue::Undefined);
1315 }
1316
1317 #[test]
1318 fn test_wfs_hamiltonian_cycle_like() {
1319 let mut pir = PirGraph::new();
1326 let const_true = pir.const_true();
1327
1328 let rules = vec![
1329 WfsRule::new(atom("edge", &[1, 2]), vec![], const_true),
1331 WfsRule::new(
1333 atom("in", &[1, 2]),
1334 vec![
1335 WfsLiteral::Positive(atom("edge", &[1, 2])),
1336 WfsLiteral::Negative(atom("out", &[1, 2])),
1337 ],
1338 const_true,
1339 ),
1340 WfsRule::new(
1342 atom("out", &[1, 2]),
1343 vec![
1344 WfsLiteral::Positive(atom("edge", &[1, 2])),
1345 WfsLiteral::Negative(atom("in", &[1, 2])),
1346 ],
1347 const_true,
1348 ),
1349 ];
1350
1351 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
1352
1353 assert_eq!(result.truth_value(&atom("edge", &[1, 2])), TruthValue::True);
1354 assert_eq!(
1356 result.truth_value(&atom("in", &[1, 2])),
1357 TruthValue::Undefined
1358 );
1359 assert_eq!(
1360 result.truth_value(&atom("out", &[1, 2])),
1361 TruthValue::Undefined
1362 );
1363 }
1364
1365 #[test]
1366 fn test_wfs_partial_grounding() {
1367 let mut pir = PirGraph::new();
1370 let const_true = pir.const_true();
1371
1372 let rules = vec![WfsRule::new(
1374 prop("p"),
1375 vec![WfsLiteral::Positive(prop("q"))],
1376 const_true,
1377 )];
1378
1379 let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
1380
1381 assert_eq!(result.truth_value(&prop("p")), TruthValue::False);
1384 assert_eq!(result.truth_value(&prop("q")), TruthValue::Undefined);
1386 }
1387}