use std::collections::BTreeSet;
use super::aggregate::AggregateBody;
use super::atom::Atom;
use super::literal::Literal;
use super::symbols::VariableSymbol;
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct Rule {
pub head: Atom,
pub body: Vec<Literal>,
}
impl Rule {
#[must_use]
pub fn positive(head: Atom, body: Vec<Atom>) -> Self {
Self {
head,
body: body.into_iter().map(Literal::Positive).collect(),
}
}
#[must_use]
pub fn new(head: Atom, body: Vec<Literal>) -> Self {
Self { head, body }
}
#[must_use]
pub fn is_safe(&self) -> bool {
let unsafe_head = self.unsafe_head_variables();
let unsafe_neg = self.unsafe_negative_variables();
let unsafe_filter = self.unsafe_filter_variables();
let unsafe_bind = self.unsafe_bind_variables();
let unsafe_aggregate = self.unsafe_aggregate_variables();
unsafe_head.is_empty()
&& unsafe_neg.is_empty()
&& unsafe_filter.is_empty()
&& unsafe_bind.is_empty()
&& unsafe_aggregate.is_empty()
}
#[must_use]
pub(super) fn unsafe_head_variables(&self) -> Vec<VariableSymbol> {
let head_vars = self.head.variables();
let bound = self.bound_body_variables();
head_vars.difference(&bound).cloned().collect()
}
#[must_use]
pub(super) fn unsafe_negative_variables(&self) -> Vec<VariableSymbol> {
let bound = self.bound_body_variables();
self.body
.iter()
.filter_map(|lit| match lit {
Literal::Negative(atom) => Some(atom),
Literal::Positive(_)
| Literal::Filter(_)
| Literal::Bind { .. }
| Literal::Aggregate(_) => None,
})
.flat_map(Atom::variables)
.filter(|v| !bound.contains(v))
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
#[must_use]
pub(super) fn unsafe_filter_variables(&self) -> Vec<VariableSymbol> {
let bound = self.bound_body_variables();
self.body
.iter()
.filter_map(|lit| match lit {
Literal::Filter(filter) => Some(filter.variables()),
Literal::Positive(_)
| Literal::Negative(_)
| Literal::Bind { .. }
| Literal::Aggregate(_) => None,
})
.flatten()
.filter(|v| !bound.contains(v))
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
#[must_use]
pub(super) fn unsafe_bind_variables(&self) -> Vec<VariableSymbol> {
let mut bound = self.positive_atom_variables();
let mut errors: BTreeSet<VariableSymbol> = BTreeSet::new();
for lit in &self.body {
let Literal::Bind { var, expr } = lit else {
continue;
};
errors.extend(expr.variables().into_iter().filter(|v| !bound.contains(v)));
if bound.contains(var) {
errors.insert(var.clone());
}
bound.insert(var.clone());
}
errors.into_iter().collect()
}
#[must_use]
pub(super) fn unsafe_aggregate_variables(&self) -> Vec<VariableSymbol> {
let mut already_bound = self.positive_atom_variables();
for lit in &self.body {
if let Literal::Bind { var, .. } = lit {
already_bound.insert(var.clone());
}
}
let mut errors: BTreeSet<VariableSymbol> = BTreeSet::new();
for lit in &self.body {
let Literal::Aggregate(agg) = lit else {
continue;
};
let inner_bound = agg.inner_bound_variables();
if !inner_bound.contains(&agg.input_var) {
errors.insert(agg.input_var.clone());
}
for key in &agg.group_by {
if !inner_bound.contains(key) {
errors.insert(key.clone());
}
}
if already_bound.contains(&agg.result_var) {
errors.insert(agg.result_var.clone());
}
errors.extend(unsafe_inner_body_variables(agg, &inner_bound));
already_bound.insert(agg.result_var.clone());
}
errors.into_iter().collect()
}
#[must_use]
pub(super) fn has_nested_aggregate_in_inner_body(&self) -> bool {
self.body.iter().any(|lit| match lit {
Literal::Aggregate(agg) => agg.contains_nested_aggregate(),
_ => false,
})
}
#[must_use]
pub(super) fn has_skolem_in_body(&self) -> bool {
self.body.iter().any(literal_contains_skolem)
}
#[must_use]
fn bound_body_variables(&self) -> BTreeSet<VariableSymbol> {
let mut out = self.positive_atom_variables();
for lit in &self.body {
match lit {
Literal::Bind { var, .. } => {
out.insert(var.clone());
}
Literal::Aggregate(agg) => {
out.extend(agg.exported_variables());
}
Literal::Positive(_) | Literal::Negative(_) | Literal::Filter(_) => {}
}
}
out
}
#[must_use]
fn positive_atom_variables(&self) -> BTreeSet<VariableSymbol> {
self.body
.iter()
.filter_map(|lit| match lit {
Literal::Positive(atom) => Some(atom.variables()),
Literal::Negative(_)
| Literal::Filter(_)
| Literal::Bind { .. }
| Literal::Aggregate(_) => None,
})
.flatten()
.collect()
}
}
fn literal_contains_skolem(lit: &Literal) -> bool {
match lit {
Literal::Positive(atom) | Literal::Negative(atom) => atom.contains_skolem(),
Literal::Aggregate(agg) => agg.inner_body.iter().any(literal_contains_skolem),
Literal::Filter(_) | Literal::Bind { .. } => false,
}
}
fn unsafe_inner_body_variables(
agg: &AggregateBody,
inner_bound: &BTreeSet<VariableSymbol>,
) -> BTreeSet<VariableSymbol> {
let mut errors: BTreeSet<VariableSymbol> = BTreeSet::new();
let mut inner_bind_bound: BTreeSet<VariableSymbol> = agg
.inner_body
.iter()
.filter_map(|lit| match lit {
Literal::Positive(atom) => Some(atom.variables()),
_ => None,
})
.flatten()
.collect();
for lit in &agg.inner_body {
match lit {
Literal::Negative(atom) => {
for v in atom.variables() {
if !inner_bound.contains(&v) {
errors.insert(v);
}
}
}
Literal::Filter(filter) => {
for v in filter.variables() {
if !inner_bound.contains(&v) {
errors.insert(v);
}
}
}
Literal::Bind { var, expr } => {
for v in expr.variables() {
if !inner_bind_bound.contains(&v) {
errors.insert(v);
}
}
if inner_bind_bound.contains(var) {
errors.insert(var.clone());
}
inner_bind_bound.insert(var.clone());
}
Literal::Positive(_) | Literal::Aggregate(_) => {}
}
}
errors
}