Skip to main content

logicaffeine_proof/
simp.rs

1//! `simp` rule sets: oriented rewrite rules compiled from proved lemmas,
2//! indexed for fast lookup, instantiated by matching.
3//!
4//! A simp rule is a lemma of shape `∀x₁…xₙ. C₁ → … → Cₖ → lhs = rhs` (or
5//! `… lhs ↔ rhs`), oriented left-to-right. Orientation is enforced at
6//! registration: `vars(rhs) ⊆ vars(lhs)`, `vars(Cᵢ) ⊆ vars(lhs)`, and `lhs`
7//! is not a bare variable — so matching the `lhs` against a goal subterm
8//! determines the entire instantiation.
9//!
10//! The tactic layer ([`crate::tactic::ProofState::simp`]) drives the rewrite
11//! loop; this module finds one certified step at a time. Each step's equality
12//! carries its own derivation — `PremiseMatch` on the ∀-lemma, a
13//! [`InferenceRule::UniversalInstTerm`] chain for the instantiation,
14//! `ModusPonens` per discharged condition — so the kernel re-checks every
15//! rewrite like any hand-written proof. Nothing here is trusted.
16
17use crate::discrimination::DiscTree;
18use crate::unify::{apply_subst_to_expr, match_expr_pattern, match_term_pattern, Substitution};
19use crate::{DerivationTree, InferenceRule, ProofExpr, ProofTerm};
20
21/// The rewriting core of a rule: a term equation or a propositional
22/// equivalence.
23enum RuleCore {
24    TermEq { lhs: ProofTerm, rhs: ProofTerm },
25    PropIff { lhs: ProofExpr, rhs: ProofExpr },
26}
27
28/// A compiled simp rule: the peeled binder list, pending conditions, oriented
29/// core, and the derivation of the un-instantiated lemma (a `PremiseMatch`
30/// leaf — valid whenever the lemma is a premise or an in-scope hypothesis).
31struct CompiledRule {
32    #[allow(dead_code)]
33    name: String,
34    params: Vec<String>,
35    conds: Vec<ProofExpr>,
36    core: RuleCore,
37    lemma: ProofExpr,
38    base: DerivationTree,
39}
40
41/// An indexed set of simp rules.
42#[derive(Default)]
43pub struct SimpSet {
44    rules: Vec<CompiledRule>,
45    term_index: DiscTree<usize>,
46    iff_index: DiscTree<usize>,
47}
48
49/// One rewrite step found in a goal: `eq` proves `Identity(from, to)`.
50pub(crate) struct RewriteStep {
51    pub eq: DerivationTree,
52    pub from: ProofTerm,
53    pub to: ProofTerm,
54}
55
56/// One top-level propositional step: `imp` proves `rhs → goal`, so refining
57/// with `ModusPonens` reduces the goal to `rhs`.
58pub(crate) struct IffStep {
59    pub imp: DerivationTree,
60    pub rhs: ProofExpr,
61}
62
63impl SimpSet {
64    pub fn new() -> Self {
65        Self::default()
66    }
67
68    pub fn len(&self) -> usize {
69        self.rules.len()
70    }
71
72    pub fn is_empty(&self) -> bool {
73        self.rules.is_empty()
74    }
75
76    /// Compile and index `lemma` as a rewrite rule. Returns `false` (and
77    /// registers nothing) if the lemma is not an oriented rule: no equational
78    /// or iff core, a bare-variable lhs, rhs/condition variables the lhs does
79    /// not bind, or shadowed binder names.
80    ///
81    /// The rule's derivation base is a `PremiseMatch` leaf, so the lemma must
82    /// be in scope (a premise or hypothesis) wherever the rule fires.
83    pub fn register_lemma(&mut self, name: &str, lemma: &ProofExpr) -> bool {
84        // Peel the ∀-prefix.
85        let mut params: Vec<String> = Vec::new();
86        let mut body = lemma;
87        while let ProofExpr::ForAll { variable, body: inner } = body {
88            if params.contains(variable) {
89                return false; // shadowed binder — instantiation would be ambiguous
90            }
91            params.push(variable.clone());
92            body = inner;
93        }
94        // Peel the condition chain.
95        let mut conds: Vec<ProofExpr> = Vec::new();
96        while let ProofExpr::Implies(c, rest) = body {
97            conds.push((**c).clone());
98            body = rest;
99        }
100        let core = match body {
101            ProofExpr::Identity(l, r) => {
102                if matches!(l, ProofTerm::Variable(_)) {
103                    return false; // a bare-variable lhs matches everything
104                }
105                RuleCore::TermEq { lhs: l.clone(), rhs: r.clone() }
106            }
107            ProofExpr::Iff(l, r) => {
108                RuleCore::PropIff { lhs: (**l).clone(), rhs: (**r).clone() }
109            }
110            _ => return false,
111        };
112        // Orientation: everything the rhs and the conditions mention, the lhs
113        // must bind — matching the lhs then determines the instantiation.
114        let lhs_vars = match &core {
115            RuleCore::TermEq { lhs, .. } => term_vars(lhs),
116            RuleCore::PropIff { lhs, .. } => expr_vars(lhs),
117        };
118        let rhs_vars = match &core {
119            RuleCore::TermEq { rhs, .. } => term_vars(rhs),
120            RuleCore::PropIff { rhs, .. } => expr_vars(rhs),
121        };
122        if !rhs_vars.iter().all(|v| lhs_vars.contains(v)) {
123            return false;
124        }
125        for c in &conds {
126            if !expr_vars(c).iter().all(|v| lhs_vars.contains(v)) {
127                return false;
128            }
129        }
130        // Every parameter must actually be determined by the lhs.
131        if !params.iter().all(|p| lhs_vars.contains(p)) {
132            return false;
133        }
134
135        let idx = self.rules.len();
136        match &core {
137            RuleCore::TermEq { lhs, .. } => self.term_index.insert_term(lhs, idx),
138            RuleCore::PropIff { lhs, .. } => self.iff_index.insert_expr(lhs, idx),
139        }
140        self.rules.push(CompiledRule {
141            name: name.to_string(),
142            params,
143            conds,
144            core,
145            lemma: lemma.clone(),
146            base: DerivationTree::leaf(lemma.clone(), InferenceRule::PremiseMatch),
147        });
148        true
149    }
150
151    /// Find one term-rewrite step in `goal`, innermost subterms first.
152    /// `hyps` are the in-scope facts (proposition + its derivation), used to
153    /// discharge rule conditions.
154    pub(crate) fn find_term_step(
155        &self,
156        goal: &ProofExpr,
157        hyps: &[(&ProofExpr, &DerivationTree)],
158    ) -> Option<RewriteStep> {
159        let mut subterms = Vec::new();
160        collect_goal_terms(goal, &mut subterms);
161        for t in subterms {
162            let mut cand: Vec<usize> = self.term_index.candidates_term(t).into_iter().copied().collect();
163            cand.sort_unstable();
164            cand.dedup();
165            for idx in cand {
166                if let Some(step) = self.try_term_rule(idx, t, hyps) {
167                    return Some(step);
168                }
169            }
170        }
171        None
172    }
173
174    /// Find a top-level propositional step: an iff rule whose lhs matches the
175    /// whole goal.
176    pub(crate) fn find_iff_step(
177        &self,
178        goal: &ProofExpr,
179        hyps: &[(&ProofExpr, &DerivationTree)],
180    ) -> Option<IffStep> {
181        let mut cand: Vec<usize> = self.iff_index.candidates_expr(goal).into_iter().copied().collect();
182        cand.sort_unstable();
183        cand.dedup();
184        for idx in cand {
185            let rule = &self.rules[idx];
186            let RuleCore::PropIff { lhs, .. } = &rule.core else { continue };
187            let Some(subst) = match_expr_pattern(lhs, goal) else { continue };
188            let Some((tree, concl)) = instantiate_rule(rule, &subst, hyps) else { continue };
189            let ProofExpr::Iff(l, r) = &concl else { continue };
190            if l.as_ref() != goal {
191                continue;
192            }
193            // Project the right-to-left implication (Iff ≡ And(→, ←)).
194            let imp = DerivationTree::new(
195                ProofExpr::Implies(r.clone(), l.clone()),
196                InferenceRule::ConjunctionElim,
197                vec![tree],
198            );
199            return Some(IffStep { imp, rhs: (**r).clone() });
200        }
201        None
202    }
203
204    fn try_term_rule(
205        &self,
206        idx: usize,
207        t: &ProofTerm,
208        hyps: &[(&ProofExpr, &DerivationTree)],
209    ) -> Option<RewriteStep> {
210        let rule = &self.rules[idx];
211        let RuleCore::TermEq { lhs, rhs } = &rule.core else { return None };
212        let subst = match_term_pattern(lhs, t)?;
213        let (tree, concl) = instantiate_rule(rule, &subst, hyps)?;
214        let ProofExpr::Identity(from, to) = concl else { return None };
215        if &from != t {
216            return None;
217        }
218        // A no-op rewrite (t = t) makes no progress and would loop.
219        if from == to {
220            return None;
221        }
222        Some(RewriteStep { eq: tree, from, to })
223    }
224}
225
226/// Instantiate a compiled rule under `subst`: chain `UniversalInstTerm` for
227/// each parameter, then discharge each condition by `ModusPonens` against the
228/// in-scope facts (or ground arithmetic). Returns the derivation and its
229/// conclusion (the instantiated core).
230fn instantiate_rule(
231    rule: &CompiledRule,
232    subst: &Substitution,
233    hyps: &[(&ProofExpr, &DerivationTree)],
234) -> Option<(DerivationTree, ProofExpr)> {
235    let mut tree = rule.base.clone();
236    let mut expr = rule.lemma.clone();
237    for param in &rule.params {
238        let witness = subst.get(param)?.clone();
239        let ProofExpr::ForAll { variable, body } = expr else { return None };
240        debug_assert_eq!(&variable, param);
241        let mut single = Substitution::new();
242        single.insert(param.clone(), witness.clone());
243        let inst = apply_subst_to_expr(&body, &single);
244        tree = DerivationTree::new(
245            inst.clone(),
246            InferenceRule::UniversalInstTerm(witness),
247            vec![tree],
248        );
249        expr = inst;
250    }
251    for _ in 0..rule.conds.len() {
252        let ProofExpr::Implies(cond, rest) = expr else { return None };
253        let cond_proof = discharge_condition(&cond, hyps)?;
254        tree = DerivationTree::new(
255            (*rest).clone(),
256            InferenceRule::ModusPonens,
257            vec![tree, cond_proof],
258        );
259        expr = *rest;
260    }
261    Some((tree, expr))
262}
263
264/// Discharge an instantiated condition: an in-scope fact that matches it
265/// exactly, or a ground arithmetic identity. Depth-1 by design — conditions
266/// do not recursively invoke the rewriter.
267fn discharge_condition(
268    cond: &ProofExpr,
269    hyps: &[(&ProofExpr, &DerivationTree)],
270) -> Option<DerivationTree> {
271    if let Some((_, proof)) = hyps.iter().find(|(prop, _)| *prop == cond) {
272        return Some((*proof).clone());
273    }
274    if let ProofExpr::Identity(l, r) = cond {
275        if ground_int(l).is_some() && ground_int(r).is_some() {
276            return Some(DerivationTree::leaf(cond.clone(), InferenceRule::ArithDecision));
277        }
278    }
279    None
280}
281
282/// Find one ground arithmetic fold (`add(2, 3) ⇝ 5`), innermost first. The
283/// equality is an `ArithDecision` leaf — the certifier re-runs the arithmetic
284/// oracle and the kernel checks the resulting term.
285pub(crate) fn find_ground_fold(goal: &ProofExpr) -> Option<RewriteStep> {
286    let mut subterms = Vec::new();
287    collect_goal_terms(goal, &mut subterms);
288    for t in subterms {
289        let ProofTerm::Function(op, args) = t else { continue };
290        if args.len() != 2 {
291            continue;
292        }
293        let (Some(a), Some(b)) = (ground_int(&args[0]), ground_int(&args[1])) else {
294            continue;
295        };
296        let result = match op.as_str() {
297            "add" => a.checked_add(b),
298            "sub" => a.checked_sub(b),
299            "mul" => a.checked_mul(b),
300            _ => None,
301        }?;
302        let to = ProofTerm::Constant(result.to_string());
303        if &to == t {
304            continue;
305        }
306        let eq = DerivationTree::leaf(
307            ProofExpr::Identity(t.clone(), to.clone()),
308            InferenceRule::ArithDecision,
309        );
310        return Some(RewriteStep { eq, from: t.clone(), to });
311    }
312    None
313}
314
315fn ground_int(t: &ProofTerm) -> Option<i64> {
316    match t {
317        ProofTerm::Constant(s) => s.parse::<i64>().ok(),
318        _ => None,
319    }
320}
321
322/// The subterms of a goal in post-order (innermost first), without duplicates.
323fn collect_goal_terms<'a>(e: &'a ProofExpr, out: &mut Vec<&'a ProofTerm>) {
324    fn walk_term<'a>(t: &'a ProofTerm, out: &mut Vec<&'a ProofTerm>) {
325        match t {
326            ProofTerm::Function(_, args) | ProofTerm::Group(args) => {
327                for a in args {
328                    walk_term(a, out);
329                }
330            }
331            _ => {}
332        }
333        if !out.contains(&t) {
334            out.push(t);
335        }
336    }
337    match e {
338        ProofExpr::Predicate { args, .. } => {
339            for a in args {
340                walk_term(a, out);
341            }
342        }
343        ProofExpr::Identity(l, r) => {
344            walk_term(l, out);
345            walk_term(r, out);
346        }
347        ProofExpr::And(l, r)
348        | ProofExpr::Or(l, r)
349        | ProofExpr::Implies(l, r)
350        | ProofExpr::Iff(l, r) => {
351            collect_goal_terms(l, out);
352            collect_goal_terms(r, out);
353        }
354        ProofExpr::Not(p) => collect_goal_terms(p, out),
355        ProofExpr::ForAll { body, .. } | ProofExpr::Exists { body, .. } => {
356            collect_goal_terms(body, out)
357        }
358        _ => {}
359    }
360}
361
362fn term_vars(t: &ProofTerm) -> Vec<String> {
363    let mut out = Vec::new();
364    collect_term_vars(t, &mut out);
365    out
366}
367
368fn collect_term_vars(t: &ProofTerm, out: &mut Vec<String>) {
369    match t {
370        ProofTerm::Variable(n) => {
371            if !out.contains(n) {
372                out.push(n.clone());
373            }
374        }
375        ProofTerm::Function(_, args) | ProofTerm::Group(args) => {
376            for a in args {
377                collect_term_vars(a, out);
378            }
379        }
380        _ => {}
381    }
382}
383
384fn expr_vars(e: &ProofExpr) -> Vec<String> {
385    let mut out = Vec::new();
386    collect_expr_vars(e, &mut out);
387    out
388}
389
390fn collect_expr_vars(e: &ProofExpr, out: &mut Vec<String>) {
391    match e {
392        ProofExpr::Predicate { args, .. } => {
393            for a in args {
394                collect_term_vars(a, out);
395            }
396        }
397        ProofExpr::Identity(l, r) => {
398            collect_term_vars(l, out);
399            collect_term_vars(r, out);
400        }
401        ProofExpr::And(l, r)
402        | ProofExpr::Or(l, r)
403        | ProofExpr::Implies(l, r)
404        | ProofExpr::Iff(l, r) => {
405            collect_expr_vars(l, out);
406            collect_expr_vars(r, out);
407        }
408        ProofExpr::Not(p) => collect_expr_vars(p, out),
409        ProofExpr::ForAll { variable, body } | ProofExpr::Exists { variable, body } => {
410            let mut inner = Vec::new();
411            collect_expr_vars(body, &mut inner);
412            for v in inner {
413                if v != *variable && !out.contains(&v) {
414                    out.push(v);
415                }
416            }
417        }
418        _ => {}
419    }
420}
421