use std::collections::BTreeMap;
use xlog_core::ScalarType;
use xlog_logic::ast::{Atom, BodyLiteral, Rule, Term};
use xlog_logic::hypergraph::{
evaluate_scc_fixpoint, AppearanceOrder, Boundary, FixpointConfig, RefEvalError, RefRelation,
RefRelationStore, RefValue, SccFixpointError,
};
fn var(name: &str) -> Term {
Term::Variable(name.to_string())
}
fn atom(predicate: &str, terms: Vec<Term>) -> Atom {
Atom {
predicate: predicate.to_string(),
terms,
}
}
fn pos(predicate: &str, terms: Vec<Term>) -> BodyLiteral {
BodyLiteral::Positive(atom(predicate, terms))
}
fn rule_with(head: Atom, body: Vec<BodyLiteral>) -> Rule {
Rule { head, body }
}
fn u32_relation(rows: &[&[u32]]) -> RefRelation {
let arity = rows.first().map(|r| r.len()).unwrap_or(0);
RefRelation {
schema: vec![ScalarType::U32; arity],
rows: rows
.iter()
.map(|r| r.iter().map(|v| RefValue::U32(*v)).collect())
.collect(),
}
}
fn store_with_one(name: &str, rel: RefRelation) -> RefRelationStore {
let mut s: RefRelationStore = BTreeMap::new();
s.insert(name.to_string(), rel);
s
}
fn rules_grouped(pairs: Vec<(&str, Vec<Rule>)>) -> BTreeMap<String, Vec<Rule>> {
let mut m: BTreeMap<String, Vec<Rule>> = BTreeMap::new();
for (name, rs) in pairs {
m.insert(name.to_string(), rs);
}
m
}
fn pairs_from_rel(rel: &RefRelation) -> Vec<(u32, u32)> {
let mut rows = rel.rows.clone();
rows.sort();
rows.iter()
.map(|r| match (&r[0], &r[1]) {
(RefValue::U32(a), RefValue::U32(b)) => (*a, *b),
other => panic!("unexpected RefValue shape: {other:?}"),
})
.collect()
}
#[test]
fn mutual_recursion_two_predicates_reaches_fixpoint() {
let even_step = rule_with(
atom("even_path", vec![var("X"), var("Z")]),
vec![
pos("edge", vec![var("X"), var("Y")]),
pos("odd_path", vec![var("Y"), var("Z")]),
],
);
let even_seed = rule_with(
atom("even_path", vec![var("X"), var("Z")]),
vec![
pos("edge", vec![var("X"), var("M")]),
pos("edge", vec![var("M"), var("Z")]),
],
);
let odd_step = rule_with(
atom("odd_path", vec![var("X"), var("Z")]),
vec![
pos("edge", vec![var("X"), var("Y")]),
pos("even_path", vec![var("Y"), var("Z")]),
],
);
let odd_seed = rule_with(
atom("odd_path", vec![var("X"), var("Z")]),
vec![
pos("edge", vec![var("X"), var("M")]),
pos("edge", vec![var("M"), var("Y")]),
pos("edge", vec![var("Y"), var("Z")]),
],
);
let edges = u32_relation(&[&[1, 2], &[2, 3], &[3, 4], &[4, 5]]);
let store = store_with_one("edge", edges);
let rules = rules_grouped(vec![
("even_path", vec![even_seed, even_step]),
("odd_path", vec![odd_seed, odd_step]),
]);
let result =
evaluate_scc_fixpoint(&rules, &store, &AppearanceOrder, &FixpointConfig::default())
.expect("scc fixpoint converges");
let mut even_expected = vec![(1u32, 3u32), (2, 4), (3, 5), (1, 5)];
even_expected.sort();
let even_actual = pairs_from_rel(
result
.get("even_path")
.expect("even_path present in result"),
);
assert_eq!(even_actual, even_expected);
let mut odd_expected = vec![(1u32, 4u32), (2, 5)];
odd_expected.sort();
let odd_actual = pairs_from_rel(result.get("odd_path").expect("odd_path present in result"));
assert_eq!(odd_actual, odd_expected);
}
#[test]
fn same_generation_multi_rule_scc_reaches_expected_rows() {
let r_base = rule_with(
atom("sg", vec![var("X"), var("Y")]),
vec![
pos("parent", vec![var("X"), var("P")]),
pos("parent", vec![var("Y"), var("P")]),
],
);
let r_step = rule_with(
atom("sg", vec![var("X"), var("Y")]),
vec![
pos("parent", vec![var("X"), var("A")]),
pos("sg", vec![var("A"), var("B")]),
pos("parent", vec![var("Y"), var("B")]),
],
);
let parent = u32_relation(&[&[1, 10], &[2, 10], &[11, 1], &[12, 2]]);
let store = store_with_one("parent", parent);
let rules = rules_grouped(vec![("sg", vec![r_base, r_step])]);
let result =
evaluate_scc_fixpoint(&rules, &store, &AppearanceOrder, &FixpointConfig::default())
.expect("single-predicate scc converges");
let mut expected = vec![
(1u32, 1u32),
(1, 2),
(2, 1),
(2, 2),
(11, 11),
(11, 12),
(12, 11),
(12, 12),
];
expected.sort();
let actual = pairs_from_rel(result.get("sg").expect("sg present"));
assert_eq!(actual, expected);
}
#[test]
fn predicate_order_does_not_change_fixpoint() {
let r_a = rule_with(
atom("p", vec![var("X"), var("Z")]),
vec![
pos("edge", vec![var("X"), var("Y")]),
pos("q", vec![var("Y"), var("Z")]),
],
);
let r_b = rule_with(
atom("q", vec![var("X"), var("Z")]),
vec![
pos("edge", vec![var("X"), var("Y")]),
pos("p", vec![var("Y"), var("Z")]),
],
);
let r_a_seed = rule_with(
atom("p", vec![var("X"), var("Z")]),
vec![
pos("edge", vec![var("X"), var("M")]),
pos("edge", vec![var("M"), var("Z")]),
],
);
let r_b_seed = rule_with(
atom("q", vec![var("X"), var("Z")]),
vec![
pos("edge", vec![var("X"), var("M")]),
pos("edge", vec![var("M"), var("Z")]),
],
);
let edges = u32_relation(&[&[1, 2], &[2, 3], &[3, 4]]);
let store = store_with_one("edge", edges);
let mut rules_pq: BTreeMap<String, Vec<Rule>> = BTreeMap::new();
rules_pq.insert("p".into(), vec![r_a_seed.clone(), r_a.clone()]);
rules_pq.insert("q".into(), vec![r_b_seed.clone(), r_b.clone()]);
let mut rules_qp: BTreeMap<String, Vec<Rule>> = BTreeMap::new();
rules_qp.insert("q".into(), vec![r_b_seed, r_b]);
rules_qp.insert("p".into(), vec![r_a_seed, r_a]);
let res_pq = evaluate_scc_fixpoint(
&rules_pq,
&store,
&AppearanceOrder,
&FixpointConfig::default(),
)
.expect("pq order");
let res_qp = evaluate_scc_fixpoint(
&rules_qp,
&store,
&AppearanceOrder,
&FixpointConfig::default(),
)
.expect("qp order");
assert_eq!(
pairs_from_rel(res_pq.get("p").unwrap()),
pairs_from_rel(res_qp.get("p").unwrap())
);
assert_eq!(
pairs_from_rel(res_pq.get("q").unwrap()),
pairs_from_rel(res_qp.get("q").unwrap())
);
}
#[test]
fn rule_order_within_predicate_does_not_change_fixpoint() {
let r_seed = rule_with(
atom("reach", vec![var("X"), var("Y")]),
vec![
pos("edge", vec![var("X"), var("M")]),
pos("edge", vec![var("M"), var("Y")]),
],
);
let r_step = rule_with(
atom("reach", vec![var("X"), var("Z")]),
vec![
pos("edge", vec![var("X"), var("Y")]),
pos("reach", vec![var("Y"), var("Z")]),
],
);
let edges = u32_relation(&[&[1, 2], &[2, 3], &[3, 4], &[4, 5]]);
let store = store_with_one("edge", edges);
let rules_a = rules_grouped(vec![("reach", vec![r_seed.clone(), r_step.clone()])]);
let rules_b = rules_grouped(vec![("reach", vec![r_step, r_seed])]);
let res_a = evaluate_scc_fixpoint(
&rules_a,
&store,
&AppearanceOrder,
&FixpointConfig::default(),
)
.expect("a");
let res_b = evaluate_scc_fixpoint(
&rules_b,
&store,
&AppearanceOrder,
&FixpointConfig::default(),
)
.expect("b");
assert_eq!(
pairs_from_rel(res_a.get("reach").unwrap()),
pairs_from_rel(res_b.get("reach").unwrap())
);
}
#[test]
fn inconsistent_head_arity_in_scc_is_error() {
let r_a = rule_with(
atom("reach", vec![var("X"), var("Y")]),
vec![
pos("edge", vec![var("X"), var("M")]),
pos("edge", vec![var("M"), var("Y")]),
],
);
let r_b = rule_with(
atom("reach", vec![var("X"), var("Y"), var("Z")]),
vec![
pos("edge", vec![var("X"), var("Y")]),
pos("edge", vec![var("Y"), var("Z")]),
],
);
let store = store_with_one("edge", u32_relation(&[&[1, 2]]));
let rules = rules_grouped(vec![("reach", vec![r_a, r_b])]);
let err = evaluate_scc_fixpoint(&rules, &store, &AppearanceOrder, &FixpointConfig::default())
.expect_err("arity mismatch must fail");
match err {
SccFixpointError::HeadArityMismatch {
predicate,
rule_index,
observed_arity,
expected_arity,
} => {
assert_eq!(predicate, "reach");
assert_eq!(rule_index, 1);
assert_eq!(observed_arity, 3);
assert_eq!(expected_arity, 2);
}
other => panic!("expected HeadArityMismatch, got {other:?}"),
}
}
#[test]
fn inconsistent_head_type_in_scc_is_error() {
let r_u32 = rule_with(
atom("p", vec![var("X"), var("Y")]),
vec![
pos("edge", vec![var("X"), var("M")]),
pos("edge", vec![var("M"), var("Y")]),
],
);
let r_i64 = rule_with(
atom("p", vec![Term::Integer(99), Term::Integer(100)]),
vec![
pos("edge", vec![var("A"), var("B")]),
pos("edge", vec![var("B"), var("C")]),
],
);
let store = store_with_one("edge", u32_relation(&[&[1, 2], &[2, 3]]));
let rules = rules_grouped(vec![("p", vec![r_u32, r_i64])]);
let err = evaluate_scc_fixpoint(&rules, &store, &AppearanceOrder, &FixpointConfig::default())
.expect_err("type drift must fail");
match err {
SccFixpointError::InconsistentHeadValueTypes { predicate, .. } => {
assert_eq!(predicate, "p");
}
other => panic!("expected InconsistentHeadValueTypes, got {other:?}"),
}
}
#[test]
fn missing_edb_relation_in_scc_is_error() {
let r = rule_with(
atom("reach", vec![var("X"), var("Y")]),
vec![
pos("edge", vec![var("X"), var("M")]),
pos("edge", vec![var("M"), var("Y")]),
],
);
let store: RefRelationStore = BTreeMap::new();
let rules = rules_grouped(vec![("reach", vec![r])]);
let err = evaluate_scc_fixpoint(&rules, &store, &AppearanceOrder, &FixpointConfig::default())
.expect_err("missing EDB must fail");
match err {
SccFixpointError::RuleEval {
predicate,
rule_index,
source: RefEvalError::MissingRelation(name),
} => {
assert_eq!(predicate, "reach");
assert_eq!(rule_index, 0);
assert_eq!(name, "edge");
}
other => panic!("expected RuleEval::MissingRelation, got {other:?}"),
}
}
#[test]
fn max_iterations_exceeded_in_scc_is_error() {
let r_seed = rule_with(
atom("reach", vec![var("X"), var("Y")]),
vec![
pos("edge", vec![var("X"), var("M")]),
pos("edge", vec![var("M"), var("Y")]),
],
);
let r_step = rule_with(
atom("reach", vec![var("X"), var("Z")]),
vec![
pos("edge", vec![var("X"), var("Y")]),
pos("reach", vec![var("Y"), var("Z")]),
],
);
let edges = u32_relation(&[&[1, 2], &[2, 3], &[3, 4], &[4, 5], &[5, 6]]);
let store = store_with_one("edge", edges);
let rules = rules_grouped(vec![("reach", vec![r_seed, r_step])]);
let cfg = FixpointConfig { max_iterations: 1 };
let err = evaluate_scc_fixpoint(&rules, &store, &AppearanceOrder, &cfg)
.expect_err("must exceed iteration cap");
match err {
SccFixpointError::MaxIterationsExceeded { limit, .. } => {
assert_eq!(limit, 1);
}
other => panic!("expected MaxIterationsExceeded, got {other:?}"),
}
}
#[test]
fn ineligible_rule_in_scc_is_error() {
let r_bad = rule_with(
atom("reach", vec![var("X"), var("Y")]),
vec![pos("edge", vec![var("X"), var("Y")])],
);
let store = store_with_one("edge", u32_relation(&[&[1, 2]]));
let rules = rules_grouped(vec![("reach", vec![r_bad])]);
let err = evaluate_scc_fixpoint(&rules, &store, &AppearanceOrder, &FixpointConfig::default())
.expect_err("ineligible rule must fail");
match err {
SccFixpointError::RuleEval {
predicate,
rule_index,
source: RefEvalError::Ineligible(bs),
} => {
assert_eq!(predicate, "reach");
assert_eq!(rule_index, 0);
assert!(bs.contains(&Boundary::InsufficientPositiveAtoms { positive_count: 1 }));
}
other => panic!("expected RuleEval::Ineligible, got {other:?}"),
}
}
#[test]
fn rule_head_predicate_mismatch_is_error() {
let r_misfiled = rule_with(
atom("sg", vec![var("X"), var("Y")]),
vec![
pos("edge", vec![var("X"), var("M")]),
pos("edge", vec![var("M"), var("Y")]),
],
);
let store = store_with_one("edge", u32_relation(&[&[1, 2]]));
let rules = rules_grouped(vec![("reach", vec![r_misfiled])]);
let err = evaluate_scc_fixpoint(&rules, &store, &AppearanceOrder, &FixpointConfig::default())
.expect_err("misgrouped rule must fail");
match err {
SccFixpointError::RuleHeadPredicateMismatch {
group_key,
rule_index,
observed,
} => {
assert_eq!(group_key, "reach");
assert_eq!(rule_index, 0);
assert_eq!(observed, "sg");
}
other => panic!("expected RuleHeadPredicateMismatch, got {other:?}"),
}
}
#[test]
fn scc_predicate_in_base_relations_is_error() {
let r = rule_with(
atom("reach", vec![var("X"), var("Y")]),
vec![
pos("edge", vec![var("X"), var("M")]),
pos("edge", vec![var("M"), var("Y")]),
],
);
let mut store: RefRelationStore = BTreeMap::new();
store.insert("edge".into(), u32_relation(&[&[1, 2]]));
store.insert("reach".into(), u32_relation(&[&[42, 42]]));
let rules = rules_grouped(vec![("reach", vec![r])]);
let err = evaluate_scc_fixpoint(&rules, &store, &AppearanceOrder, &FixpointConfig::default())
.expect_err("shadowed SCC predicate must fail");
match err {
SccFixpointError::PredicateInBaseRelations { name } => {
assert_eq!(name, "reach");
}
other => panic!("expected PredicateInBaseRelations, got {other:?}"),
}
}