Skip to main content

logicaffeine_proof/
lemma_index.rs

1//! `exact?` / `apply?` and premise selection: search over a NAMED, certified
2//! lemma library.
3//!
4//! Each lemma is peeled to `∀xs. A₁ → … → Aₖ → C`; its conclusion `C` is indexed
5//! in a discrimination tree ([`crate::discrimination`]). A query matches the
6//! goal against candidate conclusions with the one-sided matcher
7//! ([`crate::unify::match_expr_pattern`]) — so a quantified lemma instantiates
8//! at the (possibly ground) goal for free. `find_exact` reports lemmas whose
9//! conclusion IS the goal; `find_apply` reports lemmas whose conclusion matches
10//! and lists the antecedents you would still owe. `select_premises` ranks the
11//! whole library for relevance, the premise filter that keeps `auto` tractable
12//! on a large axiom base.
13//!
14//! This is the direct answer to Lean's `exact?`/`apply?` and to premise
15//! selection — search over a citable, kernel-checkable library rather than an
16//! opaque model.
17
18use crate::discrimination::DiscTree;
19use crate::unify::{apply_subst_to_expr, match_expr_pattern, Substitution};
20use crate::{ProofExpr, ProofTerm};
21
22/// A search result: which lemma, the suggested tactic text, the instantiation,
23/// and (for `apply?`) the antecedents still to prove.
24#[derive(Debug, Clone)]
25pub struct Suggestion {
26    pub lemma: String,
27    pub tactic_text: String,
28    pub subst: Substitution,
29    pub remaining: Vec<ProofExpr>,
30}
31
32/// A lemma peeled into binders, antecedents, and conclusion.
33struct CompiledLemma {
34    name: String,
35    conclusion: ProofExpr,
36    premises: Vec<ProofExpr>,
37}
38
39/// An index over a named lemma library.
40pub struct LemmaIndex {
41    lemmas: Vec<CompiledLemma>,
42    concl_index: DiscTree<usize>,
43}
44
45impl LemmaIndex {
46    /// Build the index from named lemmas (axioms and proved theorems alike).
47    pub fn build(named: &[(String, ProofExpr)]) -> Self {
48        let mut lemmas = Vec::new();
49        let mut concl_index = DiscTree::new();
50        for (name, formula) in named {
51            let (premises, conclusion) = peel(formula);
52            let idx = lemmas.len();
53            concl_index.insert_expr(&conclusion, idx);
54            lemmas.push(CompiledLemma { name: name.clone(), conclusion, premises });
55        }
56        LemmaIndex { lemmas, concl_index }
57    }
58
59    /// Lemmas whose conclusion IS the goal (an instance of it). Ranked most-
60    /// specific first (fewest variable bindings). `exact?`.
61    pub fn find_exact(&self, goal: &ProofExpr) -> Vec<Suggestion> {
62        let mut out = self.matches(goal, "exact");
63        out.sort_by_key(|s| s.subst.len());
64        out
65    }
66
67    /// Lemmas whose conclusion matches the goal AND have antecedents — applying
68    /// them reduces the goal to those (instantiated) antecedents. `apply?`.
69    pub fn find_apply(&self, goal: &ProofExpr) -> Vec<Suggestion> {
70        let mut out: Vec<Suggestion> =
71            self.matches(goal, "apply").into_iter().filter(|s| !s.remaining.is_empty()).collect();
72        out.sort_by_key(|s| (s.remaining.len(), s.subst.len()));
73        out
74    }
75
76    /// Rank the whole library by relevance to `goal` and return the top `k`
77    /// names — the premise filter for `auto` on a large base. Relevance is: an
78    /// exact/apply conclusion match first, then head-symbol overlap; lemmas
79    /// sharing no symbol with the goal are dropped entirely.
80    pub fn select_premises(&self, goal: &ProofExpr, k: usize) -> Vec<String> {
81        let goal_syms = expr_symbols(goal);
82        let matching: std::collections::HashSet<usize> =
83            self.concl_index.candidates_expr(goal).into_iter().copied().collect();
84
85        let mut scored: Vec<(i64, usize)> = self
86            .lemmas
87            .iter()
88            .enumerate()
89            .filter_map(|(i, lem)| {
90                let overlap = expr_symbols(&lem.conclusion)
91                    .intersection(&goal_syms)
92                    .count() as i64;
93                if overlap == 0 && !matching.contains(&i) {
94                    return None; // no shared structure — irrelevant
95                }
96                let mut score = overlap;
97                if matching.contains(&i) {
98                    score += 100; // a conclusion match dominates
99                }
100                Some((score, i))
101            })
102            .collect();
103        scored.sort_by(|a, b| b.0.cmp(&a.0));
104        scored.truncate(k);
105        scored.into_iter().map(|(_, i)| self.lemmas[i].name.clone()).collect()
106    }
107
108    fn matches(&self, goal: &ProofExpr, verb: &str) -> Vec<Suggestion> {
109        let mut out = Vec::new();
110        let mut cand: Vec<usize> =
111            self.concl_index.candidates_expr(goal).into_iter().copied().collect();
112        cand.sort_unstable();
113        cand.dedup();
114        for i in cand {
115            let lem = &self.lemmas[i];
116            if let Some(subst) = match_expr_pattern(&lem.conclusion, goal) {
117                let remaining =
118                    lem.premises.iter().map(|p| apply_subst_to_expr(p, &subst)).collect();
119                out.push(Suggestion {
120                    lemma: lem.name.clone(),
121                    tactic_text: format!("{verb} {}", lem.name),
122                    subst,
123                    remaining,
124                });
125            }
126        }
127        out
128    }
129}
130
131/// Peel `∀xs. A₁ → … → Aₖ → C` into `([A₁, …, Aₖ], C)`.
132fn peel(formula: &ProofExpr) -> (Vec<ProofExpr>, ProofExpr) {
133    let mut body = formula;
134    while let ProofExpr::ForAll { body: inner, .. } = body {
135        body = inner;
136    }
137    let mut premises = Vec::new();
138    while let ProofExpr::Implies(a, rest) = body {
139        premises.push((**a).clone());
140        body = rest;
141    }
142    (premises, body.clone())
143}
144
145/// The set of predicate/function/atom head symbols mentioned in an expression —
146/// the coarse relevance signature for premise selection.
147fn expr_symbols(e: &ProofExpr) -> std::collections::HashSet<String> {
148    let mut out = std::collections::HashSet::new();
149    collect_expr_symbols(e, &mut out);
150    out
151}
152
153fn collect_expr_symbols(e: &ProofExpr, out: &mut std::collections::HashSet<String>) {
154    match e {
155        ProofExpr::Predicate { name, args, .. } => {
156            out.insert(format!("p:{name}"));
157            for a in args {
158                collect_term_symbols(a, out);
159            }
160        }
161        ProofExpr::Atom(s) => {
162            out.insert(format!("a:{s}"));
163        }
164        ProofExpr::Identity(l, r) => {
165            out.insert("=".to_string());
166            collect_term_symbols(l, out);
167            collect_term_symbols(r, out);
168        }
169        ProofExpr::And(l, r)
170        | ProofExpr::Or(l, r)
171        | ProofExpr::Implies(l, r)
172        | ProofExpr::Iff(l, r) => {
173            collect_expr_symbols(l, out);
174            collect_expr_symbols(r, out);
175        }
176        ProofExpr::Not(p) => collect_expr_symbols(p, out),
177        ProofExpr::ForAll { body, .. } | ProofExpr::Exists { body, .. } => {
178            collect_expr_symbols(body, out)
179        }
180        _ => {}
181    }
182}
183
184fn collect_term_symbols(t: &ProofTerm, out: &mut std::collections::HashSet<String>) {
185    match t {
186        ProofTerm::Constant(s) => {
187            out.insert(format!("c:{s}"));
188        }
189        ProofTerm::Function(name, args) => {
190            out.insert(format!("f:{name}"));
191            for a in args {
192                collect_term_symbols(a, out);
193            }
194        }
195        // Variables carry no relevance signal.
196        _ => {}
197    }
198}