use crate::ir::{ConceptExpr, ConceptId, ConceptPool, Role};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResidualTrigger {
Eager,
DeferOr { disjuncts: Box<[ConceptId]> },
DeferNot { complement: ConceptId },
DeferAll { role: Role, body: ConceptId },
EagerExistsOrCardinality,
}
impl ResidualTrigger {
#[must_use]
pub fn classify(body: ConceptId, pool: &ConceptPool) -> Self {
match pool.get(body) {
ConceptExpr::Top
| ConceptExpr::Bot
| ConceptExpr::Atomic(_)
| ConceptExpr::Nominal(_)
| ConceptExpr::SelfRestriction(_) => Self::Eager,
ConceptExpr::And(args) => {
let pool_ref = pool;
let all_eager_atomic = args.iter().all(|&c| {
matches!(
pool_ref.get(c),
ConceptExpr::Atomic(_) | ConceptExpr::Nominal(_) | ConceptExpr::Top
)
});
if all_eager_atomic {
Self::Eager
} else {
Self::Eager
}
}
ConceptExpr::Or(args) => Self::DeferOr {
disjuncts: args.to_vec().into_boxed_slice(),
},
ConceptExpr::Not(inner) => Self::DeferNot { complement: *inner },
ConceptExpr::All(role, inner) => Self::DeferAll {
role: *role,
body: *inner,
},
ConceptExpr::Some(_, _) | ConceptExpr::Min(_, _, _) | ConceptExpr::Max(_, _, _) => {
Self::EagerExistsOrCardinality
}
}
}
#[must_use]
pub fn is_eager(&self) -> bool {
matches!(self, Self::Eager | Self::EagerExistsOrCardinality)
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ResidualTriggerStats {
pub total: usize,
pub eager: usize,
pub defer_or: usize,
pub defer_not: usize,
pub defer_all: usize,
pub eager_exists_or_cardinality: usize,
}
impl ResidualTriggerStats {
#[must_use]
pub fn deferred(&self) -> usize {
self.defer_or + self.defer_not + self.defer_all
}
}
#[must_use]
pub fn classify_residuals(
residuals: &[ConceptId],
pool: &ConceptPool,
) -> (Vec<ResidualTrigger>, ResidualTriggerStats) {
let mut triggers = Vec::with_capacity(residuals.len());
let mut stats = ResidualTriggerStats {
total: residuals.len(),
..ResidualTriggerStats::default()
};
for &r in residuals {
let t = ResidualTrigger::classify(r, pool);
match &t {
ResidualTrigger::Eager => stats.eager += 1,
ResidualTrigger::DeferOr { .. } => stats.defer_or += 1,
ResidualTrigger::DeferNot { .. } => stats.defer_not += 1,
ResidualTrigger::DeferAll { .. } => stats.defer_all += 1,
ResidualTrigger::EagerExistsOrCardinality => {
stats.eager_exists_or_cardinality += 1;
}
}
triggers.push(t);
}
(triggers, stats)
}
#[cfg(test)]
#[allow(clippy::many_single_char_names)]
mod tests {
use super::*;
use crate::ir::{ClassId, ConceptPool, Role, RoleId};
#[test]
fn atomic_body_classifies_as_eager() {
let mut pool = ConceptPool::new();
let c = pool.atomic(ClassId::new(0));
assert_eq!(ResidualTrigger::classify(c, &pool), ResidualTrigger::Eager);
}
#[test]
fn or_body_classifies_as_defer_or() {
let mut pool = ConceptPool::new();
let a = pool.atomic(ClassId::new(0));
let b = pool.atomic(ClassId::new(1));
let or_ab = pool.or([a, b]);
match ResidualTrigger::classify(or_ab, &pool) {
ResidualTrigger::DeferOr { disjuncts } => {
assert_eq!(disjuncts.as_ref(), &[a, b] as &[_]);
}
other => panic!("expected DeferOr, got {other:?}"),
}
}
#[test]
fn not_body_classifies_as_defer_not() {
let mut pool = ConceptPool::new();
let a = pool.atomic(ClassId::new(0));
let not_a = pool.not(a);
match ResidualTrigger::classify(not_a, &pool) {
ResidualTrigger::DeferNot { complement } => assert_eq!(complement, a),
other => panic!("expected DeferNot, got {other:?}"),
}
}
#[test]
fn all_body_classifies_as_defer_all() {
let mut pool = ConceptPool::new();
let r = Role::Named(RoleId::new(0));
let body = pool.atomic(ClassId::new(0));
let all = pool.all(r, body);
match ResidualTrigger::classify(all, &pool) {
ResidualTrigger::DeferAll { role, body: b } => {
assert_eq!(role, r);
assert_eq!(b, body);
}
other => panic!("expected DeferAll, got {other:?}"),
}
}
#[test]
fn exists_body_classifies_as_eager_exists() {
let mut pool = ConceptPool::new();
let r = Role::Named(RoleId::new(0));
let body = pool.atomic(ClassId::new(0));
let some = pool.some(r, body);
assert_eq!(
ResidualTrigger::classify(some, &pool),
ResidualTrigger::EagerExistsOrCardinality
);
}
#[test]
fn and_of_atomics_classifies_as_eager() {
let mut pool = ConceptPool::new();
let a = pool.atomic(ClassId::new(0));
let b = pool.atomic(ClassId::new(1));
let and_ab = pool.and([a, b]);
assert_eq!(
ResidualTrigger::classify(and_ab, &pool),
ResidualTrigger::Eager
);
}
#[test]
fn and_with_or_classifies_as_eager_for_now() {
let mut pool = ConceptPool::new();
let a = pool.atomic(ClassId::new(0));
let b = pool.atomic(ClassId::new(1));
let c = pool.atomic(ClassId::new(2));
let or_bc = pool.or([b, c]);
let and_a_orbc = pool.and([a, or_bc]);
assert_eq!(
ResidualTrigger::classify(and_a_orbc, &pool),
ResidualTrigger::Eager
);
}
#[test]
fn classify_residuals_aggregates_correctly() {
let mut pool = ConceptPool::new();
let a = pool.atomic(ClassId::new(0));
let b = pool.atomic(ClassId::new(1));
let or_ab = pool.or([a, b]);
let not_a = pool.not(a);
let r = Role::Named(RoleId::new(0));
let all = pool.all(r, a);
let residuals = vec![a, or_ab, not_a, all];
let (_triggers, stats) = classify_residuals(&residuals, &pool);
assert_eq!(stats.total, 4);
assert_eq!(stats.eager, 1);
assert_eq!(stats.defer_or, 1);
assert_eq!(stats.defer_not, 1);
assert_eq!(stats.defer_all, 1);
assert_eq!(stats.eager_exists_or_cardinality, 0);
assert_eq!(stats.deferred(), 3);
}
}