use std::collections::BTreeSet;
use super::aggregate::{AggregateBody, Aggregator};
use super::atom::Atom;
use super::bind::BindExpr;
use super::equality_mode::EqualityMode;
use super::error::ProgramValidationError;
use super::filter::BuiltinFilter;
use super::literal::Literal;
use super::program::Program;
use super::rule::Rule;
use super::symbols::{ConstantSymbol, PredicateSymbol, VariableSymbol};
use super::term::{MAX_TRIPLE_TERM_NESTING, Term};
fn var(name: &str) -> Term {
Term::Variable(VariableSymbol::new(name))
}
fn constant(name: &str) -> Term {
Term::Constant(ConstantSymbol::new(name))
}
fn atom(predicate: &str, terms: Vec<Term>) -> Atom {
Atom {
predicate: PredicateSymbol::new(predicate),
terms,
}
}
#[test]
fn safe_rule_passes_validation() {
let program = Program {
rules: vec![Rule::positive(
atom("parent", vec![var("x"), var("y")]),
vec![atom("has_parent", vec![var("x"), var("y")])],
)],
};
assert!(program.validate().is_ok());
}
#[test]
fn unsafe_rule_fails_validation() {
let program = Program {
rules: vec![Rule::positive(
atom("parent", vec![var("x"), var("y")]),
vec![atom("has_parent", vec![var("x"), constant("alice")])],
)],
};
let err = program.validate().expect_err("program should be invalid");
assert_eq!(
err,
ProgramValidationError::UnsafeRule {
rule_index: 0,
variables: vec![VariableSymbol::new("y")],
}
);
}
#[test]
fn safe_negation_passes_validation() {
let program = Program {
rules: vec![Rule::new(
atom("bachelor", vec![var("x")]),
vec![
Literal::Positive(atom("person", vec![var("x")])),
Literal::Negative(atom("married", vec![var("x")])),
],
)],
};
assert!(program.validate().is_ok());
}
#[test]
fn unsafe_negation_fails_validation() {
let program = Program {
rules: vec![Rule::new(
atom("bad", vec![var("x")]),
vec![
Literal::Positive(atom("person", vec![var("x")])),
Literal::Negative(atom("knows", vec![var("x"), var("y")])),
],
)],
};
let err = program.validate().expect_err("program should be invalid");
assert_eq!(
err,
ProgramValidationError::UnsafeNegation {
rule_index: 0,
variables: vec![VariableSymbol::new("y")],
}
);
}
#[test]
fn safe_filter_passes_validation() {
let program = Program {
rules: vec![Rule::new(
atom("adult", vec![var("p")]),
vec![
Literal::Positive(atom("person", vec![var("p"), var("age")])),
Literal::Filter(BuiltinFilter::Ge(var("age"), constant("18"))),
],
)],
};
assert!(program.validate().is_ok());
}
#[test]
fn unsafe_filter_fails_validation() {
let program = Program {
rules: vec![Rule::new(
atom("bad", vec![var("p")]),
vec![
Literal::Positive(atom("person", vec![var("p")])),
Literal::Filter(BuiltinFilter::Eq(var("ghost"), constant("x"))),
],
)],
};
let err = program.validate().expect_err("program should be invalid");
assert_eq!(
err,
ProgramValidationError::UnsafeFilter {
rule_index: 0,
variables: vec![VariableSymbol::new("ghost")],
}
);
}
#[test]
fn filter_does_not_contribute_to_stratification_dependencies() {
let program = Program {
rules: vec![Rule::new(
atom("p", vec![var("x")]),
vec![
Literal::Positive(atom("q", vec![var("x")])),
Literal::Filter(BuiltinFilter::Ne(var("x"), constant("skip"))),
],
)],
};
assert!(program.validate().is_ok());
let strat = program.stratify().expect("stratifiable");
assert!(strat.stratum_of(&PredicateSymbol::new("p")).is_some());
}
#[test]
fn safe_bind_passes_validation() {
let program = Program {
rules: vec![Rule::new(
atom("tall", vec![var("p")]),
vec![
Literal::Positive(atom("person", vec![var("p"), var("h")])),
Literal::Bind {
var: VariableSymbol::new("over"),
expr: BindExpr::Sub(
Box::new(BindExpr::Term(var("h"))),
Box::new(BindExpr::Term(constant("180"))),
),
},
Literal::Filter(BuiltinFilter::Gt(var("over"), constant("0"))),
],
)],
};
assert!(program.validate().is_ok());
}
#[test]
fn unsafe_bind_expr_unbound_variable_fails_validation() {
let program = Program {
rules: vec![Rule::new(
atom("bad", vec![var("p")]),
vec![
Literal::Positive(atom("person", vec![var("p")])),
Literal::Bind {
var: VariableSymbol::new("out"),
expr: BindExpr::Term(var("ghost")),
},
],
)],
};
let err = program.validate().expect_err("program should be invalid");
assert_eq!(
err,
ProgramValidationError::UnsafeBind {
rule_index: 0,
variables: vec![VariableSymbol::new("ghost")],
}
);
}
#[test]
fn unsafe_bind_lhs_collision_fails_validation() {
let program = Program {
rules: vec![Rule::new(
atom("bad", vec![var("p")]),
vec![
Literal::Positive(atom("person", vec![var("p"), var("h")])),
Literal::Bind {
var: VariableSymbol::new("h"),
expr: BindExpr::Term(var("h")),
},
],
)],
};
let err = program.validate().expect_err("program should be invalid");
assert_eq!(
err,
ProgramValidationError::UnsafeBind {
rule_index: 0,
variables: vec![VariableSymbol::new("h")],
}
);
}
#[test]
fn bind_output_satisfies_head_safety() {
let program = Program {
rules: vec![Rule::new(
atom("greeted", vec![var("msg")]),
vec![
Literal::Positive(atom("person", vec![var("p")])),
Literal::Bind {
var: VariableSymbol::new("msg"),
expr: BindExpr::Term(var("p")),
},
],
)],
};
assert!(program.validate().is_ok());
}
#[test]
fn recursion_through_negation_is_rejected() {
let program = Program {
rules: vec![Rule::new(
atom("p", vec![var("x")]),
vec![
Literal::Positive(atom("q", vec![var("x")])),
Literal::Negative(atom("p", vec![var("x")])),
],
)],
};
let err = program.validate().expect_err("program should be invalid");
assert!(matches!(
err,
ProgramValidationError::UnstratifiableNegation { .. }
));
}
fn triple(subject: Term, predicate: &str, object: Term) -> Term {
Term::TripleTerm(Box::new(atom(predicate, vec![subject, object])))
}
#[test]
fn triple_term_in_body_recognises_nested_variables() {
let program = Program {
rules: vec![Rule::positive(
atom("annotate", vec![var("x"), var("author")]),
vec![atom(
"says",
vec![triple(var("x"), "knows", var("y")), var("author")],
)],
)],
};
assert!(
program.validate().is_ok(),
"expected nested ?x to count as bound: {:?}",
program.validate(),
);
}
#[test]
fn triple_term_in_head_with_bound_nested_variables_is_safe() {
let program = Program {
rules: vec![Rule::positive(
atom(
"believes",
vec![var("a"), triple(var("x"), "knows", var("y"))],
),
vec![atom("says", vec![var("a"), var("x"), var("y")])],
)],
};
assert!(program.validate().is_ok());
}
#[test]
fn triple_term_in_head_with_unbound_nested_variable_is_unsafe() {
let program = Program {
rules: vec![Rule::positive(
atom(
"believes",
vec![var("a"), triple(var("x"), "knows", var("y"))],
),
vec![atom("says", vec![var("a")])],
)],
};
let err = program.validate().expect_err("program should be unsafe");
let ProgramValidationError::UnsafeRule {
rule_index,
variables,
} = err
else {
panic!("expected UnsafeRule, got {err:?}");
};
assert_eq!(rule_index, 0);
let names: BTreeSet<String> = variables.iter().map(|v| v.as_str().to_owned()).collect();
let expected: BTreeSet<String> = ["x", "y"].iter().map(|s| (*s).to_owned()).collect();
assert_eq!(names, expected);
}
#[test]
fn negated_triple_term_with_unbound_nested_variable_is_unsafe() {
let program = Program {
rules: vec![Rule::new(
atom("ok", vec![var("a")]),
vec![
Literal::Positive(atom("says", vec![var("a")])),
Literal::Negative(atom("annotated", vec![triple(var("a"), "knows", var("y"))])),
],
)],
};
let err = program.validate().expect_err("program should be unsafe");
let ProgramValidationError::UnsafeNegation {
rule_index,
variables,
} = err
else {
panic!("expected UnsafeNegation, got {err:?}");
};
assert_eq!(rule_index, 0);
let names: BTreeSet<String> = variables.iter().map(|v| v.as_str().to_owned()).collect();
assert_eq!(
names,
["y".to_owned()].iter().cloned().collect::<BTreeSet<_>>(),
);
}
#[test]
fn excessive_triple_term_nesting_is_rejected() {
let leaf = || constant("s");
let mut nested = triple(leaf(), "p", leaf());
for _ in 0..4 {
nested = triple(leaf(), "p", nested);
}
let program = Program {
rules: vec![Rule::positive(
atom("out", vec![nested]),
vec![atom("base", vec![])],
)],
};
let err = program.validate().expect_err("nesting must be rejected");
let ProgramValidationError::TripleTermNestingExceeded {
rule_index,
depth,
limit,
} = err
else {
panic!("expected TripleTermNestingExceeded, got {err:?}");
};
assert_eq!(rule_index, 0);
assert_eq!(limit, MAX_TRIPLE_TERM_NESTING);
assert!(depth > limit, "depth {depth} must exceed limit {limit}");
}
#[test]
fn triple_term_displays_with_rdf_star_syntax() {
let t = triple(constant("alice"), "knows", constant("bob"));
assert_eq!(format!("{t}"), "<< alice knows bob >>");
}
fn agg_body(
result: &str,
aggregator: Aggregator,
input: &str,
group_by: Vec<&str>,
inner_atoms: Vec<Atom>,
) -> AggregateBody {
agg_body_with_inner(
result,
aggregator,
input,
group_by,
inner_atoms.into_iter().map(Literal::Positive).collect(),
)
}
fn agg_body_with_inner(
result: &str,
aggregator: Aggregator,
input: &str,
group_by: Vec<&str>,
inner_body: Vec<Literal>,
) -> AggregateBody {
AggregateBody {
result_var: VariableSymbol::new(result),
aggregator,
input_var: VariableSymbol::new(input),
group_by: group_by.into_iter().map(VariableSymbol::new).collect(),
inner_body,
}
}
#[test]
fn safe_aggregate_passes_validation() {
let program = Program {
rules: vec![Rule::new(
atom("total", vec![var("dept"), var("total")]),
vec![Literal::Aggregate(agg_body(
"total",
Aggregator::Sum,
"sal",
vec!["dept"],
vec![atom("salary", vec![var("e"), var("dept"), var("sal")])],
))],
)],
};
assert!(program.validate().is_ok());
}
#[test]
fn unsafe_aggregate_unbound_input_fails_validation() {
let program = Program {
rules: vec![Rule::new(
atom("total", vec![var("dept"), var("total")]),
vec![Literal::Aggregate(agg_body(
"total",
Aggregator::Sum,
"missing_sal", vec!["dept"],
vec![atom("salary", vec![var("e"), var("dept"), var("sal")])],
))],
)],
};
let err = program.validate().unwrap_err();
let ProgramValidationError::UnsafeAggregate {
rule_index,
variables,
} = err
else {
panic!("expected UnsafeAggregate, got {err:?}");
};
assert_eq!(rule_index, 0);
assert!(
variables.contains(&VariableSymbol::new("missing_sal")),
"expected `missing_sal` in offending list, got {variables:?}"
);
}
#[test]
fn unsafe_aggregate_unbound_group_key_fails_validation() {
let program = Program {
rules: vec![Rule::new(
atom("total", vec![var("region"), var("total")]),
vec![Literal::Aggregate(agg_body(
"total",
Aggregator::Sum,
"sal",
vec!["region"], vec![atom("salary", vec![var("e"), var("dept"), var("sal")])],
))],
)],
};
let err = program.validate().unwrap_err();
let ProgramValidationError::UnsafeAggregate { variables, .. } = err else {
panic!("expected UnsafeAggregate, got {err:?}");
};
assert!(
variables.contains(&VariableSymbol::new("region")),
"expected `region` in offending list, got {variables:?}"
);
}
#[test]
fn unsafe_aggregate_result_var_collides_fails_validation() {
let program = Program {
rules: vec![Rule::new(
atom("h", vec![var("dept"), var("total")]),
vec![
Literal::Positive(atom("anchor", vec![var("total")])),
Literal::Aggregate(agg_body(
"total", Aggregator::Sum,
"sal",
vec!["dept"],
vec![atom("salary", vec![var("e"), var("dept"), var("sal")])],
)),
],
)],
};
let err = program.validate().unwrap_err();
let ProgramValidationError::UnsafeAggregate { variables, .. } = err else {
panic!("expected UnsafeAggregate, got {err:?}");
};
assert!(
variables.contains(&VariableSymbol::new("total")),
"expected `total` in offending list, got {variables:?}"
);
}
#[test]
fn aggregate_exports_satisfy_head_safety() {
let program = Program {
rules: vec![Rule::new(
atom("total", vec![var("dept"), var("total")]),
vec![Literal::Aggregate(agg_body(
"total",
Aggregator::Count,
"sal",
vec!["dept"],
vec![atom("salary", vec![var("e"), var("dept"), var("sal")])],
))],
)],
};
assert!(program.validate().is_ok());
}
#[test]
fn aggregate_inner_only_variable_does_not_leak_to_head() {
let program = Program {
rules: vec![Rule::new(
atom("h", vec![var("e"), var("total")]),
vec![Literal::Aggregate(agg_body(
"total",
Aggregator::Count,
"sal",
vec!["dept"],
vec![atom("salary", vec![var("e"), var("dept"), var("sal")])],
))],
)],
};
let err = program.validate().unwrap_err();
let ProgramValidationError::UnsafeRule { variables, .. } = err else {
panic!("expected UnsafeRule, got {err:?}");
};
assert!(
variables.contains(&VariableSymbol::new("e")),
"inner-only `e` must not satisfy head safety; got {variables:?}"
);
}
#[test]
fn recursion_through_aggregate_is_rejected() {
let program = Program {
rules: vec![Rule::new(
atom("r", vec![var("d"), var("t")]),
vec![Literal::Aggregate(agg_body(
"t",
Aggregator::Count,
"sal",
vec!["d"],
vec![atom("r", vec![var("d"), var("sal")])],
))],
)],
};
let err = program.validate().unwrap_err();
let ProgramValidationError::UnstratifiableNegation { predicates } = err else {
panic!("expected UnstratifiableNegation, got {err:?}");
};
assert!(
predicates.iter().any(|p| p.as_str() == "r"),
"expected `r` in unstratifiable cycle, got {predicates:?}"
);
}
#[test]
fn safe_aggregate_inner_negation_passes_validation() {
let program = Program {
rules: vec![Rule::new(
atom("total_unflagged", vec![var("dept"), var("total")]),
vec![Literal::Aggregate(agg_body_with_inner(
"total",
Aggregator::Sum,
"sal",
vec!["dept"],
vec![
Literal::Positive(atom("salary", vec![var("e"), var("dept"), var("sal")])),
Literal::Negative(atom("flagged", vec![var("e")])),
],
))],
)],
};
assert!(program.validate().is_ok());
}
#[test]
fn unsafe_aggregate_inner_negation_unbound_var_fails_validation() {
let program = Program {
rules: vec![Rule::new(
atom("h", vec![var("dept"), var("total")]),
vec![Literal::Aggregate(agg_body_with_inner(
"total",
Aggregator::Sum,
"sal",
vec!["dept"],
vec![
Literal::Positive(atom("salary", vec![var("e"), var("dept"), var("sal")])),
Literal::Negative(atom("flagged", vec![var("m")])),
],
))],
)],
};
let err = program.validate().unwrap_err();
let ProgramValidationError::UnsafeAggregate { variables, .. } = err else {
panic!("expected UnsafeAggregate, got {err:?}");
};
assert!(
variables.contains(&VariableSymbol::new("m")),
"expected unbound `m` in offending list, got {variables:?}"
);
}
#[test]
fn safe_aggregate_inner_bind_feeds_input_var() {
let program = Program {
rules: vec![Rule::new(
atom("total_bonus", vec![var("dept"), var("total")]),
vec![Literal::Aggregate(agg_body_with_inner(
"total",
Aggregator::Sum,
"bonus",
vec!["dept"],
vec![
Literal::Positive(atom("salary", vec![var("e"), var("dept"), var("sal")])),
Literal::Bind {
var: VariableSymbol::new("bonus"),
expr: BindExpr::Add(
Box::new(BindExpr::Term(var("sal"))),
Box::new(BindExpr::Term(var("sal"))),
),
},
],
))],
)],
};
assert!(program.validate().is_ok());
}
#[test]
fn safe_aggregate_inner_filter_passes_validation() {
let program = Program {
rules: vec![Rule::new(
atom("high_total", vec![var("dept"), var("total")]),
vec![Literal::Aggregate(agg_body_with_inner(
"total",
Aggregator::Sum,
"sal",
vec!["dept"],
vec![
Literal::Positive(atom("salary", vec![var("e"), var("dept"), var("sal")])),
Literal::Filter(BuiltinFilter::Gt(var("sal"), constant("0"))),
],
))],
)],
};
assert!(program.validate().is_ok());
}
#[test]
fn nested_aggregate_in_inner_body_is_rejected() {
let inner_agg = AggregateBody {
result_var: VariableSymbol::new("inner_total"),
aggregator: Aggregator::Sum,
input_var: VariableSymbol::new("sal"),
group_by: vec![VariableSymbol::new("dept")],
inner_body: vec![Literal::Positive(atom(
"salary",
vec![var("e"), var("dept"), var("sal")],
))],
};
let outer_agg = AggregateBody {
result_var: VariableSymbol::new("outer_total"),
aggregator: Aggregator::Sum,
input_var: VariableSymbol::new("inner_total"),
group_by: vec![],
inner_body: vec![Literal::Aggregate(inner_agg)],
};
let program = Program {
rules: vec![Rule::new(
atom("grand_total", vec![var("outer_total")]),
vec![Literal::Aggregate(outer_agg)],
)],
};
let err = program.validate().unwrap_err();
assert_eq!(
err,
ProgramValidationError::NestedAggregateInInnerBody { rule_index: 0 }
);
}
#[test]
fn aggregator_name_renders_canonical_strings() {
assert_eq!(Aggregator::Count.name(), "COUNT");
assert_eq!(Aggregator::Sum.name(), "SUM");
assert_eq!(Aggregator::Min.name(), "MIN");
assert_eq!(Aggregator::Max.name(), "MAX");
assert_eq!(Aggregator::Avg.name(), "AVG");
}
#[test]
fn aggregate_exported_variables_match_outer_scope() {
let agg = agg_body(
"t",
Aggregator::Sum,
"sal", vec!["dept"],
vec![atom("salary", vec![var("e"), var("dept"), var("sal")])],
);
let exported = agg.exported_variables();
let expected: BTreeSet<VariableSymbol> = ["t", "dept"]
.iter()
.copied()
.map(VariableSymbol::new)
.collect();
assert_eq!(exported, expected);
}
#[test]
fn safe_skolem_head_passes_validation() {
let head = Atom {
predicate: PredicateSymbol::new("hasDerivedKey"),
terms: vec![
var("x"),
Term::Skolem {
name: ConstantSymbol::new("example-key"),
args: vec![var("a"), var("b")],
},
],
};
let program = Program {
rules: vec![Rule::positive(
head,
vec![atom("pair", vec![var("x"), var("a"), var("b")])],
)],
};
assert!(program.validate().is_ok());
}
#[test]
fn unsafe_skolem_head_unbound_arg_fails_validation() {
let head = Atom {
predicate: PredicateSymbol::new("h"),
terms: vec![
var("x"),
Term::Skolem {
name: ConstantSymbol::new("example-key"),
args: vec![var("x"), var("orphan")],
},
],
};
let program = Program {
rules: vec![Rule::positive(head, vec![atom("anchor", vec![var("x")])])],
};
let err = program
.validate()
.expect_err("orphan SKOLEM arg should reject");
let ProgramValidationError::UnsafeRule { variables, .. } = err else {
panic!("expected UnsafeRule, got {err:?}");
};
assert!(
variables.contains(&VariableSymbol::new("orphan")),
"expected `orphan` in offending list, got {variables:?}"
);
}
#[test]
fn skolem_in_positive_body_atom_is_rejected() {
let body_atom = Atom {
predicate: PredicateSymbol::new("p"),
terms: vec![
var("x"),
Term::Skolem {
name: ConstantSymbol::new("example-key"),
args: vec![var("x")],
},
],
};
let program = Program {
rules: vec![Rule::positive(atom("h", vec![var("x")]), vec![body_atom])],
};
let err = program
.validate()
.expect_err("SKOLEM in body should reject");
assert_eq!(
err,
ProgramValidationError::SkolemInBodyAtom { rule_index: 0 }
);
}
#[test]
fn skolem_in_negative_body_atom_is_rejected() {
let negated = Atom {
predicate: PredicateSymbol::new("p"),
terms: vec![
var("x"),
Term::Skolem {
name: ConstantSymbol::new("example-key"),
args: vec![var("x")],
},
],
};
let program = Program {
rules: vec![Rule::new(
atom("h", vec![var("x")]),
vec![
Literal::Positive(atom("anchor", vec![var("x")])),
Literal::Negative(negated),
],
)],
};
let err = program
.validate()
.expect_err("SKOLEM in negative body should reject");
assert_eq!(
err,
ProgramValidationError::SkolemInBodyAtom { rule_index: 0 }
);
}
#[test]
fn skolem_in_aggregate_inner_body_is_rejected() {
let inner = Atom {
predicate: PredicateSymbol::new("salary"),
terms: vec![
var("e"),
var("dept"),
Term::Skolem {
name: ConstantSymbol::new("example-key"),
args: vec![var("e")],
},
],
};
let program = Program {
rules: vec![Rule::new(
atom("total", vec![var("dept"), var("total")]),
vec![Literal::Aggregate(AggregateBody {
result_var: VariableSymbol::new("total"),
aggregator: Aggregator::Sum,
input_var: VariableSymbol::new("dept"),
group_by: vec![VariableSymbol::new("dept")],
inner_body: vec![Literal::Positive(inner)],
})],
)],
};
let err = program
.validate()
.expect_err("SKOLEM in aggregate inner should reject");
assert_eq!(
err,
ProgramValidationError::SkolemInBodyAtom { rule_index: 0 }
);
}
#[test]
fn equality_mode_sameas_accepts_positive_only_program() {
let program = Program {
rules: vec![Rule::positive(
atom("parent", vec![var("x"), var("y")]),
vec![atom("has_parent", vec![var("x"), var("y")])],
)],
};
assert!(
program
.validate_with_equality_mode(EqualityMode::SameAs)
.is_ok()
);
}
#[test]
fn equality_mode_sameas_rejects_negation() {
let program = Program {
rules: vec![Rule::new(
atom("bachelor", vec![var("x")]),
vec![
Literal::Positive(atom("person", vec![var("x")])),
Literal::Negative(atom("married", vec![var("x")])),
],
)],
};
assert!(program.validate().is_ok());
let err = program
.validate_with_equality_mode(EqualityMode::SameAs)
.expect_err("expected extension rejection");
assert_eq!(
err,
ProgramValidationError::NegationUnderEqualityMode { rule_index: 0 }
);
}
#[test]
fn equality_mode_sameas_rejects_aggregation() {
let program = Program {
rules: vec![Rule::new(
atom("total", vec![var("dept"), var("total")]),
vec![Literal::Aggregate(AggregateBody {
result_var: VariableSymbol::new("total"),
aggregator: Aggregator::Sum,
input_var: VariableSymbol::new("sal"),
group_by: vec![VariableSymbol::new("dept")],
inner_body: vec![Literal::Positive(atom(
"salary",
vec![var("e"), var("dept"), var("sal")],
))],
})],
)],
};
assert!(program.validate().is_ok());
let err = program
.validate_with_equality_mode(EqualityMode::SameAs)
.expect_err("expected extension rejection");
assert_eq!(
err,
ProgramValidationError::AggregationUnderEqualityMode { rule_index: 0 }
);
}
#[test]
fn equality_mode_default_permits_negation_and_aggregation() {
let program = Program {
rules: vec![
Rule::new(
atom("a", vec![var("x")]),
vec![
Literal::Positive(atom("p", vec![var("x")])),
Literal::Negative(atom("q", vec![var("x")])),
],
),
Rule::new(
atom("b", vec![var("dept"), var("c")]),
vec![Literal::Aggregate(AggregateBody {
result_var: VariableSymbol::new("c"),
aggregator: Aggregator::Count,
input_var: VariableSymbol::new("e"),
group_by: vec![VariableSymbol::new("dept")],
inner_body: vec![Literal::Positive(atom(
"works_in",
vec![var("e"), var("dept")],
))],
})],
),
],
};
assert!(program.validate().is_ok());
assert!(
program
.validate_with_equality_mode(EqualityMode::Default)
.is_ok()
);
}
#[test]
fn equality_mode_sameas_reports_first_offending_rule() {
let program = Program {
rules: vec![
Rule::positive(atom("ok", vec![var("x")]), vec![atom("p", vec![var("x")])]),
Rule::new(
atom("bad", vec![var("x")]),
vec![
Literal::Positive(atom("p", vec![var("x")])),
Literal::Negative(atom("q", vec![var("x")])),
],
),
Rule::new(
atom("worse", vec![var("dept"), var("c")]),
vec![Literal::Aggregate(AggregateBody {
result_var: VariableSymbol::new("c"),
aggregator: Aggregator::Count,
input_var: VariableSymbol::new("e"),
group_by: vec![VariableSymbol::new("dept")],
inner_body: vec![Literal::Positive(atom(
"works_in",
vec![var("e"), var("dept")],
))],
})],
),
],
};
let err = program
.validate_with_equality_mode(EqualityMode::SameAs)
.expect_err("expected extension rejection");
assert_eq!(
err,
ProgramValidationError::NegationUnderEqualityMode { rule_index: 1 }
);
}