use crate::{ProofExpr, ProofTerm};
pub fn ground(e: &ProofExpr, domain: &[ProofTerm]) -> ProofExpr {
match e {
ProofExpr::ForAll { variable, body } => fold_conj(
domain
.iter()
.map(|c| ground(&subst(body, variable, c), domain)),
),
ProofExpr::Exists { variable, body } => fold_disj(
domain
.iter()
.map(|c| ground(&subst(body, variable, c), domain)),
),
ProofExpr::And(l, r) => {
ProofExpr::And(Box::new(ground(l, domain)), Box::new(ground(r, domain)))
}
ProofExpr::Or(l, r) => {
ProofExpr::Or(Box::new(ground(l, domain)), Box::new(ground(r, domain)))
}
ProofExpr::Implies(l, r) => {
ProofExpr::Implies(Box::new(ground(l, domain)), Box::new(ground(r, domain)))
}
ProofExpr::Iff(l, r) => {
ProofExpr::Iff(Box::new(ground(l, domain)), Box::new(ground(r, domain)))
}
ProofExpr::Not(x) => ProofExpr::Not(Box::new(ground(x, domain))),
ProofExpr::Temporal { operator, body } => ProofExpr::Temporal {
operator: operator.clone(),
body: Box::new(ground(body, domain)),
},
leaf => leaf.clone(),
}
}
fn fold_conj(mut it: impl Iterator<Item = ProofExpr>) -> ProofExpr {
match it.next() {
None => ProofExpr::Atom("True".to_string()),
Some(first) => it.fold(first, |acc, e| ProofExpr::And(Box::new(acc), Box::new(e))),
}
}
fn fold_disj(mut it: impl Iterator<Item = ProofExpr>) -> ProofExpr {
match it.next() {
None => ProofExpr::Atom("False".to_string()),
Some(first) => it.fold(first, |acc, e| ProofExpr::Or(Box::new(acc), Box::new(e))),
}
}
fn subst(e: &ProofExpr, var: &str, to: &ProofTerm) -> ProofExpr {
match e {
ProofExpr::Predicate { name, args, world } => ProofExpr::Predicate {
name: name.clone(),
args: args.iter().map(|a| subst_term(a, var, to)).collect(),
world: world.clone(),
},
ProofExpr::Identity(a, b) => {
ProofExpr::Identity(subst_term(a, var, to), subst_term(b, var, to))
}
ProofExpr::And(l, r) => {
ProofExpr::And(Box::new(subst(l, var, to)), Box::new(subst(r, var, to)))
}
ProofExpr::Or(l, r) => {
ProofExpr::Or(Box::new(subst(l, var, to)), Box::new(subst(r, var, to)))
}
ProofExpr::Implies(l, r) => {
ProofExpr::Implies(Box::new(subst(l, var, to)), Box::new(subst(r, var, to)))
}
ProofExpr::Iff(l, r) => {
ProofExpr::Iff(Box::new(subst(l, var, to)), Box::new(subst(r, var, to)))
}
ProofExpr::Not(x) => ProofExpr::Not(Box::new(subst(x, var, to))),
ProofExpr::ForAll { variable, body } if variable != var => ProofExpr::ForAll {
variable: variable.clone(),
body: Box::new(subst(body, var, to)),
},
ProofExpr::Exists { variable, body } if variable != var => ProofExpr::Exists {
variable: variable.clone(),
body: Box::new(subst(body, var, to)),
},
ProofExpr::Temporal { operator, body } => ProofExpr::Temporal {
operator: operator.clone(),
body: Box::new(subst(body, var, to)),
},
ProofExpr::Term(t) => ProofExpr::Term(subst_term(t, var, to)),
other => other.clone(),
}
}
pub fn domain_constants(exprs: &[ProofExpr]) -> Vec<ProofTerm> {
let mut out: Vec<String> = Vec::new();
for e in exprs {
collect_constants(e, &mut out);
}
out.sort();
out.dedup();
out.into_iter().map(ProofTerm::Constant).collect()
}
fn collect_constants(e: &ProofExpr, out: &mut Vec<String>) {
fn term(t: &ProofTerm, out: &mut Vec<String>) {
match t {
ProofTerm::Constant(s) => out.push(s.clone()),
ProofTerm::Function(_, args) | ProofTerm::Group(args) => {
args.iter().for_each(|a| term(a, out))
}
_ => {}
}
}
match e {
ProofExpr::Predicate { args, .. } => args.iter().for_each(|a| term(a, out)),
ProofExpr::Identity(a, b) => {
term(a, out);
term(b, out);
}
ProofExpr::And(l, r)
| ProofExpr::Or(l, r)
| ProofExpr::Implies(l, r)
| ProofExpr::Iff(l, r) => {
collect_constants(l, out);
collect_constants(r, out);
}
ProofExpr::Not(x) => collect_constants(x, out),
ProofExpr::ForAll { body, .. }
| ProofExpr::Exists { body, .. }
| ProofExpr::Temporal { body, .. } => collect_constants(body, out),
ProofExpr::Term(t) => term(t, out),
_ => {}
}
}
use std::collections::HashMap;
pub fn sort_domains(premises: &[ProofExpr]) -> HashMap<String, Vec<ProofTerm>> {
fn collect(e: &ProofExpr, m: &mut HashMap<String, Vec<ProofTerm>>) {
match e {
ProofExpr::Predicate { name, args, .. } if args.len() == 1 => {
if let ProofTerm::Constant(_) = &args[0] {
let dom = m.entry(name.clone()).or_default();
if !dom.contains(&args[0]) {
dom.push(args[0].clone());
}
}
}
ProofExpr::And(l, r) => {
collect(l, m);
collect(r, m);
}
_ => {}
}
}
let mut m = HashMap::new();
for p in premises {
collect(p, &mut m);
}
m
}
fn guard_sort(e: &ProofExpr, var: &str) -> Option<String> {
match e {
ProofExpr::Predicate { name, args, .. }
if args.len() == 1 && matches!(&args[0], ProofTerm::Variable(v) | ProofTerm::BoundVarRef(v) if v == var) =>
{
Some(name.clone())
}
ProofExpr::And(l, r)
| ProofExpr::Or(l, r)
| ProofExpr::Implies(l, r)
| ProofExpr::Iff(l, r) => guard_sort(l, var).or_else(|| guard_sort(r, var)),
ProofExpr::Not(x) | ProofExpr::Temporal { body: x, .. } => guard_sort(x, var),
ProofExpr::ForAll { variable, body } | ProofExpr::Exists { variable, body }
if variable != var =>
{
guard_sort(body, var)
}
_ => None,
}
}
pub fn ground_sorted(
e: &ProofExpr,
sorts: &HashMap<String, Vec<ProofTerm>>,
fallback: &[ProofTerm],
) -> ProofExpr {
let domain_for = |body: &ProofExpr, var: &str| -> Vec<ProofTerm> {
guard_sort(body, var)
.and_then(|s| sorts.get(&s).cloned())
.unwrap_or_else(|| fallback.to_vec())
};
match e {
ProofExpr::ForAll { variable, body } => {
let dom = domain_for(body, variable);
fold_conj(
dom.iter()
.map(|c| ground_sorted(&subst(body, variable, c), sorts, fallback)),
)
}
ProofExpr::Exists { variable, body } => {
let dom = domain_for(body, variable);
fold_disj(
dom.iter()
.map(|c| ground_sorted(&subst(body, variable, c), sorts, fallback)),
)
}
ProofExpr::And(l, r) => ProofExpr::And(
Box::new(ground_sorted(l, sorts, fallback)),
Box::new(ground_sorted(r, sorts, fallback)),
),
ProofExpr::Or(l, r) => ProofExpr::Or(
Box::new(ground_sorted(l, sorts, fallback)),
Box::new(ground_sorted(r, sorts, fallback)),
),
ProofExpr::Implies(l, r) => ProofExpr::Implies(
Box::new(ground_sorted(l, sorts, fallback)),
Box::new(ground_sorted(r, sorts, fallback)),
),
ProofExpr::Iff(l, r) => ProofExpr::Iff(
Box::new(ground_sorted(l, sorts, fallback)),
Box::new(ground_sorted(r, sorts, fallback)),
),
ProofExpr::Not(x) => ProofExpr::Not(Box::new(ground_sorted(x, sorts, fallback))),
ProofExpr::Temporal { operator, body } => ProofExpr::Temporal {
operator: operator.clone(),
body: Box::new(ground_sorted(body, sorts, fallback)),
},
leaf => leaf.clone(),
}
}
pub fn ground_problem_sorted(
premises: &[ProofExpr],
goal: &ProofExpr,
) -> (Vec<ProofExpr>, ProofExpr) {
let mut all: Vec<ProofExpr> = premises.to_vec();
all.push(goal.clone());
let fallback = domain_constants(&all);
let sorts = sort_domains(premises);
let gp = premises
.iter()
.map(|p| ground_sorted(p, &sorts, &fallback))
.collect();
let gg = ground_sorted(goal, &sorts, &fallback);
(gp, gg)
}
pub fn ground_problem(
premises: &[ProofExpr],
goal: &ProofExpr,
) -> (Vec<ProofExpr>, ProofExpr) {
let mut all: Vec<ProofExpr> = premises.to_vec();
all.push(goal.clone());
let domain = domain_constants(&all);
let grounded_premises = premises.iter().map(|p| ground(p, &domain)).collect();
let grounded_goal = ground(goal, &domain);
(grounded_premises, grounded_goal)
}
pub fn at_most_one_lemmas(premises: &[ProofExpr]) -> Vec<ProofExpr> {
premises.iter().filter_map(at_most_one_of).collect()
}
pub fn functionality_lemmas(premises: &[ProofExpr]) -> Vec<ProofExpr> {
let mut out = Vec::new();
for p in premises {
let ProofExpr::ForAll { variable, body } = p else {
continue;
};
let ProofExpr::Implies(_guard, cons) = body.as_ref() else {
continue;
};
let lits = flatten_or(cons);
let all_atoms_on_var = lits.len() >= 2
&& lits.iter().all(|l| {
matches!(l, ProofExpr::Predicate { args, .. }
if args.iter().any(|a| matches!(a, ProofTerm::Variable(v) | ProofTerm::BoundVarRef(v) if v == variable)))
});
if !all_atoms_on_var {
continue;
}
for (i, li) in lits.iter().enumerate() {
for (j, lj) in lits.iter().enumerate() {
if i == j {
continue;
}
out.push(ProofExpr::ForAll {
variable: variable.clone(),
body: Box::new(ProofExpr::Implies(
Box::new(li.clone()),
Box::new(ProofExpr::Not(Box::new(lj.clone()))),
)),
});
}
}
}
out
}
pub fn column_closure_lemmas(premises: &[ProofExpr]) -> Vec<ProofExpr> {
let domains = sort_domains(premises);
let mut flat: Vec<ProofExpr> = Vec::new();
for p in premises {
flatten_and_into(p, &mut flat);
}
let mut out = Vec::new();
for p in &flat {
if at_most_one_of(p).is_none() {
continue;
}
let ProofExpr::Exists { variable: x, body } = p else {
continue;
};
let ProofExpr::And(phi_x, _uniq) = body.as_ref() else {
continue;
};
let Some(sort) = guard_sort(phi_x, x) else {
continue;
};
let Some(domain) = domains.get(&sort) else {
continue;
};
if domain.len() < 2 {
continue;
}
let value = strip_sort_guard(phi_x, &sort, x);
let mut disj: Option<ProofExpr> = None;
for r in domain {
let lit_r = subst(&value, x, r);
disj = Some(match disj {
None => lit_r,
Some(acc) => ProofExpr::Or(Box::new(acc), Box::new(lit_r)),
});
}
if let Some(d) = disj {
out.push(d);
}
}
out
}
fn strip_sort_guard(phi: &ProofExpr, sort: &str, var: &str) -> ProofExpr {
let is_guard = |e: &ProofExpr| {
matches!(e, ProofExpr::Predicate { name, args, .. }
if name == sort && args.len() == 1
&& matches!(&args[0], ProofTerm::Variable(v) | ProofTerm::BoundVarRef(v) if v == var))
};
if let ProofExpr::And(l, r) = phi {
if is_guard(l) {
return strip_sort_guard(r, sort, var);
}
if is_guard(r) {
return strip_sort_guard(l, sort, var);
}
return ProofExpr::And(
Box::new(strip_sort_guard(l, sort, var)),
Box::new(strip_sort_guard(r, sort, var)),
);
}
phi.clone()
}
fn flatten_or(e: &ProofExpr) -> Vec<ProofExpr> {
match e {
ProofExpr::Or(l, r) => {
let mut v = flatten_or(l);
v.extend(flatten_or(r));
v
}
_ => vec![e.clone()],
}
}
pub fn discharge_unary_facts(premises: &[ProofExpr]) -> Vec<ProofExpr> {
let mut flat: Vec<ProofExpr> = Vec::new();
for p in premises {
flatten_and_into(p, &mut flat);
}
let mut facts: Vec<(String, String)> = Vec::new();
for p in &flat {
if let ProofExpr::Predicate { name, args, .. } = p {
if let [ProofTerm::Constant(c)] = args.as_slice() {
let key = (name.clone(), c.clone());
if !facts.contains(&key) {
facts.push(key);
}
}
}
}
premises.iter().map(|p| discharge(p, &facts)).collect()
}
fn is_true(e: &ProofExpr) -> bool {
matches!(e, ProofExpr::Atom(s) if s == "True")
}
fn is_false(e: &ProofExpr) -> bool {
matches!(e, ProofExpr::Atom(s) if s == "False")
}
pub fn simplify_trivial_identities(premises: &[ProofExpr]) -> Vec<ProofExpr> {
premises.iter().map(simplify_ident).collect()
}
fn simplify_ident(e: &ProofExpr) -> ProofExpr {
match e {
ProofExpr::Identity(a, b) if a == b => ProofExpr::Atom("True".to_string()),
ProofExpr::Not(inner) => {
let s = simplify_ident(inner);
if is_true(&s) {
ProofExpr::Atom("False".to_string())
} else if is_false(&s) {
ProofExpr::Atom("True".to_string())
} else {
ProofExpr::Not(Box::new(s))
}
}
ProofExpr::And(l, r) => {
let (l, r) = (simplify_ident(l), simplify_ident(r));
if is_false(&l) || is_false(&r) {
ProofExpr::Atom("False".to_string())
} else if is_true(&l) {
r
} else if is_true(&r) {
l
} else {
ProofExpr::And(Box::new(l), Box::new(r))
}
}
ProofExpr::Or(l, r) => {
let (l, r) = (simplify_ident(l), simplify_ident(r));
if is_true(&l) || is_true(&r) {
ProofExpr::Atom("True".to_string())
} else if is_false(&l) {
r
} else if is_false(&r) {
l
} else {
ProofExpr::Or(Box::new(l), Box::new(r))
}
}
ProofExpr::Implies(l, r) => {
let (l, r) = (simplify_ident(l), simplify_ident(r));
if is_false(&l) || is_true(&r) {
ProofExpr::Atom("True".to_string())
} else if is_true(&l) {
r
} else {
ProofExpr::Implies(Box::new(l), Box::new(r))
}
}
ProofExpr::ForAll { variable, body } => ProofExpr::ForAll {
variable: variable.clone(),
body: Box::new(simplify_ident(body)),
},
ProofExpr::Exists { variable, body } => ProofExpr::Exists {
variable: variable.clone(),
body: Box::new(simplify_ident(body)),
},
other => other.clone(),
}
}
fn discharge(e: &ProofExpr, facts: &[(String, String)]) -> ProofExpr {
match e {
ProofExpr::Predicate { name, args, .. } => {
if let [ProofTerm::Constant(c)] = args.as_slice() {
if facts.iter().any(|(n, k)| n == name && k == c) {
return ProofExpr::Atom("True".to_string());
}
}
e.clone()
}
ProofExpr::And(l, r) => {
let (l, r) = (discharge(l, facts), discharge(r, facts));
match (is_true(&l), is_true(&r)) {
(true, _) => r,
(_, true) => l,
_ => ProofExpr::And(Box::new(l), Box::new(r)),
}
}
ProofExpr::Implies(l, r) => {
let (l, r) = (discharge(l, facts), discharge(r, facts));
if is_true(&l) {
r
} else {
ProofExpr::Implies(Box::new(l), Box::new(r))
}
}
ProofExpr::Or(l, r) => {
ProofExpr::Or(Box::new(discharge(l, facts)), Box::new(discharge(r, facts)))
}
ProofExpr::Not(x) => ProofExpr::Not(Box::new(discharge(x, facts))),
ProofExpr::ForAll { variable, body } => ProofExpr::ForAll {
variable: variable.clone(),
body: Box::new(discharge(body, facts)),
},
ProofExpr::Exists { variable, body } => ProofExpr::Exists {
variable: variable.clone(),
body: Box::new(discharge(body, facts)),
},
ProofExpr::Temporal { operator, body } => ProofExpr::Temporal {
operator: operator.clone(),
body: Box::new(discharge(body, facts)),
},
other => other.clone(),
}
}
fn at_most_one_of(e: &ProofExpr) -> Option<ProofExpr> {
let ProofExpr::Exists { variable: x, body } = e else {
return None;
};
let ProofExpr::And(phi_x, uniq) = body.as_ref() else {
return None;
};
let ProofExpr::ForAll { variable: y, body: uniq_body } = uniq.as_ref() else {
return None;
};
let ProofExpr::Implies(_phi_y, ident) = uniq_body.as_ref() else {
return None;
};
let ProofExpr::Identity(l, r) = ident.as_ref() else {
return None;
};
let is_var = |t: &ProofTerm, name: &str| {
matches!(t, ProofTerm::Variable(v) | ProofTerm::BoundVarRef(v) if v == name)
};
if !((is_var(l, y) && is_var(r, x)) || (is_var(l, x) && is_var(r, y))) {
return None;
}
let y2 = format!("{x}_amo");
let phi_x = phi_x.as_ref().clone();
let phi_y2 = subst(&phi_x, x, &ProofTerm::Variable(y2.clone()));
let body = ProofExpr::Implies(
Box::new(ProofExpr::And(Box::new(phi_x), Box::new(phi_y2))),
Box::new(ProofExpr::Identity(
ProofTerm::Variable(x.clone()),
ProofTerm::Variable(y2.clone()),
)),
);
Some(ProofExpr::ForAll {
variable: x.clone(),
body: Box::new(ProofExpr::ForAll {
variable: y2,
body: Box::new(body),
}),
})
}
pub fn definite_property_implications(premises: &[ProofExpr]) -> Vec<ProofExpr> {
let row_sorts = row_sort_predicates(premises);
let singletons = singleton_value_signatures(premises, &row_sorts);
let mut flat: Vec<ProofExpr> = Vec::new();
for p in premises {
flatten_and_into(p, &mut flat);
}
let mut out = Vec::new();
for p in &flat {
let ProofExpr::Exists { variable: x, body } = p else {
continue;
};
if matches!(body.as_ref(), ProofExpr::Exists { .. }) || at_most_one_of(p).is_some() {
continue;
}
let lits = clue_literals_on(body, x, &row_sorts);
for (i, anchor) in lits.iter().enumerate() {
let Some(sig) = positive_signature(anchor) else {
continue;
};
if !singletons.contains(&sig) {
continue;
}
for (j, other) in lits.iter().enumerate() {
if i == j {
continue;
}
out.push(ProofExpr::ForAll {
variable: x.clone(),
body: Box::new(ProofExpr::Implies(Box::new(anchor.clone()), Box::new(other.clone()))),
});
}
}
}
out
}
fn flatten_and_into(e: &ProofExpr, out: &mut Vec<ProofExpr>) {
match e {
ProofExpr::And(l, r) => {
flatten_and_into(l, out);
flatten_and_into(r, out);
}
_ => out.push(e.clone()),
}
}
fn row_sort_predicates(premises: &[ProofExpr]) -> std::collections::HashSet<String> {
let mut flat: Vec<ProofExpr> = Vec::new();
for p in premises {
flatten_and_into(p, &mut flat);
}
let mut s = std::collections::HashSet::new();
for p in &flat {
if let ProofExpr::Predicate { name, args, .. } = p {
if matches!(args.as_slice(), [ProofTerm::Constant(_)]) {
s.insert(name.clone());
}
}
}
s
}
fn singleton_value_signatures(
premises: &[ProofExpr],
row_sorts: &std::collections::HashSet<String>,
) -> std::collections::HashSet<(String, String)> {
let mut s = std::collections::HashSet::new();
for p in premises {
let ProofExpr::Exists { variable: x, body } = p else {
continue;
};
let ProofExpr::And(phi, uniq) = body.as_ref() else {
continue;
};
if !matches!(uniq.as_ref(), ProofExpr::ForAll { .. }) {
continue;
}
for lit in clue_literals_on(phi, x, row_sorts) {
if let Some(sig) = positive_signature(&lit) {
s.insert(sig);
}
}
}
s
}
fn positive_signature(e: &ProofExpr) -> Option<(String, String)> {
let ProofExpr::Predicate { name, args, .. } = e else {
return None;
};
match args.as_slice() {
[_, ProofTerm::Constant(v)] => Some((name.clone(), v.clone())),
[_] => Some((name.clone(), String::new())),
_ => None,
}
}
fn term_is_var(t: &ProofTerm, x: &str) -> bool {
matches!(t, ProofTerm::Variable(v) | ProofTerm::BoundVarRef(v) if v == x)
}
fn mentions_var(args: &[ProofTerm], x: &str) -> bool {
args.iter().any(|a| term_is_var(a, x))
}
fn clue_literals_on(
e: &ProofExpr,
x: &str,
row_sorts: &std::collections::HashSet<String>,
) -> Vec<ProofExpr> {
match e {
ProofExpr::And(l, r) => {
let mut v = clue_literals_on(l, x, row_sorts);
v.extend(clue_literals_on(r, x, row_sorts));
v
}
ProofExpr::Predicate { name, args, .. } => {
if mentions_var(args, x) && !row_sorts.contains(name) {
vec![e.clone()]
} else {
vec![]
}
}
ProofExpr::Not(inner) => match drop_row_guards(inner, x, row_sorts) {
Some(lit) => vec![ProofExpr::Not(Box::new(lit))],
None => vec![],
},
_ => vec![],
}
}
fn drop_row_guards(
e: &ProofExpr,
x: &str,
row_sorts: &std::collections::HashSet<String>,
) -> Option<ProofExpr> {
let mut lits = clue_literals_on(e, x, row_sorts);
if lits.len() == 1 {
Some(lits.pop().unwrap())
} else {
None
}
}
fn subst_term(t: &ProofTerm, var: &str, to: &ProofTerm) -> ProofTerm {
match t {
ProofTerm::Variable(x) if x == var => to.clone(),
ProofTerm::BoundVarRef(x) if x == var => to.clone(),
ProofTerm::Function(name, args) => {
ProofTerm::Function(name.clone(), args.iter().map(|a| subst_term(a, var, to)).collect())
}
ProofTerm::Group(args) => {
ProofTerm::Group(args.iter().map(|a| subst_term(a, var, to)).collect())
}
other => other.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn c(s: &str) -> ProofTerm {
ProofTerm::Constant(s.to_string())
}
fn v(s: &str) -> ProofTerm {
ProofTerm::Variable(s.to_string())
}
fn pred(name: &str, args: Vec<ProofTerm>) -> ProofExpr {
ProofExpr::Predicate {
name: name.to_string(),
args,
world: None,
}
}
fn forall(var: &str, body: ProofExpr) -> ProofExpr {
ProofExpr::ForAll {
variable: var.to_string(),
body: Box::new(body),
}
}
fn exists(var: &str, body: ProofExpr) -> ProofExpr {
ProofExpr::Exists {
variable: var.to_string(),
body: Box::new(body),
}
}
#[test]
fn definite_descriptions_become_implications() {
let trip = |t: ProofTerm| pred("Trip", vec![t]);
let in_ = |t: ProofTerm, s: &str| pred("In", vec![t, c(s)]);
let with = |t: ProofTerm, f: &str| pred("With", vec![t, c(f)]);
let hunt = |t: ProofTerm| pred("Hunt", vec![t]);
let exactly_one = |phi: ProofExpr, val: ProofExpr| {
exists(
"x",
ProofExpr::And(
Box::new(ProofExpr::And(Box::new(trip(v("x"))), Box::new(phi))),
Box::new(forall(
"y",
ProofExpr::Implies(
Box::new(ProofExpr::And(Box::new(trip(v("y"))), Box::new(val))),
Box::new(ProofExpr::Identity(v("y"), v("x"))),
),
)),
),
)
};
let clue2 = exists(
"x",
ProofExpr::And(
Box::new(ProofExpr::And(
Box::new(ProofExpr::And(Box::new(trip(v("x"))), Box::new(in_(v("x"), "Florida")))),
Box::new(forall(
"y",
ProofExpr::Implies(
Box::new(ProofExpr::And(Box::new(trip(v("y"))), Box::new(in_(v("y"), "Florida")))),
Box::new(ProofExpr::Identity(v("y"), v("x"))),
),
)),
)),
Box::new(ProofExpr::And(Box::new(hunt(v("x"))), Box::new(trip(v("x"))))),
),
);
let clue4 = exists(
"x",
ProofExpr::And(
Box::new(ProofExpr::And(
Box::new(ProofExpr::And(Box::new(trip(v("x"))), Box::new(with(v("x"), "Yvonne")))),
Box::new(forall(
"y",
ProofExpr::Implies(
Box::new(ProofExpr::And(Box::new(trip(v("y"))), Box::new(with(v("y"), "Yvonne")))),
Box::new(ProofExpr::Identity(v("y"), v("x"))),
),
)),
)),
Box::new(ProofExpr::Not(Box::new(in_(v("x"), "Kentucky")))),
),
);
let premises = vec![
trip(c("Alpha")),
exactly_one(in_(v("x"), "Florida"), in_(v("y"), "Florida")),
exactly_one(hunt(v("x")), hunt(v("y"))),
exactly_one(with(v("x"), "Yvonne"), with(v("y"), "Yvonne")),
clue2,
clue4,
];
let imps = definite_property_implications(&premises);
let fwd = |a: ProofExpr, b: ProofExpr| ProofExpr::ForAll {
variable: "x".to_string(),
body: Box::new(ProofExpr::Implies(Box::new(a), Box::new(b))),
};
assert!(
imps.contains(&fwd(in_(v("x"), "Florida"), hunt(v("x")))),
"clue2 forward In(Florida)→Hunt missing; got: {imps:?}"
);
assert!(
imps.contains(&fwd(hunt(v("x")), in_(v("x"), "Florida"))),
"clue2 reverse Hunt→In(Florida) missing (biconditional); got: {imps:?}"
);
assert!(
imps.contains(&fwd(with(v("x"), "Yvonne"), ProofExpr::Not(Box::new(in_(v("x"), "Kentucky"))))),
"clue4 forward With(Yvonne)→¬In(Kentucky) missing; got: {imps:?}"
);
assert!(
!imps.iter().any(|e| matches!(e, ProofExpr::ForAll { body, .. }
if matches!(body.as_ref(), ProofExpr::Implies(a, _) if matches!(a.as_ref(), ProofExpr::Not(_))))),
"no rule may have a negative antecedent; got: {imps:?}"
);
}
#[test]
fn grounds_universal_to_conjunction() {
let e = forall("x", pred("P", vec![v("x")]));
let g = ground(&e, &[c("a"), c("b")]);
let expected = ProofExpr::And(
Box::new(pred("P", vec![c("a")])),
Box::new(pred("P", vec![c("b")])),
);
assert_eq!(g, expected);
}
#[test]
fn grounds_existential_to_disjunction() {
let e = exists("x", pred("P", vec![v("x")]));
let g = ground(&e, &[c("a"), c("b")]);
let expected = ProofExpr::Or(
Box::new(pred("P", vec![c("a")])),
Box::new(pred("P", vec![c("b")])),
);
assert_eq!(g, expected);
}
fn is_quantifier_free(e: &ProofExpr) -> bool {
match e {
ProofExpr::ForAll { .. } | ProofExpr::Exists { .. } => false,
ProofExpr::And(l, r)
| ProofExpr::Or(l, r)
| ProofExpr::Implies(l, r)
| ProofExpr::Iff(l, r) => is_quantifier_free(l) && is_quantifier_free(r),
ProofExpr::Not(x) | ProofExpr::Temporal { body: x, .. } => is_quantifier_free(x),
_ => true,
}
}
#[test]
fn domain_is_the_named_constants() {
let premises = vec![
pred("Trip", vec![c("Alpha")]),
ProofExpr::And(
Box::new(pred("Trip", vec![c("Beta")])),
Box::new(pred("In", vec![c("Beta"), c("Maine")])),
),
];
let d = domain_constants(&premises);
assert_eq!(d, vec![c("Alpha"), c("Beta"), c("Maine")]);
}
#[test]
fn grounding_the_exactly_one_form_leaves_no_quantifier() {
let inner = ProofExpr::And(
Box::new(pred("P", vec![v("x")])),
Box::new(forall(
"y",
ProofExpr::Implies(
Box::new(pred("P", vec![v("y")])),
Box::new(ProofExpr::Identity(v("y"), v("x"))),
),
)),
);
let e = exists("x", inner);
let g = ground(&e, &[c("a"), c("b")]);
assert!(is_quantifier_free(&g), "grounded exactly-one must be quantifier-free: {g:?}");
}
#[test]
fn kernel_does_grounded_disjunctive_syllogism() {
let in_ = |t: &str, s: &str| pred("In", vec![c(t), c(s)]);
let premises = vec![
ProofExpr::Or(Box::new(in_("Beta", "Florida")), Box::new(in_("Beta", "Maine"))),
ProofExpr::Not(Box::new(in_("Beta", "Florida"))),
];
let goal = in_("Beta", "Maine");
let r = crate::verify::prove_certify_check(&premises, &goal);
assert!(r.verified, "grounded disjunctive syllogism; err: {:?}", r.verification_error);
}
#[test]
fn grounded_two_value_grid_proves_with_our_kernel() {
let trip = |t: ProofTerm| pred("Trip", vec![t]);
let in_ = |t: ProofTerm, s: ProofTerm| pred("In", vec![t, s]);
let fl = || c("Florida");
let me_ = || c("Maine");
let closure = forall(
"x",
ProofExpr::Implies(
Box::new(trip(v("x"))),
Box::new(ProofExpr::Or(
Box::new(in_(v("x"), fl())),
Box::new(in_(v("x"), me_())),
)),
),
);
let exactly_one_fl = forall(
"x",
forall(
"y",
ProofExpr::Implies(
Box::new(ProofExpr::And(
Box::new(ProofExpr::And(Box::new(trip(v("x"))), Box::new(in_(v("x"), fl())))),
Box::new(ProofExpr::And(Box::new(trip(v("y"))), Box::new(in_(v("y"), fl())))),
)),
Box::new(ProofExpr::Identity(v("x"), v("y"))),
),
),
);
let premises = vec![
trip(c("Alpha")),
trip(c("Beta")),
ProofExpr::Not(Box::new(ProofExpr::Identity(c("Alpha"), c("Beta")))),
closure,
exactly_one_fl,
in_(c("Alpha"), fl()),
];
let goal = in_(c("Beta"), me_());
let (gp, gg) = ground_problem(&premises, &goal);
let r = crate::verify::prove_certify_check(&gp, &gg);
assert!(
r.verified,
"grounded grid must prove via our kernel; err: {:?}",
r.verification_error
);
}
#[test]
fn grounded_exactly_one_existential_grid_proves_with_our_kernel() {
let trip = |t: ProofTerm| pred("Trip", vec![t]);
let in_ = |t: ProofTerm, s: ProofTerm| pred("In", vec![t, s]);
let fl = || c("Florida");
let me_ = || c("Maine");
let closure = forall(
"x",
ProofExpr::Implies(
Box::new(trip(v("x"))),
Box::new(ProofExpr::Or(
Box::new(in_(v("x"), fl())),
Box::new(in_(v("x"), me_())),
)),
),
);
let phi = |t: ProofTerm| {
ProofExpr::And(Box::new(trip(t.clone())), Box::new(in_(t, fl())))
};
let exactly_one_fl = exists(
"x",
ProofExpr::And(
Box::new(phi(v("x"))),
Box::new(forall(
"y",
ProofExpr::Implies(
Box::new(phi(v("y"))),
Box::new(ProofExpr::Identity(v("y"), v("x"))),
),
)),
),
);
let premises = vec![
trip(c("Alpha")),
trip(c("Beta")),
ProofExpr::Not(Box::new(ProofExpr::Identity(c("Alpha"), c("Beta")))),
closure,
exactly_one_fl,
in_(c("Alpha"), fl()),
];
let goal = in_(c("Beta"), me_());
let mut prem = premises;
prem.extend(at_most_one_lemmas(&prem));
let (gp, gg) = ground_problem_sorted(&prem, &goal);
let r = crate::verify::prove_certify_check(&gp, &gg);
assert!(
r.verified,
"grounded exactly-one (∃∀) grid must prove via our kernel; err: {:?}",
r.verification_error
);
}
#[test]
#[test]
fn multi_category_grid_cell_forced_by_propagation() {
let trips = ["Alpha", "Beta", "Gamma", "Delta"];
let and = |l: ProofExpr, r: ProofExpr| ProofExpr::And(Box::new(l), Box::new(r));
let implies = |l: ProofExpr, r: ProofExpr| ProofExpr::Implies(Box::new(l), Box::new(r));
let neq = |a: &str, b: &str| ProofExpr::Not(Box::new(ProofExpr::Identity(c(a), c(b))));
let category = |rel: &str, vals: &[&str], out: &mut Vec<ProofExpr>| {
for t in trips {
out.push(fold_disj(vals.iter().map(|val| pred(rel, vec![c(t), c(val)]))));
}
for val in vals {
for (i, t) in trips.iter().enumerate() {
for u in &trips[i + 1..] {
out.push(implies(
and(pred(rel, vec![c(t), c(val)]), pred(rel, vec![c(u), c(val)])),
ProofExpr::Identity(c(t), c(u)),
));
}
}
}
};
let mut premises = Vec::new();
for (i, t) in trips.iter().enumerate() {
for u in &trips[i + 1..] {
premises.push(neq(t, u));
}
}
category("In", &["2001", "2002", "2003", "2004"], &mut premises); category("In", &["CT", "FL", "KY", "ME"], &mut premises); category("With", &["Bill", "Lillie", "Neal", "Yvonne"], &mut premises); premises.push(pred("In", vec![c("Alpha"), c("FL")]));
premises.push(pred("In", vec![c("Beta"), c("KY")]));
premises.push(pred("In", vec![c("Gamma"), c("CT")]));
let goal = pred("In", vec![c("Delta"), c("ME")]);
let r = crate::verify::prove_certify_check(&premises, &goal);
assert!(
r.verified,
"Delta must be forced into ME by propagation (no Z3); err: {:?}",
r.verification_error
);
}
#[test]
fn of_pair_clue_forces_cell_by_case_analysis() {
let and = |l: ProofExpr, r: ProofExpr| ProofExpr::And(Box::new(l), Box::new(r));
let implies = |l: ProofExpr, r: ProofExpr| ProofExpr::Implies(Box::new(l), Box::new(r));
let in_fl = |t: &str| pred("In", vec![c(t), c("FL")]);
let neq = |a: &str, b: &str| ProofExpr::Not(Box::new(ProofExpr::Identity(c(a), c(b))));
let amo = |t: &str, u: &str| {
implies(and(in_fl(t), in_fl(u)), ProofExpr::Identity(c(t), c(u)))
};
let of_pair = ProofExpr::Or(
Box::new(and(in_fl("A"), pred("With", vec![c("B"), c("Neal")]))),
Box::new(and(in_fl("B"), pred("With", vec![c("A"), c("Neal")]))),
);
let premises = vec![
of_pair,
amo("A", "C"),
amo("B", "C"),
neq("A", "C"),
neq("B", "C"),
neq("A", "B"),
];
let goal = ProofExpr::Not(Box::new(in_fl("C")));
let r = crate::verify::prove_certify_check(&premises, &goal);
assert!(
r.verified,
"the of-pair clue must force ¬In(C, FL) by case analysis (no Z3); err: {:?}",
r.verification_error
);
}
#[test]
fn compound_of_pair_disjunction_resolves() {
let and = |l: ProofExpr, r: ProofExpr| ProofExpr::And(Box::new(l), Box::new(r));
let or = |l: ProofExpr, r: ProofExpr| ProofExpr::Or(Box::new(l), Box::new(r));
let implies = |l: ProofExpr, r: ProofExpr| ProofExpr::Implies(Box::new(l), Box::new(r));
let not = |e: ProofExpr| ProofExpr::Not(Box::new(e));
let a = pred("A", vec![c("Obj")]);
let p = pred("P", vec![c("Obj")]);
let q = pred("Q", vec![c("Obj")]);
let bad = pred("Bad", vec![c("Obj")]);
let cc = pred("C", vec![c("Obj")]);
let r = pred("R", vec![c("Obj")]);
let of_pair = or(and(a, or(p.clone(), q.clone())), and(bad.clone(), cc));
let premises = vec![
of_pair,
not(bad),
implies(p, not(r.clone())),
implies(q, not(r.clone())),
];
let goal = not(r);
let res = crate::verify::prove_certify_check(&premises, &goal);
assert!(
res.verified,
"compound of-pair must resolve by refute + decompose + case analysis; err: {:?}",
res.verification_error
);
}
#[test]
fn at_most_one_lemma_extracted_from_exactly_one() {
let phi = |t: ProofTerm| {
ProofExpr::And(
Box::new(pred("Trip", vec![t.clone()])),
Box::new(pred("In", vec![t, c("Florida")])),
)
};
let exactly_one = exists(
"x",
ProofExpr::And(
Box::new(phi(v("x"))),
Box::new(forall(
"y",
ProofExpr::Implies(
Box::new(phi(v("y"))),
Box::new(ProofExpr::Identity(v("y"), v("x"))),
),
)),
),
);
let lemmas = at_most_one_lemmas(&[exactly_one]);
assert_eq!(lemmas.len(), 1, "one exactly-one ⇒ one at-most-one lemma");
assert!(
matches!(&lemmas[0], ProofExpr::ForAll { body, .. }
if matches!(body.as_ref(), ProofExpr::ForAll { .. })),
"lemma must be ∀x∀y(…); got {:?}",
lemmas[0]
);
assert!(at_most_one_lemmas(&[pred("Trip", vec![c("Alpha")])]).is_empty());
}
fn conjuncts(e: &ProofExpr) -> usize {
match e {
ProofExpr::And(l, r) => conjuncts(l) + conjuncts(r),
_ => 1,
}
}
#[test]
fn sort_domains_collects_declared_members() {
let premises = vec![
pred("Trip", vec![c("Alpha")]),
ProofExpr::And(
Box::new(pred("Trip", vec![c("Beta")])),
Box::new(pred("Year", vec![c("2001")])),
),
];
let sorts = sort_domains(&premises);
assert_eq!(sorts.get("Trip"), Some(&vec![c("Alpha"), c("Beta")]));
assert_eq!(sorts.get("Year"), Some(&vec![c("2001")]));
}
#[test]
fn sort_aware_grounds_over_guard_sort_only() {
let e = forall(
"x",
ProofExpr::Implies(
Box::new(pred("Trip", vec![v("x")])),
Box::new(pred("In", vec![v("x"), c("Maine")])),
),
);
let mut sorts = std::collections::HashMap::new();
sorts.insert("Trip".to_string(), vec![c("Alpha"), c("Beta")]);
let fallback = vec![c("Alpha"), c("Beta"), c("Maine"), c("Florida")];
let g = ground_sorted(&e, &sorts, &fallback);
assert_eq!(conjuncts(&g), 2, "should ground over the 2 trips only: {g:?}");
}
#[test]
fn ground_problem_removes_all_quantifiers() {
let premises = vec![
pred("Trip", vec![c("Alpha")]),
forall(
"x",
ProofExpr::Implies(
Box::new(pred("Trip", vec![v("x")])),
Box::new(pred("In", vec![v("x"), c("Maine")])),
),
),
];
let goal = pred("In", vec![c("Alpha"), c("Maine")]);
let (gp, gg) = ground_problem(&premises, &goal);
assert!(
gp.iter().all(is_quantifier_free) && is_quantifier_free(&gg),
"ground_problem must remove every quantifier"
);
}
}