use std::collections::BTreeMap;
use xlog_core::ScalarType;
use xlog_logic::ast::{Atom, BodyLiteral, CompOp, Comparison, Rule, Term};
use xlog_logic::hypergraph::{
analyze_typed, evaluate_rule, AppearanceOrder, Boundary, Eligibility, ExecutorContext,
HypergraphRule, RefEvalError, RefRelation, RefRelationStore, RefValue, VariableOrder, VertexId,
};
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 cmp(left: Term, op: CompOp, right: Term) -> BodyLiteral {
BodyLiteral::Comparison(Comparison { left, op, right })
}
fn rule_with(head: Atom, body: Vec<BodyLiteral>) -> Rule {
Rule { head, body }
}
fn int(n: i64) -> Term {
Term::Integer(n)
}
fn anon() -> Term {
Term::Anonymous
}
#[test]
fn unsupported_key_type_boundary_emitted_by_typed_analyzer() {
let r = rule_with(
atom("p", vec![var("X"), var("Z")]),
vec![
pos("e", vec![var("X"), var("Y")]),
pos("e", vec![var("Y"), var("Z")]),
],
);
let hg = HypergraphRule::from_rule(&r);
let mut types: BTreeMap<String, ScalarType> = BTreeMap::new();
types.insert("X".to_string(), ScalarType::U32);
types.insert("Y".to_string(), ScalarType::F64);
types.insert("Z".to_string(), ScalarType::U32);
let v = analyze_typed(&hg, &types, ExecutorContext::HashFallback);
let bs = v.boundaries();
assert!(
bs.iter().any(|b| matches!(
b,
Boundary::UnsupportedKeyType { var, ty: ScalarType::F64 } if var == "Y"
)),
"expected UnsupportedKeyType for Y:F64, got {bs:?}"
);
assert_eq!(bs.len(), 1, "expected exactly one boundary, got {bs:?}");
assert!(matches!(v, Eligibility::Ineligible(_)));
}
#[test]
fn typed_analyzer_eligible_when_all_join_keys_are_supported_types() {
let r = rule_with(
atom("p", vec![var("X"), var("Z")]),
vec![
pos("e", vec![var("X"), var("Y")]),
pos("e", vec![var("Y"), var("Z")]),
],
);
let hg = HypergraphRule::from_rule(&r);
let mut types: BTreeMap<String, ScalarType> = BTreeMap::new();
types.insert("X".to_string(), ScalarType::U32);
types.insert("Y".to_string(), ScalarType::U32);
types.insert("Z".to_string(), ScalarType::U32);
let v = analyze_typed(&hg, &types, ExecutorContext::HashFallback);
assert_eq!(v, Eligibility::Eligible);
}
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
}
#[test]
fn triangle_reference_evaluator_returns_expected_rows() {
let r = rule_with(
atom("tri", vec![var("X"), var("Y"), var("Z")]),
vec![
pos("e", vec![var("X"), var("Y")]),
pos("e", vec![var("Y"), var("Z")]),
pos("e", vec![var("Z"), var("X")]),
],
);
let edges = u32_relation(&[&[1, 2], &[2, 3], &[3, 1], &[1, 3], &[4, 5]]);
let store = store_with_one("e", edges);
let result = evaluate_rule(&r, &store, &AppearanceOrder).expect("evaluate triangle");
let expected: Vec<Vec<RefValue>> = vec![
vec![RefValue::U32(1), RefValue::U32(2), RefValue::U32(3)],
vec![RefValue::U32(2), RefValue::U32(3), RefValue::U32(1)],
vec![RefValue::U32(3), RefValue::U32(1), RefValue::U32(2)],
];
let mut expected_sorted = expected;
expected_sorted.sort();
assert_eq!(result, expected_sorted);
}
#[test]
fn same_generation_shape_one_step_matches_expected_rows() {
let r = 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 parent = u32_relation(&[
&[1, 100], &[2, 100], &[3, 200], &[4, 200], ]);
let store = store_with_one("parent", parent);
let result = evaluate_rule(&r, &store, &AppearanceOrder).expect("evaluate sg");
let mut expected = vec![
vec![RefValue::U32(1), RefValue::U32(1)],
vec![RefValue::U32(1), RefValue::U32(2)],
vec![RefValue::U32(2), RefValue::U32(1)],
vec![RefValue::U32(2), RefValue::U32(2)],
vec![RefValue::U32(3), RefValue::U32(3)],
vec![RefValue::U32(3), RefValue::U32(4)],
vec![RefValue::U32(4), RefValue::U32(3)],
vec![RefValue::U32(4), RefValue::U32(4)],
];
expected.sort();
assert_eq!(result, expected);
}
#[test]
fn skewed_multiway_deduplicates_set_output() {
let r = rule_with(
atom("p", vec![var("Z")]),
vec![
pos("e", vec![var("X"), var("Z")]),
pos("e", vec![var("Y"), var("Z")]),
pos("e", vec![var("W"), var("Z")]),
],
);
let edges = u32_relation(&[&[1, 99], &[2, 99], &[3, 99], &[4, 99], &[5, 99], &[7, 42]]);
let store = store_with_one("e", edges);
let result = evaluate_rule(&r, &store, &AppearanceOrder).expect("evaluate skewed");
let expected = vec![vec![RefValue::U32(42)], vec![RefValue::U32(99)]];
assert_eq!(result, expected);
}
#[test]
fn comparison_filter_is_applied_after_join() {
let r = rule_with(
atom("p", vec![var("X"), var("Z")]),
vec![
pos("e", vec![var("X"), var("Y")]),
pos("e", vec![var("Y"), var("Z")]),
cmp(var("Y"), CompOp::Lt, int(3)),
],
);
let edges = u32_relation(&[&[1, 1], &[1, 2], &[2, 3], &[3, 4], &[4, 5]]);
let store = store_with_one("e", edges);
let result = evaluate_rule(&r, &store, &AppearanceOrder).expect("evaluate filter");
let mut expected = vec![
vec![RefValue::U32(1), RefValue::U32(1)],
vec![RefValue::U32(1), RefValue::U32(2)],
vec![RefValue::U32(1), RefValue::U32(3)],
];
expected.sort();
assert_eq!(result, expected);
}
#[test]
fn constants_and_anonymous_wildcards_match_correctly() {
let r = rule_with(
atom("p", vec![var("X")]),
vec![
pos("e", vec![var("X"), int(42), anon()]),
pos("e", vec![var("X"), anon(), anon()]),
],
);
let edges = RefRelation {
schema: vec![ScalarType::U32, ScalarType::U32, ScalarType::U32],
rows: vec![
vec![RefValue::U32(1), RefValue::U32(42), RefValue::U32(100)],
vec![RefValue::U32(2), RefValue::U32(99), RefValue::U32(200)],
vec![RefValue::U32(3), RefValue::U32(42), RefValue::U32(300)],
],
};
let store = store_with_one("e", edges);
let result = evaluate_rule(&r, &store, &AppearanceOrder).expect("evaluate const+wildcard");
let expected = vec![vec![RefValue::U32(1)], vec![RefValue::U32(3)]];
assert_eq!(result, expected);
}
#[test]
fn ineligible_rule_is_rejected_by_reference_evaluator() {
let r = rule_with(
atom("p", vec![var("X")]),
vec![
pos("e", vec![var("X"), var("Y")]),
BodyLiteral::Negated(atom("f", vec![var("X"), var("Y")])),
],
);
let store: RefRelationStore = BTreeMap::new();
let err = evaluate_rule(&r, &store, &AppearanceOrder).expect_err("must reject ineligible");
match err {
RefEvalError::Ineligible(bs) => {
assert!(bs.contains(&Boundary::BodyNegation));
}
other => panic!("expected Ineligible, got {other:?}"),
}
}
#[test]
fn symbol_join_keys_evaluate_correctly() {
let r = 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 parent = RefRelation {
schema: vec![ScalarType::Symbol, ScalarType::Symbol],
rows: vec![
vec![
RefValue::Symbol("alice".to_string()),
RefValue::Symbol("mom".to_string()),
],
vec![
RefValue::Symbol("bob".to_string()),
RefValue::Symbol("mom".to_string()),
],
vec![
RefValue::Symbol("carol".to_string()),
RefValue::Symbol("dad".to_string()),
],
],
};
let store = store_with_one("parent", parent);
let result = evaluate_rule(&r, &store, &AppearanceOrder).expect("evaluate symbol sg");
let mut expected = vec![
vec![
RefValue::Symbol("alice".to_string()),
RefValue::Symbol("alice".to_string()),
],
vec![
RefValue::Symbol("alice".to_string()),
RefValue::Symbol("bob".to_string()),
],
vec![
RefValue::Symbol("bob".to_string()),
RefValue::Symbol("alice".to_string()),
],
vec![
RefValue::Symbol("bob".to_string()),
RefValue::Symbol("bob".to_string()),
],
vec![
RefValue::Symbol("carol".to_string()),
RefValue::Symbol("carol".to_string()),
],
];
expected.sort();
assert_eq!(result, expected);
}
#[test]
fn variable_variable_comparison_is_evaluated() {
let r = rule_with(
atom("p", vec![var("X"), var("Z")]),
vec![
pos("e", vec![var("X"), var("Y")]),
pos("e", vec![var("Y"), var("Z")]),
cmp(var("X"), CompOp::Lt, var("Z")),
],
);
let edges = u32_relation(&[&[1, 2], &[2, 3], &[3, 1]]);
let store = store_with_one("e", edges);
let result = evaluate_rule(&r, &store, &AppearanceOrder).expect("var-var cmp");
let expected = vec![vec![RefValue::U32(1), RefValue::U32(3)]];
assert_eq!(result, expected);
}
#[test]
fn empty_result_is_ok_and_empty_not_an_error() {
let r = rule_with(
atom("tri", vec![var("X"), var("Y"), var("Z")]),
vec![
pos("e", vec![var("X"), var("Y")]),
pos("e", vec![var("Y"), var("Z")]),
pos("e", vec![var("Z"), var("X")]),
],
);
let edges = u32_relation(&[&[1, 2], &[2, 3], &[3, 4]]);
let store = store_with_one("e", edges);
let result = evaluate_rule(&r, &store, &AppearanceOrder);
assert!(
result.is_ok(),
"no satisfying assignment must be Ok, not Err"
);
assert!(result.unwrap().is_empty(), "no triangles → empty result");
}
#[test]
fn appearance_order_does_not_change_result_set() {
struct ReverseOrder;
impl VariableOrder for ReverseOrder {
fn name(&self) -> &'static str {
"reverse"
}
fn order(&self, hg: &HypergraphRule) -> Vec<VertexId> {
let mut v: Vec<VertexId> = hg.vertex_ids().collect();
v.reverse();
v
}
}
let r = rule_with(
atom("tri", vec![var("X"), var("Y"), var("Z")]),
vec![
pos("e", vec![var("X"), var("Y")]),
pos("e", vec![var("Y"), var("Z")]),
pos("e", vec![var("Z"), var("X")]),
],
);
let edges = u32_relation(&[&[1, 2], &[2, 3], &[3, 1], &[1, 3], &[4, 5]]);
let store = store_with_one("e", edges);
let r1 = evaluate_rule(&r, &store, &AppearanceOrder).expect("appearance");
let r2 = evaluate_rule(&r, &store, &ReverseOrder).expect("reverse");
assert_eq!(r1, r2, "variable order must not affect result set");
}
#[test]
fn typed_analyzer_ignores_unsupported_types_on_projection_only_variables() {
let r = rule_with(
atom("p", vec![var("A"), var("B"), var("C"), var("D"), var("E")]),
vec![
pos("r1", vec![var("A"), var("B"), var("C")]),
pos("r2", vec![var("C"), var("D"), var("E")]),
],
);
let hg = HypergraphRule::from_rule(&r);
let mut types: BTreeMap<String, ScalarType> = BTreeMap::new();
types.insert("A".to_string(), ScalarType::F64);
types.insert("B".to_string(), ScalarType::F64);
types.insert("C".to_string(), ScalarType::U32);
types.insert("D".to_string(), ScalarType::F64);
types.insert("E".to_string(), ScalarType::F64);
let v = analyze_typed(&hg, &types, ExecutorContext::HashFallback);
assert_eq!(v, Eligibility::Eligible);
}
#[test]
fn relation_arity_mismatch_returns_dedicated_error() {
let r = rule_with(
atom("p", vec![var("X"), var("Z")]),
vec![
pos("e", vec![var("X"), var("Y")]),
pos("e", vec![var("Y"), var("Z")]),
],
);
let edges = RefRelation {
schema: vec![ScalarType::U32, ScalarType::U32, ScalarType::U32],
rows: vec![],
};
let store = store_with_one("e", edges);
let err = evaluate_rule(&r, &store, &AppearanceOrder).expect_err("must reject arity mismatch");
match err {
RefEvalError::RelationArityMismatch {
predicate,
atom_arity,
relation_arity,
} => {
assert_eq!(predicate, "e");
assert_eq!(atom_arity, 2);
assert_eq!(relation_arity, 3);
}
other => panic!("expected RelationArityMismatch, got {other:?}"),
}
}
#[test]
fn relation_row_arity_mismatch_returns_dedicated_error() {
let r = rule_with(
atom("p", vec![var("X"), var("Z")]),
vec![
pos("e", vec![var("X"), var("Y")]),
pos("e", vec![var("Y"), var("Z")]),
],
);
let edges = RefRelation {
schema: vec![ScalarType::U32, ScalarType::U32],
rows: vec![
vec![RefValue::U32(1), RefValue::U32(2)],
vec![RefValue::U32(3)], ],
};
let store = store_with_one("e", edges);
let err = evaluate_rule(&r, &store, &AppearanceOrder).expect_err("must reject row arity");
match err {
RefEvalError::RelationRowArityMismatch {
predicate,
row_index,
row_len,
schema_len,
} => {
assert_eq!(predicate, "e");
assert_eq!(row_index, 1);
assert_eq!(row_len, 1);
assert_eq!(schema_len, 2);
}
other => panic!("expected RelationRowArityMismatch, got {other:?}"),
}
}
#[test]
fn relation_value_type_mismatch_returns_dedicated_error() {
let r = rule_with(
atom("p", vec![var("X"), var("Z")]),
vec![
pos("e", vec![var("X"), var("Y")]),
pos("e", vec![var("Y"), var("Z")]),
],
);
let edges = RefRelation {
schema: vec![ScalarType::U32, ScalarType::U32],
rows: vec![
vec![RefValue::U32(1), RefValue::U32(2)],
vec![RefValue::U32(3), RefValue::U64(4)],
],
};
let store = store_with_one("e", edges);
let err = evaluate_rule(&r, &store, &AppearanceOrder).expect_err("must reject value type");
match err {
RefEvalError::RelationValueTypeMismatch {
predicate,
row_index,
column,
expected,
..
} => {
assert_eq!(predicate, "e");
assert_eq!(row_index, 1);
assert_eq!(column, 1);
assert_eq!(expected, ScalarType::U32);
}
other => panic!("expected RelationValueTypeMismatch, got {other:?}"),
}
}