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}