use std::collections::BTreeMap;
use xlog_core::ScalarType;
use xlog_logic::ast::{Atom, BodyLiteral, Rule, Term};
use xlog_logic::hypergraph::{
evaluate_fixpoint_typed, evaluate_scc_fixpoint_typed, infer_scc_predicate_schemas,
AppearanceOrder, Boundary, FixpointConfig, InferenceError, 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
}
#[test]
fn infer_propagates_u32_through_recursive_predicates() {
let even_rule = rule_with(
atom("even", vec![var("X"), var("Y")]),
vec![
pos("odd", vec![var("X"), var("Z")]),
pos("odd", vec![var("Z"), var("Y")]),
],
);
let odd_rule = rule_with(
atom("odd", 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![("even", vec![even_rule]), ("odd", vec![odd_rule])]);
let inferred = infer_scc_predicate_schemas(&rules, &store).expect("inference converges");
assert_eq!(
inferred.get("odd").map(|s| s.as_slice()),
Some([Some(ScalarType::U32), Some(ScalarType::U32)].as_slice())
);
assert_eq!(
inferred.get("even").map(|s| s.as_slice()),
Some([Some(ScalarType::U32), Some(ScalarType::U32)].as_slice())
);
}
#[test]
fn infer_propagates_unsupported_type_through_recursive_predicates() {
let even_rule = rule_with(
atom("even", vec![var("X"), var("Y")]),
vec![
pos("odd", vec![var("X"), var("Z")]),
pos("odd", vec![var("Z"), var("Y")]),
],
);
let odd_rule = rule_with(
atom("odd", vec![var("X"), var("Y")]),
vec![
pos("edge", vec![var("X"), var("M")]),
pos("edge", vec![var("M"), var("Y")]),
],
);
let edge_i64 = RefRelation {
schema: vec![ScalarType::I64, ScalarType::I64],
rows: vec![vec![RefValue::I64(1), RefValue::I64(2)]],
};
let store = store_with_one("edge", edge_i64);
let rules = rules_grouped(vec![("even", vec![even_rule]), ("odd", vec![odd_rule])]);
let inferred = infer_scc_predicate_schemas(&rules, &store).expect("inference converges");
assert_eq!(
inferred.get("odd").map(|s| s.as_slice()),
Some([Some(ScalarType::I64), Some(ScalarType::I64)].as_slice())
);
assert_eq!(
inferred.get("even").map(|s| s.as_slice()),
Some([Some(ScalarType::I64), Some(ScalarType::I64)].as_slice())
);
}
#[test]
fn infer_returns_none_for_cyclic_only_predicate() {
let r_a = rule_with(
atom("a", vec![var("X"), var("Y")]),
vec![
pos("b", vec![var("X"), var("Z")]),
pos("b", vec![var("Z"), var("Y")]),
],
);
let r_b = rule_with(
atom("b", vec![var("X"), var("Y")]),
vec![
pos("a", vec![var("X"), var("Z")]),
pos("a", vec![var("Z"), var("Y")]),
],
);
let store: RefRelationStore = BTreeMap::new();
let rules = rules_grouped(vec![("a", vec![r_a]), ("b", vec![r_b])]);
let inferred = infer_scc_predicate_schemas(&rules, &store).expect("inference converges");
assert_eq!(
inferred.get("a").map(|s| s.as_slice()),
Some([None, None].as_slice())
);
assert_eq!(
inferred.get("b").map(|s| s.as_slice()),
Some([None, None].as_slice())
);
}
#[test]
fn infer_detects_conflicting_predicate_column_types() {
let r_u32 = rule_with(
atom("p", vec![var("X"), var("Y")]),
vec![
pos("edge_u32", vec![var("X"), var("M")]),
pos("edge_u32", vec![var("M"), var("Y")]),
],
);
let r_sym = rule_with(
atom("p", vec![var("X"), var("Y")]),
vec![
pos("edge_sym", vec![var("X"), var("M")]),
pos("edge_sym", vec![var("M"), var("Y")]),
],
);
let mut store: RefRelationStore = BTreeMap::new();
store.insert("edge_u32".into(), u32_relation(&[&[1, 2]]));
store.insert(
"edge_sym".into(),
RefRelation {
schema: vec![ScalarType::Symbol, ScalarType::Symbol],
rows: vec![vec![
RefValue::Symbol("a".into()),
RefValue::Symbol("b".into()),
]],
},
);
let rules = rules_grouped(vec![("p", vec![r_u32, r_sym])]);
let err = infer_scc_predicate_schemas(&rules, &store).expect_err("conflict must fail");
match err {
InferenceError::ConflictingPredicateColumnType {
predicate,
column,
first_type,
second_type,
..
} => {
assert_eq!(predicate, "p");
assert_eq!(column, 0);
assert_eq!(first_type, ScalarType::U32);
assert_eq!(second_type, ScalarType::Symbol);
}
}
}
#[test]
fn infer_handles_single_predicate_self_recursion() {
let r_base = 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 store = store_with_one("edge", u32_relation(&[&[1, 2]]));
let rules = rules_grouped(vec![("reach", vec![r_base, r_step])]);
let inferred = infer_scc_predicate_schemas(&rules, &store).expect("converges");
assert_eq!(
inferred.get("reach").map(|s| s.as_slice()),
Some([Some(ScalarType::U32), Some(ScalarType::U32)].as_slice())
);
}
#[test]
fn scc_fixpoint_typed_rejects_recursive_only_unsupported_via_inference() {
let even_rule = rule_with(
atom("even", vec![var("X"), var("Y")]),
vec![
pos("odd", vec![var("X"), var("Z")]),
pos("odd", vec![var("Z"), var("Y")]),
],
);
let odd_rule = rule_with(
atom("odd", vec![var("X"), var("Y")]),
vec![
pos("edge", vec![var("X"), var("M")]),
pos("edge", vec![var("M"), var("Y")]),
],
);
let edge_i64 = RefRelation {
schema: vec![ScalarType::I64, ScalarType::I64],
rows: vec![vec![RefValue::I64(1), RefValue::I64(2)]],
};
let store = store_with_one("edge", edge_i64);
let rules = rules_grouped(vec![("even", vec![even_rule]), ("odd", vec![odd_rule])]);
let err =
evaluate_scc_fixpoint_typed(&rules, &store, &AppearanceOrder, &FixpointConfig::default())
.expect_err("recursive-only I64 join key must be rejected via inference");
match err {
SccFixpointError::RuleEval {
source: RefEvalError::Ineligible(bs),
..
} => {
assert!(
bs.iter().any(|b| matches!(
b,
Boundary::UnsupportedKeyType {
ty: ScalarType::I64,
..
}
)),
"expected UnsupportedKeyType I64, got {bs:?}"
);
}
other => panic!("expected RuleEval(Ineligible), got {other:?}"),
}
}
#[test]
fn scc_fixpoint_typed_evaluates_normally_for_supported_keys_via_inference() {
let even_rule = rule_with(
atom("even", vec![var("X"), var("Y")]),
vec![
pos("odd", vec![var("X"), var("Z")]),
pos("odd", vec![var("Z"), var("Y")]),
],
);
let odd_rule = rule_with(
atom("odd", vec![var("X"), var("Y")]),
vec![
pos("edge", vec![var("X"), var("M")]),
pos("edge", vec![var("M"), var("Y")]),
],
);
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", vec![even_rule]), ("odd", vec![odd_rule])]);
let result =
evaluate_scc_fixpoint_typed(&rules, &store, &AppearanceOrder, &FixpointConfig::default())
.expect("supported keys must converge");
assert!(result.contains_key("even"));
assert!(result.contains_key("odd"));
}
#[test]
fn fixpoint_typed_rejects_target_recursive_only_unsupported_via_inference() {
let r_base = 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 edge_i64 = RefRelation {
schema: vec![ScalarType::I64, ScalarType::I64],
rows: vec![vec![RefValue::I64(1), RefValue::I64(2)]],
};
let store = store_with_one("edge", edge_i64);
let rules = vec![r_base, r_step];
let err = evaluate_fixpoint_typed(
&rules,
&store,
"reach",
&AppearanceOrder,
&FixpointConfig::default(),
)
.expect_err("target schema with I64 must be rejected via inference");
match err {
FixpointError::RuleEval {
source: RefEvalError::Ineligible(bs),
..
} => {
assert!(
bs.iter().any(|b| matches!(
b,
Boundary::UnsupportedKeyType {
ty: ScalarType::I64,
..
}
)),
"expected UnsupportedKeyType I64, got {bs:?}"
);
}
other => panic!("expected RuleEval(Ineligible), got {other:?}"),
}
}
#[test]
fn cyclic_only_predicate_still_passes_typed_gate_locked_policy() {
let r_a = rule_with(
atom("a", vec![var("X"), var("Y")]),
vec![
pos("b", vec![var("X"), var("Z")]),
pos("b", vec![var("Z"), var("Y")]),
],
);
let r_b = rule_with(
atom("b", vec![var("X"), var("Y")]),
vec![
pos("a", vec![var("X"), var("Z")]),
pos("a", vec![var("Z"), var("Y")]),
],
);
let store: RefRelationStore = BTreeMap::new();
let rules = rules_grouped(vec![("a", vec![r_a]), ("b", vec![r_b])]);
let result = evaluate_scc_fixpoint_typed(
&rules,
&store,
&AppearanceOrder,
&FixpointConfig { max_iterations: 2 },
);
match result {
Ok(store) => {
assert!(store.contains_key("a"));
assert!(store.contains_key("b"));
assert_eq!(store.get("a").unwrap().rows.len(), 0);
assert_eq!(store.get("b").unwrap().rows.len(), 0);
}
Err(SccFixpointError::RuleEval {
source: RefEvalError::Ineligible(bs),
..
}) => {
panic!("cyclic-only predicate should NOT be rejected by typed gate, got: {bs:?}");
}
Err(other) => {
panic!("unexpected error: {other:?}");
}
}
}
use xlog_logic::hypergraph::FixpointError;