Skip to main content

logicaffeine_proof/
crush.rs

1//! `crush` — the grind-style closer: E-match quantified equality lemmas into
2//! the ground e-graph, then discharge the goal by certified congruence closure.
3//!
4//! Lean's `grind` fuses an e-graph, E-matching of annotated lemmas, and theory
5//! solvers. This is the congruence+instantiation core of that, built entirely
6//! on machinery that already certifies: the ground equality graph and its
7//! transitive/congruence explanations ([`crate::engine::cert_congruence_path`]),
8//! and the one-sided matcher that fires a `∀`-lemma at a ground subterm
9//! ([`crate::unify::match_term_pattern`]). Each instantiation is a
10//! [`InferenceRule::UniversalInstTerm`] chain from the lemma; the final goal
11//! closes by rewriting a known atom through the congruence-derived equalities.
12//! The whole result is one kernel-checked derivation — unlike `grind`, whose
13//! core is trusted.
14//!
15//! Signature demo (which plain `auto` cannot do): `∀x. f(x)=g(x)`, `a=b`,
16//! `P(g(b))` ⊢ `P(f(a))`.
17
18use std::collections::HashSet;
19
20use crate::engine::cert_congruence_path;
21use crate::unify::{apply_subst_to_expr, match_term_pattern, Substitution};
22use crate::{DerivationTree, InferenceRule, ProofExpr, ProofTerm};
23
24/// Prove `goal` from `premises` by E-matching the `∀`-equality lemmas among
25/// them and closing by congruence. Returns a kernel-checkable derivation, or
26/// `None` when this fragment cannot close the goal.
27pub(crate) fn crush_prove(premises: &[ProofExpr], goal: &ProofExpr) -> Option<DerivationTree> {
28    // Ground facts (with PremiseMatch proofs) and the `∀`-equality lemmas.
29    let mut known: Vec<(ProofExpr, DerivationTree)> = Vec::new();
30    let mut eq_lemmas: Vec<&ProofExpr> = Vec::new();
31    for prem in premises {
32        if is_forall_eq_lemma(prem) {
33            eq_lemmas.push(prem);
34        } else {
35            known.push((prem.clone(), DerivationTree::leaf(prem.clone(), InferenceRule::PremiseMatch)));
36        }
37    }
38
39    // The ground term universe: every subterm of a ground fact or the goal.
40    let mut universe: Vec<ProofTerm> = Vec::new();
41    for (prop, _) in &known {
42        collect_expr_terms(prop, &mut universe);
43    }
44    collect_expr_terms(goal, &mut universe);
45    dedup_terms(&mut universe);
46
47    // E-match each lemma's LHS against ground subterms; add the instances.
48    for lemma in &eq_lemmas {
49        let (params, lhs, _rhs) = peel_eq_lemma(lemma).expect("checked by is_forall_eq_lemma");
50        let mut fired: HashSet<String> = HashSet::new();
51        for g in &universe {
52            let Some(subst) = match_term_pattern(&lhs, g) else { continue };
53            if !params.iter().all(|p| subst.contains_key(p)) {
54                continue; // trigger did not pin every variable
55            }
56            let key = format!("{g}");
57            if !fired.insert(key) {
58                continue;
59            }
60            if let Some(inst) = instantiate_eq_lemma(lemma, &params, &subst) {
61                known.push(inst);
62            }
63        }
64    }
65
66    // Close.
67    match goal {
68        ProofExpr::Identity(a, b) => cert_congruence_path(a, b, &known),
69        ProofExpr::Predicate { name, args, world } => {
70            close_predicate(name, args, world.as_deref(), &known)
71        }
72        _ => None,
73    }
74}
75
76/// Prove `P(s̄)` from a known atom `P(t̄)` and the equalities in `known`: rewrite
77/// each argument `tᵢ ⇝ sᵢ` (justified by a congruence path) one at a time,
78/// starting from the known atom.
79fn close_predicate(
80    name: &str,
81    goal_args: &[ProofTerm],
82    world: Option<&str>,
83    known: &[(ProofExpr, DerivationTree)],
84) -> Option<DerivationTree> {
85    let pred = |args: &[ProofTerm]| ProofExpr::Predicate {
86        name: name.to_string(),
87        args: args.to_vec(),
88        world: world.map(str::to_string),
89    };
90    for (prop, atom_proof) in known {
91        let ProofExpr::Predicate { name: kn, args: kargs, world: kw } = prop else { continue };
92        if kn != name || kargs.len() != goal_args.len() || kw.as_deref() != world {
93            continue;
94        }
95        // Rewrite kargs → goal_args argument by argument.
96        let mut cur = kargs.clone();
97        let mut proof = atom_proof.clone();
98        let mut ok = true;
99        for i in 0..goal_args.len() {
100            if cur[i] == goal_args[i] {
101                continue;
102            }
103            // A proof `cur[i] = goal_args[i]` from the e-graph.
104            let Some(eq) = cert_congruence_path(&cur[i], &goal_args[i], known) else {
105                ok = false;
106                break;
107            };
108            let mut next = cur.clone();
109            next[i] = goal_args[i].clone();
110            // Forward Rewrite (as `build_congruence_proof`): the equality proves
111            // `from = to`, the source proves `P(cur)`, the conclusion is `P(next)`.
112            proof = DerivationTree::new(
113                pred(&next),
114                InferenceRule::Rewrite { from: cur[i].clone(), to: goal_args[i].clone() },
115                vec![eq, proof],
116            );
117            cur = next;
118        }
119        if ok && cur == goal_args {
120            return Some(proof);
121        }
122    }
123    None
124}
125
126/// A premise of shape `∀xs. lhs = rhs`.
127fn is_forall_eq_lemma(e: &ProofExpr) -> bool {
128    peel_eq_lemma(e).is_some()
129}
130
131/// Peel `∀xs. lhs = rhs` into `(xs, lhs, rhs)`; `None` if it is not that shape
132/// (must have at least one binder and an equational body).
133fn peel_eq_lemma(e: &ProofExpr) -> Option<(Vec<String>, ProofTerm, ProofTerm)> {
134    let mut params = Vec::new();
135    let mut body = e;
136    while let ProofExpr::ForAll { variable, body: inner } = body {
137        params.push(variable.clone());
138        body = inner;
139    }
140    if params.is_empty() {
141        return None;
142    }
143    match body {
144        ProofExpr::Identity(l, r) => Some((params, l.clone(), r.clone())),
145        _ => None,
146    }
147}
148
149/// Instantiate `∀xs. body` at `subst`, producing the ground fact and its
150/// `UniversalInstTerm`-chain proof from the lemma's `PremiseMatch`.
151fn instantiate_eq_lemma(
152    lemma: &ProofExpr,
153    params: &[String],
154    subst: &Substitution,
155) -> Option<(ProofExpr, DerivationTree)> {
156    let mut tree = DerivationTree::leaf(lemma.clone(), InferenceRule::PremiseMatch);
157    let mut expr = lemma.clone();
158    for param in params {
159        let witness = subst.get(param)?.clone();
160        let ProofExpr::ForAll { variable, body } = expr else { return None };
161        debug_assert_eq!(&variable, param);
162        let mut single = Substitution::new();
163        single.insert(param.clone(), witness.clone());
164        let inst = apply_subst_to_expr(&body, &single);
165        tree = DerivationTree::new(
166            inst.clone(),
167            InferenceRule::UniversalInstTerm(witness),
168            vec![tree],
169        );
170        expr = inst;
171    }
172    Some((expr, tree))
173}
174
175fn collect_expr_terms(e: &ProofExpr, out: &mut Vec<ProofTerm>) {
176    match e {
177        ProofExpr::Predicate { args, .. } => {
178            for a in args {
179                collect_term(a, out);
180            }
181        }
182        ProofExpr::Identity(l, r) => {
183            collect_term(l, out);
184            collect_term(r, out);
185        }
186        ProofExpr::And(l, r)
187        | ProofExpr::Or(l, r)
188        | ProofExpr::Implies(l, r)
189        | ProofExpr::Iff(l, r) => {
190            collect_expr_terms(l, out);
191            collect_expr_terms(r, out);
192        }
193        ProofExpr::Not(p) => collect_expr_terms(p, out),
194        ProofExpr::ForAll { body, .. } | ProofExpr::Exists { body, .. } => {
195            collect_expr_terms(body, out)
196        }
197        _ => {}
198    }
199}
200
201fn collect_term(t: &ProofTerm, out: &mut Vec<ProofTerm>) {
202    out.push(t.clone());
203    if let ProofTerm::Function(_, args) | ProofTerm::Group(args) = t {
204        for a in args {
205            collect_term(a, out);
206        }
207    }
208}
209
210fn dedup_terms(v: &mut Vec<ProofTerm>) {
211    let mut seen = HashSet::new();
212    v.retain(|t| seen.insert(format!("{t}")));
213}