Skip to main content

rlean_search/
search.rs

1//! Fast type-aware pattern matching with holes and named holes.
2
3use crate::ast::{Declaration, TypeExpr};
4use crate::parser::{parse_search_pattern, SearchPattern};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8/// One search hit.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct SearchHit {
11    pub name: String,
12    pub full_name: String,
13    pub kind: String,
14    pub type_surface: String,
15    pub file: String,
16    pub line: usize,
17    pub score: i32,
18}
19
20/// Match pattern against a type expression with unification for named holes.
21pub fn matches_type(pattern: &TypeExpr, target: &TypeExpr) -> bool {
22    let mut env: HashMap<String, TypeExpr> = HashMap::new();
23    unify(pattern, target, &mut env)
24}
25
26fn unify(pat: &TypeExpr, target: &TypeExpr, env: &mut HashMap<String, TypeExpr>) -> bool {
27    match pat {
28        TypeExpr::Hole => true,
29        TypeExpr::NamedHole(name) => {
30            if let Some(bound) = env.get(name) {
31                // Must be structurally equal to previous binding
32                alpha_eq(bound, target)
33            } else {
34                env.insert(name.clone(), target.clone());
35                true
36            }
37        }
38        TypeExpr::Ident(a) => match target {
39            TypeExpr::Ident(b) => idents_compatible(a, b),
40            // Allow matching against projections / raw loosely? no
41            _ => false,
42        },
43        TypeExpr::NatLit(a) => matches!(target, TypeExpr::NatLit(b) if a == b),
44        TypeExpr::Literal(a) => matches!(target, TypeExpr::Literal(b) if a == b),
45        TypeExpr::App(pf, pa) => match target {
46            TypeExpr::App(tf, ta) => unify(pf, tf, env) && unify(pa, ta, env),
47            _ => false,
48        },
49        TypeExpr::BinOp {
50            op: po,
51            left: pl,
52            right: pr,
53        } => match target {
54            TypeExpr::BinOp {
55                op: to,
56                left: tl,
57                right: tr,
58            } if ops_compatible(po, to) => unify(pl, tl, env) && unify(pr, tr, env),
59            _ => false,
60        },
61        TypeExpr::UnaryOp { op: po, arg: pa } => match target {
62            TypeExpr::UnaryOp { op: to, arg: ta } if ops_compatible(po, to) => {
63                unify(pa, ta, env)
64            }
65            _ => false,
66        },
67        TypeExpr::Postfix { arg: pa, op: po } => match target {
68            TypeExpr::Postfix { arg: ta, op: to } if ops_compatible(po, to) => {
69                unify(pa, ta, env)
70            }
71            _ => false,
72        },
73        TypeExpr::Arrow(pa, pb) => match target {
74            TypeExpr::Arrow(ta, tb) => unify(pa, ta, env) && unify(pb, tb, env),
75            TypeExpr::Pi { binder, body } => {
76                // Match domain against binder type if present
77                let domain_ok = match &binder.ty {
78                    Some(ty) => unify(pa, ty, env),
79                    None => true,
80                };
81                domain_ok && unify(pb, body, env)
82            }
83            _ => false,
84        },
85        TypeExpr::Forall { body: pb, .. } => {
86            // Pattern quantifiers: match body against target conclusion-ish or full forall
87            match target {
88                TypeExpr::Forall { body: tb, .. } => unify(pb, tb, env) || unify(pb, target, env),
89                _ => unify(pb, target, env),
90            }
91        }
92        TypeExpr::Exists { body: pb, .. } => match target {
93            TypeExpr::Exists { body: tb, .. } => unify(pb, tb, env),
94            _ => unify(pb, target, env),
95        },
96        TypeExpr::Lambda { body: pb, .. } => match target {
97            TypeExpr::Lambda { body: tb, .. } => unify(pb, tb, env),
98            _ => false,
99        },
100        TypeExpr::Pi {
101            binder: _,
102            body: pb,
103        } => match target {
104            TypeExpr::Pi { body: tb, .. } => unify(pb, tb, env),
105            TypeExpr::Arrow(_, tb) => unify(pb, tb, env),
106            _ => unify(pb, target, env),
107        },
108        TypeExpr::Proj {
109            base: pb,
110            field: pf,
111        } => match target {
112            TypeExpr::Proj {
113                base: tb,
114                field: tf,
115            } if pf == tf => unify(pb, tb, env),
116            _ => false,
117        },
118        TypeExpr::Sort { name: pn, level: pl } => match target {
119            TypeExpr::Sort { name: tn, level: tl } if pn == tn => match (pl, tl) {
120                (None, _) => true,
121                (Some(a), Some(b)) => unify(a, b, env),
122                (Some(_), None) => false,
123            },
124            _ => false,
125        },
126        TypeExpr::Raw(a) => match target {
127            TypeExpr::Raw(b) => a == b,
128            other => other.surface().contains(a.as_str()) || a == &other.surface(),
129        },
130    }
131}
132
133fn alpha_eq(a: &TypeExpr, b: &TypeExpr) -> bool {
134    match (a, b) {
135        (TypeExpr::Hole, _) | (_, TypeExpr::Hole) => true,
136        (TypeExpr::NamedHole(x), TypeExpr::NamedHole(y)) => x == y,
137        (TypeExpr::Ident(x), TypeExpr::Ident(y)) => idents_compatible(x, y),
138        (TypeExpr::NatLit(x), TypeExpr::NatLit(y)) => x == y,
139        (TypeExpr::Literal(x), TypeExpr::Literal(y)) => x == y,
140        (TypeExpr::App(f1, a1), TypeExpr::App(f2, a2)) => alpha_eq(f1, f2) && alpha_eq(a1, a2),
141        (
142            TypeExpr::BinOp {
143                op: o1,
144                left: l1,
145                right: r1,
146            },
147            TypeExpr::BinOp {
148                op: o2,
149                left: l2,
150                right: r2,
151            },
152        ) => ops_compatible(o1, o2) && alpha_eq(l1, l2) && alpha_eq(r1, r2),
153        (TypeExpr::UnaryOp { op: o1, arg: a1 }, TypeExpr::UnaryOp { op: o2, arg: a2 }) => {
154            ops_compatible(o1, o2) && alpha_eq(a1, a2)
155        }
156        (TypeExpr::Postfix { arg: a1, op: o1 }, TypeExpr::Postfix { arg: a2, op: o2 }) => {
157            ops_compatible(o1, o2) && alpha_eq(a1, a2)
158        }
159        (TypeExpr::Arrow(a1, b1), TypeExpr::Arrow(a2, b2)) => alpha_eq(a1, a2) && alpha_eq(b1, b2),
160        (TypeExpr::Forall { body: b1, .. }, TypeExpr::Forall { body: b2, .. }) => alpha_eq(b1, b2),
161        (TypeExpr::Exists { body: b1, .. }, TypeExpr::Exists { body: b2, .. }) => alpha_eq(b1, b2),
162        (TypeExpr::Pi { body: b1, .. }, TypeExpr::Pi { body: b2, .. }) => alpha_eq(b1, b2),
163        (TypeExpr::Proj { base: b1, field: f1 }, TypeExpr::Proj { base: b2, field: f2 }) => {
164            f1 == f2 && alpha_eq(b1, b2)
165        }
166        (TypeExpr::Sort { name: n1, level: l1 }, TypeExpr::Sort { name: n2, level: l2 }) => {
167            n1 == n2
168                && match (l1, l2) {
169                    (None, None) => true,
170                    (Some(a), Some(b)) => alpha_eq(a, b),
171                    _ => false,
172                }
173        }
174        (TypeExpr::Raw(x), TypeExpr::Raw(y)) => x == y,
175        _ => false,
176    }
177}
178
179fn idents_compatible(a: &str, b: &str) -> bool {
180    if a == b {
181        return true;
182    }
183    // Allow suffix match: `add` vs `Nat.add`, `HAdd.hAdd` vs etc.
184    let a_last = a.rsplit('.').next().unwrap_or(a);
185    let b_last = b.rsplit('.').next().unwrap_or(b);
186    a_last == b_last
187}
188
189fn ops_compatible(a: &str, b: &str) -> bool {
190    a == b
191}
192
193/// Does `pattern` match declaration type (or conclusion if requested)?
194pub fn matches_decl(pat: &SearchPattern, decl: &Declaration) -> bool {
195    let effective = decl.effective_type();
196    if pat.conclusion_only {
197        // Match pattern against conclusion; also try matching if pattern itself is full type
198        // whose conclusion matches.
199        let conc = effective.conclusion();
200        if matches_type(&pat.expr, conc) {
201            return true;
202        }
203        // Pattern may include binders; use its conclusion
204        if matches_type(pat.expr.conclusion(), conc) {
205            return true;
206        }
207        // Also allow matching conclusion pattern against any sub-conclusion along arrows
208        return match_any_conclusion(&pat.expr, &effective);
209    }
210
211    // Full type match, or match against conclusion (common UX: `_ + _ = _` finds theorems)
212    if matches_type(&pat.expr, &effective) {
213        return true;
214    }
215    if matches_type(&pat.expr, effective.conclusion()) {
216        return true;
217    }
218    // Match ignoring outer binders on both sides
219    if matches_type(pat.expr.conclusion(), effective.conclusion()) {
220        return true;
221    }
222    // Subterm match for patterns that are pure relational goals
223    match_subterm(&pat.expr, &effective)
224}
225
226fn match_any_conclusion(pat: &TypeExpr, ty: &TypeExpr) -> bool {
227    if matches_type(pat, ty.conclusion()) {
228        return true;
229    }
230    match ty {
231        TypeExpr::Arrow(_, r) | TypeExpr::Pi { body: r, .. } | TypeExpr::Forall { body: r, .. } => {
232            match_any_conclusion(pat, r)
233        }
234        _ => false,
235    }
236}
237
238fn match_subterm(pat: &TypeExpr, ty: &TypeExpr) -> bool {
239    if matches_type(pat, ty) {
240        return true;
241    }
242    match ty {
243        TypeExpr::App(f, a) => match_subterm(pat, f) || match_subterm(pat, a),
244        TypeExpr::BinOp { left, right, .. } => {
245            match_subterm(pat, left) || match_subterm(pat, right)
246        }
247        TypeExpr::UnaryOp { arg, .. } | TypeExpr::Postfix { arg, .. } => match_subterm(pat, arg),
248        TypeExpr::Arrow(a, b) => match_subterm(pat, a) || match_subterm(pat, b),
249        TypeExpr::Forall { body, .. }
250        | TypeExpr::Exists { body, .. }
251        | TypeExpr::Lambda { body, .. }
252        | TypeExpr::Pi { body, .. } => match_subterm(pat, body),
253        TypeExpr::Proj { base, .. } => match_subterm(pat, base),
254        _ => false,
255    }
256}
257
258/// Score a hit for ranking (higher is better).
259pub fn score_hit(pat: &SearchPattern, decl: &Declaration) -> i32 {
260    let mut score = 0;
261    let effective = decl.effective_type();
262    if matches_type(&pat.expr, effective.conclusion()) {
263        score += 100;
264    }
265    if pat.conclusion_only {
266        score += 10;
267    }
268    // Prefer shorter names slightly
269    score += 50usize.saturating_sub(decl.full_name.len()) as i32;
270    // Prefer theorems
271    score += match decl.kind {
272        crate::ast::DeclKind::Theorem => 3,
273        crate::ast::DeclKind::Lemma => 2,
274        crate::ast::DeclKind::Axiom => 1,
275    };
276    score
277}
278
279pub fn parse_pattern(input: &str) -> anyhow::Result<SearchPattern> {
280    parse_search_pattern(input).map_err(|e| anyhow::anyhow!(e.to_string()))
281}
282
283/// Candidate filtering keys from a pattern for inverted index lookup.
284pub fn pattern_index_keys(pat: &SearchPattern) -> Vec<String> {
285    let expr = if pat.conclusion_only {
286        pat.expr.conclusion()
287    } else {
288        &pat.expr
289    };
290    let mut keys = Vec::new();
291    let head = expr.head_key();
292    if !head.starts_with("hole") {
293        keys.push(head);
294    }
295    for op in expr.operators() {
296        keys.push(format!("op:{op}"));
297    }
298    // also conclusion head of full pattern
299    let ch = pat.expr.conclusion().head_key();
300    if !ch.starts_with("hole") {
301        keys.push(ch);
302    }
303    keys.sort();
304    keys.dedup();
305    keys
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311    use crate::ast::DeclKind;
312    use crate::parser::parse_type;
313
314    fn decl_with_type(surface: &str) -> Declaration {
315        let ty = parse_type(surface).unwrap_or(TypeExpr::Raw(surface.into()));
316        Declaration {
317            kind: DeclKind::Theorem,
318            name: "t".into(),
319            full_name: "T.t".into(),
320            binders: vec![],
321            ty,
322            type_surface: surface.into(),
323            file: "t.lean".into(),
324            line: 1,
325            module: None,
326            namespace_path: vec![],
327            attributes: vec![],
328        }
329    }
330
331    #[test]
332    fn hole_match_add_eq_zero() {
333        let d = decl_with_type("n + m = 0");
334        let p = parse_pattern("_ + _ = 0").unwrap();
335        assert!(matches_decl(&p, &d));
336    }
337
338    #[test]
339    fn named_hole_same() {
340        let d = decl_with_type("x - x = 0");
341        let p = parse_pattern("?a - ?a = 0").unwrap();
342        assert!(matches_decl(&p, &d));
343        let d2 = decl_with_type("x - y = 0");
344        assert!(!matches_decl(&p, &d2));
345    }
346
347    #[test]
348    fn turnstile_conclusion() {
349        let d = decl_with_type("∀ (n : Nat), n + 0 = n");
350        let p = parse_pattern("|- _ + 0 = _").unwrap();
351        assert!(matches_decl(&p, &d));
352    }
353
354    #[test]
355    fn no_match_different_op() {
356        let d = decl_with_type("n * m = 0");
357        let p = parse_pattern("_ + _ = 0").unwrap();
358        assert!(!matches_decl(&p, &d));
359    }
360}