use crate::pir::{PirGraph, PirNodeId};
use crate::provenance::Value;
use std::collections::{HashMap, HashSet};
use xlog_core::{Result, XlogError};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct WfsAtom {
pub predicate: String,
pub args: Vec<Value>,
}
impl WfsAtom {
pub fn new(predicate: impl Into<String>, args: Vec<Value>) -> Self {
Self {
predicate: predicate.into(),
args,
}
}
}
impl std::fmt::Display for WfsAtom {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}(", self.predicate)?;
for (i, arg) in self.args.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
match arg {
Value::I64(v) => write!(f, "{}", v)?,
Value::F64(v) => write!(f, "{}", f64::from_bits(*v))?,
Value::Symbol(v) => write!(f, "sym{}", v)?,
Value::String(v) => write!(f, "\"{}\"", v)?,
}
}
write!(f, ")")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TruthValue {
True,
False,
Undefined,
}
impl TruthValue {
pub fn is_defined(&self) -> bool {
matches!(self, TruthValue::True | TruthValue::False)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum WfsLiteral {
Positive(WfsAtom),
Negative(WfsAtom),
}
impl WfsLiteral {
pub fn atom(&self) -> &WfsAtom {
match self {
WfsLiteral::Positive(a) | WfsLiteral::Negative(a) => a,
}
}
pub fn is_positive(&self) -> bool {
matches!(self, WfsLiteral::Positive(_))
}
pub fn is_negative(&self) -> bool {
matches!(self, WfsLiteral::Negative(_))
}
}
#[derive(Debug, Clone)]
pub struct WfsRule {
pub head: WfsAtom,
pub body: Vec<WfsLiteral>,
pub provenance: PirNodeId,
}
impl WfsRule {
pub fn new(head: WfsAtom, body: Vec<WfsLiteral>, provenance: PirNodeId) -> Self {
Self {
head,
body,
provenance,
}
}
fn is_satisfied(
&self,
true_set: &HashSet<WfsAtom>,
false_set: &HashSet<WfsAtom>,
scc_atoms: &HashSet<WfsAtom>,
) -> Option<bool> {
for lit in &self.body {
match lit {
WfsLiteral::Positive(atom) => {
if false_set.contains(atom) {
return Some(false);
}
if true_set.contains(atom) {
continue; }
if scc_atoms.contains(atom) {
return None; }
return Some(false);
}
WfsLiteral::Negative(atom) => {
if true_set.contains(atom) {
return Some(false);
}
if false_set.contains(atom) {
continue; }
if scc_atoms.contains(atom) {
return None; }
continue;
}
}
}
Some(true)
}
#[allow(dead_code)] pub fn is_definitely_unsatisfiable(
&self,
true_set: &HashSet<WfsAtom>,
false_set: &HashSet<WfsAtom>,
) -> bool {
for lit in &self.body {
match lit {
WfsLiteral::Positive(atom) => {
if false_set.contains(atom) {
return true;
}
}
WfsLiteral::Negative(atom) => {
if true_set.contains(atom) {
return true;
}
}
}
}
false
}
}
#[derive(Debug, Clone)]
pub struct WfsResult {
pub true_set: HashMap<WfsAtom, PirNodeId>,
pub false_set: HashSet<WfsAtom>,
}
impl WfsResult {
pub fn new() -> Self {
Self {
true_set: HashMap::new(),
false_set: HashSet::new(),
}
}
pub fn truth_value(&self, atom: &WfsAtom) -> TruthValue {
if self.true_set.contains_key(atom) {
TruthValue::True
} else if self.false_set.contains(atom) {
TruthValue::False
} else {
TruthValue::Undefined
}
}
pub fn provenance(&self, atom: &WfsAtom) -> Option<PirNodeId> {
self.true_set.get(atom).copied()
}
pub fn is_undefined(&self, atom: &WfsAtom) -> bool {
!self.true_set.contains_key(atom) && !self.false_set.contains(atom)
}
pub fn undefined_atoms<'a>(
&self,
atoms: impl Iterator<Item = &'a WfsAtom>,
) -> Vec<&'a WfsAtom> {
atoms.filter(|a| self.is_undefined(a)).collect()
}
}
impl Default for WfsResult {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct WfsConfig {
pub max_iterations: usize,
}
impl Default for WfsConfig {
fn default() -> Self {
Self {
max_iterations: 1000,
}
}
}
pub struct WfsContext<'a> {
rules: &'a [WfsRule],
scc_atoms: HashSet<WfsAtom>,
rules_by_head: HashMap<WfsAtom, Vec<usize>>,
pir: &'a mut PirGraph,
}
impl<'a> WfsContext<'a> {
fn new(rules: &'a [WfsRule], pir: &'a mut PirGraph) -> Self {
let mut scc_atoms = HashSet::new();
let mut rules_by_head: HashMap<WfsAtom, Vec<usize>> = HashMap::new();
for (i, rule) in rules.iter().enumerate() {
scc_atoms.insert(rule.head.clone());
rules_by_head.entry(rule.head.clone()).or_default().push(i);
}
Self {
rules,
scc_atoms,
rules_by_head,
pir,
}
}
}
fn compute_unfounded_set(
ctx: &WfsContext,
true_set: &HashSet<WfsAtom>,
false_set: &HashSet<WfsAtom>,
) -> HashSet<WfsAtom> {
let mut unfounded: HashSet<WfsAtom> = ctx
.scc_atoms
.iter()
.filter(|a| !true_set.contains(*a))
.cloned()
.collect();
loop {
let mut changed = false;
let mut to_remove = Vec::new();
for atom in &unfounded {
if let Some(rule_indices) = ctx.rules_by_head.get(atom) {
for &rule_idx in rule_indices {
let rule = &ctx.rules[rule_idx];
if rule_is_potentially_supporting(
rule,
true_set,
false_set,
&unfounded,
&ctx.scc_atoms,
) {
to_remove.push(atom.clone());
changed = true;
break;
}
}
}
}
for atom in to_remove {
unfounded.remove(&atom);
}
if !changed {
break;
}
}
unfounded
}
fn rule_is_potentially_supporting(
rule: &WfsRule,
true_set: &HashSet<WfsAtom>,
false_set: &HashSet<WfsAtom>,
unfounded: &HashSet<WfsAtom>,
scc_atoms: &HashSet<WfsAtom>,
) -> bool {
for lit in &rule.body {
match lit {
WfsLiteral::Positive(atom) => {
if false_set.contains(atom) {
return false;
}
if true_set.contains(atom) {
continue;
}
if scc_atoms.contains(atom) {
if unfounded.contains(atom) {
return false;
}
continue;
}
return false;
}
WfsLiteral::Negative(atom) => {
if true_set.contains(atom) {
return false;
}
}
}
}
true
}
fn derive_consequences(
ctx: &mut WfsContext,
true_set: &HashSet<WfsAtom>,
false_set: &HashSet<WfsAtom>,
existing_provenance: &HashMap<WfsAtom, PirNodeId>,
) -> HashMap<WfsAtom, PirNodeId> {
let mut new_true: HashMap<WfsAtom, PirNodeId> = HashMap::new();
for rule in ctx.rules.iter() {
if true_set.contains(&rule.head) || false_set.contains(&rule.head) {
continue;
}
if let Some(true) = rule.is_satisfied(true_set, false_set, &ctx.scc_atoms) {
let mut prov_parts = vec![rule.provenance];
for lit in &rule.body {
if let WfsLiteral::Positive(atom) = lit {
if let Some(&atom_prov) = existing_provenance.get(atom) {
prov_parts.push(atom_prov);
}
}
}
let rule_prov = if prov_parts.len() == 1 {
prov_parts[0]
} else {
ctx.pir.and(prov_parts)
};
let entry = new_true
.entry(rule.head.clone())
.or_insert_with(|| ctx.pir.const_false());
*entry = ctx.pir.or(vec![*entry, rule_prov]);
}
}
new_true
}
pub fn evaluate_wfs_rules(
rules: &[WfsRule],
pir: &mut PirGraph,
config: &WfsConfig,
) -> Result<WfsResult> {
if rules.is_empty() {
return Ok(WfsResult::new());
}
let mut ctx = WfsContext::new(rules, pir);
let mut true_set: HashSet<WfsAtom> = HashSet::new();
let mut true_provenance: HashMap<WfsAtom, PirNodeId> = HashMap::new();
let mut false_set: HashSet<WfsAtom> = HashSet::new();
for iteration in 0..config.max_iterations {
let unfounded = compute_unfounded_set(&ctx, &true_set, &false_set);
let new_false: Vec<WfsAtom> = unfounded
.into_iter()
.filter(|a| !false_set.contains(a))
.collect();
let new_false_count = new_false.len();
false_set.extend(new_false);
let new_true = derive_consequences(&mut ctx, &true_set, &false_set, &true_provenance);
let new_true_count = new_true.len();
for (atom, prov) in new_true {
if !true_set.contains(&atom) {
true_set.insert(atom.clone());
let entry = true_provenance
.entry(atom)
.or_insert_with(|| ctx.pir.const_false());
*entry = ctx.pir.or(vec![*entry, prov]);
}
}
if new_false_count == 0 && new_true_count == 0 {
break;
}
if iteration == config.max_iterations - 1 {
return Err(XlogError::Execution(format!(
"WFS evaluation did not converge after {} iterations",
config.max_iterations
)));
}
}
Ok(WfsResult {
true_set: true_provenance,
false_set,
})
}
pub fn evaluate_wfs_with_rules(rules: Vec<WfsRule>, pir: &mut PirGraph) -> Result<WfsResult> {
evaluate_wfs_rules(&rules, pir, &WfsConfig::default())
}
#[cfg(test)]
fn evaluate_wfs_scc_with_config(
scc_predicates: &[String],
pir: &mut PirGraph,
config: &WfsConfig,
) -> Result<WfsResult> {
if scc_predicates.is_empty() {
return Ok(WfsResult::new());
}
let false_set: HashSet<WfsAtom> = scc_predicates
.iter()
.map(|p| WfsAtom::new(p.clone(), vec![]))
.collect();
let _ = config;
let _ = pir;
Ok(WfsResult {
true_set: HashMap::new(),
false_set,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pir::LeafId;
fn prop(name: &str) -> WfsAtom {
WfsAtom::new(name, vec![])
}
fn atom(name: &str, args: &[i64]) -> WfsAtom {
WfsAtom::new(name, args.iter().map(|&i| Value::I64(i)).collect())
}
#[test]
fn test_wfs_config_default() {
let config = WfsConfig::default();
assert_eq!(config.max_iterations, 1000);
}
#[test]
fn test_wfs_result_default() {
let result = WfsResult::default();
assert!(result.true_set.is_empty());
assert!(result.false_set.is_empty());
}
#[test]
fn test_wfs_result_truth_value() {
let mut result = WfsResult::new();
let atom = prop("p");
assert_eq!(result.truth_value(&atom), TruthValue::Undefined);
result.false_set.insert(atom.clone());
assert_eq!(result.truth_value(&atom), TruthValue::False);
result.false_set.remove(&atom);
let mut pir = PirGraph::new();
let node_id = pir.lit(LeafId::new(0));
result.true_set.insert(atom.clone(), node_id);
assert_eq!(result.truth_value(&atom), TruthValue::True);
}
#[test]
fn test_wfs_atom_equality() {
let atom1 = WfsAtom::new("p", vec![Value::I64(1)]);
let atom2 = WfsAtom::new("p", vec![Value::I64(1)]);
let atom3 = WfsAtom::new("p", vec![Value::I64(2)]);
assert_eq!(atom1, atom2);
assert_ne!(atom1, atom3);
}
#[test]
fn test_wfs_empty_rules() {
let mut pir = PirGraph::new();
let result = evaluate_wfs_rules(&[], &mut pir, &WfsConfig::default()).unwrap();
assert!(result.true_set.is_empty());
assert!(result.false_set.is_empty());
}
#[test]
fn test_wfs_simple_fact() {
let mut pir = PirGraph::new();
let const_true = pir.const_true();
let rules = vec![WfsRule::new(prop("p"), vec![], const_true)];
let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
assert_eq!(result.truth_value(&prop("p")), TruthValue::True);
assert!(result.provenance(&prop("p")).is_some());
}
#[test]
fn test_wfs_simple_negation() {
let mut pir = PirGraph::new();
let const_true = pir.const_true();
let rules = vec![
WfsRule::new(prop("p"), vec![WfsLiteral::Negative(prop("q"))], const_true),
WfsRule::new(prop("q"), vec![WfsLiteral::Negative(prop("p"))], const_true),
];
let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
assert_eq!(result.truth_value(&prop("p")), TruthValue::Undefined);
assert_eq!(result.truth_value(&prop("q")), TruthValue::Undefined);
}
#[test]
fn test_wfs_asymmetric_negation() {
let mut pir = PirGraph::new();
let const_true = pir.const_true();
let rules = vec![
WfsRule::new(prop("p"), vec![WfsLiteral::Negative(prop("q"))], const_true),
WfsRule::new(prop("q"), vec![], const_true),
];
let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
assert_eq!(result.truth_value(&prop("q")), TruthValue::True);
assert_eq!(result.truth_value(&prop("p")), TruthValue::False);
}
#[test]
fn test_wfs_three_way_cycle() {
let mut pir = PirGraph::new();
let const_true = pir.const_true();
let rules = vec![
WfsRule::new(prop("p"), vec![WfsLiteral::Negative(prop("q"))], const_true),
WfsRule::new(prop("q"), vec![WfsLiteral::Negative(prop("r"))], const_true),
WfsRule::new(prop("r"), vec![WfsLiteral::Negative(prop("p"))], const_true),
];
let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
assert_eq!(result.truth_value(&prop("p")), TruthValue::Undefined);
assert_eq!(result.truth_value(&prop("q")), TruthValue::Undefined);
assert_eq!(result.truth_value(&prop("r")), TruthValue::Undefined);
}
#[test]
fn test_wfs_win_lose() {
let mut pir = PirGraph::new();
let const_true = pir.const_true();
let rules = vec![
WfsRule::new(
atom("win", &[1]),
vec![WfsLiteral::Negative(atom("win", &[2]))],
const_true, ),
WfsRule::new(
atom("win", &[2]),
vec![WfsLiteral::Negative(atom("win", &[1]))],
const_true, ),
];
let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
assert_eq!(
result.truth_value(&atom("win", &[1])),
TruthValue::Undefined
);
assert_eq!(
result.truth_value(&atom("win", &[2])),
TruthValue::Undefined
);
}
#[test]
fn test_wfs_win_with_base_case() {
let mut pir = PirGraph::new();
let const_true = pir.const_true();
let rules = vec![
WfsRule::new(
atom("win", &[1]),
vec![WfsLiteral::Negative(atom("win", &[2]))],
const_true,
),
WfsRule::new(
atom("win", &[2]),
vec![WfsLiteral::Positive(atom("win", &[3]))],
const_true,
),
];
let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
assert_eq!(result.truth_value(&atom("win", &[1])), TruthValue::True);
assert_eq!(result.truth_value(&atom("win", &[2])), TruthValue::False);
}
#[test]
fn test_wfs_chain_with_grounding() {
let mut pir = PirGraph::new();
let const_true = pir.const_true();
let rules = vec![
WfsRule::new(prop("a"), vec![], const_true),
WfsRule::new(prop("b"), vec![WfsLiteral::Negative(prop("a"))], const_true),
WfsRule::new(prop("c"), vec![WfsLiteral::Negative(prop("b"))], const_true),
];
let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
assert_eq!(result.truth_value(&prop("a")), TruthValue::True);
assert_eq!(result.truth_value(&prop("b")), TruthValue::False);
assert_eq!(result.truth_value(&prop("c")), TruthValue::True);
}
#[test]
fn test_wfs_multiple_rules_same_head() {
let mut pir = PirGraph::new();
let const_true = pir.const_true();
let rules = vec![
WfsRule::new(prop("q"), vec![], const_true),
WfsRule::new(prop("r"), vec![], const_true),
WfsRule::new(prop("p"), vec![WfsLiteral::Positive(prop("q"))], const_true),
WfsRule::new(prop("p"), vec![WfsLiteral::Negative(prop("r"))], const_true),
];
let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
assert_eq!(result.truth_value(&prop("q")), TruthValue::True);
assert_eq!(result.truth_value(&prop("r")), TruthValue::True);
assert_eq!(result.truth_value(&prop("p")), TruthValue::True);
}
#[test]
fn test_wfs_provenance_tracking() {
let mut pir = PirGraph::new();
let leaf = pir.lit(LeafId::new(42));
let rules = vec![WfsRule::new(prop("p"), vec![], leaf)];
let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
let prov = result.provenance(&prop("p"));
assert!(prov.is_some());
assert!(prov.unwrap().as_u32() > 0);
}
#[test]
fn test_wfs_combined_provenance() {
let mut pir = PirGraph::new();
let leaf_q = pir.lit(LeafId::new(1));
let const_true = pir.const_true();
let rules = vec![
WfsRule::new(prop("q"), vec![], leaf_q),
WfsRule::new(prop("p"), vec![WfsLiteral::Positive(prop("q"))], const_true),
];
let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
assert_eq!(result.truth_value(&prop("q")), TruthValue::True);
assert_eq!(result.truth_value(&prop("p")), TruthValue::True);
let p_prov = result.provenance(&prop("p")).unwrap();
assert!(p_prov.as_u32() > 0);
}
#[test]
fn test_wfs_no_rules_for_predicate() {
let result = evaluate_wfs_scc_with_config(
&["p".to_string(), "q".to_string()],
&mut PirGraph::new(),
&WfsConfig::default(),
)
.unwrap();
assert_eq!(result.truth_value(&prop("p")), TruthValue::False);
assert_eq!(result.truth_value(&prop("q")), TruthValue::False);
}
#[test]
fn test_wfs_positive_cycle() {
let mut pir = PirGraph::new();
let const_true = pir.const_true();
let rules = vec![
WfsRule::new(prop("p"), vec![WfsLiteral::Positive(prop("q"))], const_true),
WfsRule::new(prop("q"), vec![WfsLiteral::Positive(prop("p"))], const_true),
];
let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
assert_eq!(result.truth_value(&prop("p")), TruthValue::False);
assert_eq!(result.truth_value(&prop("q")), TruthValue::False);
}
#[test]
fn test_wfs_positive_cycle_with_base() {
let mut pir = PirGraph::new();
let const_true = pir.const_true();
let rules = vec![
WfsRule::new(prop("p"), vec![], const_true), WfsRule::new(prop("p"), vec![WfsLiteral::Positive(prop("q"))], const_true),
WfsRule::new(prop("q"), vec![WfsLiteral::Positive(prop("p"))], const_true),
];
let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
assert_eq!(result.truth_value(&prop("p")), TruthValue::True);
assert_eq!(result.truth_value(&prop("q")), TruthValue::True);
}
#[test]
fn test_wfs_mixed_dependencies() {
let mut pir = PirGraph::new();
let const_true = pir.const_true();
let rules = vec![
WfsRule::new(prop("a"), vec![], const_true),
WfsRule::new(prop("b"), vec![WfsLiteral::Positive(prop("a"))], const_true),
WfsRule::new(prop("c"), vec![WfsLiteral::Negative(prop("b"))], const_true),
WfsRule::new(
prop("d"),
vec![
WfsLiteral::Positive(prop("c")),
WfsLiteral::Negative(prop("e")),
],
const_true,
),
WfsRule::new(prop("e"), vec![WfsLiteral::Negative(prop("d"))], const_true),
];
let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
assert_eq!(result.truth_value(&prop("a")), TruthValue::True);
assert_eq!(result.truth_value(&prop("b")), TruthValue::True);
assert_eq!(result.truth_value(&prop("c")), TruthValue::False);
assert_eq!(result.truth_value(&prop("d")), TruthValue::False);
assert_eq!(result.truth_value(&prop("e")), TruthValue::True);
}
#[test]
fn test_wfs_undefined_atoms_method() {
let mut result = WfsResult::new();
let p = prop("p");
let q = prop("q");
let r = prop("r");
result.true_set.insert(p.clone(), PirNodeId::from(0));
result.false_set.insert(q.clone());
let atoms = vec![&p, &q, &r];
let undefined = result.undefined_atoms(atoms.into_iter());
assert_eq!(undefined.len(), 1);
assert_eq!(undefined[0], &r);
}
impl From<u32> for PirNodeId {
fn from(v: u32) -> Self {
unsafe { std::mem::transmute(v) }
}
}
#[test]
fn test_wfs_even_odd_cycle() {
let mut pir = PirGraph::new();
let const_true = pir.const_true();
let rules = vec![
WfsRule::new(atom("even", &[0]), vec![], const_true),
WfsRule::new(
atom("odd", &[1]),
vec![WfsLiteral::Positive(atom("even", &[0]))],
const_true,
),
WfsRule::new(
atom("even", &[2]),
vec![WfsLiteral::Positive(atom("odd", &[1]))],
const_true,
),
WfsRule::new(
atom("odd", &[3]),
vec![WfsLiteral::Positive(atom("even", &[2]))],
const_true,
),
];
let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
assert_eq!(result.truth_value(&atom("even", &[0])), TruthValue::True);
assert_eq!(result.truth_value(&atom("odd", &[1])), TruthValue::True);
assert_eq!(result.truth_value(&atom("even", &[2])), TruthValue::True);
assert_eq!(result.truth_value(&atom("odd", &[3])), TruthValue::True);
}
#[test]
fn test_wfs_default_logic() {
let mut pir = PirGraph::new();
let const_true = pir.const_true();
let rules = vec![
WfsRule::new(atom("penguin", &[1]), vec![], const_true), WfsRule::new(
atom("bird", &[1]),
vec![WfsLiteral::Positive(atom("penguin", &[1]))],
const_true,
),
WfsRule::new(
atom("ab", &[1]),
vec![WfsLiteral::Positive(atom("penguin", &[1]))],
const_true,
),
WfsRule::new(
atom("flies", &[1]),
vec![
WfsLiteral::Positive(atom("bird", &[1])),
WfsLiteral::Negative(atom("ab", &[1])),
],
const_true,
),
];
let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
assert_eq!(result.truth_value(&atom("penguin", &[1])), TruthValue::True);
assert_eq!(result.truth_value(&atom("bird", &[1])), TruthValue::True);
assert_eq!(result.truth_value(&atom("ab", &[1])), TruthValue::True);
assert_eq!(result.truth_value(&atom("flies", &[1])), TruthValue::False);
}
#[test]
fn test_wfs_non_ground_birds() {
let mut pir = PirGraph::new();
let const_true = pir.const_true();
let rules = vec![
WfsRule::new(atom("penguin", &[1]), vec![], const_true), WfsRule::new(atom("eagle", &[2]), vec![], const_true), WfsRule::new(
atom("bird", &[1]),
vec![WfsLiteral::Positive(atom("penguin", &[1]))],
const_true,
),
WfsRule::new(
atom("bird", &[2]),
vec![WfsLiteral::Positive(atom("eagle", &[2]))],
const_true,
),
WfsRule::new(
atom("ab", &[1]),
vec![WfsLiteral::Positive(atom("penguin", &[1]))],
const_true,
),
WfsRule::new(
atom("flies", &[1]),
vec![
WfsLiteral::Positive(atom("bird", &[1])),
WfsLiteral::Negative(atom("ab", &[1])),
],
const_true,
),
WfsRule::new(
atom("flies", &[2]),
vec![
WfsLiteral::Positive(atom("bird", &[2])),
WfsLiteral::Negative(atom("ab", &[2])),
],
const_true,
),
];
let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
assert_eq!(result.truth_value(&atom("penguin", &[1])), TruthValue::True);
assert_eq!(result.truth_value(&atom("bird", &[1])), TruthValue::True);
assert_eq!(result.truth_value(&atom("ab", &[1])), TruthValue::True);
assert_eq!(result.truth_value(&atom("flies", &[1])), TruthValue::False);
assert_eq!(result.truth_value(&atom("eagle", &[2])), TruthValue::True);
assert_eq!(result.truth_value(&atom("bird", &[2])), TruthValue::True);
assert_eq!(result.truth_value(&atom("flies", &[2])), TruthValue::True);
}
#[test]
fn test_wfs_probabilistic_provenance_or() {
let mut pir = PirGraph::new();
let leaf_q = pir.lit(LeafId::new(1));
let leaf_r = pir.lit(LeafId::new(2));
let rules = vec![
WfsRule::new(prop("q"), vec![], leaf_q),
WfsRule::new(prop("r"), vec![], leaf_r),
WfsRule::new(
prop("p"),
vec![WfsLiteral::Positive(prop("q"))],
pir.const_true(),
),
WfsRule::new(
prop("p"),
vec![WfsLiteral::Positive(prop("r"))],
pir.const_true(),
),
];
let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
assert_eq!(result.truth_value(&prop("p")), TruthValue::True);
assert_eq!(result.truth_value(&prop("q")), TruthValue::True);
assert_eq!(result.truth_value(&prop("r")), TruthValue::True);
let p_prov = result.provenance(&prop("p")).unwrap();
assert!(p_prov.as_u32() > 0);
}
#[test]
fn test_wfs_stable_model_unique() {
let mut pir = PirGraph::new();
let const_true = pir.const_true();
let rules = vec![
WfsRule::new(prop("d"), vec![], const_true),
WfsRule::new(
prop("a"),
vec![
WfsLiteral::Negative(prop("b")),
WfsLiteral::Negative(prop("c")),
],
const_true,
),
WfsRule::new(
prop("b"),
vec![
WfsLiteral::Negative(prop("a")),
WfsLiteral::Negative(prop("c")),
],
const_true,
),
WfsRule::new(
prop("c"),
vec![
WfsLiteral::Negative(prop("a")),
WfsLiteral::Negative(prop("b")),
],
const_true,
),
];
let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
assert_eq!(result.truth_value(&prop("d")), TruthValue::True);
assert_eq!(result.truth_value(&prop("a")), TruthValue::Undefined);
assert_eq!(result.truth_value(&prop("b")), TruthValue::Undefined);
assert_eq!(result.truth_value(&prop("c")), TruthValue::Undefined);
}
#[test]
fn test_wfs_hamiltonian_cycle_like() {
let mut pir = PirGraph::new();
let const_true = pir.const_true();
let rules = vec![
WfsRule::new(atom("edge", &[1, 2]), vec![], const_true),
WfsRule::new(
atom("in", &[1, 2]),
vec![
WfsLiteral::Positive(atom("edge", &[1, 2])),
WfsLiteral::Negative(atom("out", &[1, 2])),
],
const_true,
),
WfsRule::new(
atom("out", &[1, 2]),
vec![
WfsLiteral::Positive(atom("edge", &[1, 2])),
WfsLiteral::Negative(atom("in", &[1, 2])),
],
const_true,
),
];
let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
assert_eq!(result.truth_value(&atom("edge", &[1, 2])), TruthValue::True);
assert_eq!(
result.truth_value(&atom("in", &[1, 2])),
TruthValue::Undefined
);
assert_eq!(
result.truth_value(&atom("out", &[1, 2])),
TruthValue::Undefined
);
}
#[test]
fn test_wfs_partial_grounding() {
let mut pir = PirGraph::new();
let const_true = pir.const_true();
let rules = vec![WfsRule::new(
prop("p"),
vec![WfsLiteral::Positive(prop("q"))],
const_true,
)];
let result = evaluate_wfs_rules(&rules, &mut pir, &WfsConfig::default()).unwrap();
assert_eq!(result.truth_value(&prop("p")), TruthValue::False);
assert_eq!(result.truth_value(&prop("q")), TruthValue::Undefined);
}
}