Skip to main content

logicaffeine_kernel/
equations.rs

1//! Equation lemmas — Lean's `f.eq_def`, `f.eq_1 … f.eq_n` — the NAME a proof cites when it appeals to a
2//! definition's behaviour, which is what `rw [f]` / `simp [f]` / `unfold f` all need.
3//!
4//! A LOGOS definition is δ-transparent and its `Fix`/`Match` ι-compute, so the facts about a
5//! recursive function are all TRUE by conversion the moment it is declared: `dbl 3 ≡ 6`, and
6//! `dbl (Succ n) ≡ Succ (Succ (dbl n))` for a free `n`. What was missing is a name. Every appeal
7//! to a definition in a real proof — `rw [f]`, `simp [f]`, `unfold f` — cites a CONSTANT, and
8//! without one every such appeal degrades to a full `normalize` whose fuel silently returns a
9//! partially reduced term when it runs out.
10//!
11//! # What Lean does, and where
12//!
13//! * **The names.** `Lean/Meta/Eqns.lean:59-68` fixes them: `eqnThmSuffixBase := "eq"`,
14//!   `eqn1ThmSuffix := "eq_1"`, `unfoldThmSuffix := "eq_def"`. In Lean's hierarchical namespace
15//!   those are `f.eq_def` and `f.eq_1 … f.eq_n`; LOGOS's context is flat, so the same suffixes
16//!   are spelled `f_eq_def` and `f_eq_1 … f_eq_n`.
17//! * **The shape and the proof.** `Lean/Meta/Eqns.lean:149-162` (`mkSimpleEqThm`) telescopes the
18//!   definition's VALUE, states `∀ xs, f xs = body`, and proves it with `Eq.refl (f xs)` under
19//!   the same telescope. That is exactly the shape built here: the theorem is true by conversion,
20//!   so `refl` is the whole proof, and the kernel's own registration gateway re-checks it.
21//! * **When.** `Lean/Meta/Eqns.lean:142-144` — *"We generate the equations on demand"* — via
22//!   `getEqnsFor?` and the reserved-name action at `Lean/Meta/Eqns.lean:281-289`. LOGOS generates
23//!   EAGERLY instead, and the reason is a signature rather than a preference:
24//!   `Context::get_global` returns `Option<&Term>`, a reference into the context, so a lookup
25//!   cannot synthesise a term it does not already hold. Eager generation costs one `refl` per
26//!   definition and makes the lemma visible to every reader, including `Check`.
27//!
28//! # The three hazards, and how each is closed
29//!
30//! * **Re-entrancy.** A lemma is itself a definition, so generating one would generate its own.
31//!   [`is_equation_name`] is checked first and an equation name generates nothing.
32//! * **Bootstrap.** `Eq` and `refl` are registered part-way through the prelude
33//!   (`prelude.rs::register_eq`), so everything declared before them has no equality to state an
34//!   equation IN. Generation is a no-op while they are absent, and
35//!   [`generate_missing_for_all`] back-fills once they exist.
36//! * **Displacement.** `Context::declare` displaces the name it registers. A lemma is skipped
37//!   whenever its name is already taken, so generation can never remove something a user wrote.
38
39use crate::type_checker::substitute;
40use crate::{Context, Term};
41
42/// Lean's `unfoldThmSuffix` (`Lean/Meta/Eqns.lean:68`), flattened.
43pub const UNFOLD_SUFFIX: &str = "_eq_def";
44
45/// Lean's `eqnThmSuffixBasePrefix` (`Lean/Meta/Eqns.lean:60`), flattened.
46pub const EQUATION_INFIX: &str = "_eq_";
47
48/// Whether a name is one this module MINTS, rather than one it generates lemmas for.
49///
50/// This is Lean's `isEqnReservedNameSuffix` (`Lean/Meta/Eqns.lean:63-66`) — the `eq_<digits>`
51/// test — plus the unfold suffix. It is the re-entrancy guard: without it the first lemma
52/// generated would ask for its own lemmas, for ever.
53pub fn is_equation_name(name: &str) -> bool {
54    if name.ends_with(UNFOLD_SUFFIX) {
55        return true;
56    }
57    match name.rfind(EQUATION_INFIX) {
58        Some(at) => {
59            let (head, tail) = name.split_at(at);
60            let idx = &tail[EQUATION_INFIX.len()..];
61            !head.is_empty() && !idx.is_empty() && idx.bytes().all(|b| b.is_ascii_digit())
62        }
63        None => false,
64    }
65}
66
67/// `Eq A lhs rhs`.
68fn eq_prop(carrier: &Term, lhs: &Term, rhs: &Term) -> Term {
69    Term::App(
70        Box::new(Term::App(
71            Box::new(Term::App(
72                Box::new(Term::Global("Eq".to_string())),
73                Box::new(carrier.clone()),
74            )),
75            Box::new(lhs.clone()),
76        )),
77        Box::new(rhs.clone()),
78    )
79}
80
81/// `refl A x` — the proof of every equation here, because every equation here is true by
82/// conversion. Lean's `mkEqRefl` at `Lean/Meta/Eqns.lean:156`.
83fn refl_proof(carrier: &Term, at: &Term) -> Term {
84    Term::App(
85        Box::new(Term::App(
86            Box::new(Term::Global("refl".to_string())),
87            Box::new(carrier.clone()),
88        )),
89        Box::new(at.clone()),
90    )
91}
92
93fn pi_over(binders: &[(String, Term)], body: Term) -> Term {
94    binders.iter().rev().fold(body, |acc, (n, t)| Term::Pi {
95        param: n.clone(),
96        param_type: Box::new(t.clone()),
97        body_type: Box::new(acc),
98    })
99}
100
101fn lam_over(binders: &[(String, Term)], body: Term) -> Term {
102    binders.iter().rev().fold(body, |acc, (n, t)| Term::Lambda {
103        param: n.clone(),
104        param_type: Box::new(t.clone()),
105        body: Box::new(acc),
106    })
107}
108
109fn apply_all(head: Term, args: &[Term]) -> Term {
110    args.iter()
111        .fold(head, |acc, a| Term::App(Box::new(acc), Box::new(a.clone())))
112}
113
114/// Apply a case's λ-telescope to the fields, β-reducing as it goes.
115///
116/// A `Term::Match` case IS a λ-telescope over its constructor's fields, so applying it to the
117/// field binders is correct either way — but `dbl_eq_2 : dbl (Succ k) = (λk. Succ (Succ (dbl k))) k`
118/// is a redex on the right-hand side, and Lean's `dbl.eq_2` reads `dbl n.succ = (dbl n).succ.succ`.
119/// A lemma is cited by `rw`, which matches its shape, so the shape is part of the lemma.
120fn apply_case(case: &Term, args: &[Term]) -> Term {
121    let mut out = case.clone();
122    let mut rest = args;
123    while let (Term::Lambda { param, body, .. }, Some((arg, tail))) = (&out, rest.split_first()) {
124        let next = substitute(body, param, arg);
125        out = next;
126        rest = tail;
127    }
128    apply_all(out, rest)
129}
130
131/// Every name a term spells, so a generated binder can be stepped clear of all of them.
132fn spelled(term: &Term, out: &mut Vec<String>) {
133    match term {
134        Term::Var(n) | Term::Global(n) => out.push(n.clone()),
135        Term::Const { name, .. } => out.push(name.clone()),
136        Term::App(f, x) => {
137            spelled(f, out);
138            spelled(x, out);
139        }
140        Term::Pi {
141            param,
142            param_type,
143            body_type,
144        } => {
145            out.push(param.clone());
146            spelled(param_type, out);
147            spelled(body_type, out);
148        }
149        Term::Lambda {
150            param,
151            param_type,
152            body,
153        } => {
154            out.push(param.clone());
155            spelled(param_type, out);
156            spelled(body, out);
157        }
158        Term::Fix { name, body } => {
159            out.push(name.clone());
160            spelled(body, out);
161        }
162        Term::Let {
163            name,
164            ty,
165            value,
166            body,
167        } => {
168            out.push(name.clone());
169            spelled(ty, out);
170            spelled(value, out);
171            spelled(body, out);
172        }
173        Term::Match {
174            discriminant,
175            motive,
176            cases,
177        } => {
178            spelled(discriminant, out);
179            spelled(motive, out);
180            for c in cases {
181                spelled(c, out);
182            }
183        }
184        _ => {}
185    }
186}
187
188/// The head inductive of a type application and its arguments: `List A` → `("List", [A])`.
189fn split_type_app(ty: &Term) -> Option<(String, Vec<Term>)> {
190    let mut args = Vec::new();
191    let mut cur = ty;
192    while let Term::App(f, a) = cur {
193        args.push((**a).clone());
194        cur = f;
195    }
196    args.reverse();
197    match cur {
198        Term::Global(n) => Some((n.clone(), args)),
199        _ => None,
200    }
201}
202
203/// A constructor's VALUE fields — its `Π` telescope past the inductive's parameters, with those
204/// parameters instantiated by the discriminant type's own arguments. The same walk
205/// `interface::match_compile::constructor_field_types` does, and for the same reason: it is what
206/// `type_checker::compute_case_type` will check the case against.
207fn constructor_fields(ctor_ty: &Term, type_args: &[Term]) -> Vec<(String, Term)> {
208    let mut all: Vec<(String, Term)> = Vec::new();
209    let mut cur = ctor_ty;
210    while let Term::Pi {
211        param,
212        param_type,
213        body_type,
214    } = cur
215    {
216        all.push((param.clone(), (**param_type).clone()));
217        cur = body_type;
218    }
219    let split = type_args.len().min(all.len());
220    let params: Vec<String> = all[..split].iter().map(|(n, _)| n.clone()).collect();
221    all[split..]
222        .iter()
223        .map(|(n, t)| {
224            let mut ty = t.clone();
225            for (p, a) in params.iter().zip(type_args.iter()) {
226                if p != "_" {
227                    ty = substitute(&ty, p, a);
228                }
229            }
230            (n.clone(), ty)
231        })
232        .collect()
233}
234
235/// The equation lemmas a definition owes, as `(name, statement, proof)` in Lean's order:
236/// the unfold theorem first, then one equation per constructor of the matched argument.
237///
238/// ★ TWO CANDIDATES MAY SHARE THE NAME `<f>_eq_def`, IN PREFERENCE ORDER, and [`generate`]
239/// registers the first the kernel actually CHECKS. This is not indecision; it is the one place
240/// where LOGOS's reduction differs from Lean's and the difference is visible.
241///
242/// Lean's unfold theorem is the APPLIED form — `dbl.eq_def : ∀ x, dbl x = match x with …`,
243/// confirmed by `#check @dbl.eq_def` on this machine — and Lean proves it with a real derivation
244/// (`Lean/Elab/PreDefinition/Eqns.lean:312` `mkUnfoldEq`), not with `rfl`. It cannot be `rfl` in
245/// LOGOS either, and for a sharper reason: `reduction.rs`'s `try_fix_unroll` unrolls a fixpoint
246/// only when its STRUCTURAL argument is already a constructor, so `dbl n` for a free `n` is a
247/// normal form and is not convertible with the `match` it would step to. The applied form is
248/// therefore offered first and, where the guard blocks it, the UNAPPLIED `dbl = fix dbl. …`
249/// stands in — true by one δ step, always, and still a name `unfold dbl` can cite.
250///
251/// Returns an empty vector when there is nothing to state — an equation name, a context with no
252/// `Eq`/`refl` yet, or a body whose shape carries no equations.
253pub fn equation_lemmas(
254    ctx: &Context,
255    name: &str,
256    ty: &Term,
257    body: &Term,
258) -> Vec<(String, Term, Term)> {
259    if is_equation_name(name) {
260        return Vec::new();
261    }
262    if ctx.get_global("Eq").is_none() || ctx.get_global("refl").is_none() {
263        return Vec::new();
264    }
265
266    // ONE δι STEP. The stored body of a recursive definition is `fix f. e`; the equation Lean
267    // states is about the function APPLIED, so the self-reference is folded back to the name it
268    // now has. `f ≡ e[f := f]` by δ followed by ι, so `refl` still proves every statement below.
269    let unfolded = match body {
270        Term::Fix {
271            name: fix_binder,
272            body,
273        } => substitute(body, fix_binder, &Term::Global(name.to_string())),
274        other => other.clone(),
275    };
276
277    // Lean's `lambdaTelescope info.value` (`Lean/Meta/Eqns.lean:151`), walked against the
278    // declared type so the result type is known at the leaf.
279    let mut binders: Vec<(String, Term)> = Vec::new();
280    let mut rest = unfolded;
281    let mut result_ty = ty.clone();
282    loop {
283        let (param, param_type, next_body) = match &rest {
284            Term::Lambda {
285                param,
286                param_type,
287                body,
288            } => (param.clone(), (**param_type).clone(), (**body).clone()),
289            _ => break,
290        };
291        let next_ty = match &result_ty {
292            Term::Pi {
293                param: p,
294                body_type,
295                ..
296            } => {
297                if *p == param {
298                    (**body_type).clone()
299                } else {
300                    substitute(body_type, p, &Term::Var(param.clone()))
301                }
302            }
303            _ => break,
304        };
305        binders.push((param, param_type));
306        rest = next_body;
307        result_ty = next_ty;
308    }
309
310    let head = Term::Global(name.to_string());
311    let args: Vec<Term> = binders.iter().map(|(n, _)| Term::Var(n.clone())).collect();
312    let applied = apply_all(head.clone(), &args);
313
314    let mut out = Vec::new();
315
316    // THE UNFOLD THEOREM, Lean's shape first: `∀ xs, f xs = <body>`
317    // (`Lean/Meta/Eqns.lean:152-157`, and `#check @dbl.eq_def`).
318    out.push((
319        format!("{name}{UNFOLD_SUFFIX}"),
320        pi_over(&binders, eq_prop(&result_ty, &applied, &rest)),
321        lam_over(&binders, refl_proof(&result_ty, &applied)),
322    ));
323    // …and the fallback the fixpoint guard forces: `f = <its stored body>`, one δ step.
324    out.push((
325        format!("{name}{UNFOLD_SUFFIX}"),
326        eq_prop(ty, &head, body),
327        refl_proof(ty, &head),
328    ));
329
330    // THE PER-CONSTRUCTOR EQUATIONS. These are what `rw [f]` at one constructor actually cites,
331    // and they exist only when the definition's leaf is a `Match` on one of its own parameters —
332    // which is every definition the `Consider` surface compiles.
333    let Term::Match {
334        discriminant,
335        cases,
336        ..
337    } = &rest
338    else {
339        return out;
340    };
341    let Term::Var(scrutinee) = discriminant.as_ref() else {
342        return out;
343    };
344    let Some(position) = binders.iter().position(|(n, _)| n == scrutinee) else {
345        return out;
346    };
347    let Some((ind, type_args)) = split_type_app(&binders[position].1) else {
348        return out;
349    };
350    let constructors: Vec<(String, Term)> = ctx
351        .get_constructors(&ind)
352        .into_iter()
353        .map(|(n, t)| (n.to_string(), t.clone()))
354        .collect();
355    if constructors.is_empty() || constructors.len() != cases.len() {
356        return out;
357    }
358
359    let mut taken: Vec<String> = binders.iter().map(|(n, _)| n.clone()).collect();
360    spelled(&rest, &mut taken);
361    spelled(&result_ty, &mut taken);
362
363    for (i, (ctor_name, ctor_ty)) in constructors.iter().enumerate() {
364        let fields = constructor_fields(ctor_ty, &type_args);
365
366        // One fresh binder per value field, stepped clear of every name the definition spells —
367        // the field names come from the constructor's own telescope and may collide with the
368        // definition's parameters, and a collision would make the substitution below rewrite the
369        // wrong occurrence.
370        let mut field_binders: Vec<(String, Term)> = Vec::new();
371        for (orig, fty) in &fields {
372            // A constructor telescope often names its fields `_` (`Succ : Nat -> Nat`), which is
373            // a hole rather than a name; Lean's generated equation calls that field `n`. Give an
374            // anonymous field a readable base so the lemma reads as a statement.
375            let base = if orig == "_" { "x" } else { orig.as_str() };
376            let mut candidate = format!("{base}#eq");
377            let mut step = 0usize;
378            while taken.contains(&candidate) {
379                candidate = format!("{base}#eq{step}");
380                step += 1;
381            }
382            taken.push(candidate.clone());
383            // A DEPENDENT constructor's later field types name its earlier fields; rewrite
384            // each to the binder actually introduced for it.
385            let mut closed = fty.clone();
386            for (j, (prev_name, _)) in field_binders.iter().enumerate() {
387                closed = substitute(&closed, &fields[j].0, &Term::Var(prev_name.clone()));
388            }
389            field_binders.push((candidate, closed));
390        }
391
392        // `C typeargs… fields…` — the value this equation's scrutinee is fixed to.
393        let mut ctor_value = Term::Global(ctor_name.clone());
394        for ta in &type_args {
395            ctor_value = Term::App(Box::new(ctor_value), Box::new(ta.clone()));
396        }
397        for (n, _) in &field_binders {
398            ctor_value = Term::App(Box::new(ctor_value), Box::new(Term::Var(n.clone())));
399        }
400
401        // The binders of the equation: everything before the scrutinee, the constructor's
402        // fields in its place, and everything after it with the scrutinee already fixed.
403        let mut lemma_binders: Vec<(String, Term)> = binders[..position].to_vec();
404        lemma_binders.extend(field_binders.iter().cloned());
405        for (n, t) in &binders[position + 1..] {
406            lemma_binders.push((n.clone(), substitute(t, scrutinee, &ctor_value)));
407        }
408
409        let lemma_args: Vec<Term> = binders
410            .iter()
411            .map(|(n, _)| {
412                if n == scrutinee {
413                    ctor_value.clone()
414                } else {
415                    Term::Var(n.clone())
416                }
417            })
418            .collect();
419        let lhs = apply_all(head.clone(), &lemma_args);
420        let rhs = apply_case(
421            &cases[i],
422            &field_binders
423                .iter()
424                .map(|(n, _)| Term::Var(n.clone()))
425                .collect::<Vec<_>>(),
426        );
427        let carrier = substitute(&result_ty, scrutinee, &ctor_value);
428
429        out.push((
430            format!("{name}{EQUATION_INFIX}{}", i + 1),
431            pi_over(&lemma_binders, eq_prop(&carrier, &lhs, &rhs)),
432            lam_over(&lemma_binders, refl_proof(&carrier, &lhs)),
433        ));
434    }
435
436    out
437}
438
439/// Generate and REGISTER the equation lemmas a definition owes.
440///
441/// Every candidate goes through inference and the checked declaration gateway.
442/// Failed optional candidates are omitted; they cannot poison a valid source
443/// declaration or enter the pending-obligation/refusal ledger. Existing names,
444/// including pending user definitions, are reserved. Successful lemmas share the
445/// existing auxiliary ownership bookkeeping so source replacement invalidates them.
446pub fn generate(ctx: &mut Context, name: &str) {
447    if ctx.equations_suspended()
448        || is_equation_name(name)
449        || ctx.get_global("Eq").is_none()
450        || ctx.get_global("refl").is_none()
451    {
452        return;
453    }
454    let Some((ty, body)) = ctx
455        .get_definition_type(name)
456        .cloned()
457        .zip(ctx.get_definition_body(name).cloned())
458    else {
459        return;
460    };
461    for (lemma, statement, proof) in equation_lemmas(ctx, name, &ty, &body) {
462        // Never displace: a name already taken keeps whatever it holds, whether that is a user's
463        // lemma or an earlier candidate for this same suffix.
464        if ctx.constant_name_reserved(&lemma) {
465            continue;
466        }
467        // Check exactly once, through the ordinary gateway with deferral
468        // disabled. A failed optional candidate creates neither an assumption
469        // nor a pending obligation, and leaves the refusal ledger unchanged.
470        if ctx
471            .declare_checked_auxiliary(&lemma, statement, proof)
472            .is_ok()
473        {
474            ctx.record_derived_aux(name, &lemma);
475        }
476    }
477}
478
479/// Back-fill every definition that was registered before `Eq`/`refl` existed.
480///
481/// The prelude declares hundreds of definitions before `register_eq` runs, and none of them can
482/// state an equation until there is an equality to state it in. Call this once at the end of
483/// `StandardLibrary::register`.
484pub fn generate_missing_for_all(ctx: &mut Context) {
485    if ctx.equations_suspended()
486        || ctx.get_global("Eq").is_none()
487        || ctx.get_global("refl").is_none()
488    {
489        return;
490    }
491    let names: Vec<String> = ctx
492        .iter_definitions()
493        .map(|(n, _, _)| n.to_string())
494        .filter(|n| !is_equation_name(n))
495        .collect();
496    for n in names {
497        generate(ctx, &n);
498    }
499}