Skip to main content

logicaffeine_kernel/
recursor.rs

1//! R2 — auto-derived recursors (dependent eliminators) for inductive types.
2//!
3//! Today a user who declares an inductive must hand-write the `match`/`fix` to recurse
4//! over it. Lean/Coq instead AUTO-GENERATE the recursor `I.rec` (the dependent
5//! eliminator) from the declaration, so "declare `ℕ`, get induction for free." This
6//! module is that derivation: from an inductive's registered constructors it synthesizes
7//!
8//! ```text
9//! I_rec : Π(P : I → Type). minor₀ → … → minorₖ → Π(x : I). P x
10//! I_rec := λP. λf₀ … λfₖ. fix rec. λx. match x return (λx. P x) with
11//!            | Cᵢ a… => fᵢ a… (rec aⱼ)…        -- one rec-call per recursive argument
12//! ```
13//!
14//! where each minor premise is `Π(args). Π(IH : P aⱼ for each recursive arg). P (Cᵢ args)`.
15//! The synthesized term is an ordinary kernel `Term`, so it is re-checked by `infer_type`
16//! for coverage + termination — and (the point of building it now) independently
17//! re-derived by the `recheck` second kernel. A Prop motive may be
18//! passed wherever the `Type` motive is expected, by cumulativity (`Prop ≤ Type 0`), so
19//! the single derived recursor serves BOTH computation and induction.
20//!
21//! Scope: parametric AND indexed inductive families. Beyond the uniform PARAMETERS of a
22//! type like `List A`, an indexed family has INDICES that vary per constructor — `Eq A x :
23//! A → Prop` with `refl : Eq A x x`. Their eliminator's motive abstracts over the indices,
24//! so `derive_recursor("Eq")` synthesizes FULL Paulin-Mohring J:
25//! `Π(A). Π(x:A). Π(P : Π(y:A). Eq A x y → Sort). P x (refl A x) → Π(y). Π(h:Eq A x y). P y h`
26//! — the identity eliminator as a kernel-checked term, not an axiom.
27
28use crate::context::Context;
29use crate::error::{KernelError, KernelResult};
30use crate::infer_type;
31use crate::term::{Term, Universe};
32
33/// Build `I_rec`'s term and type for the inductive `ind`. Returns `(recursor_type,
34/// recursor_term)` where `recursor_type` is exactly `infer_type(recursor_term)` — so the
35/// type is, by construction, the one the kernel certifies the term against.
36pub fn derive_recursor(ctx: &Context, ind: &str) -> KernelResult<(Term, Term)> {
37    // A member of a mutual block gets the mutual recursor (one shared `MutualFix`).
38    if let Some(block) = ctx.mutual_block_of(ind) {
39        let block = block.to_vec();
40        return derive_mutual_recursor(ctx, ind, &block);
41    }
42
43    let ind_full_ty = ctx
44        .get_global(ind)
45        .ok_or_else(|| KernelError::UnboundVariable(ind.to_string()))?
46        .clone();
47    if !ctx.is_inductive(ind) {
48        return Err(KernelError::UnboundVariable(ind.to_string()));
49    }
50
51    // Split the inductive's arity telescope into uniform PARAMETERS (the first
52    // `num_params`) and INDICES (the rest). `List : Type → Type` is 1 param / 0 index;
53    // `Eq : Π(A). A → A → Prop` is 2 params (`A`, `x`) / 1 index (`y`). Parameters are
54    // bound once at the top as `A0 … A_{p-1}`; indices are re-abstracted inside the
55    // recursion (`Idx0 … Idx_{k-1}`) because each constructor targets its own index.
56    let (arity_tele, ind_sort) = peel_pis(&ind_full_ty);
57    let num_params = ctx.inductive_num_params(ind).min(arity_tele.len());
58
59    // The sort the motive targets. A `Prop` inductive may be eliminated into a larger sort
60    // (`Type`) only if it is a subsingleton (0 constructors, or 1 with propositional args) —
61    // so `Eq`/`False` get the full `Type` eliminator (J, ex falso), but a multi-constructor
62    // proposition like `le` gets its `Prop` induction principle. Everything else eliminates
63    // into `Type 0` (a `Prop` motive still passes there by cumulativity).
64    let motive_target = match ind_sort {
65        Term::Sort(Universe::Prop)
66            if !crate::type_checker::is_subsingleton_prop(ctx, ind).unwrap_or(false) =>
67        {
68            Term::Sort(Universe::Prop)
69        }
70        _ => Term::Sort(Universe::Type(0)),
71    };
72    let param_tele: Vec<(String, Term)> = arity_tele[..num_params].to_vec();
73    let index_tele: Vec<(String, Term)> = arity_tele[num_params..].to_vec();
74    let k = index_tele.len();
75
76    let tp_names: Vec<String> = (0..num_params).map(|i| format!("A{i}")).collect();
77    let idx_names: Vec<String> = (0..k).map(|i| format!("Idx{i}")).collect();
78
79    // Parameter binder types, closed over the earlier parameters' recursor names.
80    let param_types: Vec<Term> = (0..num_params)
81        .map(|i| rename_tele_types(&param_tele[i].1, &param_tele[..i], &tp_names))
82        .collect();
83    // Index binder types, closed over ALL parameters and the earlier indices — with the
84    // given index binder names (the fix and the match motive use different names).
85    let index_types = |names: &[String]| -> Vec<Term> {
86        (0..k)
87            .map(|j| {
88                let with_params = rename_tele_types(&index_tele[j].1, &param_tele, &tp_names);
89                rename_index_refs(&with_params, &index_tele[..j], names)
90            })
91            .collect::<Vec<_>>()
92    };
93
94    // `I A0 … A_{p-1}` (applied to the parameters only).
95    let mut ind_params = Term::Global(ind.to_string());
96    for name in &tp_names {
97        ind_params = app(ind_params, var(name));
98    }
99
100    // The motive `P : Π(Idx0:T0)…Π(Idx_{k-1}). (I A… Idx…) → Type 0`. `Prop` motives pass
101    // by cumulativity, so this one recursor serves computation and induction alike.
102    let idx_types_motive = index_types(&idx_names);
103    let mut ind_at_idx = ind_params.clone();
104    for name in &idx_names {
105        ind_at_idx = app(ind_at_idx, var(name));
106    }
107    let mut motive_ty = arrow(ind_at_idx.clone(), motive_target);
108    for j in (0..k).rev() {
109        motive_ty = pi(&idx_names[j], idx_types_motive[j].clone(), motive_ty);
110    }
111
112    let ctors: Vec<(String, Term)> = ctx
113        .get_constructors(ind)
114        .iter()
115        .map(|(n, t)| (n.to_string(), (*t).clone()))
116        .collect();
117
118    let mut minors: Vec<(String, Term)> = Vec::with_capacity(ctors.len());
119    let mut cases: Vec<Term> = Vec::with_capacity(ctors.len());
120    // A standalone inductive is a mutual block of one: its only motive is `P`, its only
121    // recursive name `rec`, and every recursive occurrence is of member 0 (itself).
122    let block_single = [ind];
123    let motive_single = ["P".to_string()];
124    let rec_single = ["rec".to_string()];
125    for (i, (cname, ctype)) in ctors.iter().enumerate() {
126        let an = constructor_analysis(ctype, ind, &block_single, num_params, &tp_names)?;
127        minors.push((format!("f{i}"), minor_type("P", &motive_single, cname, &tp_names, &an)));
128        cases.push(case_term(cname, &tp_names, &an, &format!("f{i}"), &rec_single));
129    }
130
131    // The match's return clause `λj0…λj_{k-1}. λx:(I A… j…). P j… x` — the motive
132    // re-abstracted with FRESH index binders so applying it at the discriminant's own
133    // indices captures nothing.
134    let mj_names: Vec<String> = (0..k).map(|i| format!("mj{i}")).collect();
135    let idx_types_match = index_types(&mj_names);
136    let mut ind_at_mj = ind_params.clone();
137    for name in &mj_names {
138        ind_at_mj = app(ind_at_mj, var(name));
139    }
140    let mut motive_body = var("P");
141    for name in &mj_names {
142        motive_body = app(motive_body, var(name));
143    }
144    motive_body = app(motive_body, var("x"));
145    let mut motive_fn = lam("x", ind_at_mj, motive_body);
146    for j in (0..k).rev() {
147        motive_fn = lam(&mj_names[j], idx_types_match[j].clone(), motive_fn);
148    }
149
150    // fix rec. λIdx0…λIdx_{k-1}. λx:(I A… Idx…). match x return motive_fn with { cases }
151    let match_term = Term::Match {
152        discriminant: Box::new(var("x")),
153        motive: Box::new(motive_fn),
154        cases,
155    };
156    let mut fix_body = lam("x", ind_at_idx.clone(), match_term);
157    for j in (0..k).rev() {
158        fix_body = lam(&idx_names[j], idx_types_motive[j].clone(), fix_body);
159    }
160    let mut term = Term::Fix { name: "rec".to_string(), body: Box::new(fix_body) };
161
162    // Wrap `λfₙ … λf₀`, then `λP`, then the parameters `λA_{p-1} … λA₀`.
163    for (fname, fty) in minors.iter().rev() {
164        term = lam(fname, fty.clone(), term);
165    }
166    term = lam("P", motive_ty, term);
167    for i in (0..num_params).rev() {
168        term = lam(&tp_names[i], param_types[i].clone(), term);
169    }
170
171    // The recursor's type is, by construction, the type the kernel certifies for it —
172    // coverage and termination included, in both kernels.
173    let ty = infer_type(ctx, &term)?;
174    Ok((ty, term))
175}
176
177/// Derive the recursor for `ind`, a member of the MUTUAL block `block`. Every member
178/// shares one `MutualFix`; this returns `(type, term)` for `ind`'s recursor, whose body
179/// selects `ind`'s component. The eliminator takes ONE motive per member and ALL
180/// members' minor premises; a recursive occurrence of member `k` in any constructor
181/// uses motive `P_k` for its induction hypothesis and calls `rec_k`.
182///
183/// Scope: mutual blocks with shared uniform PARAMETERS (`Tree A`/`Forest A`) and per-member
184/// indices are both supported; every member must agree on the parameter count.
185fn derive_mutual_recursor(ctx: &Context, ind: &str, block: &[String]) -> KernelResult<(Term, Term)> {
186    let m = block.len();
187    let bi = block.iter().position(|n| n == ind).ok_or_else(|| {
188        KernelError::CertificationError(format!("'{ind}' is not in its own mutual block"))
189    })?;
190    let block_refs: Vec<&str> = block.iter().map(|s| s.as_str()).collect();
191
192    // Shared PARAMETERS — a mutual block shares its uniform parameters (`Tree A`/`Forest A`).
193    // Taken from the first member; every member must agree on the count.
194    let block0_ty = ctx
195        .get_global(&block[0])
196        .ok_or_else(|| KernelError::UnboundVariable(block[0].clone()))?
197        .clone();
198    let (arity0, _) = peel_pis(&block0_ty);
199    let num_params = ctx.inductive_num_params(&block[0]).min(arity0.len());
200    for name in block {
201        let full = ctx.get_global(name).ok_or_else(|| KernelError::UnboundVariable(name.clone()))?;
202        if ctx.inductive_num_params(name).min(peel_pis(full).0.len()) != num_params {
203            return Err(KernelError::CertificationError(format!(
204                "auto-recursor: mutual block members disagree on parameter count ('{name}')"
205            )));
206        }
207    }
208    let param_tele: Vec<(String, Term)> = arity0[..num_params].to_vec();
209    let tp_names: Vec<String> = (0..num_params).map(|i| format!("A{i}")).collect();
210    let param_types: Vec<Term> = (0..num_params)
211        .map(|i| rename_tele_types(&param_tele[i].1, &param_tele[..i], &tp_names))
212        .collect();
213
214    let motive_names: Vec<String> = (0..m).map(|k| format!("P{k}")).collect();
215    let rec_names: Vec<String> = (0..m).map(|k| format!("rec{k}")).collect();
216
217    // `I_k` applied to the shared parameters `A0 … A_{p-1}`.
218    let ind_at_params = |name: &str| {
219        tp_names.iter().fold(Term::Global(name.to_string()), |acc, n| app(acc, var(n)))
220    };
221
222    // Per-member skeleton: the index telescope, the motive type, and the applied form.
223    struct MemberInfo {
224        idx_names: Vec<String>,
225        idx_types: Vec<Term>,
226        ind_at_idx: Term,
227        motive_ty: Term,
228    }
229    let mut infos: Vec<MemberInfo> = Vec::with_capacity(m);
230    for (k, name) in block.iter().enumerate() {
231        let full_ty = ctx
232            .get_global(name)
233            .ok_or_else(|| KernelError::UnboundVariable(name.clone()))?
234            .clone();
235        let (arity_tele, ind_sort) = peel_pis(&full_ty);
236        let index_tele: Vec<(String, Term)> = arity_tele[num_params..].to_vec();
237        let target = match ind_sort {
238            Term::Sort(Universe::Prop)
239                if !crate::type_checker::is_subsingleton_prop(ctx, name).unwrap_or(false) =>
240            {
241                Term::Sort(Universe::Prop)
242            }
243            _ => Term::Sort(Universe::Type(0)),
244        };
245        let idx_names: Vec<String> = (0..index_tele.len()).map(|j| format!("Idx{k}_{j}")).collect();
246        // Index binder types: rename the shared params → `A_i`, and earlier index refs → `Idx…`.
247        let idx_types: Vec<Term> = (0..index_tele.len())
248            .map(|j| {
249                let with_params = rename_tele_types(&index_tele[j].1, &param_tele, &tp_names);
250                rename_index_refs(&with_params, &index_tele[..j], &idx_names)
251            })
252            .collect();
253        // I_k A0 … A_{p-1} Idx0 … Idx_{k-1}
254        let mut ind_at_idx = ind_at_params(name);
255        for n in &idx_names {
256            ind_at_idx = app(ind_at_idx, var(n));
257        }
258        // motive_k : Π(idx…). (I_k params idx…) → target  (params are bound by the outer λ)
259        let mut motive_ty = arrow(ind_at_idx.clone(), target);
260        for j in (0..idx_names.len()).rev() {
261            motive_ty = pi(&idx_names[j], idx_types[j].clone(), motive_ty);
262        }
263        infos.push(MemberInfo { idx_names, idx_types, ind_at_idx, motive_ty });
264    }
265
266    // All minor premises (across every member, in block order) and, per member, the
267    // `match` cases of its own fixpoint body.
268    let mut minors: Vec<(String, Term)> = Vec::new();
269    let mut member_cases: Vec<Vec<Term>> = Vec::with_capacity(m);
270    for (k, name) in block.iter().enumerate() {
271        let ctors: Vec<(String, Term)> = ctx
272            .get_constructors(name)
273            .iter()
274            .map(|(n, t)| (n.to_string(), (*t).clone()))
275            .collect();
276        let mut cases = Vec::with_capacity(ctors.len());
277        for (cname, ctype) in &ctors {
278            let an = constructor_analysis(ctype, name, &block_refs, num_params, &tp_names)?;
279            let fi = format!("f{}", minors.len());
280            minors.push((
281                fi.clone(),
282                minor_type(&motive_names[k], &motive_names, cname, &tp_names, &an),
283            ));
284            cases.push(case_term(cname, &tp_names, &an, &fi, &rec_names));
285        }
286        member_cases.push(cases);
287    }
288
289    // Each member's fixpoint body: `λidx…. λx:(I_k params idx…). match x return motive_fn`.
290    let mut defs: Vec<(String, Term)> = Vec::with_capacity(m);
291    for (k, name) in block.iter().enumerate() {
292        let info = &infos[k];
293        // The match return clause `λmj…. λx. P_k mj… x`, fresh index binders.
294        let mj_names: Vec<String> = (0..info.idx_names.len()).map(|j| format!("mj{k}_{j}")).collect();
295        let mut motive_body = var(&motive_names[k]);
296        for n in &mj_names {
297            motive_body = app(motive_body, var(n));
298        }
299        motive_body = app(motive_body, var("x"));
300        let mut ind_at_mj = ind_at_params(name);
301        for n in &mj_names {
302            ind_at_mj = app(ind_at_mj, var(n));
303        }
304        let mut motive_fn = lam("x", ind_at_mj, motive_body);
305        for j in (0..mj_names.len()).rev() {
306            // Re-express the index type with the FRESH match binders: `Idx_i → mj_i`.
307            let mut mj_ty = info.idx_types[j].clone();
308            for i in 0..j {
309                mj_ty = subst_name(&mj_ty, &info.idx_names[i], &var(&mj_names[i]));
310            }
311            motive_fn = lam(&mj_names[j], mj_ty, motive_fn);
312        }
313        let match_term = Term::Match {
314            discriminant: Box::new(var("x")),
315            motive: Box::new(motive_fn),
316            cases: member_cases[k].clone(),
317        };
318        let mut fix_body = lam("x", info.ind_at_idx.clone(), match_term);
319        for j in (0..info.idx_names.len()).rev() {
320            fix_body = lam(&info.idx_names[j], info.idx_types[j].clone(), fix_body);
321        }
322        defs.push((rec_names[k].clone(), fix_body));
323    }
324
325    // The whole recursor: `λA0…A_{p-1}. λP0…P_{m-1}. λf0…f_{last}. (mutualfix { rec_k := … }).bi`.
326    let mut term = Term::MutualFix { defs, index: bi };
327    for (fname, fty) in minors.iter().rev() {
328        term = lam(fname, fty.clone(), term);
329    }
330    for k in (0..m).rev() {
331        term = lam(&motive_names[k], infos[k].motive_ty.clone(), term);
332    }
333    for i in (0..num_params).rev() {
334        term = lam(&tp_names[i], param_types[i].clone(), term);
335    }
336
337    let ty = infer_type(ctx, &term)?;
338    Ok((ty, term))
339}
340
341/// A constructor's shape in the recursor's scope: the value-argument types, whether each is
342/// recursive (and if so, at which INDEX arguments), and the index arguments of the
343/// constructor's own result. Names are already the recursor's (`A0…`, `a0…`).
344struct CtorAnalysis {
345    value_types: Vec<Term>,
346    /// `Some(occ)` if value argument `j` is a recursive occurrence — either the DIRECT
347    /// form `I A… e…` (`occ.tele` empty) or the strictly-positive FUNCTIONAL form
348    /// `Π(z:B…). I A… e…` (`occ.tele` = the `(z, B)` binders), as in `Acc_intro`'s
349    /// `Π(y). R y x → Acc A R y`. `None` for a non-recursive argument.
350    recursive: Vec<Option<RecOcc>>,
351    /// The index arguments of the constructor's result `I A… e…` (empty when non-indexed).
352    result_indices: Vec<Term>,
353}
354
355/// A recursive occurrence in a constructor argument: WHICH block member it is an
356/// occurrence of (`0` for a standalone inductive — the whole block is itself), the
357/// domain telescope of a functional argument (empty for a direct argument), and the
358/// occurrence's index arguments (in scope of the telescope).
359struct RecOcc {
360    member: usize,
361    tele: Vec<(String, Term)>,
362    indices: Vec<Term>,
363}
364
365/// Analyse a constructor in the recursor's scope. Its leading `num_params` parameters are
366/// the inductive's parameters (renamed to `A₀ … A_{p-1}`); the rest are value arguments
367/// (renamed `a₀ … aₘ`). A value argument is recursive iff its (rewritten) type's head is
368/// the inductive, and we record that occurrence's INDEX arguments. We also record the
369/// constructor result's own index arguments — for `refl : Π(A). Π(x). Eq A x x` the value
370/// arguments are empty and the result index is `[x]`, so the refl case demands `P x (refl
371/// A x)`.
372fn constructor_analysis(
373    ctor_type: &Term,
374    owner: &str,
375    block: &[&str],
376    num_params: usize,
377    tp_names: &[String],
378) -> KernelResult<CtorAnalysis> {
379    let (params, residual) = peel_pis(ctor_type);
380    if params.len() < num_params {
381        return Err(KernelError::CertificationError(format!(
382            "auto-recursor: constructor of '{}' has fewer parameters than the inductive",
383            owner
384        )));
385    }
386    if head_global(residual) != Some(owner) {
387        return Err(KernelError::CertificationError(format!(
388            "auto-recursor: constructor result {} is not the inductive '{}'",
389            residual, owner
390        )));
391    }
392    let (type_params, value_params) = params.split_at(num_params);
393
394    // Rewrite a term from the constructor's scope to the recursor's: parameter names →
395    // `A_i`, and the first `upto` value names → `a_k`.
396    let rewrite = |t: &Term, upto: usize| -> Term {
397        let mut out = t.clone();
398        for (i, (tp_orig, _)) in type_params.iter().enumerate() {
399            if tp_orig != "_" {
400                out = subst_name(&out, tp_orig, &var(&tp_names[i]));
401            }
402        }
403        for (kk, (vk_orig, _)) in value_params.iter().enumerate().take(upto) {
404            if vk_orig != "_" {
405                out = subst_name(&out, vk_orig, &var(&format!("a{kk}")));
406            }
407        }
408        out
409    };
410
411    let mut value_types = Vec::with_capacity(value_params.len());
412    let mut recursive = Vec::with_capacity(value_params.len());
413    for (j, (_, ty)) in value_params.iter().enumerate() {
414        let t = rewrite(ty, j);
415        // Peel any functional telescope: a strictly-positive argument may be
416        // `Π(z:B…). I A… e…` (Acc's `Π(y). R y x → Acc A R y`) as well as the
417        // direct `I A… e…`. The occurrence is recursive iff, after peeling, the
418        // head is the inductive.
419        let (tele, occ) = peel_pis(&t);
420        // Recursive iff the occurrence's head is ANY block member; record which one so
421        // the minor premise's IH and the recursive call route to that member's motive/fix.
422        if let Some(member) = head_global(occ).and_then(|h| block.iter().position(|m| *m == h)) {
423            let indices: Vec<Term> = app_args(occ).into_iter().skip(num_params).collect();
424            recursive.push(Some(RecOcc { member, tele, indices }));
425        } else {
426            recursive.push(None);
427        }
428        value_types.push(t);
429    }
430
431    // The constructor result's own index arguments, closed over all value arguments.
432    let result_indices: Vec<Term> = app_args(residual)
433        .into_iter()
434        .skip(num_params)
435        .map(|e| rewrite(&e, value_params.len()))
436        .collect();
437
438    Ok(CtorAnalysis { value_types, recursive, result_indices })
439}
440
441/// The minor-premise type for one constructor:
442/// `Π(a₀:T₀)…Π(aₘ:Tₘ). Π(IHⱼ : P eⱼ… aⱼ per recursive arg). P r… (C A… a₀ … aₘ)`,
443/// where `r…` are the constructor's result indices and `eⱼ…` the recursive occurrence's.
444fn minor_type(
445    owner_motive: &str,
446    motive_names: &[String],
447    cname: &str,
448    tp_names: &[String],
449    an: &CtorAnalysis,
450) -> Term {
451    let mut ctor_applied = Term::Global(cname.to_string());
452    for name in tp_names {
453        ctor_applied = app(ctor_applied, var(name));
454    }
455    for j in 0..an.value_types.len() {
456        ctor_applied = app(ctor_applied, var(&format!("a{j}")));
457    }
458    // The OWNER's motive applied to the constructor's result indices, then the value.
459    let mut body = var(owner_motive);
460    for e in &an.result_indices {
461        body = app(body, e.clone());
462    }
463    body = app(body, ctor_applied);
464    // One induction hypothesis per recursive argument, using the motive of the block
465    // MEMBER the occurrence is of (self or sibling). For a DIRECT occurrence it is
466    // `P_member (its indices) aⱼ`; for a FUNCTIONAL occurrence `aⱼ : Π(z:B…). I e…`
467    // it is `Π(z:B…). P_member (its indices) (aⱼ z…)`.
468    for j in (0..an.value_types.len()).rev() {
469        if let Some(occ) = &an.recursive[j] {
470            let mut ih = var(&motive_names[occ.member]);
471            for e in &occ.indices {
472                ih = app(ih, e.clone());
473            }
474            let mut aj = var(&format!("a{j}"));
475            for (tn, _) in &occ.tele {
476                aj = app(aj, var(tn));
477            }
478            ih = app(ih, aj);
479            for (tn, tt) in occ.tele.iter().rev() {
480                ih = pi(tn, tt.clone(), ih);
481            }
482            body = pi(&format!("ih{j}"), ih, body);
483        }
484    }
485    for j in (0..an.value_types.len()).rev() {
486        body = pi(&format!("a{j}"), an.value_types[j].clone(), body);
487    }
488    body
489}
490
491/// The `match` case term for one constructor (binds only the VALUE arguments — the
492/// parameters are fixed by the discriminant):
493/// `λa₀ … λaₘ. fᵢ a₀ … aₘ (rec eⱼ… aⱼ)…`, where the recursive call carries the recursive
494/// occurrence's index arguments so it lands at the right motive instance.
495fn case_term(
496    cname: &str,
497    _tp_names: &[String],
498    an: &CtorAnalysis,
499    fi: &str,
500    rec_names: &[String],
501) -> Term {
502    let _ = cname;
503    let mut body = var(fi);
504    for j in 0..an.value_types.len() {
505        body = app(body, var(&format!("a{j}")));
506    }
507    for j in 0..an.value_types.len() {
508        if let Some(occ) = &an.recursive[j] {
509            // DIRECT: `rec (its indices) aⱼ`. FUNCTIONAL: `λ(z:B…). rec (its
510            // indices) (aⱼ z…)` — recurse on each sub-structure the accessibility
511            // function yields. The recursive call's structural argument `aⱼ z…`
512            // is an APPLICATION of the smaller field `aⱼ`, which the guard admits
513            // (an application headed by a smaller variable is smaller). The call routes
514            // to the fixpoint of the block MEMBER the occurrence is of.
515            let mut call = var(&rec_names[occ.member]);
516            for e in &occ.indices {
517                call = app(call, e.clone());
518            }
519            let mut aj = var(&format!("a{j}"));
520            for (tn, _) in &occ.tele {
521                aj = app(aj, var(tn));
522            }
523            call = app(call, aj);
524            for (tn, tt) in occ.tele.iter().rev() {
525                call = lam(tn, tt.clone(), call);
526            }
527            body = app(body, call);
528        }
529    }
530    for j in (0..an.value_types.len()).rev() {
531        body = lam(&format!("a{j}"), an.value_types[j].clone(), body);
532    }
533    body
534}
535
536/// Rewrite an inductive-parameter binder type from the declaration's scope into the
537/// recursor's: each of the earlier parameter binders `tele[i]` becomes `names[i]`.
538fn rename_tele_types(ty: &Term, tele: &[(String, Term)], names: &[String]) -> Term {
539    let mut out = ty.clone();
540    for (i, (orig, _)) in tele.iter().enumerate() {
541        if orig != "_" {
542            out = subst_name(&out, orig, &var(&names[i]));
543        }
544    }
545    out
546}
547
548/// Rewrite an index binder type's references to the EARLIER index binders `tele[i]` to the
549/// recursor-scope `names[i]` (parameters must already be renamed).
550fn rename_index_refs(ty: &Term, tele: &[(String, Term)], names: &[String]) -> Term {
551    rename_tele_types(ty, tele, names)
552}
553
554/// The argument spine of an application `f a b c` → `[a, b, c]` (`[]` for a non-application).
555fn app_args(t: &Term) -> Vec<Term> {
556    let mut args = Vec::new();
557    let mut cur = t;
558    while let Term::App(f, a) = cur {
559        args.push((**a).clone());
560        cur = f;
561    }
562    args.reverse();
563    args
564}
565
566/// Peel a term's leading `Π`s into `(name, type)` pairs plus the residual.
567fn peel_pis(t: &Term) -> (Vec<(String, Term)>, &Term) {
568    let mut params = Vec::new();
569    let mut cur = t;
570    while let Term::Pi { param, param_type, body_type } = cur {
571        params.push((param.clone(), (**param_type).clone()));
572        cur = body_type;
573    }
574    (params, cur)
575}
576
577/// The head `Global` of an application spine, if any (`List A B` → `List`).
578fn head_global(t: &Term) -> Option<&str> {
579    let mut cur = t;
580    while let Term::App(f, _) = cur {
581        cur = f;
582    }
583    match cur {
584        Term::Global(n) => Some(n),
585        _ => None,
586    }
587}
588
589/// Substitute the free variable `old` by `repl` (binder-respecting).
590fn subst_name(t: &Term, old: &str, repl: &Term) -> Term {
591    match t {
592        Term::Var(n) if n == old => repl.clone(),
593        Term::Var(_) | Term::Global(_) | Term::Sort(_) | Term::Lit(_) | Term::Hole
594        | Term::Const { .. } => t.clone(),
595        Term::Pi { param, param_type, body_type } => Term::Pi {
596            param: param.clone(),
597            param_type: Box::new(subst_name(param_type, old, repl)),
598            body_type: if param == old {
599                body_type.clone()
600            } else {
601                Box::new(subst_name(body_type, old, repl))
602            },
603        },
604        Term::Lambda { param, param_type, body } => Term::Lambda {
605            param: param.clone(),
606            param_type: Box::new(subst_name(param_type, old, repl)),
607            body: if param == old { body.clone() } else { Box::new(subst_name(body, old, repl)) },
608        },
609        Term::App(f, a) => {
610            Term::App(Box::new(subst_name(f, old, repl)), Box::new(subst_name(a, old, repl)))
611        }
612        Term::Match { discriminant, motive, cases } => Term::Match {
613            discriminant: Box::new(subst_name(discriminant, old, repl)),
614            motive: Box::new(subst_name(motive, old, repl)),
615            cases: cases.iter().map(|c| subst_name(c, old, repl)).collect(),
616        },
617        Term::Fix { name, body } => Term::Fix {
618            name: name.clone(),
619            body: if name == old { body.clone() } else { Box::new(subst_name(body, old, repl)) },
620        },
621        Term::MutualFix { defs, index } => {
622            // Every def name binds in every body; `old` is shadowed iff it matches any.
623            if defs.iter().any(|(n, _)| n == old) {
624                t.clone()
625            } else {
626                Term::MutualFix {
627                    defs: defs.iter().map(|(n, b)| (n.clone(), subst_name(b, old, repl))).collect(),
628                    index: *index,
629                }
630            }
631        }
632        Term::Let { name, ty, value, body } => Term::Let {
633            name: name.clone(),
634            ty: Box::new(subst_name(ty, old, repl)),
635            value: Box::new(subst_name(value, old, repl)),
636            body: if name == old { body.clone() } else { Box::new(subst_name(body, old, repl)) },
637        },
638    }
639}
640
641// --- small term builders ---
642fn var(n: &str) -> Term {
643    Term::Var(n.to_string())
644}
645fn app(f: Term, x: Term) -> Term {
646    Term::App(Box::new(f), Box::new(x))
647}
648fn lam(p: &str, ty: Term, body: Term) -> Term {
649    Term::Lambda { param: p.to_string(), param_type: Box::new(ty), body: Box::new(body) }
650}
651fn pi(p: &str, ty: Term, body: Term) -> Term {
652    Term::Pi { param: p.to_string(), param_type: Box::new(ty), body_type: Box::new(body) }
653}
654fn arrow(a: Term, b: Term) -> Term {
655    pi("_", a, b)
656}